"""Campaign and Briefing repository.

Provides typed CRUD operations for Campaign and Briefing entities.
Maps between Pydantic models and SQLite rows using model_validate()
and model_dump().
"""

from __future__ import annotations

import json
import sqlite3
from datetime import UTC, datetime

from pydantic import TypeAdapter

from unfamiliar.constants import (
    CampaignMode,
    CampaignStatus,
    CharacterArchetype,
    Theme,
)
from unfamiliar.db.connection import DatabaseError
from unfamiliar.models import Briefing, Campaign

# Type adapters for parsing database rows into models
_campaign_adapter = TypeAdapter(Campaign)
_briefing_adapter = TypeAdapter(Briefing)


class CampaignRepository:
    """Repository for Campaign and Briefing entities.

    All methods map between Pydantic models and database rows.
    Timestamps are managed automatically (created_at on insert,
    updated_at on every write).
    """

    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(self, campaign: Campaign) -> Campaign:
        """Insert a new campaign and return it with id populated.

        Args:
            campaign: A Campaign without an id.

        Returns:
            The Campaign with id and timestamps set.

        Raises:
            DatabaseError: On duplicate repo_path or insert failure.
        """
        now = datetime.now(UTC).isoformat()
        try:
            cursor = self._conn.execute(
                """
                INSERT INTO campaign (
                    repo_path, schema_version, mode, character, status, theme,
                    created_at, updated_at
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    campaign.repo_path,
                    campaign.schema_version,
                    str(campaign.mode),
                    str(campaign.character),
                    str(campaign.status),
                    str(campaign.theme),
                    now,
                    now,
                ),
            )
            self._conn.commit()
            return _campaign_adapter.validate_python(
                {
                    **campaign.model_dump(),
                    "id": cursor.lastrowid,
                    "created_at": now,
                    "updated_at": now,
                }
            )
        except sqlite3.IntegrityError as exc:
            raise DatabaseError(
                f"Campaign already exists for repo_path: {campaign.repo_path}"
            ) from exc
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to create campaign") from exc

    def get(self, campaign_id: int) -> Campaign | None:
        """Fetch a campaign by id.

        Args:
            campaign_id: The campaign's primary key.

        Returns:
            The Campaign or None if not found.
        """
        row = self._conn.execute(
            """
            SELECT id, repo_path, schema_version, mode, character, status, theme,
                   created_at, updated_at
            FROM campaign WHERE id = ?
            """,
            (campaign_id,),
        ).fetchone()
        return _row_to_campaign(row) if row else None

    def get_by_repo_path(self, repo_path: str) -> Campaign | None:
        """Fetch a campaign by repository path.

        Args:
            repo_path: The absolute path to the repository.

        Returns:
            The Campaign or None if not found.
        """
        row = self._conn.execute(
            """
            SELECT id, repo_path, schema_version, mode, character, status, theme,
                   created_at, updated_at
            FROM campaign WHERE repo_path = ?
            """,
            (repo_path,),
        ).fetchone()
        return _row_to_campaign(row) if row else None

    def update_status(self, campaign_id: int, status: CampaignStatus) -> None:
        """Update only the status field of a campaign.

        Args:
            campaign_id: The campaign's primary key.
            status: The new status value.

        Raises:
            DatabaseError: If the campaign doesn't exist.
        """
        now = datetime.now(UTC).isoformat()
        try:
            cursor = self._conn.execute(
                """
                UPDATE campaign SET status = ?, updated_at = ?
                WHERE id = ?
                """,
                (str(status), now, campaign_id),
            )
            self._conn.commit()
            if cursor.rowcount == 0:
                raise DatabaseError(f"No campaign found with id: {campaign_id}")
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to update campaign status") from exc

    def update(self, campaign: Campaign) -> None:
        """Update all mutable fields of a campaign.

        Args:
            campaign: A Campaign with id set.

        Raises:
            DatabaseError: If campaign.id is None or campaign not found.
        """
        if campaign.id is None:
            raise DatabaseError("Cannot update campaign without id")

        now = datetime.now(UTC).isoformat()
        try:
            cursor = self._conn.execute(
                """
                UPDATE campaign SET
                    repo_path = ?, schema_version = ?, mode = ?,
                    character = ?, status = ?, theme = ?, updated_at = ?
                WHERE id = ?
                """,
                (
                    campaign.repo_path,
                    campaign.schema_version,
                    str(campaign.mode),
                    str(campaign.character),
                    str(campaign.status),
                    str(campaign.theme),
                    now,
                    campaign.id,
                ),
            )
            self._conn.commit()
            if cursor.rowcount == 0:
                raise DatabaseError(f"No campaign found with id: {campaign.id}")
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to update campaign") from exc

    def delete(self, campaign_id: int) -> None:
        """Delete a campaign and all associated data (cascades).

        All related records (briefing, modules, missions, chains, etc.)
        are deleted automatically via ON DELETE CASCADE foreign keys.

        Args:
            campaign_id: The campaign's primary key.

        Raises:
            DatabaseError: If deletion fails.
        """
        try:
            cursor = self._conn.execute(
                "DELETE FROM campaign WHERE id = ?",
                (campaign_id,),
            )
            self._conn.commit()
            if cursor.rowcount == 0:
                raise DatabaseError(f"No campaign found with id: {campaign_id}")
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to delete campaign") from exc

    def create_briefing(self, briefing: Briefing) -> Briefing:
        """Insert a new briefing and return it with id populated.

        Args:
            briefing: A Briefing without an id.

        Returns:
            The Briefing with id and timestamps set.

        Raises:
            DatabaseError: On duplicate campaign_id or insert failure.
        """
        now = datetime.now(UTC).isoformat()

        try:
            cursor = self._conn.execute(
                """
                INSERT INTO briefing (
                    campaign_id, summary, tech_stack, security_posture,
                    created_at, updated_at
                ) VALUES (?, ?, ?, ?, ?, ?)
                """,
                (
                    briefing.campaign_id,
                    briefing.summary,
                    json.dumps(briefing.tech_stack),
                    briefing.security_posture,
                    now,
                    now,
                ),
            )
            self._conn.commit()
            return _briefing_adapter.validate_python(
                {
                    **briefing.model_dump(),
                    "id": cursor.lastrowid,
                    "created_at": now,
                    "updated_at": now,
                }
            )
        except sqlite3.IntegrityError as exc:
            raise DatabaseError(
                f"Briefing already exists for campaign_id: {briefing.campaign_id}"
            ) from exc
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to create briefing") from exc

    def get_briefing(self, campaign_id: int) -> Briefing | None:
        """Fetch the briefing for a campaign.

        Args:
            campaign_id: The parent campaign's id.

        Returns:
            The Briefing or None if not found.
        """
        row = self._conn.execute(
            """
            SELECT id, campaign_id, summary, tech_stack, security_posture,
                   created_at, updated_at
            FROM briefing WHERE campaign_id = ?
            """,
            (campaign_id,),
        ).fetchone()
        return _row_to_briefing(row) if row else None

    def update_briefing(self, briefing: Briefing) -> None:
        """Update an existing briefing.

        Args:
            briefing: A Briefing with id set.

        Raises:
            DatabaseError: If briefing.id is None or briefing not found.
        """
        if briefing.id is None:
            raise DatabaseError("Cannot update briefing without id")

        now = datetime.now(UTC).isoformat()
        try:
            self._conn.execute(
                """
                UPDATE briefing SET
                    summary = ?, tech_stack = ?, security_posture = ?,
                    updated_at = ?
                WHERE id = ?
                """,
                (
                    briefing.summary,
                    json.dumps(briefing.tech_stack),
                    briefing.security_posture,
                    now,
                    briefing.id,
                ),
            )
            self._conn.commit()
        except sqlite3.Error as exc:
            raise DatabaseError("Failed to update briefing") from exc


def _row_to_campaign(row: sqlite3.Row) -> Campaign:
    """Convert a database row to a Campaign model."""
    return _campaign_adapter.validate_python(
        {
            "id": row["id"],
            "repo_path": row["repo_path"],
            "schema_version": row["schema_version"],
            "mode": CampaignMode(row["mode"]),
            "character": CharacterArchetype(row["character"]),
            "status": CampaignStatus(row["status"]),
            "theme": Theme(row["theme"]),
            "created_at": row["created_at"],
            "updated_at": row["updated_at"],
        }
    )


def _row_to_briefing(row: sqlite3.Row) -> Briefing:
    """Convert a database row to a Briefing model."""
    tech_stack = json.loads(row["tech_stack"]) if row["tech_stack"] else []
    return _briefing_adapter.validate_python(
        {
            "id": row["id"],
            "campaign_id": row["campaign_id"],
            "summary": row["summary"],
            "tech_stack": tech_stack,
            "security_posture": row["security_posture"],
            "created_at": row["created_at"],
            "updated_at": row["updated_at"],
        }
    )
