"""
Immutable Audit Trail — Enterprise-grade hash-chained audit logging (Phase 34).

Provides a tamper-evident audit log using SHA-256 hash chains. Each entry
includes the hash of the previous entry, so modifying any past entry breaks
the chain and is detected by ``verify_chain()``.

Supplements ``compliance.py`` (which handles policy enforcement) with:
  - Hash-chained, tamper-evident event logging
  - Rich query API (by action, actor, target, date range)
  - Retention management and log rotation
  - Export to JSONL and CSV
  - Suspicious activity detection

Storage layout:
  state/broker/audit/
    trail.jsonl               — active hash-chained audit log
    archive/YYYY-MM-DD.jsonl  — rotated archive files

Functions are sync (no async) for use from CLI and MCP tool wrappers.
"""

import csv
import hashlib
import io
import json
import uuid
from collections import Counter
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional

from brynq.server import BROKER_DIR, _append_jsonl_sync, _read_jsonl_sync

# ── Paths ──────────────────────────────────────────────────────────────────

AUDIT_DIR = BROKER_DIR / "audit"
TRAIL_PATH = AUDIT_DIR / "trail.jsonl"
ARCHIVE_DIR = AUDIT_DIR / "archive"

for _d in (AUDIT_DIR, ARCHIVE_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# ── Valid Actions ──────────────────────────────────────────────────────────

VALID_ACTIONS = (
    "agent_registered",
    "agent_exiled",
    "task_created",
    "task_completed",
    "task_failed",
    "message_sent",
    "file_claimed",
    "file_released",
    "job_posted",
    "job_hired",
    "payment_made",
    "login",
    "logout",
    "password_reset",
    "config_changed",
    "violation_recorded",
    "pardon_granted",
)

# ── Hash Helpers ───────────────────────────────────────────────────────────


def _compute_hash(prev_hash: str, timestamp: str, action: str, actor_id: str) -> str:
    """Compute the SHA-256 hash for a chain entry."""
    payload = f"{prev_hash}|{timestamp}|{action}|{actor_id}"
    return hashlib.sha256(payload.encode("utf-8")).hexdigest()


def _get_last_hash() -> str:
    """Read the hash of the last entry in the active trail, or '0' * 64 for genesis."""
    entries = _read_jsonl_sync(TRAIL_PATH)
    if not entries:
        return "0" * 64
    return entries[-1].get("hash", "0" * 64)


# ── Core: Hash-Chained Event Logging ──────────────────────────────────────


def log_event(
    action: str,
    actor_id: str,
    actor_name: str,
    details: Optional[dict] = None,
    target: Optional[str] = None,
    target_type: Optional[str] = None,
) -> dict:
    """Append a hash-chained audit event to the trail.

    Args:
        action: One of VALID_ACTIONS describing what happened.
        actor_id: Unique identifier of the agent/user performing the action.
        actor_name: Human-readable name of the actor.
        details: Optional dict with action-specific metadata.
        target: Optional identifier of the entity affected (task ID, file path, etc.).
        target_type: Optional type label for the target (e.g., "task", "file", "agent").

    Returns:
        The full audit entry dict that was appended.

    Raises:
        ValueError: If action is not in VALID_ACTIONS.
    """
    if action not in VALID_ACTIONS:
        raise ValueError(
            f"Invalid action '{action}'. Must be one of: {', '.join(VALID_ACTIONS)}"
        )

    prev_hash = _get_last_hash()
    timestamp = datetime.now(timezone.utc).isoformat()
    entry_id = uuid.uuid4().hex[:16]
    entry_hash = _compute_hash(prev_hash, timestamp, action, actor_id)

    entry = {
        "id": entry_id,
        "timestamp": timestamp,
        "action": action,
        "actor_id": actor_id,
        "actor_name": actor_name,
        "details": details or {},
        "target": target,
        "target_type": target_type,
        "prev_hash": prev_hash,
        "hash": entry_hash,
    }

    _append_jsonl_sync(TRAIL_PATH, entry)
    return entry


# ── Chain Verification ─────────────────────────────────────────────────────


def verify_chain() -> tuple:
    """Verify the integrity of the entire hash chain.

    Reads all entries from the active trail and checks that each entry's
    ``hash`` field matches the expected SHA-256 of
    ``(prev_hash + timestamp + action + actor_id)``.

    Returns:
        Tuple of (valid, broken_at, total) where:
          - valid: True if the entire chain is intact.
          - broken_at: Index of the first broken link, or None if valid.
          - total: Total number of entries checked.
    """
    entries = _read_jsonl_sync(TRAIL_PATH)
    total = len(entries)

    if total == 0:
        return (True, None, 0)

    # Verify the first entry's prev_hash is the genesis hash
    expected_prev = "0" * 64
    for i, entry in enumerate(entries):
        prev_hash = entry.get("prev_hash", "")
        if prev_hash != expected_prev:
            return (False, i, total)

        expected_hash = _compute_hash(
            prev_hash,
            entry.get("timestamp", ""),
            entry.get("action", ""),
            entry.get("actor_id", ""),
        )
        actual_hash = entry.get("hash", "")
        if actual_hash != expected_hash:
            return (False, i, total)

        expected_prev = actual_hash

    return (True, None, total)


# ── Query API ──────────────────────────────────────────────────────────────


def _parse_ts(ts_str: str) -> Optional[datetime]:
    """Parse an ISO timestamp string to a timezone-aware datetime."""
    try:
        ts = datetime.fromisoformat(ts_str)
        if ts.tzinfo is None:
            ts = ts.replace(tzinfo=timezone.utc)
        return ts
    except (ValueError, TypeError):
        return None


def get_events(
    action: Optional[str] = None,
    actor_id: Optional[str] = None,
    target: Optional[str] = None,
    since: Optional[str] = None,
    until: Optional[str] = None,
    limit: int = 100,
) -> list[dict]:
    """Query audit events with flexible filters.

    Args:
        action: Filter by action type.
        actor_id: Filter by actor identifier.
        target: Filter by target identifier.
        since: ISO timestamp — only events at or after this time.
        until: ISO timestamp — only events at or before this time.
        limit: Maximum number of events to return (default 100).

    Returns:
        List of matching events, newest first.
    """
    entries = _read_jsonl_sync(TRAIL_PATH)

    since_dt = _parse_ts(since) if since else None
    until_dt = _parse_ts(until) if until else None

    filtered = []
    for entry in entries:
        if action is not None and entry.get("action") != action:
            continue
        if actor_id is not None and entry.get("actor_id") != actor_id:
            continue
        if target is not None and entry.get("target") != target:
            continue

        if since_dt or until_dt:
            ts = _parse_ts(entry.get("timestamp", ""))
            if ts is None:
                continue
            if since_dt and ts < since_dt:
                continue
            if until_dt and ts > until_dt:
                continue

        filtered.append(entry)

    # Newest first
    filtered.sort(key=lambda e: e.get("timestamp", ""), reverse=True)
    return filtered[:limit]


def get_actor_history(actor_id: str, limit: int = 50) -> list[dict]:
    """Get all audit events performed by a specific actor.

    Args:
        actor_id: The actor's unique identifier.
        limit: Maximum events to return (default 50).

    Returns:
        List of events by this actor, newest first.
    """
    return get_events(actor_id=actor_id, limit=limit)


def get_target_history(target: str, limit: int = 50) -> list[dict]:
    """Get all audit events affecting a specific target.

    Args:
        target: The target identifier (task ID, file path, agent ID, etc.).
        limit: Maximum events to return (default 50).

    Returns:
        List of events targeting this entity, newest first.
    """
    return get_events(target=target, limit=limit)


def count_events(
    action: Optional[str] = None,
    actor_id: Optional[str] = None,
    since: Optional[str] = None,
) -> int:
    """Count audit events matching the given filters.

    Args:
        action: Filter by action type.
        actor_id: Filter by actor identifier.
        since: ISO timestamp — only count events at or after this time.

    Returns:
        Integer count of matching events.
    """
    # Use a high limit to count all
    events = get_events(action=action, actor_id=actor_id, since=since, limit=1_000_000)
    return len(events)


# ── Retention & Export ─────────────────────────────────────────────────────


def export_events(
    format: str = "jsonl",
    since: Optional[str] = None,
    until: Optional[str] = None,
) -> str:
    """Export audit events as a JSONL or CSV string.

    Args:
        format: Output format — "jsonl" or "csv".
        since: ISO timestamp — only events at or after this time.
        until: ISO timestamp — only events at or before this time.

    Returns:
        String representation of the matching events.

    Raises:
        ValueError: If format is not "jsonl" or "csv".
    """
    if format not in ("jsonl", "csv"):
        raise ValueError(f"Unsupported format '{format}'. Use 'jsonl' or 'csv'.")

    events = get_events(since=since, until=until, limit=1_000_000)
    # Re-sort oldest first for export
    events.sort(key=lambda e: e.get("timestamp", ""))

    if format == "jsonl":
        lines = [json.dumps(e, default=str) for e in events]
        return "\n".join(lines)

    # CSV format
    if not events:
        return ""

    fieldnames = [
        "id", "timestamp", "action", "actor_id", "actor_name",
        "target", "target_type", "details", "prev_hash", "hash",
    ]
    buf = io.StringIO()
    writer = csv.DictWriter(buf, fieldnames=fieldnames, extrasaction="ignore")
    writer.writeheader()
    for event in events:
        row = dict(event)
        # Serialize details dict to JSON string for CSV
        if isinstance(row.get("details"), dict):
            row["details"] = json.dumps(row["details"], default=str)
        writer.writerow(row)

    return buf.getvalue()


def rotate_log(keep_days: int = 90) -> dict:
    """Archive old entries and keep only recent ones in the active trail.

    Entries older than ``keep_days`` are moved to a dated archive file.
    The active trail retains only entries within the retention window.

    Args:
        keep_days: Number of days of entries to keep in the active log.

    Returns:
        Dict with keys: archived_count, kept_count, archive_file (or None).
    """
    entries = _read_jsonl_sync(TRAIL_PATH)
    if not entries:
        return {"archived_count": 0, "kept_count": 0, "archive_file": None}

    cutoff = datetime.now(timezone.utc) - timedelta(days=keep_days)

    keep = []
    archive = []
    for entry in entries:
        ts = _parse_ts(entry.get("timestamp", ""))
        if ts is None:
            keep.append(entry)  # Keep unparseable entries (safe default)
            continue
        if ts < cutoff:
            archive.append(entry)
        else:
            keep.append(entry)

    archive_file = None
    if archive:
        # Write archived entries to a dated file
        date_str = datetime.now(timezone.utc).strftime("%Y-%m-%d")
        archive_path = ARCHIVE_DIR / f"{date_str}.jsonl"
        for entry in archive:
            _append_jsonl_sync(archive_path, entry)
        archive_file = str(archive_path)

    # Rewrite the active trail with only kept entries
    if archive:
        # Overwrite trail with kept entries only
        TRAIL_PATH.write_text("", encoding="utf-8")
        for entry in keep:
            _append_jsonl_sync(TRAIL_PATH, entry)

    return {
        "archived_count": len(archive),
        "kept_count": len(keep),
        "archive_file": archive_file,
    }


def get_stats() -> dict:
    """Generate summary statistics for the audit trail.

    Returns:
        Dict with: total_events, date_range (earliest/latest), events_per_action,
        top_actors (list of {actor_id, actor_name, count}).
    """
    entries = _read_jsonl_sync(TRAIL_PATH)

    if not entries:
        return {
            "total_events": 0,
            "date_range": {"earliest": None, "latest": None},
            "events_per_action": {},
            "top_actors": [],
        }

    # Date range
    timestamps = []
    for e in entries:
        ts = _parse_ts(e.get("timestamp", ""))
        if ts:
            timestamps.append(ts)

    earliest = min(timestamps).isoformat() if timestamps else None
    latest = max(timestamps).isoformat() if timestamps else None

    # Events per action
    action_counter: Counter = Counter()
    actor_counter: Counter = Counter()
    actor_names: dict[str, str] = {}

    for e in entries:
        action_counter[e.get("action", "unknown")] += 1
        aid = e.get("actor_id", "unknown")
        actor_counter[aid] += 1
        actor_names[aid] = e.get("actor_name", aid)

    # Top actors (sorted by count descending)
    top_actors = [
        {"actor_id": aid, "actor_name": actor_names.get(aid, aid), "count": count}
        for aid, count in actor_counter.most_common(10)
    ]

    return {
        "total_events": len(entries),
        "date_range": {"earliest": earliest, "latest": latest},
        "events_per_action": dict(action_counter),
        "top_actors": top_actors,
    }


# ── Alert Integration ──────────────────────────────────────────────────────

# Thresholds for anomaly detection
_THRESHOLDS = {
    "task_failed_max": 5,       # Max failed tasks per agent in the window
    "login_max": 10,            # Max login attempts per agent in the window
    "file_claimed_max": 20,     # Max file claims per agent in the window
    "violation_spike_max": 3,   # Max violations per agent in the window
}


def get_suspicious_activity(hours: int = 24) -> list[dict]:
    """Detect anomalous patterns in the recent audit trail.

    Checks for:
      - Agents with too many failed tasks
      - Rapid login attempts (brute force indicator)
      - Mass file claims (resource hoarding)
      - Violation spikes (repeated policy breaches)

    Args:
        hours: How far back to look (default 24 hours).

    Returns:
        List of alert dicts, each with: alert_type, actor_id, actor_name,
        count, threshold, description.
    """
    cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
    entries = _read_jsonl_sync(TRAIL_PATH)

    # Collect recent events per actor per action
    actor_actions: dict[str, Counter] = {}
    actor_names: dict[str, str] = {}

    for entry in entries:
        ts = _parse_ts(entry.get("timestamp", ""))
        if ts is None or ts < cutoff:
            continue

        aid = entry.get("actor_id", "unknown")
        action = entry.get("action", "")
        actor_names[aid] = entry.get("actor_name", aid)

        if aid not in actor_actions:
            actor_actions[aid] = Counter()
        actor_actions[aid][action] += 1

    alerts: list[dict] = []

    checks = [
        ("task_failed", _THRESHOLDS["task_failed_max"], "excessive_failures",
         "Too many failed tasks — possible malfunction or abuse"),
        ("login", _THRESHOLDS["login_max"], "rapid_logins",
         "Excessive login attempts — possible brute force"),
        ("file_claimed", _THRESHOLDS["file_claimed_max"], "mass_file_claims",
         "Mass file claiming — possible resource hoarding"),
        ("violation_recorded", _THRESHOLDS["violation_spike_max"], "violation_spike",
         "Spike in violations — repeated policy breaches"),
    ]

    for action, threshold, alert_type, description in checks:
        for aid, counter in actor_actions.items():
            count = counter.get(action, 0)
            if count >= threshold:
                alerts.append({
                    "alert_type": alert_type,
                    "actor_id": aid,
                    "actor_name": actor_names.get(aid, aid),
                    "count": count,
                    "threshold": threshold,
                    "description": description,
                })

    return alerts
