"""SQLite persistence — case store + append-only ledger. SQLAlchemy Core."""

from __future__ import annotations

from pathlib import Path
from typing import List, Optional

from sqlalchemy import (
    Boolean,
    Column,
    DateTime,
    Engine,
    Index,
    Integer,
    MetaData,
    String,
    Table,
    Text,
    create_engine,
    func,
    insert,
    select,
)
from sqlalchemy.dialects.sqlite import insert as sqlite_insert

from psyc import DATA_DIR, log
from psyc.models import Case
from psyc.result import Err, Ok, Result


DB_PATH = DATA_DIR / "psyc.db"

_metadata = MetaData()

cases = Table(
    "cases", _metadata,
    Column("case_id", String, primary_key=True),
    Column("summary", Text, nullable=False),
    Column("source_type", String, nullable=False),
    Column("source_ref", Text, nullable=False),
    Column("observed_at", String, nullable=False),
    Column("ingested_at", String, nullable=False),
    Column("tlp", String, nullable=False),
    Column("severity", String, nullable=True),
    Column("incident_type", String, nullable=True),
    Column("payload", Text, nullable=False),
)
Index("cases_ingested_idx", cases.c.ingested_at.desc())
Index("cases_severity_idx", cases.c.severity)

ledger = Table(
    "ledger", _metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("timestamp", String, nullable=False),
    Column("case_id", String, nullable=False),
    Column("destination", String, nullable=False),
    Column("payload_hash", String, nullable=False),
    Column("submitter_identity", String, nullable=False),
    Column("tlp", String, nullable=False),
    Column("response_id", String, nullable=True),
    Column("outcome", String, nullable=False),
    Column("detail", Text, nullable=True),
)
Index("ledger_case_idx", ledger.c.case_id)
Index("ledger_time_idx", ledger.c.timestamp.desc())

pending = Table(
    "pending_submissions", _metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("case_id", String, nullable=False),
    Column("destination_name", String, nullable=False),
    Column("payload_kind", String, nullable=False),
    Column("payload_hash", String, nullable=False),
    Column("payload_json", Text, nullable=False),
    Column("tlp", String, nullable=False),
    Column("created_at", String, nullable=False),
    Column("status", String, nullable=False),
    Column("reviewer", String, nullable=True),
    Column("reviewed_at", String, nullable=True),
    Column("reason", String, nullable=True),
)
Index("pending_status_idx", pending.c.status)
Index("pending_case_idx", pending.c.case_id)

response_actions = Table(
    "response_actions", _metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("case_id", String, nullable=False),
    Column("action_type", String, nullable=False),
    Column("target", String, nullable=False),
    Column("summary", Text, nullable=False),
    Column("payload_json", Text, nullable=False),
    Column("severity", String, nullable=True),
    Column("status", String, nullable=False),
    Column("created_at", String, nullable=False),
    Column("approver", String, nullable=True),
    Column("executed_at", String, nullable=True),
    Column("detail", Text, nullable=True),
)
Index("actions_status_idx", response_actions.c.status)
Index("actions_case_idx", response_actions.c.case_id)

iocs = Table(
    "iocs", _metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("value", String, nullable=False),       # normalized indicator
    Column("ioc_type", String, nullable=False),     # url | domain | ip | hash | cve
    Column("case_id", String, nullable=False),
    Column("feed", String, nullable=True),
    Column("severity", String, nullable=True),
    Column("first_seen", String, nullable=True),
)
Index("iocs_value_idx", iocs.c.value)
Index("iocs_type_idx", iocs.c.ioc_type)
Index("iocs_case_idx", iocs.c.case_id)


_log = log.get(__name__)
_engine: Optional[Engine] = None


def engine(db_path: Path = DB_PATH) -> Engine:
    global _engine
    if _engine is None:
        db_path.parent.mkdir(parents=True, exist_ok=True)
        _engine = create_engine(f"sqlite:///{db_path}", future=True)
    return _engine


def init_db(db_path: Path = DB_PATH) -> Path:
    eng = engine(db_path)
    _metadata.create_all(eng, checkfirst=True)
    _log.info("db.ready", path=str(db_path))
    return db_path


def upsert_case(case: Case, db_path: Path = DB_PATH) -> None:
    payload = case.model_dump_json()
    values = dict(
        case_id=case.case_id,
        summary=case.summary,
        source_type=case.source_type,
        source_ref=case.source_ref,
        observed_at=case.observed_at.isoformat(),
        ingested_at=case.ingested_at.isoformat(),
        tlp=case.classification.tlp.value,
        severity=case.classification.severity.value if case.classification.severity else None,
        incident_type=case.classification.incident_type.value if case.classification.incident_type else None,
        payload=payload,
    )
    stmt = sqlite_insert(cases).values(**values)
    stmt = stmt.on_conflict_do_update(
        index_elements=[cases.c.case_id],
        set_=dict(
            summary=stmt.excluded.summary,
            tlp=stmt.excluded.tlp,
            severity=stmt.excluded.severity,
            incident_type=stmt.excluded.incident_type,
            payload=stmt.excluded.payload,
        ),
    )
    with engine(db_path).begin() as conn:
        conn.execute(stmt)


def list_cases(limit: int = 100, db_path: Path = DB_PATH) -> List[Case]:
    stmt = select(cases.c.payload).order_by(cases.c.ingested_at.desc()).limit(limit)
    with engine(db_path).connect() as conn:
        rows = conn.execute(stmt).fetchall()
    return [Case.model_validate_json(r.payload) for r in rows]


def get_case(case_id: str, db_path: Path = DB_PATH) -> Result[Case, str]:
    stmt = select(cases.c.payload).where(cases.c.case_id == case_id)
    with engine(db_path).connect() as conn:
        row = conn.execute(stmt).fetchone()
    if row is None:
        return Err(f"case not found: {case_id}")
    return Ok(Case.model_validate_json(row.payload))


def case_count(db_path: Path = DB_PATH) -> int:
    stmt = select(func.count()).select_from(cases)
    with engine(db_path).connect() as conn:
        return conn.execute(stmt).scalar_one()


# ---------- IOC index ----------------------------------------------------

def replace_iocs(rows: List[dict], db_path: Path = DB_PATH) -> int:
    """Rebuild the IOC index: clear it, then bulk-insert rows. Returns count."""
    with engine(db_path).begin() as conn:
        conn.execute(iocs.delete())
        if rows:
            conn.execute(iocs.insert(), rows)
    return len(rows)


def find_iocs(value: str, db_path: Path = DB_PATH) -> List[dict]:
    """Exact-match lookup of one normalized indicator. Returns matching index rows."""
    stmt = select(iocs).where(iocs.c.value == value).order_by(iocs.c.first_seen.desc())
    with engine(db_path).connect() as conn:
        return [dict(r._mapping) for r in conn.execute(stmt).fetchall()]


def iocs_by_type(ioc_type: str, db_path: Path = DB_PATH) -> List[dict]:
    """All index rows of one type, newest first — caller filters/dedupes."""
    stmt = select(iocs).where(iocs.c.ioc_type == ioc_type).order_by(iocs.c.first_seen.desc())
    with engine(db_path).connect() as conn:
        return [dict(r._mapping) for r in conn.execute(stmt).fetchall()]


def ioc_count(db_path: Path = DB_PATH) -> int:
    stmt = select(func.count()).select_from(iocs)
    with engine(db_path).connect() as conn:
        return conn.execute(stmt).scalar_one()
