"""
src/zt_audit_logger.py

DPDPA Zero Trust — Visibility Pillar
Legal basis: DPDP Rules 2025, Rule 6(1)(c)
    "visibility on the accessing of such personal data, through appropriate
     logs, monitoring and review, for enabling detection of unauthorised
     access, its investigation and remediation to prevent recurrence"

Also supports Rule 6(1)(g) ("appropriate technical and organisational
measures to ensure effective observance of security safeguards"): the
tamper-evident trail is evidence for periodic control-effectiveness reviews.

Log retention:
    - Security logs: minimum 1 year (Rule 6(1)(e)); personal data, traffic
      data and processing logs: minimum 1 year (Rule 8(3)), then erase
      unless another law requires longer
    - Longer periods (e.g. 5-7 years) come from sectoral law (SEBI/RBI/IT
      Act): name that law. Do NOT attribute a 7-year floor to DPDPA.

Implementation notes:
    - ensure_ascii=False is MANDATORY. PostgreSQL emits UTF-8 literals for
      Hindi, Tamil, Kannada identifiers. Python's default ASCII-escapes them,
      causing hash divergence when verifying against a DB-stored canonical.
    - Do NOT add separators=(',', ':') — compact form diverges from
      PostgreSQL's jsonb_canonical() which uses standard spacing.
    - SHA-256 is computed over json.dumps(sort_keys=True, ensure_ascii=False)
      to match Python verification against PostgreSQL's jsonb_canonical().
"""

from __future__ import annotations

import hashlib
import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Literal

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class AuditEvent:
    """Structured audit event for Rule 6(1)(c) compliance."""

    principal: str           # Service account or user identity
    resource: str            # Resource identifier (table, bucket, API path)
    action: str              # e.g. "read", "write", "delete"
    outcome: Literal["allow", "deny"]
    device_id: str           # Device or pod identifier
    session_id: str          # JIT session or request correlation ID
    resource_scope: str      # e.g. "personal_data", "pseudonymous", "non_personal"
    timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    legal_basis: str = "Rule 6(1)(c) — DPDP Rules 2025"

    def to_dict(self) -> dict:
        return {
            "timestamp": self.timestamp,
            "principal": self.principal,
            "resource": self.resource,
            "action": self.action,
            "outcome": self.outcome,
            "device_id": self.device_id,
            "session_id": self.session_id,
            "resource_scope": self.resource_scope,
            "legal_basis": self.legal_basis,
        }

    def canonical_json(self) -> str:
        """
        Canonical JSON for integrity hashing.

        sort_keys=True: deterministic key ordering.
        ensure_ascii=False: preserve UTF-8 — mandatory to avoid hash
            divergence when principal/resource names contain non-ASCII
            characters (e.g. Hindi script identifiers).
        """
        return json.dumps(self.to_dict(), sort_keys=True, ensure_ascii=False)

    def integrity_hash(self) -> str:
        """SHA-256 over canonical JSON — tamper-evident seal."""
        return hashlib.sha256(self.canonical_json().encode("utf-8")).hexdigest()

    def sealed(self) -> dict:
        """Event dict with integrity_hash appended."""
        d = self.to_dict()
        d["integrity_hash"] = self.integrity_hash()
        return d


def log_access(
    principal: str,
    resource: str,
    action: str,
    outcome: Literal["allow", "deny"],
    device_id: str,
    session_id: str,
    resource_scope: str = "personal_data",
) -> dict:
    """
    Emit a Rule 6(1)(c)-compliant audit event and return the sealed dict.

    Usage:
        event = log_access(
            principal="svc-analytics@project.iam.gserviceaccount.com",
            resource="pii_db.users",
            action="read",
            outcome="allow",
            device_id="pod/pii-reader-7f9b4-x2k8n",
            session_id="jit-sess-20240315T093012Z-abc123",
        )
    """
    ev = AuditEvent(
        principal=principal,
        resource=resource,
        action=action,
        outcome=outcome,
        device_id=device_id,
        session_id=session_id,
        resource_scope=resource_scope,
    )
    sealed = ev.sealed()

    if outcome == "deny":
        logger.warning(
            "DPDPA_ACCESS_DENIED principal=%s resource=%s action=%s device=%s session=%s",
            principal, resource, action, device_id, session_id,
        )
    else:
        logger.info(
            "DPDPA_ACCESS_ALLOWED principal=%s resource=%s action=%s",
            principal, resource, action,
        )

    return sealed


def verify_integrity(event_dict: dict) -> bool:
    """
    Verify the integrity_hash of a stored audit event.

    Returns True if the stored hash matches a freshly computed hash
    over the event fields (excluding the integrity_hash key itself).
    """
    stored_hash = event_dict.get("integrity_hash")
    if not stored_hash:
        return False

    fields = {k: v for k, v in event_dict.items() if k != "integrity_hash"}
    canonical = json.dumps(fields, sort_keys=True, ensure_ascii=False)
    expected = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    return stored_hash == expected
