"""SQLite-backed document store for snippet metadata and raw text."""

from __future__ import annotations

import json
import logging
import sqlite3
from collections.abc import Sequence
from datetime import UTC, datetime
from pathlib import Path

from domesday.core import models

logger = logging.getLogger(__name__)

SCHEMA = """\
CREATE TABLE IF NOT EXISTS snippets (
    id          TEXT PRIMARY KEY,
    project     TEXT NOT NULL DEFAULT 'default',
    raw_text    TEXT NOT NULL,
    summary     TEXT DEFAULT '',
    snippet_type TEXT DEFAULT 'prose',
    author      TEXT DEFAULT 'anonymous',
    created_at  TEXT NOT NULL,
    updated_at  TEXT NOT NULL,
    tags        TEXT DEFAULT '[]',
    source_file TEXT,
    parent_id   TEXT,
    is_active   INTEGER DEFAULT 1
);

CREATE INDEX IF NOT EXISTS idx_snippets_project ON snippets(project);
CREATE INDEX IF NOT EXISTS idx_snippets_active ON snippets(is_active);
CREATE INDEX IF NOT EXISTS idx_snippets_project_active ON snippets(project, is_active);
CREATE INDEX IF NOT EXISTS idx_snippets_created ON snippets(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_snippets_author ON snippets(author);
"""

# Migration: add project column to existing databases
MIGRATIONS = [
    """\
    ALTER TABLE snippets ADD COLUMN project TEXT NOT NULL DEFAULT 'default';
    """,
]


class SQLiteSnippetStore:
    """SnippetStore implementation backed by a single SQLite file.

    Lazily initializes the database connection on first use.

    Usage:
        store = SQLiteSnippetStore(path="./data/domesday.db")
        store.add(snippet)  # auto-initializes
    """

    def __init__(self, *, path: str | Path) -> None:
        self.path = Path(path)
        self.path.parent.mkdir(parents=True, exist_ok=True)
        self._db: sqlite3.Connection | None = None

    def ensure_initialized(self) -> None:
        """Initialize the database connection and schema on first use."""
        if self._db is not None:
            return
        self._db = sqlite3.connect(str(self.path))
        self._db.row_factory = sqlite3.Row
        self._db.executescript(SCHEMA)
        self._apply_migrations()
        self._db.commit()
        logger.info("SQLite store initialized at %s", self.path)

    def _apply_migrations(self) -> None:
        """Apply schema migrations for existing databases."""
        for i, migration in enumerate(MIGRATIONS):
            try:
                self.db.executescript(migration)
                logger.debug("Applied migration %d", i)
            except Exception:
                logger.debug("Migration %d already applied or not needed", i)

    def close(self) -> None:
        if self._db:
            self._db.close()
            self._db = None
            logger.debug("SQLite connection closed")

    @property
    def db(self) -> sqlite3.Connection:
        if self._db is None:
            raise RuntimeError(
                "Store not initialized — call store.ensure_initialized()"
            )
        return self._db

    # ---------------------------------------------------------------
    # CRUD
    # ---------------------------------------------------------------

    def add(self, snippet: models.Snippet) -> models.Snippet:
        self.ensure_initialized()
        self.db.execute(
            """
            INSERT INTO snippets
                (id, project, raw_text, summary, snippet_type, author,
                 created_at, updated_at, tags, source_file, parent_id, is_active)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                snippet.id,
                snippet.project,
                snippet.raw_text,
                snippet.summary,
                snippet.snippet_type.value,
                snippet.author,
                snippet.created_at.isoformat(),
                snippet.updated_at.isoformat(),
                json.dumps(snippet.tags),
                snippet.source_file,
                snippet.parent_id,
                int(snippet.is_active),
            ),
        )
        self.db.commit()
        logger.debug(
            "Stored snippet %s (project=%s, author=%s, %d chars)",
            snippet.id[:8],
            snippet.project,
            snippet.author,
            len(snippet.raw_text),
        )
        return snippet

    def get(self, snippet_id: str) -> models.Snippet | None:
        self.ensure_initialized()
        cursor = self.db.execute("SELECT * FROM snippets WHERE id = ?", (snippet_id,))
        row = cursor.fetchone()
        if row:
            logger.debug("Retrieved snippet %s", snippet_id[:8])
            return self._row_to_snippet(row)
        logger.debug("Snippet %s not found", snippet_id[:8])
        return None

    def update(self, snippet: models.Snippet) -> models.Snippet:
        self.ensure_initialized()
        snippet.updated_at = datetime.now(UTC)
        self.db.execute(
            """
            UPDATE snippets SET
                project = ?, raw_text = ?, summary = ?, snippet_type = ?,
                author = ?, updated_at = ?, tags = ?, source_file = ?,
                parent_id = ?, is_active = ?
            WHERE id = ?
            """,
            (
                snippet.project,
                snippet.raw_text,
                snippet.summary,
                snippet.snippet_type.value,
                snippet.author,
                snippet.updated_at.isoformat(),
                json.dumps(snippet.tags),
                snippet.source_file,
                snippet.parent_id,
                int(snippet.is_active),
                snippet.id,
            ),
        )
        self.db.commit()
        logger.debug("Updated snippet %s", snippet.id[:8])
        return snippet

    def deactivate(self, snippet_id: str) -> None:
        self.ensure_initialized()
        self.db.execute(
            "UPDATE snippets SET is_active = 0, updated_at = ? WHERE id = ?",
            (datetime.now(UTC).isoformat(), snippet_id),
        )
        self.db.commit()
        logger.info("Deactivated snippet %s", snippet_id[:8])

    def list_recent(
        self,
        n: int = 20,
        *,
        project: str | None = None,
        author: str | None = None,
        tags: Sequence[str] | None = None,
        active_only: bool = True,
    ) -> list[models.Snippet]:
        self.ensure_initialized()
        conditions: list[str] = []
        params: list[object] = []

        if active_only:
            conditions.append("is_active = 1")
        if project is not None:
            conditions.append("project = ?")
            params.append(project)
        if author:
            conditions.append("author = ?")
            params.append(author)

        where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
        query = f"SELECT * FROM snippets {where} ORDER BY created_at DESC LIMIT ?"
        params.append(n)

        cursor = self.db.execute(query, params)
        rows = cursor.fetchall()

        snippets = [self._row_to_snippet(r) for r in rows]

        # Tag filtering done in Python since tags are JSON-encoded
        if tags:
            tag_set = set(tags)
            pre_filter = len(snippets)
            snippets = [s for s in snippets if tag_set.intersection(s.tags)]
            logger.debug(
                "Tag filter %s: %d → %d snippets",
                tags,
                pre_filter,
                len(snippets),
            )

        logger.debug(
            "list_recent(n=%d, project=%s, author=%s) → %d results",
            n,
            project,
            author,
            len(snippets),
        )
        return snippets

    def get_all_active(self, *, project: str | None = None) -> list[models.Snippet]:
        """Return all active snippets, optionally filtered by project."""
        self.ensure_initialized()
        if project is not None:
            query = "SELECT * FROM snippets WHERE is_active = 1 AND project = ? ORDER BY created_at"
            params: tuple[object, ...] = (project,)
        else:
            query = "SELECT * FROM snippets WHERE is_active = 1 ORDER BY created_at"
            params = ()

        cursor = self.db.execute(query, params)
        rows = cursor.fetchall()
        result = [self._row_to_snippet(r) for r in rows]
        logger.debug("get_all_active(project=%s) → %d snippets", project, len(result))
        return result

    def count(self, *, active_only: bool = True, project: str | None = None) -> int:
        self.ensure_initialized()
        conditions: list[str] = []
        params: list[object] = []

        if active_only:
            conditions.append("is_active = 1")
        if project is not None:
            conditions.append("project = ?")
            params.append(project)

        where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
        cursor = self.db.execute(f"SELECT COUNT(*) FROM snippets {where}", params)
        row = cursor.fetchone()
        return int(row[0]) if row else 0

    def list_projects(self) -> list[str]:
        """Return all distinct project names."""
        self.ensure_initialized()
        cursor = self.db.execute(
            "SELECT DISTINCT project FROM snippets WHERE is_active = 1 ORDER BY project"
        )
        rows = cursor.fetchall()
        return [row[0] for row in rows]

    def rename_project(self, old_name: str, new_name: str) -> int:
        """Rename all snippets from old_name to new_name. Returns count updated."""
        self.ensure_initialized()
        cursor = self.db.execute(
            "UPDATE snippets SET project = ?, updated_at = ? WHERE project = ?",
            (new_name, datetime.now(UTC).isoformat(), old_name),
        )
        self.db.commit()
        logger.info(
            "Renamed project '%s' → '%s' in SQLite (%d rows)",
            old_name,
            new_name,
            cursor.rowcount,
        )
        return cursor.rowcount

    # ---------------------------------------------------------------
    # Internal
    # ---------------------------------------------------------------

    @staticmethod
    def _row_to_snippet(row: sqlite3.Row) -> models.Snippet:
        return models.Snippet(
            id=row["id"],
            project=row["project"],
            raw_text=row["raw_text"],
            summary=row["summary"],
            snippet_type=models.SnippetType(row["snippet_type"]),
            author=row["author"],
            created_at=datetime.fromisoformat(row["created_at"]),
            updated_at=datetime.fromisoformat(row["updated_at"]),
            tags=json.loads(row["tags"]),
            source_file=row["source_file"],
            parent_id=row["parent_id"],
            is_active=bool(row["is_active"]),
        )
