import json
import aiosqlite
from pathlib import Path
from datetime import datetime

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

SCHEMA_VERSION = "4"

_TABLES = [
    "cache_metadata",
    "merge_requests",
    "pipelines",
    "vulnerabilities",
    "events",
    "ui_state",
    "pull_status",
]

_DDL = """
CREATE TABLE IF NOT EXISTS cache_metadata (
    key TEXT PRIMARY KEY,
    value TEXT
);

CREATE TABLE IF NOT EXISTS merge_requests (
    id INTEGER,
    iid INTEGER,
    title TEXT,
    author TEXT,
    state TEXT,
    source_branch TEXT,
    target_branch TEXT,
    web_url TEXT,
    user_notes_count INTEGER,
    approved INTEGER,
    created_at TEXT,
    updated_at TEXT,
    repo_name TEXT
);

CREATE TABLE IF NOT EXISTS pipelines (
    id INTEGER,
    status TEXT,
    ref TEXT,
    web_url TEXT,
    created_at TEXT,
    repo_name TEXT
);

CREATE TABLE IF NOT EXISTS vulnerabilities (
    id INTEGER,
    name TEXT,
    severity TEXT,
    state TEXT,
    source TEXT,
    web_url TEXT,
    repo_name TEXT,
    mitigation_state TEXT,
    mitigation_details TEXT,
    location TEXT,
    solution TEXT
);

CREATE TABLE IF NOT EXISTS events (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    type TEXT,
    repo_name TEXT,
    summary TEXT,
    detail TEXT,
    created_at TEXT,
    seen INTEGER DEFAULT 0
);

CREATE TABLE IF NOT EXISTS ui_state (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS pull_status (
    repo_name TEXT PRIMARY KEY,
    status TEXT NOT NULL
);
"""


class Cache:
    def __init__(self, db_path: Path):
        self._db_path = db_path
        self._db: aiosqlite.Connection | None = None

    async def initialize(self) -> None:
        if self._db is None:
            self._db = await aiosqlite.connect(self._db_path)

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

        # Check schema version
        await self._db.execute(
            "CREATE TABLE IF NOT EXISTS cache_metadata (key TEXT PRIMARY KEY, value TEXT)"
        )
        await self._db.commit()

        async with self._db.execute(
            "SELECT value FROM cache_metadata WHERE key = 'schema_version'"
        ) as cur:
            row = await cur.fetchone()

        if row is not None and row[0] != SCHEMA_VERSION:
            # Drop all known tables to force clean recreation
            for table in _TABLES:
                await self._db.execute(f"DROP TABLE IF EXISTS {table}")
            await self._db.commit()

        # Create tables
        await self._db.executescript(_DDL)

        # Store or update schema version
        await self._db.execute(
            "INSERT OR REPLACE INTO cache_metadata (key, value) VALUES ('schema_version', ?)",
            (SCHEMA_VERSION,),
        )
        await self._db.commit()

    async def close(self) -> None:
        if self._db is not None:
            await self._db.close()
            self._db = None

    async def get_tables(self) -> list[str]:
        async with self._db.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ) as cur:
            rows = await cur.fetchall()
        return [row[0] for row in rows]

    # --- MRs ---

    async def store_mrs(self, repo_name: str, mrs: list[MR]) -> None:
        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)
               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,
                )
                for mr in mrs
            ],
        )
        await self._db.commit()

    async def get_mrs(self, repo_name: str | None = None) -> list[MR]:
        if repo_name is not None:
            query = "SELECT * FROM merge_requests WHERE repo_name LIKE ?"
            params = (f"%{repo_name}%",)
        else:
            query = "SELECT * FROM merge_requests"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        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],
            )
            for row in rows
        ]

    # --- Pipelines ---

    async def store_pipelines(self, repo_name: str, pipelines: list[Pipeline]) -> None:
        async with self._db.execute(
            "DELETE FROM pipelines WHERE repo_name = ?", (repo_name,)
        ):
            pass
        await self._db.executemany(
            "INSERT INTO pipelines (id, status, ref, web_url, created_at, repo_name) VALUES (?, ?, ?, ?, ?, ?)",
            [
                (p.id, p.status, p.ref, p.web_url, p.created_at, p.repo_name)
                for p in pipelines
            ],
        )
        await self._db.commit()

    async def get_pipelines(self, repo_name: str | None = None) -> list[Pipeline]:
        if repo_name is not None:
            query = "SELECT * FROM pipelines WHERE repo_name LIKE ?"
            params = (f"%{repo_name}%",)
        else:
            query = "SELECT * FROM pipelines"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        return [
            Pipeline(
                id=row[0],
                status=row[1],
                ref=row[2],
                web_url=row[3],
                created_at=row[4],
                repo_name=row[5],
            )
            for row in rows
        ]

    # --- Vulnerabilities ---

    async def store_vulns(self, repo_name: str, vulns: list[Vulnerability]) -> None:
        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) 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,
                )
                for v in vulns
            ],
        )
        await self._db.commit()

    async def get_vulns(self, repo_name: str | None = None) -> list[Vulnerability]:
        if repo_name is not None:
            query = "SELECT * FROM vulnerabilities WHERE repo_name LIKE ?"
            params = (f"%{repo_name}%",)
        else:
            query = "SELECT * FROM vulnerabilities"
            params = ()
        async with self._db.execute(query, params) as cur:
            rows = await cur.fetchall()
        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,
            )
            for row in rows
        ]

    # --- Events ---

    async def store_events(self, events: list[Event]) -> None:
        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]:
        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 [
            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,
            )
            for row in rows
        ]

    async def mark_events_seen(self, event_ids: list[int]) -> None:
        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:
        await self._db.execute("UPDATE events SET seen = 1 WHERE seen = 0")
        await self._db.commit()

    # --- Internal helper ---

    async def _execute(self, sql: str, params=None) -> None:
        if params is not None:
            await self._db.execute(sql, params)
        else:
            await self._db.execute(sql)
        await self._db.commit()

    # --- 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]:
        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:
        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:
        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]:
        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:
        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:
        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:
        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]:
        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 LIKE ?"
            params = (f"%{repo_name}%",)
        else:
            query = "SELECT * FROM merge_requests"
            params = ()

        conn = sqlite3.connect(str(db_path))
        try:
            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
            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],
                )
                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 LIKE ?"
            params = (f"%{repo_name}%",)
        else:
            query = "SELECT * FROM pipelines"
            params = ()

        conn = sqlite3.connect(str(self._db_path))
        try:
            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
            return [
                Pipeline(
                    id=row[0],
                    status=row[1],
                    ref=row[2],
                    web_url=row[3],
                    created_at=row[4],
                    repo_name=row[5],
                )
                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 LIKE ?"
            params = (f"%{repo_name}%",)
        else:
            query = "SELECT * FROM vulnerabilities"
            params = ()

        conn = sqlite3.connect(str(self._db_path))
        try:
            cursor = conn.execute(query, params)
            rows = cursor.fetchall()
            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,
                )
                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))
        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 [
                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,
                )
                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))
        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))
        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))
        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))
        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()
