#!/usr/bin/env python3
from __future__ import annotations

import json
import sqlite3
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterator

from evidence_chain import evidence_sha256, normalize_evidence_chain


class ForensicsHostMismatchError(ValueError):
    pass


class CaseStorage:
    def __init__(self, db_path: Path) -> None:
        self.db_path = db_path
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self._init_db()

    @contextmanager
    def conn(self) -> Iterator[sqlite3.Connection]:
        c = sqlite3.connect(self.db_path)
        c.row_factory = sqlite3.Row
        c.execute("PRAGMA journal_mode=WAL")
        c.execute("PRAGMA foreign_keys=ON")
        try:
            yield c
        finally:
            c.close()

    def _init_db(self) -> None:
        with self.conn() as c:
            c.executescript(
                """
                CREATE TABLE IF NOT EXISTS cases (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  incident_id TEXT NOT NULL,
                  host TEXT,
                  title TEXT NOT NULL,
                  severity TEXT NOT NULL DEFAULT 'medium',
                  assignee TEXT,
                  status TEXT NOT NULL DEFAULT 'open',
                  source_bucket TEXT,
                  source_event_ts TEXT,
                  evidence_json TEXT,
                  forensics_json TEXT,
                  created_at TEXT NOT NULL,
                  updated_at TEXT NOT NULL
                );
                CREATE INDEX IF NOT EXISTS idx_cases_incident_id ON cases(incident_id);
                CREATE INDEX IF NOT EXISTS idx_cases_status ON cases(status);

                CREATE TABLE IF NOT EXISTS case_comments (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  case_id INTEGER NOT NULL,
                  comment TEXT NOT NULL,
                  author TEXT,
                  created_at TEXT NOT NULL,
                  FOREIGN KEY(case_id) REFERENCES cases(id) ON DELETE CASCADE
                );

                CREATE TABLE IF NOT EXISTS case_audit (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  case_id INTEGER NOT NULL,
                  action TEXT NOT NULL,
                  actor TEXT,
                  details_json TEXT,
                  created_at TEXT NOT NULL,
                  FOREIGN KEY(case_id) REFERENCES cases(id) ON DELETE CASCADE
                );
                """
            )
            self._ensure_column(c, "cases", "forensics_json", "TEXT")
            c.commit()

    @staticmethod
    def _ensure_column(c: sqlite3.Connection, table: str, column: str, definition: str) -> None:
        columns = {
            str(row["name"])
            for row in c.execute(f"PRAGMA table_info({table})").fetchall()
        }
        if column not in columns:
            c.execute(f"ALTER TABLE {table} ADD COLUMN {column} {definition}")

    @staticmethod
    def _now() -> str:
        return datetime.now(timezone.utc).isoformat()

    @staticmethod
    def _normalize_host(value: Any) -> str:
        return str(value or "").strip().lower()

    @staticmethod
    def _load_json_field(raw: Any) -> dict[str, Any] | None:
        if not raw:
            return None
        try:
            return json.loads(raw)
        except Exception:
            return None

    @classmethod
    def _to_case_dict(cls, row: sqlite3.Row) -> dict[str, Any]:
        evidence = cls._load_json_field(row["evidence_json"])
        forensics = cls._load_json_field(row["forensics_json"])
        return {
            "id": int(row["id"]),
            "incident_id": row["incident_id"],
            "host": row["host"],
            "title": row["title"],
            "severity": row["severity"],
            "assignee": row["assignee"],
            "status": row["status"],
            "source_bucket": row["source_bucket"],
            "source_event_ts": row["source_event_ts"],
            "evidence": evidence,
            "forensics": forensics,
            "created_at": row["created_at"],
            "updated_at": row["updated_at"],
        }

    def create_case(self, payload: dict[str, Any], actor: str | None = None) -> dict[str, Any]:
        now = self._now()
        normalized_evidence = None
        evidence_digest = None
        if payload.get("evidence") is not None:
            normalized_evidence = normalize_evidence_chain(
                payload=payload.get("evidence"),
                source_bucket=payload.get("source_bucket"),
                source_event_ts=payload.get("source_event_ts"),
            )
            evidence_digest = normalized_evidence.get("latest_sha256") or evidence_sha256(payload.get("evidence"))
        with self.conn() as c:
            existing = c.execute(
                """
                SELECT * FROM cases
                WHERE incident_id = ? AND COALESCE(host, '') = COALESCE(?, '')
                ORDER BY id DESC
                LIMIT 1
                """,
                (payload["incident_id"], payload.get("host")),
            ).fetchone()
            if existing:
                return self._to_case_dict(existing)
            cur = c.execute(
                """
                INSERT INTO cases (
                  incident_id, host, title, severity, assignee, status,
                  source_bucket, source_event_ts, evidence_json, forensics_json, created_at, updated_at
                ) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?)
                """,
                (
                    payload["incident_id"],
                    payload.get("host"),
                    payload["title"],
                    payload.get("severity", "medium"),
                    payload.get("assignee"),
                    payload.get("source_bucket"),
                    payload.get("source_event_ts"),
                    json.dumps(normalized_evidence, ensure_ascii=False) if normalized_evidence is not None else None,
                    None,
                    now,
                    now,
                ),
            )
            case_id = int(cur.lastrowid)
            self._insert_audit(
                c,
                case_id=case_id,
                action="create",
                actor=actor,
                details={
                    "fields": {k: v for k, v in payload.items() if k != "evidence"},
                    "evidence_sha256": evidence_digest,
                },
            )
            c.commit()
            return self.get_case(case_id, c)

    def list_cases(self, status: str | None = None, host: str | None = None, limit: int = 200) -> list[dict[str, Any]]:
        q = "SELECT * FROM cases"
        clauses = []
        args: list[Any] = []
        if status:
            clauses.append("status = ?")
            args.append(status)
        if host:
            clauses.append("host = ?")
            args.append(host)
        if clauses:
            q += " WHERE " + " AND ".join(clauses)
        q += " ORDER BY id DESC LIMIT ?"
        args.append(int(limit))
        with self.conn() as c:
            rows = c.execute(q, args).fetchall()
            return [self._to_case_dict(r) for r in rows]

    def get_case(self, case_id: int, c: sqlite3.Connection | None = None) -> dict[str, Any]:
        own = False
        if c is None:
            own = True
            c = sqlite3.connect(self.db_path)
            c.row_factory = sqlite3.Row
        try:
            row = c.execute("SELECT * FROM cases WHERE id = ?", (int(case_id),)).fetchone()
            if not row:
                raise KeyError(case_id)
            return self._to_case_dict(row)
        finally:
            if own:
                c.close()

    def update_case(self, case_id: int, patch: dict[str, Any], actor: str | None = None) -> dict[str, Any]:
        fields = []
        args: list[Any] = []
        for key in ("status", "assignee", "title", "severity"):
            if key in patch and patch[key] is not None:
                fields.append(f"{key} = ?")
                args.append(patch[key])
        if not fields:
            return self.get_case(case_id)
        fields.append("updated_at = ?")
        args.append(self._now())
        args.append(int(case_id))
        with self.conn() as c:
            c.execute(f"UPDATE cases SET {', '.join(fields)} WHERE id = ?", args)
            self._insert_audit(c, case_id=case_id, action="update", actor=actor, details=patch)
            c.commit()
            return self.get_case(case_id, c)

    def link_hayabusa(self, case_id: int, payload: dict[str, Any], actor: str | None = None) -> dict[str, Any]:
        now = self._now()
        with self.conn() as c:
            existing = self.get_case(case_id, c)
            case_host = self._normalize_host(existing.get("host"))
            forensic_host = self._normalize_host(payload.get("host"))
            if case_host and forensic_host and case_host != forensic_host:
                raise ForensicsHostMismatchError(
                    f"hayabusa host mismatch: case host={existing.get('host')} payload host={payload.get('host')}"
                )
            forensics = existing.get("forensics") or {}
            forensics["hayabusa"] = {
                "tool": "hayabusa",
                "host": payload["host"],
                "mode": payload["mode"],
                "status": payload["status"],
                "intake_id": payload.get("intake_id"),
                "package_path": payload.get("package_path"),
                "sha256": payload.get("sha256"),
                "report_dir": payload.get("report_dir"),
                "summary_html": payload.get("summary_html"),
                "timeline_path": payload.get("timeline_path"),
                "manifest_path": payload.get("manifest_path"),
                "linked_at": payload.get("linked_at") or now,
                "link_source": payload.get("link_source") or "api",
            }
            c.execute(
                "UPDATE cases SET forensics_json = ?, updated_at = ? WHERE id = ?",
                (json.dumps(forensics, ensure_ascii=False), now, int(case_id)),
            )
            self._insert_audit(
                c,
                case_id=case_id,
                action="link_hayabusa",
                actor=actor,
                details={
                    "host": payload["host"],
                    "mode": payload["mode"],
                    "status": payload["status"],
                    "intake_id": payload.get("intake_id"),
                    "report_dir": payload.get("report_dir"),
                },
            )
            c.commit()
            return self.get_case(case_id, c)

    def add_comment(self, case_id: int, comment: str, author: str | None = None) -> dict[str, Any]:
        now = self._now()
        with self.conn() as c:
            cur = c.execute(
                "INSERT INTO case_comments (case_id, comment, author, created_at) VALUES (?, ?, ?, ?)",
                (int(case_id), comment, author, now),
            )
            cid = int(cur.lastrowid)
            self._insert_audit(
                c,
                case_id=case_id,
                action="comment",
                actor=author,
                details={"comment_id": cid},
            )
            c.commit()
            row = c.execute("SELECT id, case_id, comment, author, created_at FROM case_comments WHERE id = ?", (cid,)).fetchone()
            return dict(row)

    def list_comments(self, case_id: int, limit: int = 200) -> list[dict[str, Any]]:
        with self.conn() as c:
            rows = c.execute(
                "SELECT id, case_id, comment, author, created_at FROM case_comments WHERE case_id = ? ORDER BY id DESC LIMIT ?",
                (int(case_id), int(limit)),
            ).fetchall()
            return [dict(r) for r in rows]

    def list_audit(self, case_id: int, limit: int = 200) -> list[dict[str, Any]]:
        with self.conn() as c:
            rows = c.execute(
                "SELECT id, case_id, action, actor, details_json, created_at FROM case_audit WHERE case_id = ? ORDER BY id DESC LIMIT ?",
                (int(case_id), int(limit)),
            ).fetchall()
            out: list[dict[str, Any]] = []
            for r in rows:
                details = None
                if r["details_json"]:
                    try:
                        details = json.loads(r["details_json"])
                    except Exception:
                        details = None
                out.append(
                    {
                        "id": int(r["id"]),
                        "case_id": int(r["case_id"]),
                        "action": r["action"],
                        "actor": r["actor"],
                        "details": details,
                        "created_at": r["created_at"],
                    }
                )
            return out

    def _insert_audit(
        self,
        c: sqlite3.Connection,
        case_id: int,
        action: str,
        actor: str | None,
        details: dict[str, Any] | None = None,
    ) -> None:
        c.execute(
            "INSERT INTO case_audit (case_id, action, actor, details_json, created_at) VALUES (?, ?, ?, ?, ?)",
            (
                int(case_id),
                action,
                actor,
                json.dumps(details, ensure_ascii=False) if details is not None else None,
                self._now(),
            ),
        )
