"""MCP Audit Trail — Structured audit logging for scans and actions.

Records every scan, fix, guard action, and configuration change with
timestamp, actor, target, and result. Supports querying, filtering,
export to JSON Lines, CSV, and HTML timeline.

Zero external dependencies — pure Python stdlib.
"""

from __future__ import annotations

import csv
import io
import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any


# ---------- Data Models ----------

@dataclass
class AuditEntry:
    """A single audit trail entry."""

    timestamp: str  # ISO 8601
    action: str  # scan, fix, guard, harden, drift, etc.
    target: str  # file or config path
    result: str  # pass, fail, error, skipped
    user: str = ""  # who performed the action
    details: dict[str, Any] = field(default_factory=dict)
    entry_id: str = ""  # unique ID

    def __post_init__(self) -> None:
        if not self.entry_id:
            self.entry_id = f"{self.action}-{int(time.time() * 1000)}"

    def to_dict(self) -> dict[str, Any]:
        result: dict[str, Any] = {
            "entry_id": self.entry_id,
            "timestamp": self.timestamp,
            "action": self.action,
            "target": self.target,
            "result": self.result,
        }
        if self.user:
            result["user"] = self.user
        if self.details:
            result["details"] = self.details
        return result

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> AuditEntry:
        """Create an AuditEntry from a dictionary."""
        return cls(
            entry_id=data.get("entry_id", ""),
            timestamp=data.get("timestamp", ""),
            action=data.get("action", ""),
            target=data.get("target", ""),
            result=data.get("result", ""),
            user=data.get("user", ""),
            details=data.get("details", {}),
        )


@dataclass
class AuditLog:
    """Collection of audit entries with query and filter capabilities."""

    entries: list[AuditEntry] = field(default_factory=list)
    log_path: str = ""

    @property
    def total_entries(self) -> int:
        return len(self.entries)

    def append(self, entry: AuditEntry) -> None:
        """Add an entry to the log."""
        self.entries.append(entry)

    def query(
        self,
        *,
        action: str | None = None,
        target: str | None = None,
        result: str | None = None,
        user: str | None = None,
        since: str | None = None,
        until: str | None = None,
        limit: int = 0,
    ) -> list[AuditEntry]:
        """Query audit entries with filters.

        Args:
            action: Filter by action type.
            target: Filter by target (substring match).
            result: Filter by result.
            user: Filter by user.
            since: Filter entries on or after this ISO timestamp.
            until: Filter entries on or before this ISO timestamp.
            limit: Max entries to return (0 = unlimited).

        Returns:
            List of matching AuditEntry objects.
        """
        results: list[AuditEntry] = []

        for entry in self.entries:
            if action and entry.action != action:
                continue
            if target and target not in entry.target:
                continue
            if result and entry.result != result:
                continue
            if user and entry.user != user:
                continue
            if since and entry.timestamp < since:
                continue
            if until and entry.timestamp > until:
                continue
            results.append(entry)

        if limit > 0:
            results = results[-limit:]

        return results

    def filter_by_date(self, since: str, until: str = "") -> list[AuditEntry]:
        """Filter entries by date range.

        Args:
            since: Start date (ISO format).
            until: End date (ISO format, empty = no upper bound).

        Returns:
            Filtered entries.
        """
        return self.query(since=since, until=until or None)

    def filter_by_action(self, action: str) -> list[AuditEntry]:
        """Filter entries by action type.

        Args:
            action: Action string (scan, fix, guard, etc.).

        Returns:
            Matching entries.
        """
        return self.query(action=action)

    def filter_by_target(self, target: str) -> list[AuditEntry]:
        """Filter entries by target (substring match).

        Args:
            target: Target path or substring.

        Returns:
            Matching entries.
        """
        return self.query(target=target)

    def get_actions(self) -> list[str]:
        """Get unique action types in the log."""
        return sorted({e.action for e in self.entries})

    def get_targets(self) -> list[str]:
        """Get unique targets in the log."""
        return sorted({e.target for e in self.entries})

    def get_users(self) -> list[str]:
        """Get unique users in the log."""
        return sorted({e.user for e in self.entries if e.user})

    def count_by_action(self) -> dict[str, int]:
        """Count entries grouped by action."""
        counts: dict[str, int] = {}
        for entry in self.entries:
            counts[entry.action] = counts.get(entry.action, 0) + 1
        return counts

    def count_by_result(self) -> dict[str, int]:
        """Count entries grouped by result."""
        counts: dict[str, int] = {}
        for entry in self.entries:
            counts[entry.result] = counts.get(entry.result, 0) + 1
        return counts

    def prune(self, days: int) -> int:
        """Remove entries older than N days.

        Args:
            days: Maximum age in days.

        Returns:
            Number of entries removed.
        """
        cutoff = time.time() - (days * 86400)
        cutoff_str = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(cutoff))

        original_count = len(self.entries)
        self.entries = [e for e in self.entries if e.timestamp >= cutoff_str]
        return original_count - len(self.entries)

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_entries": self.total_entries,
            "log_path": self.log_path,
            "entries": [e.to_dict() for e in self.entries],
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> AuditLog:
        """Create an AuditLog from a dictionary."""
        log = cls(log_path=data.get("log_path", ""))
        for entry_data in data.get("entries", []):
            log.entries.append(AuditEntry.from_dict(entry_data))
        return log


# ---------- Entry Creation Helpers ----------

def create_entry(
    action: str,
    target: str,
    result: str,
    *,
    user: str = "",
    details: dict[str, Any] | None = None,
) -> AuditEntry:
    """Create a new audit entry with current timestamp.

    Args:
        action: Action performed (scan, fix, guard, harden, etc.).
        target: Target of the action.
        result: Result (pass, fail, error, skipped).
        user: Optional user/actor.
        details: Optional extra details.

    Returns:
        AuditEntry with current timestamp.
    """
    return AuditEntry(
        timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        action=action,
        target=target,
        result=result,
        user=user,
        details=details or {},
    )


def create_scan_entry(
    target: str,
    findings_count: int,
    grade: str,
    *,
    user: str = "",
) -> AuditEntry:
    """Create a scan audit entry.

    Args:
        target: Scanned path.
        findings_count: Number of findings.
        grade: Security grade.
        user: Optional user.

    Returns:
        AuditEntry for a scan action.
    """
    result = "pass" if findings_count == 0 else "fail"
    return create_entry(
        action="scan",
        target=target,
        result=result,
        user=user,
        details={"findings_count": findings_count, "grade": grade},
    )


def create_fix_entry(
    target: str,
    fixed_count: int,
    total_count: int,
    *,
    user: str = "",
) -> AuditEntry:
    """Create a fix audit entry.

    Args:
        target: Fixed file path.
        fixed_count: Number of fixes applied.
        total_count: Total fixable issues.
        user: Optional user.

    Returns:
        AuditEntry for a fix action.
    """
    result = "pass" if fixed_count == total_count else "partial"
    return create_entry(
        action="fix",
        target=target,
        result=result,
        user=user,
        details={"fixed": fixed_count, "total": total_count},
    )


def create_guard_entry(
    target: str,
    issues_count: int,
    *,
    user: str = "",
) -> AuditEntry:
    """Create a guard audit entry.

    Args:
        target: Config file path.
        issues_count: Number of guard issues found.
        user: Optional user.

    Returns:
        AuditEntry for a guard action.
    """
    result = "pass" if issues_count == 0 else "fail"
    return create_entry(
        action="guard",
        target=target,
        result=result,
        user=user,
        details={"issues_count": issues_count},
    )


# ---------- Persistence ----------

def save_log(log: AuditLog, path: Path | str) -> None:
    """Save audit log to a JSON Lines file.

    Args:
        log: AuditLog to save.
        path: File path.
    """
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    with open(p, "w", encoding="utf-8") as f:
        for entry in log.entries:
            f.write(json.dumps(entry.to_dict(), ensure_ascii=False) + "\n")


def load_log(path: Path | str) -> AuditLog:
    """Load audit log from a JSON Lines file.

    Args:
        path: File path.

    Returns:
        AuditLog with loaded entries.
    """
    p = Path(path)
    log = AuditLog(log_path=str(p))

    if not p.exists():
        return log

    with open(p, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                data = json.loads(line)
                log.entries.append(AuditEntry.from_dict(data))

    return log


def append_to_log(entry: AuditEntry, path: Path | str) -> None:
    """Append a single entry to a JSON Lines audit log file.

    Args:
        entry: Entry to append.
        path: File path.
    """
    p = Path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    with open(p, "a", encoding="utf-8") as f:
        f.write(json.dumps(entry.to_dict(), ensure_ascii=False) + "\n")


# ---------- Export Formats ----------

def export_jsonl(log: AuditLog) -> str:
    """Export audit log as JSON Lines.

    Args:
        log: AuditLog to export.

    Returns:
        JSON Lines string.
    """
    lines: list[str] = []
    for entry in log.entries:
        lines.append(json.dumps(entry.to_dict(), ensure_ascii=False))
    return "\n".join(lines)


def export_csv(log: AuditLog) -> str:
    """Export audit log as CSV.

    Args:
        log: AuditLog to export.

    Returns:
        CSV string.
    """
    output = io.StringIO()
    writer = csv.writer(output)
    writer.writerow(["entry_id", "timestamp", "action", "target", "result", "user", "details"])

    for entry in log.entries:
        details_str = json.dumps(entry.details) if entry.details else ""
        writer.writerow([
            entry.entry_id,
            entry.timestamp,
            entry.action,
            entry.target,
            entry.result,
            entry.user,
            details_str,
        ])

    return output.getvalue()


def export_html(log: AuditLog) -> str:
    """Export audit log as an HTML timeline.

    Args:
        log: AuditLog to export.

    Returns:
        HTML string.
    """
    rows: list[str] = []
    for entry in log.entries:
        color = {
            "pass": "#2ecc71",
            "fail": "#e74c3c",
            "error": "#e67e22",
            "partial": "#f1c40f",
            "skipped": "#95a5a6",
        }.get(entry.result, "#bdc3c7")

        details_html = ""
        if entry.details:
            details_html = "<br>".join(f"{k}: {v}" for k, v in entry.details.items())

        rows.append(f"""
        <tr>
            <td>{entry.timestamp}</td>
            <td><strong>{entry.action}</strong></td>
            <td>{entry.target}</td>
            <td style="color:{color};font-weight:bold">{entry.result}</td>
            <td>{entry.user}</td>
            <td>{details_html}</td>
        </tr>""")

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MCP Audit Trail</title>
    <style>
        body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2em; background: #f8f9fa; }}
        h1 {{ color: #2c3e50; }}
        table {{ border-collapse: collapse; width: 100%; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        th {{ background: #2c3e50; color: white; padding: 12px; text-align: left; }}
        td {{ padding: 10px 12px; border-bottom: 1px solid #ecf0f1; }}
        tr:hover {{ background: #f5f5f5; }}
        .summary {{ margin: 1em 0; padding: 1em; background: white; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
    </style>
</head>
<body>
    <h1>MCP Audit Trail</h1>
    <div class="summary">
        <strong>Total Entries:</strong> {log.total_entries} |
        <strong>Actions:</strong> {', '.join(log.get_actions())} |
        <strong>Results:</strong> {', '.join(f'{k}: {v}' for k, v in log.count_by_result().items())}
    </div>
    <table>
        <thead>
            <tr>
                <th>Timestamp</th>
                <th>Action</th>
                <th>Target</th>
                <th>Result</th>
                <th>User</th>
                <th>Details</th>
            </tr>
        </thead>
        <tbody>
            {''.join(rows)}
        </tbody>
    </table>
</body>
</html>"""


# ---------- Text Formatters ----------

def format_audit_text(log: AuditLog) -> str:
    """Format audit log as human-readable text.

    Args:
        log: AuditLog to format.

    Returns:
        Formatted text.
    """
    lines: list[str] = []
    lines.append("")
    lines.append("MCP Audit Trail")
    lines.append("=" * 50)
    lines.append(f"Total entries: {log.total_entries}")
    lines.append("")

    if not log.entries:
        lines.append("No audit entries recorded.")
        return "\n".join(lines)

    for entry in log.entries:
        result_marker = {
            "pass": "[OK]",
            "fail": "[FAIL]",
            "error": "[ERR]",
            "partial": "[PART]",
            "skipped": "[SKIP]",
        }.get(entry.result, f"[{entry.result.upper()}]")

        user_str = f" by {entry.user}" if entry.user else ""
        lines.append(f"  {entry.timestamp} {result_marker} {entry.action}: {entry.target}{user_str}")

        if entry.details:
            for k, v in entry.details.items():
                lines.append(f"    {k}: {v}")

    lines.append("")
    return "\n".join(lines)


def format_audit_json(log: AuditLog) -> str:
    """Format audit log as JSON.

    Args:
        log: AuditLog to format.

    Returns:
        JSON string.
    """
    return json.dumps(log.to_dict(), indent=2, ensure_ascii=False)
