from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import json
import sqlite3
from typing import Any, Iterable


@dataclass(slots=True)
class FileState:
    rel_path: str
    source_hash: str | None
    target_hash: str | None


@dataclass(slots=True)
class ConflictRecord:
    conflict_id: int
    created_at: str
    rel_path: str
    winner_side: str
    loser_side: str
    winner_hash: str
    loser_hash: str
    conflict_copy_path: str
    restored_at: str | None
    restored_side: str | None


@dataclass(slots=True)
class SnapshotRecord:
    snapshot_id: int
    created_at: str
    rel_path: str
    side: str
    event_type: str
    file_hash: str
    snapshot_path: str
    restored_at: str | None
    restored_side: str | None


@dataclass(slots=True)
class ActivityRecord:
    activity_id: int
    created_at: str
    action: str
    rel_path: str | None
    side: str | None
    details: dict[str, Any]


class StateStore:
    def __init__(self, db_path: Path) -> None:
        self.db_path = db_path
        self.db_path.parent.mkdir(parents=True, exist_ok=True)
        self._conn = sqlite3.connect(str(self.db_path))
        self._conn.row_factory = sqlite3.Row
        self._init_schema()

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

    def _init_schema(self) -> None:
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS file_state (
                rel_path TEXT PRIMARY KEY,
                source_hash TEXT,
                target_hash TEXT,
                updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
            )
            """
        )
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS metadata (
                key TEXT PRIMARY KEY,
                value TEXT NOT NULL
            )
            """
        )
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS conflict_log (
                conflict_id INTEGER PRIMARY KEY AUTOINCREMENT,
                created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
                rel_path TEXT NOT NULL,
                winner_side TEXT NOT NULL,
                loser_side TEXT NOT NULL,
                winner_hash TEXT NOT NULL,
                loser_hash TEXT NOT NULL,
                conflict_copy_path TEXT NOT NULL,
                restored_at TEXT,
                restored_side TEXT
            )
            """
        )
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS snapshot_log (
                snapshot_id INTEGER PRIMARY KEY AUTOINCREMENT,
                created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
                rel_path TEXT NOT NULL,
                side TEXT NOT NULL,
                event_type TEXT NOT NULL,
                file_hash TEXT NOT NULL,
                snapshot_path TEXT NOT NULL,
                restored_at TEXT,
                restored_side TEXT
            )
            """
        )
        self._conn.execute(
            """
            CREATE TABLE IF NOT EXISTS activity_log (
                activity_id INTEGER PRIMARY KEY AUTOINCREMENT,
                created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
                action TEXT NOT NULL,
                rel_path TEXT,
                side TEXT,
                details_json TEXT NOT NULL DEFAULT '{}'
            )
            """
        )
        self._conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_snapshot_rel_path ON snapshot_log(rel_path)"
        )
        self._conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_snapshot_event_type ON snapshot_log(event_type)"
        )
        self._conn.execute(
            "CREATE INDEX IF NOT EXISTS idx_activity_rel_path ON activity_log(rel_path)"
        )
        self._conn.commit()

    def get_file_state(self, rel_path: str) -> FileState | None:
        row = self._conn.execute(
            "SELECT rel_path, source_hash, target_hash FROM file_state WHERE rel_path = ?",
            (rel_path,),
        ).fetchone()
        if row is None:
            return None
        return FileState(
            rel_path=row["rel_path"],
            source_hash=row["source_hash"],
            target_hash=row["target_hash"],
        )

    def upsert_file_states(self, states: Iterable[FileState]) -> None:
        self._conn.executemany(
            """
            INSERT INTO file_state (rel_path, source_hash, target_hash)
            VALUES (?, ?, ?)
            ON CONFLICT(rel_path) DO UPDATE SET
                source_hash = excluded.source_hash,
                target_hash = excluded.target_hash,
                updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
            """,
            [(s.rel_path, s.source_hash, s.target_hash) for s in states],
        )
        self._conn.commit()

    def list_file_states(self) -> list[FileState]:
        rows = self._conn.execute(
            """
            SELECT rel_path, source_hash, target_hash
            FROM file_state
            """
        ).fetchall()
        return [
            FileState(
                rel_path=str(row["rel_path"]),
                source_hash=row["source_hash"],
                target_hash=row["target_hash"],
            )
            for row in rows
        ]

    def delete_not_in(self, rel_paths: set[str]) -> None:
        if not rel_paths:
            self._conn.execute("DELETE FROM file_state")
            self._conn.commit()
            return

        placeholders = ",".join("?" for _ in rel_paths)
        self._conn.execute(
            f"DELETE FROM file_state WHERE rel_path NOT IN ({placeholders})",
            tuple(rel_paths),
        )
        self._conn.commit()

    def count_files(self) -> int:
        row = self._conn.execute("SELECT COUNT(*) AS c FROM file_state").fetchone()
        return int(row["c"] if row else 0)

    def get_metadata(self, key: str) -> str | None:
        row = self._conn.execute("SELECT value FROM metadata WHERE key = ?", (key,)).fetchone()
        if row is None:
            return None
        return str(row["value"])

    def set_metadata(self, key: str, value: str) -> None:
        self._conn.execute(
            """
            INSERT INTO metadata (key, value)
            VALUES (?, ?)
            ON CONFLICT(key) DO UPDATE SET value = excluded.value
            """,
            (key, value),
        )
        self._conn.commit()

    def add_conflict(
        self,
        *,
        rel_path: str,
        winner_side: str,
        loser_side: str,
        winner_hash: str,
        loser_hash: str,
        conflict_copy_path: str,
    ) -> int:
        cur = self._conn.execute(
            """
            INSERT INTO conflict_log (
                rel_path, winner_side, loser_side, winner_hash, loser_hash, conflict_copy_path
            ) VALUES (?, ?, ?, ?, ?, ?)
            """,
            (rel_path, winner_side, loser_side, winner_hash, loser_hash, conflict_copy_path),
        )
        self._conn.commit()
        return int(cur.lastrowid)

    def list_conflicts(self, *, limit: int = 20, include_resolved: bool = False) -> list[ConflictRecord]:
        if limit <= 0:
            limit = 1
        if include_resolved:
            rows = self._conn.execute(
                """
                SELECT *
                FROM conflict_log
                ORDER BY conflict_id DESC
                LIMIT ?
                """,
                (limit,),
            ).fetchall()
        else:
            rows = self._conn.execute(
                """
                SELECT *
                FROM conflict_log
                WHERE restored_at IS NULL
                ORDER BY conflict_id DESC
                LIMIT ?
                """,
                (limit,),
            ).fetchall()
        return [_to_conflict_record(row) for row in rows]

    def get_conflict(self, conflict_id: int) -> ConflictRecord | None:
        row = self._conn.execute(
            "SELECT * FROM conflict_log WHERE conflict_id = ?",
            (conflict_id,),
        ).fetchone()
        if row is None:
            return None
        return _to_conflict_record(row)

    def mark_conflict_restored(self, conflict_id: int, side: str) -> None:
        self._conn.execute(
            """
            UPDATE conflict_log
            SET restored_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
                restored_side = ?
            WHERE conflict_id = ?
            """,
            (side, conflict_id),
        )
        self._conn.commit()

    def get_unresolved_conflicts(self) -> list[ConflictRecord]:
        rows = self._conn.execute(
            """
            SELECT *
            FROM conflict_log
            WHERE restored_at IS NULL
            ORDER BY conflict_id ASC
            """
        ).fetchall()
        return [_to_conflict_record(row) for row in rows]

    def count_conflicts(self, *, unresolved_only: bool = True) -> int:
        if unresolved_only:
            row = self._conn.execute(
                "SELECT COUNT(*) AS c FROM conflict_log WHERE restored_at IS NULL"
            ).fetchone()
        else:
            row = self._conn.execute("SELECT COUNT(*) AS c FROM conflict_log").fetchone()
        return int(row["c"] if row else 0)

    def add_snapshot(
        self,
        *,
        rel_path: str,
        side: str,
        event_type: str,
        file_hash: str,
        snapshot_path: str,
    ) -> int:
        cur = self._conn.execute(
            """
            INSERT INTO snapshot_log (
                rel_path, side, event_type, file_hash, snapshot_path
            ) VALUES (?, ?, ?, ?, ?)
            """,
            (rel_path, side, event_type, file_hash, snapshot_path),
        )
        self._conn.commit()
        return int(cur.lastrowid)

    def list_snapshots(
        self,
        *,
        limit: int = 50,
        rel_path: str | None = None,
        event_type: str | None = None,
        include_restored: bool = True,
    ) -> list[SnapshotRecord]:
        clauses: list[str] = []
        params: list[Any] = []

        if rel_path:
            clauses.append("rel_path = ?")
            params.append(rel_path)
        if event_type:
            clauses.append("event_type = ?")
            params.append(event_type)
        if not include_restored:
            clauses.append("restored_at IS NULL")

        where = ""
        if clauses:
            where = "WHERE " + " AND ".join(clauses)

        rows = self._conn.execute(
            f"""
            SELECT *
            FROM snapshot_log
            {where}
            ORDER BY snapshot_id DESC
            LIMIT ?
            """,
            (*params, max(1, limit)),
        ).fetchall()
        return [_to_snapshot_record(row) for row in rows]

    def get_snapshot(self, snapshot_id: int) -> SnapshotRecord | None:
        row = self._conn.execute(
            "SELECT * FROM snapshot_log WHERE snapshot_id = ?",
            (snapshot_id,),
        ).fetchone()
        if row is None:
            return None
        return _to_snapshot_record(row)

    def mark_snapshot_restored(self, snapshot_id: int, side: str) -> None:
        self._conn.execute(
            """
            UPDATE snapshot_log
            SET restored_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
                restored_side = ?
            WHERE snapshot_id = ?
            """,
            (side, snapshot_id),
        )
        self._conn.commit()

    def count_deleted_files(self, *, unresolved_only: bool = True) -> int:
        if unresolved_only:
            row = self._conn.execute(
                """
                SELECT COUNT(*) AS c
                FROM snapshot_log
                WHERE event_type = 'deleted'
                  AND restored_at IS NULL
                """
            ).fetchone()
        else:
            row = self._conn.execute(
                "SELECT COUNT(*) AS c FROM snapshot_log WHERE event_type = 'deleted'"
            ).fetchone()
        return int(row["c"] if row else 0)

    def add_activity(
        self,
        *,
        action: str,
        rel_path: str | None,
        side: str | None,
        details: dict[str, Any] | None = None,
    ) -> int:
        cur = self._conn.execute(
            """
            INSERT INTO activity_log (action, rel_path, side, details_json)
            VALUES (?, ?, ?, ?)
            """,
            (action, rel_path, side, json.dumps(details or {}, sort_keys=True)),
        )
        self._conn.commit()
        return int(cur.lastrowid)

    def list_activity(self, *, limit: int = 100, rel_path: str | None = None) -> list[ActivityRecord]:
        if rel_path:
            rows = self._conn.execute(
                """
                SELECT *
                FROM activity_log
                WHERE rel_path = ?
                ORDER BY activity_id DESC
                LIMIT ?
                """,
                (rel_path, max(1, limit)),
            ).fetchall()
        else:
            rows = self._conn.execute(
                """
                SELECT *
                FROM activity_log
                ORDER BY activity_id DESC
                LIMIT ?
                """,
                (max(1, limit),),
            ).fetchall()
        return [_to_activity_record(row) for row in rows]

    def count_activity(self, *, action: str | None = None, since_iso: str | None = None) -> int:
        clauses: list[str] = []
        params: list[object] = []
        if action:
            clauses.append("action = ?")
            params.append(action)
        if since_iso:
            clauses.append("created_at >= ?")
            params.append(since_iso)

        where = ""
        if clauses:
            where = "WHERE " + " AND ".join(clauses)

        row = self._conn.execute(
            f"SELECT COUNT(*) AS c FROM activity_log {where}",
            tuple(params),
        ).fetchone()
        return int(row["c"] if row else 0)



def _to_conflict_record(row: sqlite3.Row) -> ConflictRecord:
    return ConflictRecord(
        conflict_id=int(row["conflict_id"]),
        created_at=str(row["created_at"]),
        rel_path=str(row["rel_path"]),
        winner_side=str(row["winner_side"]),
        loser_side=str(row["loser_side"]),
        winner_hash=str(row["winner_hash"]),
        loser_hash=str(row["loser_hash"]),
        conflict_copy_path=str(row["conflict_copy_path"]),
        restored_at=row["restored_at"],
        restored_side=row["restored_side"],
    )



def _to_snapshot_record(row: sqlite3.Row) -> SnapshotRecord:
    return SnapshotRecord(
        snapshot_id=int(row["snapshot_id"]),
        created_at=str(row["created_at"]),
        rel_path=str(row["rel_path"]),
        side=str(row["side"]),
        event_type=str(row["event_type"]),
        file_hash=str(row["file_hash"]),
        snapshot_path=str(row["snapshot_path"]),
        restored_at=row["restored_at"],
        restored_side=row["restored_side"],
    )



def _to_activity_record(row: sqlite3.Row) -> ActivityRecord:
    raw_details = row["details_json"]
    if raw_details:
        details = json.loads(str(raw_details))
    else:
        details = {}

    return ActivityRecord(
        activity_id=int(row["activity_id"]),
        created_at=str(row["created_at"]),
        action=str(row["action"]),
        rel_path=row["rel_path"],
        side=row["side"],
        details=details,
    )
