"""SQLite history database helpers."""
from __future__ import annotations

import hashlib
import json
import os
import sqlite3
from contextlib import closing
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

SCHEMA_VERSION = 1

_SCHEMA_STATEMENTS = [
    """
    CREATE TABLE IF NOT EXISTS runs (
        id INTEGER PRIMARY KEY,
        started_at TEXT NOT NULL,
        finished_at TEXT,
        status TEXT NOT NULL,
        engine TEXT,
        format TEXT,
        exit_code INTEGER,
        options_json TEXT,
        cwd TEXT,
        version TEXT
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS inputs (
        id INTEGER PRIMARY KEY,
        run_id INTEGER NOT NULL,
        path TEXT NOT NULL,
        sha256 TEXT,
        size_bytes INTEGER,
        mtime_ns INTEGER,
        FOREIGN KEY(run_id) REFERENCES runs(id) ON DELETE CASCADE
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS outputs (
        id INTEGER PRIMARY KEY,
        run_id INTEGER NOT NULL,
        path TEXT,
        bytes INTEGER,
        sha256 TEXT,
        written INTEGER NOT NULL DEFAULT 0,
        FOREIGN KEY(run_id) REFERENCES runs(id) ON DELETE CASCADE
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS warnings (
        id INTEGER PRIMARY KEY,
        run_id INTEGER NOT NULL,
        code TEXT,
        message TEXT NOT NULL,
        FOREIGN KEY(run_id) REFERENCES runs(id) ON DELETE CASCADE
    )
    """,
    """
    CREATE TABLE IF NOT EXISTS timings (
        id INTEGER PRIMARY KEY,
        run_id INTEGER NOT NULL,
        stage TEXT NOT NULL,
        duration_ms REAL NOT NULL,
        meta_json TEXT,
        FOREIGN KEY(run_id) REFERENCES runs(id) ON DELETE CASCADE
    )
    """,
    "CREATE INDEX IF NOT EXISTS idx_inputs_run_id ON inputs(run_id)",
    "CREATE INDEX IF NOT EXISTS idx_outputs_run_id ON outputs(run_id)",
    "CREATE INDEX IF NOT EXISTS idx_warnings_run_id ON warnings(run_id)",
    "CREATE INDEX IF NOT EXISTS idx_timings_run_id ON timings(run_id)",
]


def initialize_history_db(path: Path) -> None:
    conn = _open_history_db(path)
    conn.close()


def history_db_path() -> Optional[Path]:
    override = os.environ.get("DOC2MD_HISTORY_DB")
    if override:
        lowered = override.strip().lower()
        if lowered in {"0", "false", "off", "none", "disable"}:
            return None
        return Path(override)
    return Path.home() / ".doc2md_history.sqlite"


def _open_history_db(path: Path) -> sqlite3.Connection:
    path.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(path)
    conn.execute("PRAGMA foreign_keys = ON")
    for statement in _SCHEMA_STATEMENTS:
        conn.execute(statement)
    conn.commit()
    return conn


def _utc_now() -> str:
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def sha256_bytes(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def sha256_file(path: Path) -> str:
    hasher = hashlib.sha256()
    with open(path, "rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            hasher.update(chunk)
    return hasher.hexdigest()


class HistoryLogger:
    def __init__(self, path: Path):
        self._path = path
        self._conn = _open_history_db(path)

    @property
    def path(self) -> Path:
        return self._path

    def close(self) -> None:
        self._conn.close()

    def start_run(
        self,
        *,
        engine: Optional[str],
        fmt: Optional[str],
        options: dict,
        cwd: str,
        version: str,
    ) -> int:
        options_json = json.dumps(options, sort_keys=True)
        cursor = self._conn.execute(
            """
            INSERT INTO runs
                (started_at, status, engine, format, exit_code, options_json, cwd, version)
            VALUES
                (?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                _utc_now(),
                "running",
                engine,
                fmt,
                None,
                options_json,
                cwd,
                version,
            ),
        )
        self._conn.commit()
        return int(cursor.lastrowid)

    def finish_run(self, run_id: int, *, status: str, exit_code: int) -> None:
        self._conn.execute(
            """
            UPDATE runs
            SET finished_at = ?, status = ?, exit_code = ?
            WHERE id = ?
            """,
            (_utc_now(), status, exit_code, run_id),
        )
        self._conn.commit()

    def add_input(
        self,
        run_id: int,
        *,
        path: Path,
        sha256: str,
        size_bytes: int,
        mtime_ns: int,
    ) -> None:
        self._conn.execute(
            """
            INSERT INTO inputs (run_id, path, sha256, size_bytes, mtime_ns)
            VALUES (?, ?, ?, ?, ?)
            """,
            (run_id, str(path), sha256, size_bytes, mtime_ns),
        )
        self._conn.commit()

    def add_output(
        self,
        run_id: int,
        *,
        path: Optional[Path],
        bytes_len: int,
        sha256: str,
        written: bool,
    ) -> None:
        self._conn.execute(
            """
            INSERT INTO outputs (run_id, path, bytes, sha256, written)
            VALUES (?, ?, ?, ?, ?)
            """,
            (run_id, str(path) if path is not None else None, bytes_len, sha256, int(written)),
        )
        self._conn.commit()

    def add_warning(self, run_id: int, *, code: Optional[str], message: str) -> None:
        self._conn.execute(
            """
            INSERT INTO warnings (run_id, code, message)
            VALUES (?, ?, ?)
            """,
            (run_id, code, message),
        )
        self._conn.commit()

    def add_timing(
        self,
        run_id: int,
        *,
        stage: str,
        duration_ms: float,
        meta: Optional[dict] = None,
    ) -> None:
        meta_json = json.dumps(meta, sort_keys=True) if meta else None
        self._conn.execute(
            """
            INSERT INTO timings (run_id, stage, duration_ms, meta_json)
            VALUES (?, ?, ?, ?)
            """,
            (run_id, stage, duration_ms, meta_json),
        )
        self._conn.commit()


def _row_to_run(row: tuple) -> dict:
    return {
        "id": row[0],
        "started_at": row[1],
        "finished_at": row[2],
        "status": row[3],
        "engine": row[4],
        "format": row[5],
        "exit_code": row[6],
        "input_count": row[7],
        "output_count": row[8],
    }


def list_runs(path: Path, *, limit: int = 20) -> list[dict]:
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        rows = conn.execute(
            """
            SELECT
                runs.id,
                runs.started_at,
                runs.finished_at,
                runs.status,
                runs.engine,
                runs.format,
                runs.exit_code,
                (SELECT COUNT(*) FROM inputs WHERE inputs.run_id = runs.id) AS input_count,
                (SELECT COUNT(*) FROM outputs WHERE outputs.run_id = runs.id) AS output_count
            FROM runs
            ORDER BY runs.started_at DESC
            LIMIT ?
            """,
            (limit,),
        ).fetchall()
    return [_row_to_run(row) for row in rows]


def search_runs(path: Path, query: str, *, limit: int = 20) -> list[dict]:
    pattern = f"%{query}%"
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        rows = conn.execute(
            """
            SELECT
                runs.id,
                runs.started_at,
                runs.finished_at,
                runs.status,
                runs.engine,
                runs.format,
                runs.exit_code,
                (SELECT COUNT(*) FROM inputs WHERE inputs.run_id = runs.id) AS input_count,
                (SELECT COUNT(*) FROM outputs WHERE outputs.run_id = runs.id) AS output_count
            FROM runs
            LEFT JOIN inputs ON inputs.run_id = runs.id
            LEFT JOIN outputs ON outputs.run_id = runs.id
            WHERE inputs.path LIKE ? OR outputs.path LIKE ?
            GROUP BY runs.id
            ORDER BY runs.started_at DESC
            LIMIT ?
            """,
            (pattern, pattern, limit),
        ).fetchall()
    return [_row_to_run(row) for row in rows]


def load_run(path: Path, run_id: int) -> Optional[dict]:
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        run_row = conn.execute(
            """
            SELECT id, started_at, finished_at, status, engine, format, exit_code,
                   options_json, cwd, version
            FROM runs
            WHERE id = ?
            """,
            (run_id,),
        ).fetchone()
        if run_row is None:
            return None
        inputs = conn.execute(
            """
            SELECT path, sha256, size_bytes, mtime_ns
            FROM inputs
            WHERE run_id = ?
            ORDER BY id ASC
            """,
            (run_id,),
        ).fetchall()
        outputs = conn.execute(
            """
            SELECT path, bytes, sha256, written
            FROM outputs
            WHERE run_id = ?
            ORDER BY id ASC
            """,
            (run_id,),
        ).fetchall()
        warnings = conn.execute(
            """
            SELECT code, message
            FROM warnings
            WHERE run_id = ?
            ORDER BY id ASC
            """,
            (run_id,),
        ).fetchall()
        timings = conn.execute(
            """
            SELECT stage, duration_ms, meta_json
            FROM timings
            WHERE run_id = ?
            ORDER BY id ASC
            """,
            (run_id,),
        ).fetchall()
    return {
        "id": run_row[0],
        "started_at": run_row[1],
        "finished_at": run_row[2],
        "status": run_row[3],
        "engine": run_row[4],
        "format": run_row[5],
        "exit_code": run_row[6],
        "options_json": run_row[7],
        "cwd": run_row[8],
        "version": run_row[9],
        "inputs": [
            {
                "path": row[0],
                "sha256": row[1],
                "size_bytes": row[2],
                "mtime_ns": row[3],
            }
            for row in inputs
        ],
        "outputs": [
            {
                "path": row[0],
                "bytes": row[1],
                "sha256": row[2],
                "written": bool(row[3]),
            }
            for row in outputs
        ],
        "warnings": [
            {"code": row[0], "message": row[1]}
            for row in warnings
        ],
        "timings": [
            {
                "stage": row[0],
                "duration_ms": row[1],
                "meta": json.loads(row[2]) if row[2] else None,
            }
            for row in timings
        ],
    }


def prune_runs(path: Path, keep: int) -> tuple[int, int]:
    if keep < 0:
        raise ValueError("keep must be >= 0")
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        before = conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0]
        if keep == 0:
            conn.execute("DELETE FROM runs")
        else:
            keep_ids = [
                row[0]
                for row in conn.execute(
                    "SELECT id FROM runs ORDER BY started_at DESC LIMIT ?",
                    (keep,),
                ).fetchall()
            ]
            if keep_ids:
                placeholders = ",".join("?" for _ in keep_ids)
                conn.execute(
                    f"DELETE FROM runs WHERE id NOT IN ({placeholders})",
                    keep_ids,
                )
            else:
                conn.execute("DELETE FROM runs")
        conn.commit()
        after = conn.execute("SELECT COUNT(*) FROM runs").fetchone()[0]
    return before - after, after


def list_input_paths(path: Path, query: Optional[str], limit: int) -> list[dict]:
    pattern = None
    if query:
        pattern = f"%{query}%"
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        if pattern:
            rows = conn.execute(
                """
                SELECT inputs.path, COUNT(*) as count, MAX(runs.started_at) as last_seen
                FROM inputs
                JOIN runs ON runs.id = inputs.run_id
                WHERE inputs.path LIKE ?
                GROUP BY inputs.path
                ORDER BY count DESC, last_seen DESC
                LIMIT ?
                """,
                (pattern, limit),
            ).fetchall()
        else:
            rows = conn.execute(
                """
                SELECT inputs.path, COUNT(*) as count, MAX(runs.started_at) as last_seen
                FROM inputs
                JOIN runs ON runs.id = inputs.run_id
                GROUP BY inputs.path
                ORDER BY count DESC, last_seen DESC
                LIMIT ?
                """,
                (limit,),
            ).fetchall()
    return [
        {"path": row[0], "count": row[1], "last_seen": row[2]}
        for row in rows
    ]


def list_recent_inputs(path: Path, limit: int) -> list[dict]:
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        rows = conn.execute(
            """
            SELECT inputs.path, COUNT(*) as count, MAX(runs.started_at) as last_seen
            FROM inputs
            JOIN runs ON runs.id = inputs.run_id
            GROUP BY inputs.path
            ORDER BY last_seen DESC, count DESC
            LIMIT ?
            """,
            (limit,),
        ).fetchall()
    return [
        {"path": row[0], "count": row[1], "last_seen": row[2]}
        for row in rows
    ]


def list_recent_projects(path: Path, limit: int) -> list[dict]:
    with closing(sqlite3.connect(path)) as conn:
        conn.execute("PRAGMA foreign_keys = ON")
        rows = conn.execute(
            """
            SELECT inputs.path, runs.started_at
            FROM inputs
            JOIN runs ON runs.id = inputs.run_id
            """
        ).fetchall()
    projects: dict[str, dict] = {}
    for path_str, started_at in rows:
        project_path = str(Path(path_str).parent)
        entry = projects.get(project_path)
        if entry is None:
            projects[project_path] = {
                "path": project_path,
                "count": 1,
                "last_seen": started_at,
            }
            continue
        entry["count"] += 1
        if started_at and (entry["last_seen"] is None or started_at > entry["last_seen"]):
            entry["last_seen"] = started_at
    items = list(projects.values())
    items.sort(
        key=lambda item: (item["last_seen"] or "", item["count"]),
        reverse=True,
    )
    return items[:limit]
