"""Tamper-evident audit logging for Skytale channels.

Provides cryptographically chained audit logs for EU AI Act compliance.
Each log entry contains a hash of the previous entry, forming an
append-only chain that detects tampering.

Skytale NEVER logs plaintext or key material. Audit entries record:
- Agent identity events (DID verification)
- Channel lifecycle events (create, join, leave)
- Message metadata (ciphertext length, channel ID, delivery status)
- Group state transitions (membership changes, epoch hashes)

Usage::

    from skytale_sdk.audit import AuditLog, AuditEvent

    log = AuditLog()
    log.record(AuditEvent.channel_created("acme/research/results", agent_did="did:key:z6Mk..."))
    log.record(AuditEvent.message_sent("acme/research/results", ciphertext_len=256))

    # Export for compliance review
    entries = log.export_chain()
    assert log.verify_chain()
"""

from __future__ import annotations

import hashlib
import json
import os
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

try:
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM

    _HAS_CRYPTO = True
except ImportError:
    _HAS_CRYPTO = False


class EventType(str, Enum):
    """Types of auditable events."""

    CHANNEL_CREATED = "channel_created"
    CHANNEL_JOINED = "channel_joined"
    CHANNEL_LEFT = "channel_left"
    MEMBER_ADDED = "member_added"
    MEMBER_REMOVED = "member_removed"
    MESSAGE_SENT = "message_sent"
    MESSAGE_RECEIVED = "message_received"
    IDENTITY_VERIFIED = "identity_verified"
    INVITE_CREATED = "invite_created"
    INVITE_USED = "invite_used"
    GROUP_EPOCH_ADVANCED = "group_epoch_advanced"


@dataclass
class AuditEntry:
    """A single tamper-evident audit log entry.

    Attributes:
        sequence: Monotonically increasing sequence number.
        event_type: Type of event being recorded.
        timestamp: Unix timestamp (seconds).
        channel: Channel name (if applicable).
        agent_did: Agent DID (if applicable).
        metadata: Additional event-specific data.
        prev_hash: SHA-256 hash of the previous entry (empty for first entry).
        entry_hash: SHA-256 hash of this entry (including prev_hash).
    """

    sequence: int
    event_type: EventType
    timestamp: float
    channel: str = ""
    agent_did: str = ""
    metadata: dict = field(default_factory=dict)
    prev_hash: str = ""
    entry_hash: str = ""


class AuditEvent:
    """Factory methods for creating audit events."""

    @staticmethod
    def channel_created(channel: str, agent_did: str = "") -> dict:
        """Record a channel creation event.

        Args:
            channel: Channel name.
            agent_did: DID of the creating agent.
        """
        return {
            "event_type": EventType.CHANNEL_CREATED,
            "channel": channel,
            "agent_did": agent_did,
        }

    @staticmethod
    def channel_joined(channel: str, agent_did: str = "", method: str = "invite") -> dict:
        """Record a channel join event.

        Args:
            channel: Channel name.
            agent_did: DID of the joining agent.
            method: Join method ("invite", "direct").
        """
        return {
            "event_type": EventType.CHANNEL_JOINED,
            "channel": channel,
            "agent_did": agent_did,
            "metadata": {"method": method},
        }

    @staticmethod
    def message_sent(channel: str, ciphertext_len: int = 0) -> dict:
        """Record a message send event (metadata only -- never plaintext).

        Args:
            channel: Channel name.
            ciphertext_len: Length of the ciphertext in bytes.
        """
        return {
            "event_type": EventType.MESSAGE_SENT,
            "channel": channel,
            "metadata": {"ciphertext_len": ciphertext_len},
        }

    @staticmethod
    def message_received(channel: str, ciphertext_len: int = 0, count: int = 1) -> dict:
        """Record a message receive event.

        Args:
            channel: Channel name.
            ciphertext_len: Length of the ciphertext.
            count: Number of messages received.
        """
        return {
            "event_type": EventType.MESSAGE_RECEIVED,
            "channel": channel,
            "metadata": {"ciphertext_len": ciphertext_len, "count": count},
        }

    @staticmethod
    def member_added(channel: str, member_did: str = "") -> dict:
        """Record a member addition event.

        Args:
            channel: Channel name.
            member_did: DID of the added member.
        """
        return {
            "event_type": EventType.MEMBER_ADDED,
            "channel": channel,
            "agent_did": member_did,
        }

    @staticmethod
    def identity_verified(agent_did: str, method: str = "did:key") -> dict:
        """Record an identity verification event.

        Args:
            agent_did: DID that was verified.
            method: Verification method used.
        """
        return {
            "event_type": EventType.IDENTITY_VERIFIED,
            "agent_did": agent_did,
            "metadata": {"method": method},
        }

    @staticmethod
    def invite_created(channel: str, max_uses: int = 1, ttl: int = 3600) -> dict:
        """Record an invite creation event.

        Args:
            channel: Channel name.
            max_uses: Maximum uses for the invite.
            ttl: Time-to-live in seconds.
        """
        return {
            "event_type": EventType.INVITE_CREATED,
            "channel": channel,
            "metadata": {"max_uses": max_uses, "ttl": ttl},
        }


class AuditLog:
    """Tamper-evident audit log with cryptographic hash chaining.

    Each entry's hash includes the previous entry's hash, forming
    an append-only chain. Any modification to a past entry invalidates
    all subsequent hashes.

    Args:
        agent_did: DID of the agent owning this log.

    Example::

        log = AuditLog(agent_did="did:key:z6Mk...")
        log.record(AuditEvent.channel_created("acme/ns/svc"))
        log.record(AuditEvent.message_sent("acme/ns/svc", ciphertext_len=128))
        assert log.verify_chain()
    """

    def __init__(self, agent_did: str = ""):
        self._entries: list[AuditEntry] = []
        self._agent_did = agent_did

    def record(self, event: dict) -> AuditEntry:
        """Record an audit event.

        Args:
            event: Event dict from :class:`AuditEvent` factory methods.

        Returns:
            The created :class:`AuditEntry` with computed hash.
        """
        prev_hash = self._entries[-1].entry_hash if self._entries else ""
        seq = len(self._entries)

        entry = AuditEntry(
            sequence=seq,
            event_type=event.get("event_type", EventType.CHANNEL_CREATED),
            timestamp=time.time(),
            channel=event.get("channel", ""),
            agent_did=event.get("agent_did") or self._agent_did,
            metadata=event.get("metadata", {}),
            prev_hash=prev_hash,
        )

        entry.entry_hash = _compute_entry_hash(entry)
        self._entries.append(entry)
        return entry

    def verify_chain(self) -> bool:
        """Verify the integrity of the entire audit chain.

        Returns:
            ``True`` if all hashes are valid and the chain is unbroken.
        """
        for i, entry in enumerate(self._entries):
            expected_prev = self._entries[i - 1].entry_hash if i > 0 else ""
            if entry.prev_hash != expected_prev:
                return False
            if entry.entry_hash != _compute_entry_hash(entry):
                return False
        return True

    def export_chain(self) -> list[dict]:
        """Export the audit chain as a list of dicts.

        Returns:
            List of serializable audit entry dicts.
        """
        return [
            {
                "sequence": e.sequence,
                "event_type": e.event_type.value,
                "timestamp": e.timestamp,
                "channel": e.channel,
                "agent_did": e.agent_did,
                "metadata": e.metadata,
                "prev_hash": e.prev_hash,
                "entry_hash": e.entry_hash,
            }
            for e in self._entries
        ]

    def export_json(self) -> str:
        """Export the audit chain as a JSON string.

        Returns:
            JSON string of the audit chain.
        """
        return json.dumps(self.export_chain(), indent=2)

    @property
    def entries(self) -> list[AuditEntry]:
        """Read-only access to audit entries."""
        return list(self._entries)

    def __len__(self) -> int:
        return len(self._entries)


def _compute_entry_hash(entry: AuditEntry) -> str:
    """Compute the SHA-256 hash of an audit entry."""
    canonical = json.dumps(
        {
            "sequence": entry.sequence,
            "event_type": entry.event_type.value,
            "timestamp": entry.timestamp,
            "channel": entry.channel,
            "agent_did": entry.agent_did,
            "metadata": entry.metadata,
            "prev_hash": entry.prev_hash,
        },
        sort_keys=True,
    )
    return hashlib.sha256(canonical.encode()).hexdigest()


def _require_crypto() -> None:
    """Raise an error if the ``cryptography`` package is not installed."""
    if not _HAS_CRYPTO:
        raise ImportError(
            "EncryptedAuditLog requires the 'cryptography' package. "
            "Install it with: pip install cryptography"
        )


class EncryptedAuditLog:
    """Encrypted audit log with server-side storage.

    Wraps :class:`AuditLog` to encrypt entries with an MLS-derived
    AES-256-GCM key before uploading to the Skytale API. The API stores
    opaque ciphertext -- only channel members with the exporter secret
    can decrypt.

    Requires the ``cryptography`` package (``pip install cryptography``).

    Args:
        audit_key: 32-byte AES-256-GCM key (from
            ``MlsEngine.export_audit_key()``).
        channel: Channel name for API storage.
        api_key: Skytale API key for server uploads.
        api_url: API base URL.
        agent_did: DID of the owning agent.

    Example::

        key = engine.export_audit_key(group_id)
        elog = EncryptedAuditLog(
            audit_key=key,
            channel="acme/research/results",
            api_key="sk_live_...",
            api_url="https://api.skytale.sh",
        )
        elog.record(AuditEvent.channel_created("acme/research/results"))
        encrypted = elog.export_encrypted()  # list[bytes] ready for API upload
    """

    def __init__(
        self,
        audit_key: bytes,
        channel: str,
        api_key: str,
        api_url: str = "https://api.skytale.sh",
        agent_did: str = "",
    ) -> None:
        _require_crypto()
        if len(audit_key) != 32:
            raise ValueError("audit_key must be 32 bytes")
        self._key = audit_key
        self._channel = channel
        self._api_key = api_key
        self._api_url = api_url.rstrip("/")
        self._log = AuditLog(agent_did=agent_did)
        self._uploaded_count = 0

    def record(self, event: dict) -> AuditEntry:
        """Record an audit event (same API as :meth:`AuditLog.record`).

        Args:
            event: Event dict from :class:`AuditEvent` factory methods.

        Returns:
            The created :class:`AuditEntry` with computed hash.
        """
        return self._log.record(event)

    def verify_chain(self) -> bool:
        """Verify the local audit chain integrity.

        Returns:
            ``True`` if all hashes are valid and the chain is unbroken.
        """
        return self._log.verify_chain()

    def encrypt_entry(self, entry: AuditEntry) -> bytes:
        """Encrypt a single audit entry with AES-256-GCM.

        Args:
            entry: The audit entry to encrypt.

        Returns:
            Bytes in the format: nonce (12 bytes) || ciphertext || tag (16 bytes).
        """
        plaintext = json.dumps(
            {
                "sequence": entry.sequence,
                "event_type": entry.event_type.value,
                "timestamp": entry.timestamp,
                "channel": entry.channel,
                "agent_did": entry.agent_did,
                "metadata": entry.metadata,
                "prev_hash": entry.prev_hash,
                "entry_hash": entry.entry_hash,
            },
            sort_keys=True,
        ).encode()

        nonce = os.urandom(12)
        aesgcm = AESGCM(self._key)
        ciphertext = aesgcm.encrypt(nonce, plaintext, None)
        return nonce + ciphertext

    @staticmethod
    def decrypt_entry(audit_key: bytes, encrypted: bytes) -> dict:
        """Decrypt an encrypted audit entry.

        Args:
            audit_key: 32-byte AES-256-GCM key.
            encrypted: Bytes in the format:
                nonce (12 bytes) || ciphertext || tag (16 bytes).

        Returns:
            Decrypted audit entry as a dict.

        Raises:
            ImportError: If ``cryptography`` is not installed.
            ValueError: If *audit_key* is not 32 bytes.
        """
        _require_crypto()
        if len(audit_key) != 32:
            raise ValueError("audit_key must be 32 bytes")
        nonce = encrypted[:12]
        ciphertext = encrypted[12:]
        aesgcm = AESGCM(audit_key)
        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
        return json.loads(plaintext)

    def pending_entries(self) -> list[AuditEntry]:
        """Return entries that have not been uploaded yet.

        Returns:
            List of :class:`AuditEntry` objects pending upload.
        """
        return self._log.entries[self._uploaded_count :]

    def export_encrypted(self) -> list[bytes]:
        """Encrypt all pending entries.

        Returns:
            List of encrypted byte strings, one per pending entry.
        """
        return [self.encrypt_entry(e) for e in self.pending_entries()]

    def mark_uploaded(self, count: int) -> None:
        """Mark *count* pending entries as uploaded.

        Call this after successfully sending entries to the API so they
        are no longer returned by :meth:`pending_entries`.

        Args:
            count: Number of entries to mark as uploaded (from the front
                of the pending list).
        """
        self._uploaded_count = min(
            self._uploaded_count + count, len(self._log)
        )

    @property
    def entries(self) -> list[AuditEntry]:
        """Read-only access to all audit entries."""
        return self._log.entries

    def __len__(self) -> int:
        return len(self._log)
