"""SQLite cache layer for persisting MRs, pipelines, vulnerabilities, events, and UI state."""

import json
import logging
import sqlite3
import threading
from datetime import datetime
from importlib.resources import files as pkg_files
from pathlib import Path

import aiosqlite

from .models import MR, Issue, Pipeline, Vulnerability, Event, EventType

log = logging.getLogger(__name__)

_MR_COLS = ("id, iid, title, author, state, source_branch, target_branch, "
            "web_url, user_notes_count, approved, created_at, updated_at, "
            "repo_name, sha, assignees, reviewers")
_ISSUE_COLS = ("id, iid, title, author, state, web_url, user_notes_count, "
               "created_at, updated_at, repo_name, labels, assignees, participants, confidential")
_VULN_COLS = ("id, name, severity, state, source, web_url, repo_name, "
              "mitigation_state, mitigation_details, location, solution, branch_status, "
              "report_stale, report_stale_since")
_PIPELINE_COLS = "id, status, ref, web_url, created_at, repo_name"
_EVENT_COLS = "id, type, repo_name, summary, detail, created_at, seen"


class CacheMigrationRequired(Exception):
    """Raised when the DB needs a destructive migration and --drop-current-cache was not passed."""


def _discover_migrations() -> list[tuple[str, str]]:
    """Return sorted (name, sql) pairs from the migrations/ package."""
    pkg = pkg_files("project_hub.core.migrations")
    found = []
    for item in pkg.iterdir():
        if item.name.endswith(".sql"):
            found.append((item.name, item.read_text(encoding="utf-8")))
    found.sort(key=lambda x: x[0])
    return found


def _row_to_mr(row) -> MR:
    """Convert a sqlite Row to an MR dataclass."""
    return MR(
        id=row["id"],
        iid=row["iid"],
        title=row["title"],
        author=row["author"],
        state=row["state"],
        source_branch=row["source_branch"],
        target_branch=row["target_branch"],
        web_url=row["web_url"],
        user_notes_count=row["user_notes_count"],
        approved=bool(row["approved"]),
        created_at=row["created_at"],
        updated_at=row["updated_at"],
        repo_name=row["repo_name"],
        sha=row["sha"],
        assignees=json.loads(row["assignees"] or "[]"),
        reviewers=json.loads(row["reviewers"] or "[]"),
    )


def _row_to_pipeline(row) -> Pipeline:
    """Convert a sqlite Row to a Pipeline dataclass."""
    return Pipeline(
        id=row["id"],
        status=row["status"],
        ref=row["ref"],
        web_url=row["web_url"],
        created_at=row["created_at"],
        repo_name=row["repo_name"],
    )


def _row_to_vuln(row) -> Vulnerability:
    """Convert a sqlite Row to a Vulnerability dataclass."""
    return Vulnerability(
        id=row["id"],
        name=row["name"],
        severity=row["severity"],
        state=row["state"],
        source=row["source"],
        web_url=row["web_url"],
        repo_name=row["repo_name"],
        mitigation_state=row["mitigation_state"],
        mitigation_details=row["mitigation_details"],
        location=row["location"],
        solution=row["solution"],
        branch_status=row["branch_status"],
        report_stale=bool(row["report_stale"] or 0),
        report_stale_since=row["report_stale_since"],
    )


def _row_to_issue(row) -> Issue:
    """Convert a sqlite Row to an Issue dataclass."""
    return Issue(
        id=row["id"],
        iid=row["iid"],
        title=row["title"],
        author=row["author"],
        state=row["state"],
        web_url=row["web_url"],
        user_notes_count=row["user_notes_count"],
        created_at=row["created_at"],
        updated_at=row["updated_at"],
        repo_name=row["repo_name"],
        labels=json.loads(row["labels"] or "[]"),
        assignees=json.loads(row["assignees"] or "[]"),
        participants=json.loads(row["participants"] or "[]"),
        confidential=bool(row["confidential"] or 0),
    )


def _row_to_event(row) -> Event:
    """Convert a sqlite Row to an Event dataclass."""
    return Event(
        id=row["id"],
        type=EventType(row["type"]),
        repo_name=row["repo_name"],
        summary=row["summary"],
        detail=row["detail"],
        created_at=datetime.fromisoformat(row["created_at"]),
        seen=bool(row["seen"]),
    )



class Cache:
    """Async/sync SQLite cache for workspace data (WAL mode, aiosqlite)."""

    def __init__(self, db_path: Path):
        self._db_path = db_path
        self._db: aiosqlite.Connection | None = None
        self._sync_conn: sqlite3.Connection | None = None
        self._sync_lock = threading.Lock()

    @staticmethod
    def _check_existing_db(db_path: Path, drop_cache: bool) -> None:
        """Check if existing DB needs rebuild; drop if needed. Raises CacheMigrationRequired."""
        if not db_path.exists():
            return
        import sqlite3
        conn = sqlite3.connect(str(db_path), timeout=10)
        try:
            tables = [r[0] for r in conn.execute(
                "SELECT name FROM sqlite_master WHERE type='table'"
            ).fetchall()]
        finally:
            conn.close()

        needs_rebuild = bool(tables) and "migrations_applied" not in tables
        if needs_rebuild and not drop_cache:
            raise CacheMigrationRequired(
                "Cache database uses the old schema system and must be rebuilt. "
                "Run with --drop-current-cache to allow this (data will be refreshed automatically)."
            )
        if drop_cache or needs_rebuild:
            log.info("Dropping cache: %s", db_path)
            db_path.unlink()

    async def _run_pending_migrations(self) -> None:
        """Discover and apply any unapplied SQL migrations."""
        all_migrations = _discover_migrations()
        async with self._db.execute("SELECT name FROM migrations_applied") as cur:
            applied = {row["name"] for row in await cur.fetchall()}

        for name, sql in all_migrations:
            if name in applied:
                continue
            log.info("Applying migration: %s", name)
            await self._db.execute("BEGIN IMMEDIATE")
            for statement in sql.split(";"):
                statement = statement.strip()
                if statement:
                    await self._db.execute(statement)
            await self._db.execute(
                "INSERT INTO migrations_applied (name) VALUES (?)", (name,)
            )
            await self._db.commit()

    async def initialize(self, drop_cache: bool = False) -> None:
        """Open the database, run pending migrations, and enable WAL mode."""
        db_path = Path(self._db_path)
        self._check_existing_db(db_path, drop_cache)

        if not db_path.exists():
            import os
            fd = os.open(str(db_path), os.O_CREAT | os.O_WRONLY, 0o600)
            os.close(fd)

        if self._db is None:
            self._db = await aiosqlite.connect(self._db_path)
            self._db.row_factory = aiosqlite.Row

        await self._db.execute("PRAGMA journal_mode=WAL")
        await self._db.execute(
            "CREATE TABLE IF NOT EXISTS migrations_applied (name TEXT PRIMARY KEY)"
        )
        await self._db.commit()
        await self._run_pending_migrations()

    async def checkpoint(self) -> None:
        """Flush WAL to main database file."""
        if self._db is not None:
            await self._db.execute("PRAGMA wal_checkpoint(TRUNCATE)")

    async def close(self) -> None:
        """Checkpoint WAL and close all database connections."""
        if self._sync_conn is not None:
            self._sync_conn.close()
            self._sync_conn = None
        if self._db is not None:
            await self.checkpoint()
            await self._db.close()
            self._db = None

    def _ensure_sync_conn(self) -> sqlite3.Connection:
        """Return a persistent read-only sync connection (lazy init)."""
        if self._sync_conn is None:
            self._sync_conn = sqlite3.connect(
                str(self._db_path), timeout=10, check_same_thread=False
            )
            self._sync_conn.row_factory = sqlite3.Row
        return self._sync_conn

    def _sync_fetchall(self, query: str, params=()) -> list:
        """Thread-safe fetchall on the persistent sync connection."""
        with self._sync_lock:
            return self._ensure_sync_conn().execute(query, params).fetchall()

    def _sync_fetchone(self, query: str, params=()):
        """Thread-safe fetchone on the persistent sync connection."""
        with self._sync_lock:
            return self._ensure_sync_conn().execute(query, params).fetchone()

    # --- MRs ---

    async def store_mrs(self, repo_name: str, mrs: list[MR]) -> None:
        """Replace all cached MRs for a repo."""
        await self._db.execute("BEGIN IMMEDIATE")
        async with self._db.execute(
            "DELETE FROM merge_requests WHERE repo_name = ?", (repo_name,)
        ):
            pass
        await self._db.executemany(
            """INSERT INTO merge_requests
               (id, iid, title, author, state, source_branch, target_branch,
                web_url, user_notes_count, approved, created_at, updated_at, repo_name, sha,
                assignees, reviewers)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            [
                (
                    mr.id,
                    mr.iid,
                    mr.title,
                    mr.author,
                    mr.state,
                    mr.source_branch,
                    mr.target_branch,
                    mr.web_url,
                    mr.user_notes_count,
                    int(mr.approved),
                    mr.created_at,
                    mr.updated_at,
                    mr.repo_name,
                    mr.sha,
                    json.dumps(mr.assignees or []),
                    json.dumps(mr.reviewers or []),
                )
                for mr in mrs
            ],
        )
        await self._db.commit()

    async def get_mrs(self, repo_name: str | None = None) -> list[MR]:
        """Return cached MRs, optionally filtered by repo name."""
        if repo_name is not None:
            query = f"SELECT {_MR_COLS} FROM merge_requests WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = f"SELECT {_MR_COLS} FROM merge_requests"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        return [_row_to_mr(row) for row in rows]

    # --- Issues ---

    async def store_issues(self, repo_name: str, issues: list[Issue]) -> None:
        """Replace all cached issues for a repo."""
        await self._db.execute("BEGIN IMMEDIATE")
        async with self._db.execute(
            "DELETE FROM issues WHERE repo_name = ?", (repo_name,)
        ):
            pass
        await self._db.executemany(
            """INSERT INTO issues
               (id, iid, title, author, state, web_url,
                user_notes_count, created_at, updated_at, repo_name,
                labels, assignees, participants, confidential)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            [
                (
                    i.id, i.iid, i.title, i.author, i.state, i.web_url,
                    i.user_notes_count, i.created_at, i.updated_at, i.repo_name,
                    json.dumps(i.labels or []),
                    json.dumps(i.assignees or []),
                    json.dumps(i.participants or []),
                    int(i.confidential),
                )
                for i in issues
            ],
        )
        await self._db.commit()

    async def get_issues(self, repo_name: str | None = None) -> list[Issue]:
        """Return cached issues, optionally filtered by repo name."""
        if repo_name is not None:
            query = f"SELECT {_ISSUE_COLS} FROM issues WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = f"SELECT {_ISSUE_COLS} FROM issues"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        return [_row_to_issue(row) for row in rows]

    # --- Pipelines ---

    async def store_pipelines(self, repo_name: str, pipelines: list[Pipeline]) -> None:
        """Upsert pipelines for a repo and prune stale terminal ones."""
        new_ids = {p.id for p in pipelines}
        # Upsert pipelines from API
        await self._db.executemany(
            """INSERT INTO pipelines (id, status, ref, web_url, created_at, repo_name)
               VALUES (?, ?, ?, ?, ?, ?)
               ON CONFLICT(id, repo_name) DO UPDATE SET status=excluded.status""",
            [
                (p.id, p.status, p.ref, p.web_url, p.created_at, p.repo_name)
                for p in pipelines
            ],
        )
        # Prune pipelines no longer returned by API — only terminal ones to avoid
        # deleting legitimately running pipelines during transient API gaps
        if new_ids:
            placeholders = ",".join("?" for _ in new_ids)
            await self._db.execute(
                f"""DELETE FROM pipelines
                    WHERE repo_name = ?
                      AND status IN ('success', 'failed', 'canceled', 'skipped')
                      AND id NOT IN ({placeholders})""",
                (repo_name, *new_ids),
            )
        else:
            # API returned zero pipelines — prune only terminal entries
            await self._db.execute(
                """DELETE FROM pipelines WHERE repo_name = ?
                   AND status IN ('success', 'failed', 'canceled', 'skipped')""",
                (repo_name,),
            )
        await self._db.commit()

    async def get_pipelines(self, repo_name: str | None = None) -> list[Pipeline]:
        """Return cached pipelines, optionally filtered by repo name."""
        if repo_name is not None:
            query = f"SELECT {_PIPELINE_COLS} FROM pipelines WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = f"SELECT {_PIPELINE_COLS} FROM pipelines"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        return [_row_to_pipeline(row) for row in rows]

    # --- Vulnerabilities ---

    async def store_vulns(self, repo_name: str, vulns: list[Vulnerability]) -> None:
        """Replace all cached vulnerabilities for a repo."""
        await self._db.execute("BEGIN IMMEDIATE")
        async with self._db.execute(
            "DELETE FROM vulnerabilities WHERE repo_name = ?", (repo_name,)
        ):
            pass
        await self._db.executemany(
            """INSERT INTO vulnerabilities
               (id, name, severity, state, source, web_url, repo_name,
                mitigation_state, mitigation_details, location, solution, branch_status,
                report_stale, report_stale_since)
               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
            [
                (
                    v.id, v.name, v.severity, v.state, v.source, v.web_url,
                    v.repo_name, v.mitigation_state, v.mitigation_details,
                    v.location, v.solution, v.branch_status,
                    int(v.report_stale), v.report_stale_since,
                )
                for v in vulns
            ],
        )
        await self._db.commit()

    async def get_vulns(self, repo_name: str | None = None) -> list[Vulnerability]:
        """Return cached vulnerabilities, optionally filtered by repo name."""
        if repo_name is not None:
            query = f"SELECT {_VULN_COLS} FROM vulnerabilities WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = f"SELECT {_VULN_COLS} FROM vulnerabilities"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        return [_row_to_vuln(row) for row in rows]

    # --- Events ---

    async def store_events(self, events: list[Event]) -> None:
        """Insert new events as unseen."""
        await self._db.executemany(
            "INSERT INTO events (type, repo_name, summary, detail, created_at, seen) VALUES (?, ?, ?, ?, ?, 0)",
            [
                (
                    e.type.value,
                    e.repo_name,
                    e.summary,
                    e.detail,
                    e.created_at.isoformat()
                    if isinstance(e.created_at, datetime)
                    else e.created_at,
                )
                for e in events
            ],
        )
        await self._db.commit()

    async def get_unseen_events(self) -> list[Event]:
        """Return all events not yet marked as seen."""
        async with self._db.execute(
            f"SELECT {_EVENT_COLS} FROM events WHERE seen = 0 ORDER BY created_at"
        ) as cur:
            rows = await cur.fetchall()
        return [_row_to_event(row) for row in rows]

    async def get_events_by_type(self, event_type: str) -> set[tuple[str, str]]:
        """Return (repo_name, detail) pairs for all events of a given type (seen or not)."""
        async with self._db.execute(
            "SELECT repo_name, detail FROM events WHERE type = ?", (event_type,)
        ) as cur:
            rows = await cur.fetchall()
        return {(row["repo_name"], row["detail"]) for row in rows}

    async def mark_events_seen(self, event_ids: list[int]) -> None:
        """Mark specific events as seen by ID."""
        if not event_ids:
            return
        placeholders = ",".join("?" * len(event_ids))
        await self._db.execute(
            f"UPDATE events SET seen = 1 WHERE id IN ({placeholders})",
            tuple(event_ids),
        )
        await self._db.commit()

    async def dismiss_events_by_type_and_detail(
        self, event_type: str, repo_detail_pairs: list[tuple[str, str]]
    ) -> None:
        """Mark events as seen by type + (repo_name, detail) pairs."""
        for repo_name, detail in repo_detail_pairs:
            await self._db.execute(
                "UPDATE events SET seen = 1 WHERE type = ? AND repo_name = ? AND detail = ?",
                (event_type, repo_name, detail),
            )
        await self._db.commit()

    async def delete_seen_events(self, event_type: str, repo_name: str, detail: str) -> None:
        """Delete seen events matching type, repo, and detail. Allows re-alerting if condition recurs."""
        await self._db.execute(
            "DELETE FROM events WHERE type = ? AND repo_name = ? AND detail = ? AND seen = 1",
            (event_type, repo_name, detail),
        )
        await self._db.commit()

    async def mark_all_events_seen(self) -> None:
        """Mark all unseen events as seen."""
        await self._db.execute("UPDATE events SET seen = 1 WHERE seen = 0")
        await self._db.commit()

    # --- Maintenance ---

    async def run_maintenance(self, active_repos: set[str]) -> dict[str, int]:
        """Periodic cleanup: purge old seen events and orphaned repo data.

        Returns counts of deleted rows per category for logging.
        """
        counts: dict[str, int] = {}

        # 1. Purge seen events older than 7 days
        cur = await self._db.execute(
            "DELETE FROM events WHERE seen = 1 AND created_at < datetime('now', '-7 days')"
        )
        counts["old_events"] = cur.rowcount

        # 2. Purge data for repos no longer in the workspace
        if active_repos:
            placeholders = ",".join("?" * len(active_repos))
            names = tuple(active_repos)
            for table in ("merge_requests", "pipelines", "vulnerabilities", "issues", "events", "pull_status"):
                cur = await self._db.execute(
                    f"DELETE FROM {table} WHERE repo_name NOT IN ({placeholders})",
                    names,
                )
                if cur.rowcount:
                    counts[f"orphan_{table}"] = cur.rowcount

        await self._db.commit()
        return counts

    # --- Internal helper ---

    # --- Metadata helpers ---

    async def set_metadata(self, key: str, value: str) -> None:
        """Set a metadata key-value pair."""
        await self._db.execute(
            "INSERT OR REPLACE INTO cache_metadata (key, value) VALUES (?, ?)",
            (key, value),
        )
        await self._db.commit()

    async def get_metadata(self, key: str) -> str | None:
        """Get a metadata value by key."""
        async with self._db.execute(
            "SELECT value FROM cache_metadata WHERE key = ?", (key,)
        ) as cur:
            row = await cur.fetchone()
        return row["value"] if row else None

    # --- UI state: pinning ---

    async def get_pinned(self) -> list[str]:
        """Return the list of pinned repo names."""
        async with self._db.execute("SELECT value FROM ui_state WHERE key = 'pinned'") as cur:
            row = await cur.fetchone()
        if not row or not row["value"]:
            return []
        return json.loads(row["value"])

    async def pin_repo(self, repo_name: str) -> None:
        """Add a repo to the pinned list."""
        pinned = await self.get_pinned()
        if repo_name not in pinned:
            pinned.append(repo_name)
        await self._db.execute(
            "INSERT OR REPLACE INTO ui_state (key, value) VALUES ('pinned', ?)",
            (json.dumps(pinned),),
        )
        await self._db.commit()

    async def unpin_repo(self, repo_name: str) -> None:
        """Remove a repo from the pinned list."""
        pinned = await self.get_pinned()
        pinned = [r for r in pinned if r != repo_name]
        await self._db.execute(
            "INSERT OR REPLACE INTO ui_state (key, value) VALUES ('pinned', ?)",
            (json.dumps(pinned),),
        )
        await self._db.commit()

    # --- UI state: repo order ---

    async def get_repo_order(self) -> list[str]:
        """Return the user-defined repo display order."""
        async with self._db.execute("SELECT value FROM ui_state WHERE key = 'repo_order'") as cur:
            row = await cur.fetchone()
        if not row or not row["value"]:
            return []
        return json.loads(row["value"])

    async def save_repo_order(self, order: list[str]) -> None:
        """Persist the user-defined repo display order."""
        await self._db.execute(
            "INSERT OR REPLACE INTO ui_state (key, value) VALUES ('repo_order', ?)",
            (json.dumps(order),),
        )
        await self._db.commit()

    # --- Pull status ---

    async def store_pull_status(self, repo_name: str, status: str) -> None:
        """Store the pull status for a repo."""
        await self._db.execute(
            "INSERT OR REPLACE INTO pull_status (repo_name, status) VALUES (?, ?)",
            (repo_name, status),
        )
        await self._db.commit()

    async def get_pull_status(self, repo_name: str) -> str | None:
        """Return the pull status for a repo, or None if not cached."""
        async with self._db.execute(
            "SELECT status FROM pull_status WHERE repo_name = ?", (repo_name,)
        ) as cur:
            row = await cur.fetchone()
        return row["status"] if row else None

    async def get_all_pull_statuses(self) -> dict[str, str]:
        """Return pull statuses for all repos as {repo_name: status}."""
        async with self._db.execute("SELECT repo_name, status FROM pull_status") as cur:
            rows = await cur.fetchall()
        return {row["repo_name"]: row["status"] for row in rows}

    # --- Sync read methods (persistent connection, thread-safe) ---

    def get_mrs_sync(self, repo_name: str | None = None) -> list[MR]:
        if repo_name is not None:
            rows = self._sync_fetchall(f"SELECT {_MR_COLS} FROM merge_requests WHERE repo_name = ?", (repo_name,))
        else:
            rows = self._sync_fetchall(f"SELECT {_MR_COLS} FROM merge_requests")
        return [_row_to_mr(row) for row in rows]

    def get_pipelines_sync(self, repo_name: str | None = None) -> list[Pipeline]:
        if repo_name is not None:
            rows = self._sync_fetchall(f"SELECT {_PIPELINE_COLS} FROM pipelines WHERE repo_name = ?", (repo_name,))
        else:
            rows = self._sync_fetchall(f"SELECT {_PIPELINE_COLS} FROM pipelines")
        return [_row_to_pipeline(row) for row in rows]

    def get_vulns_sync(self, repo_name: str | None = None) -> list[Vulnerability]:
        if repo_name is not None:
            rows = self._sync_fetchall(f"SELECT {_VULN_COLS} FROM vulnerabilities WHERE repo_name = ?", (repo_name,))
        else:
            rows = self._sync_fetchall(f"SELECT {_VULN_COLS} FROM vulnerabilities")
        return [_row_to_vuln(row) for row in rows]

    def get_unseen_events_sync(self) -> list[Event]:
        rows = self._sync_fetchall(f"SELECT {_EVENT_COLS} FROM events WHERE seen = 0 ORDER BY created_at")
        return [_row_to_event(row) for row in rows]

    def get_pinned_sync(self) -> list[str]:
        row = self._sync_fetchone("SELECT value FROM ui_state WHERE key = 'pinned'")
        if not row or not row["value"]:
            return []
        return json.loads(row["value"])

    def get_repo_order_sync(self) -> list[str]:
        row = self._sync_fetchone("SELECT value FROM ui_state WHERE key = 'repo_order'")
        if not row or not row["value"]:
            return []
        return json.loads(row["value"])

    def get_all_pull_statuses_sync(self) -> dict[str, str]:
        rows = self._sync_fetchall("SELECT repo_name, status FROM pull_status")
        return {row["repo_name"]: row["status"] for row in rows}

    def get_metadata_sync(self, key: str) -> str | None:
        row = self._sync_fetchone("SELECT value FROM cache_metadata WHERE key = ?", (key,))
        return row["value"] if row else None

    def get_issues_sync(self, repo_name: str | None = None) -> list[Issue]:
        if repo_name is not None:
            rows = self._sync_fetchall(f"SELECT {_ISSUE_COLS} FROM issues WHERE repo_name = ?", (repo_name,))
        else:
            rows = self._sync_fetchall(f"SELECT {_ISSUE_COLS} FROM issues")
        return [_row_to_issue(row) for row in rows]
