from __future__ import annotations
import json
import sqlite3
import time
from pathlib import Path
from typing import Any

# Retention policy
# ----------------
# The DB column `expires_at` is kept for schema compatibility but is no longer
# a hard cutoff. The policy is:
#   • Every row is preserved for at least MIN_RETENTION_SECONDS (7 days) after
#     it was created.
#   • Beyond that, rows are kept for up to MAX_RETENTION_SECONDS (1 year).
#   • After 7 days, rows are only evicted when the total row count exceeds
#     MAX_KEPT_ROWS (100). In that case, rows older than 7 days are deleted
#     oldest-first until the total row count falls back to MAX_KEPT_ROWS.
MIN_RETENTION_SECONDS = 7 * 24 * 60 * 60
MAX_RETENTION_SECONDS = 365 * 24 * 60 * 60
MAX_KEPT_ROWS = 100
# Kept for backwards compatibility with any importer. `expires_at` is filled in
# with MAX_RETENTION_SECONDS at creation time (hard upper bound).
RETENTION_SECONDS = MAX_RETENTION_SECONDS


class TlsTestDB:
    def __init__(self, filepath: str | Path | None = None):
        if filepath is None:
            filepath = Path.cwd() / "databases" / "tls_test.db"
        self.filepath = str(filepath)
        Path(self.filepath).parent.mkdir(parents=True, exist_ok=True)
        self._init_schema()

    def _conn(self) -> sqlite3.Connection:
        conn = sqlite3.connect(self.filepath, timeout=30)
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA busy_timeout=10000")
        return conn

    def _init_schema(self) -> None:
        conn = self._conn()
        try:
            conn.executescript(
                """
                CREATE TABLE IF NOT EXISTS tests (
                    id              TEXT PRIMARY KEY,
                    target          TEXT NOT NULL,
                    client_ip       TEXT,
                    status          TEXT NOT NULL,
                    created_at      INTEGER NOT NULL,
                    started_at      INTEGER,
                    finished_at     INTEGER,
                    expires_at      INTEGER NOT NULL,
                    rank            TEXT,
                    score           REAL,
                    result_json     TEXT,
                    error_message   TEXT
                );
                CREATE INDEX IF NOT EXISTS idx_tests_expires ON tests(expires_at);
                CREATE INDEX IF NOT EXISTS idx_tests_ip_created ON tests(client_ip, created_at);
                CREATE INDEX IF NOT EXISTS idx_tests_target ON tests(target, created_at);

                CREATE TABLE IF NOT EXISTS test_progress (
                    test_id         TEXT NOT NULL,
                    seq             INTEGER NOT NULL,
                    ts              INTEGER NOT NULL,
                    phase           TEXT NOT NULL,
                    detail          TEXT,
                    progress        REAL,
                    severity        TEXT,
                    PRIMARY KEY(test_id, seq)
                );
                """
            )
            conn.commit()
        finally:
            conn.close()

    def create_job(self, test_id: str, target: str, client_ip: str | None) -> None:
        now = int(time.time())
        conn = self._conn()
        try:
            conn.execute(
                "INSERT INTO tests(id,target,client_ip,status,created_at,expires_at)"
                " VALUES (?,?,?,?,?,?)",
                (test_id, target, client_ip, "queued", now, now + RETENTION_SECONDS),
            )
            conn.commit()
        finally:
            conn.close()

    def update_status(
        self,
        test_id: str,
        status: str,
        *,
        started_at: int | None = None,
        finished_at: int | None = None,
        rank: str | None = None,
        score: float | None = None,
        result_json: str | None = None,
        error_message: str | None = None,
    ) -> None:
        sets = ["status = ?"]
        args: list[Any] = [status]
        if started_at is not None:
            sets.append("started_at = ?"); args.append(started_at)
        if finished_at is not None:
            sets.append("finished_at = ?"); args.append(finished_at)
        if rank is not None:
            sets.append("rank = ?"); args.append(rank)
        if score is not None:
            sets.append("score = ?"); args.append(score)
        if result_json is not None:
            sets.append("result_json = ?"); args.append(result_json)
        if error_message is not None:
            sets.append("error_message = ?"); args.append(error_message)
        args.append(test_id)
        conn = self._conn()
        try:
            conn.execute(f"UPDATE tests SET {', '.join(sets)} WHERE id = ?", args)
            conn.commit()
        finally:
            conn.close()

    def append_progress(
        self,
        test_id: str,
        seq: int,
        phase: str,
        detail: str,
        progress: float,
        severity: str,
    ) -> None:
        conn = self._conn()
        try:
            conn.execute(
                "INSERT OR REPLACE INTO test_progress(test_id,seq,ts,phase,detail,progress,severity)"
                " VALUES (?,?,?,?,?,?,?)",
                (test_id, seq, int(time.time()), phase, detail, progress, severity),
            )
            conn.commit()
        finally:
            conn.close()

    def get_job(self, test_id: str) -> dict[str, Any] | None:
        conn = self._conn()
        try:
            cur = conn.cursor()
            cur.execute("SELECT * FROM tests WHERE id = ?", (test_id,))
            row = cur.fetchone()
            if row is None:
                return None
            cols = [c[0] for c in cur.description]
            d = dict(zip(cols, row))
            if d.get("result_json"):
                try:
                    d["result"] = json.loads(d["result_json"])
                except json.JSONDecodeError:
                    d["result"] = None
            return d
        finally:
            conn.close()

    def get_progress(self, test_id: str) -> list[dict[str, Any]]:
        conn = self._conn()
        try:
            cur = conn.cursor()
            cur.execute(
                "SELECT seq,ts,phase,detail,progress,severity FROM test_progress"
                " WHERE test_id = ? ORDER BY seq ASC",
                (test_id,),
            )
            rows = cur.fetchall()
            return [
                {
                    "seq": r[0],
                    "ts": r[1],
                    "phase": r[2],
                    "detail": r[3],
                    "progress": r[4],
                    "severity": r[5],
                }
                for r in rows
            ]
        finally:
            conn.close()

    def get_history_by_target(self, target: str, exclude_id: str | None = None, limit: int = 10) -> list[dict[str, Any]]:
        conn = self._conn()
        try:
            cur = conn.cursor()
            if exclude_id:
                cur.execute(
                    "SELECT id, target, status, created_at, finished_at, rank, score, error_message"
                    " FROM tests WHERE target = ? AND id != ? AND status = 'done'"
                    " ORDER BY created_at DESC LIMIT ?",
                    (target, exclude_id, limit),
                )
            else:
                cur.execute(
                    "SELECT id, target, status, created_at, finished_at, rank, score, error_message"
                    " FROM tests WHERE target = ? AND status = 'done'"
                    " ORDER BY created_at DESC LIMIT ?",
                    (target, limit),
                )
            rows = cur.fetchall()
            cols = [c[0] for c in cur.description]
            return [dict(zip(cols, r)) for r in rows]
        finally:
            conn.close()

    def count_ip_in_window(self, client_ip: str, window_seconds: int) -> int:
        cutoff = int(time.time()) - window_seconds
        conn = self._conn()
        try:
            cur = conn.cursor()
            cur.execute(
                "SELECT COUNT(*) FROM tests WHERE client_ip = ? AND created_at >= ?",
                (client_ip, cutoff),
            )
            return cur.fetchone()[0]
        finally:
            conn.close()

    def count_ip_active(self, client_ip: str) -> int:
        conn = self._conn()
        try:
            cur = conn.cursor()
            cur.execute(
                "SELECT COUNT(*) FROM tests WHERE client_ip = ? AND status IN ('queued','running')",
                (client_ip,),
            )
            return cur.fetchone()[0]
        finally:
            conn.close()

    def delete_expired(self) -> int:
        """Apply the retention policy.

        Step 1: unconditionally drop rows past the 1-year hard ceiling.
        Step 2: if total row count exceeds MAX_KEPT_ROWS, drop rows older than
                MIN_RETENTION_SECONDS (7 days) in oldest-first order until the
                count falls back to MAX_KEPT_ROWS. Rows within 7 days are
                never evicted by this step.
        Returns the number of rows deleted.
        """
        now = int(time.time())
        deleted = 0
        conn = self._conn()
        try:
            cur = conn.cursor()

            # Step 1 — hard ceiling: 1-year-old rows.
            hard_cutoff = now - MAX_RETENTION_SECONDS
            cur.execute("SELECT id FROM tests WHERE created_at < ?", (hard_cutoff,))
            old_ids = [r[0] for r in cur.fetchall()]
            for tid in old_ids:
                conn.execute("DELETE FROM test_progress WHERE test_id = ?", (tid,))
                conn.execute("DELETE FROM tests WHERE id = ?", (tid,))
            deleted += len(old_ids)

            # Step 2 — count-triggered eviction of >7-day-old rows.
            cur.execute("SELECT COUNT(*) FROM tests")
            total = cur.fetchone()[0]
            if total > MAX_KEPT_ROWS:
                excess = total - MAX_KEPT_ROWS
                soft_cutoff = now - MIN_RETENTION_SECONDS
                cur.execute(
                    "SELECT id FROM tests"
                    " WHERE created_at < ?"
                    " ORDER BY created_at ASC"
                    " LIMIT ?",
                    (soft_cutoff, excess),
                )
                evict_ids = [r[0] for r in cur.fetchall()]
                for tid in evict_ids:
                    conn.execute("DELETE FROM test_progress WHERE test_id = ?", (tid,))
                    conn.execute("DELETE FROM tests WHERE id = ?", (tid,))
                deleted += len(evict_ids)

            conn.commit()
            return deleted
        finally:
            conn.close()
