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

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

import aiosqlite

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

log = logging.getLogger(__name__)


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


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 tuple to an MR dataclass."""
    return MR(
        id=row[0],
        iid=row[1],
        title=row[2],
        author=row[3],
        state=row[4],
        source_branch=row[5],
        target_branch=row[6],
        web_url=row[7],
        user_notes_count=row[8],
        approved=bool(row[9]),
        created_at=row[10],
        updated_at=row[11],
        repo_name=row[12],
        sha=row[13] if len(row) > 13 else None,
    )


def _row_to_pipeline(row) -> Pipeline:
    """Convert a sqlite row tuple to a Pipeline dataclass."""
    return Pipeline(
        id=row[0],
        status=row[1],
        ref=row[2],
        web_url=row[3],
        created_at=row[4],
        repo_name=row[5],
    )


def _row_to_vuln(row) -> Vulnerability:
    """Convert a sqlite row tuple to a Vulnerability dataclass."""
    return Vulnerability(
        id=row[0],
        name=row[1],
        severity=row[2],
        state=row[3],
        source=row[4],
        web_url=row[5],
        repo_name=row[6],
        mitigation_state=row[7],
        mitigation_details=row[8],
        location=row[9],
        solution=row[10] if len(row) > 10 else None,
        fixed_in_branch=bool(row[11]) if len(row) > 11 else False,
    )


def _row_to_event(row) -> Event:
    """Convert a sqlite row tuple to an Event dataclass."""
    return Event(
        id=row[0],
        type=EventType(row[1]),
        repo_name=row[2],
        summary=row[3],
        detail=row[4],
        created_at=datetime.fromisoformat(row[5]),
        seen=False,
    )



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

    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)

        if db_path.exists():
            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()

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

        await self._db.execute("PRAGMA journal_mode=WAL")

        # Ensure migration tracking exists
        await self._db.execute(
            "CREATE TABLE IF NOT EXISTS migrations_applied (name TEXT PRIMARY KEY)"
        )
        await self._db.commit()

        # Discover and run pending migrations
        all_migrations = _discover_migrations()
        async with self._db.execute("SELECT name FROM migrations_applied") as cur:
            applied = {row[0] for row in await cur.fetchall()}

        for name, sql in all_migrations:
            if name not in applied:
                log.info("Applying migration: %s", name)
                await self._db.executescript(sql)
                await self._db.execute(
                    "INSERT INTO migrations_applied (name) VALUES (?)", (name,)
                )
                await self._db.commit()

    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 the database connection."""
        if self._db is not None:
            await self.checkpoint()
            await self._db.close()
            self._db = None

    # --- MRs ---

    async def store_mrs(self, repo_name: str, mrs: list[MR]) -> None:
        """Replace all cached MRs for a repo."""
        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)
               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,
                )
                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 = "SELECT * FROM merge_requests WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = "SELECT * 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]

    # --- 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 terminal pipelines no longer returned by API
        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),
            )
        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 = "SELECT * FROM pipelines WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = "SELECT * 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."""
        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, fixed_in_branch)
               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,
                    int(v.fixed_in_branch),
                )
                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 = "SELECT * FROM vulnerabilities WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = "SELECT * 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(
            "SELECT id, type, repo_name, summary, detail, created_at 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[0], row[1]) 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 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()

    # --- 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[0] 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[0]:
            return []
        return json.loads(row[0])

    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[0]:
            return []
        return json.loads(row[0])

    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[0] 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[0]: row[1] for row in rows}

    # --- Sync wrappers for standalone mode ---

    def get_mrs_sync(self, repo_name: str | None = None) -> list[MR]:
        """Synchronous wrapper for get_mrs using raw sqlite3."""
        import sqlite3

        db_path = Path(self._db_path).resolve()
        if repo_name is not None:
            query = "SELECT * FROM merge_requests WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = "SELECT * FROM merge_requests"
            params = ()

        conn = sqlite3.connect(str(db_path), timeout=10)
        try:
            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
            return [_row_to_mr(row) for row in rows]
        finally:
            conn.close()

    def get_pipelines_sync(self, repo_name: str | None = None) -> list[Pipeline]:
        """Synchronous wrapper for get_pipelines using raw sqlite3."""
        import sqlite3

        if repo_name is not None:
            query = "SELECT * FROM pipelines WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = "SELECT * FROM pipelines"
            params = ()

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
            return [_row_to_pipeline(row) for row in rows]
        finally:
            conn.close()

    def get_vulns_sync(self, repo_name: str | None = None) -> list[Vulnerability]:
        """Synchronous wrapper for get_vulns using raw sqlite3."""
        import sqlite3

        if repo_name is not None:
            query = "SELECT * FROM vulnerabilities WHERE repo_name = ?"
            params = (repo_name,)
        else:
            query = "SELECT * FROM vulnerabilities"
            params = ()

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
            return [_row_to_vuln(row) for row in rows]
        finally:
            conn.close()

    def get_unseen_events_sync(self) -> list[Event]:
        """Synchronous wrapper for get_unseen_events using raw sqlite3."""
        import sqlite3

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute(
                "SELECT id, type, repo_name, summary, detail, created_at FROM events WHERE seen = 0 ORDER BY created_at"
            )
            rows = cursor.fetchall()
            return [_row_to_event(row) for row in rows]
        finally:
            conn.close()

    def get_pinned_sync(self) -> list[str]:
        """Synchronous wrapper for get_pinned using raw sqlite3."""
        import sqlite3

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute("SELECT value FROM ui_state WHERE key = 'pinned'")
            row = cursor.fetchone()
            if not row or not row[0]:
                return []
            return json.loads(row[0])
        finally:
            conn.close()

    def get_repo_order_sync(self) -> list[str]:
        """Synchronous wrapper for get_repo_order using raw sqlite3."""
        import sqlite3

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute("SELECT value FROM ui_state WHERE key = 'repo_order'")
            row = cursor.fetchone()
            if not row or not row[0]:
                return []
            return json.loads(row[0])
        finally:
            conn.close()

    def get_all_pull_statuses_sync(self) -> dict[str, str]:
        """Synchronous wrapper for get_all_pull_statuses using raw sqlite3."""
        import sqlite3

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute("SELECT repo_name, status FROM pull_status")
            rows = cursor.fetchall()
            return {row[0]: row[1] for row in rows}
        finally:
            conn.close()

    def get_metadata_sync(self, key: str) -> str | None:
        """Synchronous wrapper for get_metadata using raw sqlite3."""
        import sqlite3

        conn = sqlite3.connect(str(self._db_path), timeout=10)
        try:
            cursor = conn.execute(
                "SELECT value FROM cache_metadata WHERE key = ?", (key,)
            )
            row = cursor.fetchone()
            return row[0] if row else None
        finally:
            conn.close()
