"""Audit log for Gremlin Autopilot.

Provides:
- Append-only JSONL audit logging
- Privacy modes (full, summary, minimal)
- PII redaction
- Retention policy enforcement
"""

from __future__ import annotations

import hashlib
import json
import logging
import re
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

from .models import AuditPrivacyMode

logger = logging.getLogger(__name__)

# Default paths
AUDIT_LOG_PATH = Path.home() / ".g_gremlin" / "autopilot" / "audit.jsonl"

# PII patterns for redaction
EMAIL_PATTERN = re.compile(r"\b[A-Za-z0-9._%+-]+@([A-Za-z0-9.-]+\.[A-Z|a-z]{2,})\b")
PHONE_PATTERN = re.compile(r"\b(\+?1?[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b")


def redact_email(email: str) -> str:
    """Redact email, keeping only domain."""
    match = EMAIL_PATTERN.match(email)
    if match:
        return f"***@{match.group(1)}"
    return "***@redacted"


def redact_phone(phone: str) -> str:
    """Redact phone, keeping only last 4 digits."""
    match = PHONE_PATTERN.search(phone)
    if match:
        return f"***-***-{match.group(4)}"
    return "***-***-****"


def redact_value(value: Any) -> Any:
    """Recursively redact PII from a value."""
    if isinstance(value, str):
        # Check for email
        if EMAIL_PATTERN.search(value):
            return EMAIL_PATTERN.sub(lambda m: f"***@{m.group(1)}", value)
        # Check for phone
        if PHONE_PATTERN.search(value):
            return PHONE_PATTERN.sub(lambda m: f"***-***-{m.group(4)}", value)
        return value
    elif isinstance(value, dict):
        return {k: redact_value(v) for k, v in value.items()}
    elif isinstance(value, list):
        return [redact_value(item) for item in value]
    return value


class AuditLogWriter:
    """Writer for autopilot audit logs.

    Supports three privacy modes:
    - full: All data including samples (for debugging, solo use)
    - summary: Row counts, tool names, hashes only (default)
    - minimal: Timestamps + outcome only (enterprise/compliance)
    """

    def __init__(
        self,
        privacy_mode: AuditPrivacyMode = AuditPrivacyMode.SUMMARY,
        log_path: Path | None = None,
        max_file_mb: int = 100,
        max_entries: int = 100000,
    ):
        """Initialize the audit log writer.

        Args:
            privacy_mode: Privacy mode for logging.
            log_path: Path to the audit log file.
            max_file_mb: Maximum file size in MB before rotation.
            max_entries: Maximum entries per file.
        """
        self.privacy_mode = privacy_mode
        self.log_path = log_path or AUDIT_LOG_PATH
        self.max_file_bytes = max_file_mb * 1024 * 1024
        self.max_entries = max_entries
        self._entry_count = 0

        # Ensure directory exists
        self.log_path.parent.mkdir(parents=True, exist_ok=True)

    def log_action(
        self,
        tool: str,
        status: str,
        rows_affected: int = 0,
        args: dict[str, Any] | None = None,
        sample_before: list[dict[str, Any]] | None = None,
        sample_after: list[dict[str, Any]] | None = None,
        duration_ms: int | None = None,
        snapshot_id: str | None = None,
        session_id: str | None = None,
        job_id: str | None = None,
    ) -> None:
        """Log an executed action.

        Args:
            tool: Tool name that was executed.
            status: Execution status (success, error, skipped).
            rows_affected: Number of rows affected.
            args: Tool arguments (may be redacted based on privacy mode).
            sample_before: Sample rows before execution.
            sample_after: Sample rows after execution.
            duration_ms: Execution duration in milliseconds.
            snapshot_id: ID of the pre-execution snapshot.
            session_id: Session identifier.
            job_id: Job/plan identifier.
        """
        entry = self._build_entry(
            event="action_executed",
            tool=tool,
            status=status,
            rows_affected=rows_affected,
            args=args,
            sample_before=sample_before,
            sample_after=sample_after,
            duration_ms=duration_ms,
            snapshot_id=snapshot_id,
            session_id=session_id,
            job_id=job_id,
        )
        self._write(entry)

    def log_llm_context(
        self,
        context_level: str,
        sample_count: int = 0,
        consent_type: str = "persistent",
        llm_provider: str = "anthropic",
        session_id: str | None = None,
    ) -> None:
        """Log when LLM context is shared.

        Args:
            context_level: Level of context shared (schema_only, with_samples).
            sample_count: Number of sample rows shared.
            consent_type: Type of consent (persistent, per_request).
            llm_provider: LLM provider name.
            session_id: Session identifier.
        """
        entry = {
            "event": "llm_context_shared",
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "context_level": context_level,
            "sample_count": sample_count,
            "consent_type": consent_type,
            "llm_provider": llm_provider,
        }
        if session_id:
            entry["session_id"] = session_id

        self._write(entry)

    def log_session_start(
        self,
        session_id: str,
        client_ip: str | None = None,
        remote_access: bool = False,
    ) -> None:
        """Log session start."""
        entry = {
            "event": "session_start",
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "session_id": session_id,
            "remote_access": remote_access,
        }
        if client_ip and self.privacy_mode == AuditPrivacyMode.FULL:
            entry["client_ip"] = client_ip

        self._write(entry)

    def log_pairing(
        self,
        client_ip: str,
        success: bool,
        session_id: str | None = None,
    ) -> None:
        """Log pairing attempt."""
        entry = {
            "event": "pairing_attempt",
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "success": success,
        }
        if self.privacy_mode == AuditPrivacyMode.FULL:
            entry["client_ip"] = client_ip
        if session_id:
            entry["session_id"] = session_id

        self._write(entry)

    def _build_entry(
        self,
        event: str,
        tool: str,
        status: str,
        rows_affected: int = 0,
        args: dict[str, Any] | None = None,
        sample_before: list[dict[str, Any]] | None = None,
        sample_after: list[dict[str, Any]] | None = None,
        duration_ms: int | None = None,
        snapshot_id: str | None = None,
        session_id: str | None = None,
        job_id: str | None = None,
    ) -> dict[str, Any]:
        """Build a log entry based on privacy mode."""
        timestamp = datetime.now(timezone.utc).isoformat()

        # Base entry (all modes)
        entry: dict[str, Any] = {
            "event": event,
            "timestamp": timestamp,
            "tool": tool,
            "status": status,
        }

        if self.privacy_mode == AuditPrivacyMode.MINIMAL:
            # Minimal: just timestamp + outcome
            return entry

        # Summary and full modes include more data
        entry["rows_affected"] = rows_affected

        if duration_ms is not None:
            entry["duration_ms"] = duration_ms
        if snapshot_id:
            entry["snapshot_id"] = snapshot_id
        if session_id:
            entry["session_id"] = session_id
        if job_id:
            entry["job_id"] = job_id

        if self.privacy_mode == AuditPrivacyMode.SUMMARY:
            # Summary: include args hash but not actual values
            if args:
                entry["args_hash"] = hashlib.sha256(
                    json.dumps(args, sort_keys=True).encode()
                ).hexdigest()[:8]
        else:
            # Full mode: include args and samples with PII redaction
            if args:
                entry["args"] = redact_value(args)
            if sample_before:
                entry["sample_before"] = [redact_value(row) for row in sample_before[:5]]
            if sample_after:
                entry["sample_after"] = [redact_value(row) for row in sample_after[:5]]

        return entry

    def _write(self, entry: dict[str, Any]) -> None:
        """Write an entry to the audit log."""
        try:
            # Check if rotation needed
            self._maybe_rotate()

            with open(self.log_path, "a", encoding="utf-8") as f:
                f.write(json.dumps(entry) + "\n")
            self._entry_count += 1

        except Exception as e:
            logger.error(f"Failed to write audit log: {e}")

    def _maybe_rotate(self) -> None:
        """Rotate log file if it exceeds size or entry limits."""
        if not self.log_path.exists():
            return

        stats = self.log_path.stat()

        if stats.st_size >= self.max_file_bytes or self._entry_count >= self.max_entries:
            self._rotate()

    def _rotate(self) -> None:
        """Rotate the log file."""
        if not self.log_path.exists():
            return

        # Rotate existing backups
        for i in range(2, -1, -1):
            old_path = self.log_path.with_suffix(f".jsonl.{i}") if i > 0 else self.log_path
            new_path = self.log_path.with_suffix(f".jsonl.{i + 1}")

            if old_path.exists():
                if i == 2:
                    # Delete oldest backup
                    old_path.unlink()
                else:
                    old_path.rename(new_path)

        self._entry_count = 0
        logger.info(f"Rotated audit log: {self.log_path}")


def cleanup_audit_logs(
    log_path: Path | None = None,
    retention_days: int = 90,
) -> int:
    """Remove audit log entries older than retention period.

    Args:
        log_path: Path to the audit log file.
        retention_days: Number of days to retain entries.

    Returns:
        Number of entries removed.
    """
    path = log_path or AUDIT_LOG_PATH
    if not path.exists():
        return 0

    temp_path = path.with_suffix(".jsonl.tmp")
    cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)

    removed = 0
    kept = 0

    try:
        with open(path, "r", encoding="utf-8") as src:
            with open(temp_path, "w", encoding="utf-8") as dst:
                for line in src:
                    try:
                        entry = json.loads(line)
                        entry_time = datetime.fromisoformat(entry["timestamp"].replace("Z", "+00:00"))

                        if entry_time >= cutoff:
                            dst.write(line)
                            kept += 1
                        else:
                            removed += 1
                    except (json.JSONDecodeError, KeyError, ValueError):
                        # Keep malformed entries
                        dst.write(line)
                        kept += 1

        temp_path.replace(path)
        logger.info(
            f"Audit log cleanup: removed {removed} entries, kept {kept} "
            f"(retention: {retention_days} days)"
        )

    except Exception as e:
        logger.error(f"Failed to cleanup audit log: {e}")
        if temp_path.exists():
            temp_path.unlink()
        raise

    return removed


def get_audit_stats(log_path: Path | None = None) -> dict[str, Any]:
    """Get statistics about the audit log.

    Args:
        log_path: Path to the audit log file.

    Returns:
        Dictionary with stats (entry_count, size_bytes, oldest, newest).
    """
    path = log_path or AUDIT_LOG_PATH
    if not path.exists():
        return {
            "entry_count": 0,
            "size_bytes": 0,
            "size_mb": 0.0,
            "oldest": None,
            "newest": None,
        }

    stats = path.stat()
    entry_count = 0
    oldest = None
    newest = None

    with open(path, "r", encoding="utf-8") as f:
        for line in f:
            entry_count += 1
            try:
                entry = json.loads(line)
                timestamp = entry.get("timestamp")
                if timestamp:
                    if oldest is None or timestamp < oldest:
                        oldest = timestamp
                    if newest is None or timestamp > newest:
                        newest = timestamp
            except json.JSONDecodeError:
                pass

    return {
        "entry_count": entry_count,
        "size_bytes": stats.st_size,
        "size_mb": round(stats.st_size / (1024 * 1024), 2),
        "oldest": oldest,
        "newest": newest,
    }
