"""File snapshot repository.

Provides typed CRUD operations for FileSnapshot entities.
Supports upsert for re-analysis scenarios.
"""

from __future__ import annotations

import logging
import sqlite3
from datetime import UTC, datetime

from pydantic import TypeAdapter

from unfamiliar.db.connection import DatabaseError
from unfamiliar.models import FileSnapshot

logger = logging.getLogger(__name__)

# Type adapter for parsing database rows into models
_snapshot_adapter = TypeAdapter(FileSnapshot)


class FileSnapshotRepository:
    """Repository for FileSnapshot entities.

    Tracks file state at analysis time for change detection.
    Supports upsert for re-analysis when files change.
    """

    def __init__(self, conn: sqlite3.Connection) -> None:
        """Initialize repository with a database connection.

        Args:
            conn: A sqlite3.Connection with Row factory enabled.
        """
        self._conn = conn

    def create_many(self, snapshots: list[FileSnapshot]) -> None:
        """Bulk insert file snapshots.

        Args:
            snapshots: List of FileSnapshots without ids.

        Raises:
            DatabaseError: On insert failure.
        """
        if not snapshots:
            return

        now = datetime.now(UTC).isoformat()
        rows = [
            (
                s.campaign_id,
                s.file_path,
                s.file_hash,
                s.last_analyzed.isoformat() if s.last_analyzed else now,
            )
            for s in snapshots
        ]

        try:
            self._conn.executemany(
                """
                INSERT INTO file_snapshot (
                    campaign_id, file_path, file_hash, last_analyzed
                ) VALUES (?, ?, ?, ?)
                """,
                rows,
            )
            self._conn.commit()
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to create file snapshots") from exc

    def get_by_campaign(self, campaign_id: int) -> list[FileSnapshot]:
        """Fetch all file snapshots for a campaign.

        Args:
            campaign_id: The parent campaign's id.

        Returns:
            List of FileSnapshots (may be empty).
        """
        rows = self._conn.execute(
            """
            SELECT id, campaign_id, file_path, file_hash, last_analyzed
            FROM file_snapshot WHERE campaign_id = ? ORDER BY file_path
            """,
            (campaign_id,),
        ).fetchall()
        return [_row_to_snapshot(row) for row in rows]

    def get_by_path(self, campaign_id: int, file_path: str) -> FileSnapshot | None:
        """Fetch a file snapshot by path.

        Args:
            campaign_id: The parent campaign's id.
            file_path: The file's path relative to repo root.

        Returns:
            The FileSnapshot or None if not found.
        """
        row = self._conn.execute(
            """
            SELECT id, campaign_id, file_path, file_hash, last_analyzed
            FROM file_snapshot WHERE campaign_id = ? AND file_path = ?
            """,
            (campaign_id, file_path),
        ).fetchone()
        return _row_to_snapshot(row) if row else None

    def upsert(self, snapshot: FileSnapshot) -> None:
        """Insert or update a file snapshot.

        Uses INSERT OR REPLACE to handle re-analysis when files change.
        The unique index on (campaign_id, file_path) ensures one snapshot
        per file per campaign.

        Args:
            snapshot: A FileSnapshot (id is ignored if set).

        Raises:
            DatabaseError: On insert failure.
        """
        now = datetime.now(UTC).isoformat()
        last_analyzed = (
            snapshot.last_analyzed.isoformat() if snapshot.last_analyzed else now
        )

        try:
            self._conn.execute(
                """
                INSERT OR REPLACE INTO file_snapshot (
                    campaign_id, file_path, file_hash, last_analyzed
                ) VALUES (?, ?, ?, ?)
                """,
                (
                    snapshot.campaign_id,
                    snapshot.file_path,
                    snapshot.file_hash,
                    last_analyzed,
                ),
            )
            self._conn.commit()
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to upsert file snapshot") from exc


def _row_to_snapshot(row: sqlite3.Row) -> FileSnapshot:
    """Convert a database row to a FileSnapshot model."""

    last_analyzed = None
    if row["last_analyzed"]:
        try:
            last_analyzed = datetime.fromisoformat(row["last_analyzed"])
        except ValueError:
            logger.warning("Could not parse last_analyzed: %r", row["last_analyzed"])

    return _snapshot_adapter.validate_python(
        {
            "id": row["id"],
            "campaign_id": row["campaign_id"],
            "file_path": row["file_path"],
            "file_hash": row["file_hash"],
            "last_analyzed": last_analyzed,
        }
    )
