"""Persona storage system for CadeCoder.

This module provides persistent storage for AI personas - custom system prompts
that can fully replace or augment the default agent behavior.
"""

import json
import sqlite3
import threading
from datetime import UTC, datetime
from functools import lru_cache
from pathlib import Path
from typing import Any

from pydantic import BaseModel, ConfigDict, Field
from ulid import ulid

from cadecoder.core.config import get_config
from cadecoder.core.errors import StorageError
from cadecoder.core.logging import log

# --- Persona Models ---


class Persona(BaseModel):
    """Persona model for custom AI behavior."""

    model_config = ConfigDict(populate_by_name=True)

    persona_id: str = Field(alias="id")
    name: str
    system_prompt: str
    description: str | None = None
    replace_base: bool = Field(
        default=False,
        description="If True, fully replaces the base system prompt. "
        "If False, prepends to the base prompt.",
    )
    created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
    last_modified_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
    metadata: dict[str, Any] = Field(default_factory=dict)

    def model_dump_db(self) -> dict[str, Any]:
        """Prepare model for DB storage."""
        dump = self.model_dump(by_alias=True)
        dump["created_at"] = dump["created_at"].isoformat()
        dump["last_modified_at"] = dump["last_modified_at"].isoformat()
        dump["metadata_json"] = json.dumps(dump.pop("metadata", {}))
        dump["replace_base"] = 1 if dump["replace_base"] else 0
        return dump

    @classmethod
    def model_validate_db(cls, data: dict[str, Any]) -> "Persona":
        """Validate data coming from DB."""
        db_data = data.copy()
        db_data["created_at"] = datetime.fromisoformat(db_data["created_at"])
        db_data["last_modified_at"] = datetime.fromisoformat(db_data["last_modified_at"])

        metadata_json = db_data.pop("metadata_json", None)
        if metadata_json:
            db_data["metadata"] = json.loads(metadata_json)

        # SQLite stores booleans as integers
        db_data["replace_base"] = bool(db_data.get("replace_base", 0))

        return cls.model_validate(db_data)


# --- SQLite Schema ---

PERSONA_SCHEMA = """
CREATE TABLE IF NOT EXISTS personas (
    id TEXT PRIMARY KEY,
    name TEXT UNIQUE NOT NULL,
    system_prompt TEXT NOT NULL,
    description TEXT,
    replace_base INTEGER DEFAULT 0,
    created_at TEXT NOT NULL,
    last_modified_at TEXT NOT NULL,
    metadata_json TEXT DEFAULT '{}'
);

CREATE INDEX IF NOT EXISTS idx_personas_name ON personas(name);
"""


# --- Storage Implementation ---


class PersonaStore:
    """SQLite-based persona storage implementation."""

    def __init__(self, db_path: str | Path | None = None):
        """Initialize persona store.

        Args:
            db_path: Path to SQLite database. If None, uses the default
                     cadecoder database path.
        """
        if db_path:
            self.db_path = Path(db_path)
        else:
            app_dir = Path(get_config().ensure_app_dir())
            self.db_path = app_dir / "cadecoder_history.db"

        self._conn: sqlite3.Connection | None = None
        self._db_lock = threading.Lock()
        self._connect()
        self._initialize_schema()
        log.debug(f"Initialized PersonaStore at {self.db_path}")

    def _connect(self) -> None:
        """Establish SQLite connection."""
        if self._conn is not None:
            return
        try:
            self._conn = sqlite3.connect(
                self.db_path, isolation_level=None, check_same_thread=False
            )
            self._conn.row_factory = sqlite3.Row
            log.debug(f"Connected to SQLite database: {self.db_path}")
        except sqlite3.Error as e:
            log.error(f"Error connecting to SQLite database: {e}")
            raise StorageError(f"Failed to connect to database: {e}") from e

    def _initialize_schema(self) -> None:
        """Create personas table if it doesn't exist."""
        if not self._conn:
            self._connect()

        try:
            self._conn.executescript(PERSONA_SCHEMA)
            log.debug("Persona schema initialized.")
        except sqlite3.Error as e:
            log.error(f"Error initializing persona schema: {e}")
            raise StorageError(f"Failed to initialize persona schema: {e}") from e

    def _execute(self, query: str, params: tuple | dict = ()) -> sqlite3.Cursor:
        """Execute a SQL query with error handling."""
        if not self._conn:
            raise StorageError("Database connection is not available.")
        with self._db_lock:
            try:
                cursor = self._conn.cursor()
                cursor.execute(query, params)
                return cursor
            except sqlite3.Error as e:
                log.error(f"SQLite error: {e}")
                raise StorageError(f"Database error: {e}") from e

    def close(self) -> None:
        """Close the database connection."""
        if self._conn:
            self._conn.close()
            self._conn = None
            log.debug("Closed PersonaStore database connection.")

    def create_persona(
        self,
        name: str,
        system_prompt: str,
        description: str | None = None,
        replace_base: bool = False,
        metadata: dict[str, Any] | None = None,
    ) -> Persona:
        """Create a new persona.

        Args:
            name: Unique name for the persona (e.g., 'code-reviewer')
            system_prompt: The system prompt text
            description: Optional description of what this persona does
            replace_base: If True, replaces the base system prompt entirely
            metadata: Optional metadata dictionary

        Returns:
            The created Persona object

        Raises:
            StorageError: If a persona with this name already exists
        """
        # Validate persona name for filesystem safety
        import re

        if not name or not re.match(r"^[a-zA-Z0-9_-]+$", name):
            raise StorageError(
                f"Invalid persona name '{name}'. "
                "Use only letters, numbers, hyphens, and underscores."
            )

        # Check for existing persona with same name
        existing = self.get_persona_by_name(name)
        if existing:
            raise StorageError(f"Persona with name '{name}' already exists.")

        persona = Persona(
            id=str(ulid()).lower(),
            name=name,
            system_prompt=system_prompt,
            description=description,
            replace_base=replace_base,
            metadata=metadata or {},
        )

        persona_data = persona.model_dump_db()
        query = """
            INSERT INTO personas (
                id, name, system_prompt, description, replace_base,
                created_at, last_modified_at, metadata_json
            )
            VALUES (
                :id, :name, :system_prompt, :description, :replace_base,
                :created_at, :last_modified_at, :metadata_json
            )
        """
        try:
            self._execute(query, persona_data)
            log.info(f"Created persona: {persona.name} (ID: {persona.persona_id})")
            return persona
        except sqlite3.IntegrityError as e:
            log.error(f"Failed to create persona: {e}")
            raise StorageError(f"Persona name '{name}' already exists.") from e

    def get_persona(self, persona_id: str) -> Persona | None:
        """Get a persona by ID.

        Args:
            persona_id: The persona's unique ID

        Returns:
            Persona object or None if not found
        """
        query = "SELECT * FROM personas WHERE id = ?"
        cursor = self._execute(query, (persona_id,))
        row = cursor.fetchone()
        cursor.close()
        if row:
            return Persona.model_validate_db(dict(row))
        return None

    def get_persona_by_name(self, name: str) -> Persona | None:
        """Get a persona by name.

        Args:
            name: The persona's unique name

        Returns:
            Persona object or None if not found
        """
        query = "SELECT * FROM personas WHERE name = ?"
        cursor = self._execute(query, (name,))
        row = cursor.fetchone()
        cursor.close()
        if row:
            return Persona.model_validate_db(dict(row))
        return None

    def list_personas(self) -> list[Persona]:
        """List all personas, ordered by name.

        Returns:
            List of Persona objects
        """
        query = "SELECT * FROM personas ORDER BY name ASC"
        cursor = self._execute(query)
        rows = cursor.fetchall()
        cursor.close()
        return [Persona.model_validate_db(dict(row)) for row in rows]

    def update_persona(
        self,
        persona_id: str,
        name: str | None = None,
        system_prompt: str | None = None,
        description: str | None = None,
        replace_base: bool | None = None,
        metadata: dict[str, Any] | None = None,
    ) -> Persona | None:
        """Update an existing persona.

        Args:
            persona_id: The persona's unique ID
            name: New name (optional)
            system_prompt: New system prompt (optional)
            description: New description (optional)
            replace_base: New replace_base value (optional)
            metadata: New metadata (optional)

        Returns:
            Updated Persona object or None if not found

        Raises:
            StorageError: If update fails (e.g., name conflict)
        """
        persona = self.get_persona(persona_id)
        if not persona:
            return None

        # Build update query dynamically
        updates = []
        params: dict[str, Any] = {"id": persona_id}

        if name is not None:
            updates.append("name = :name")
            params["name"] = name
        if system_prompt is not None:
            updates.append("system_prompt = :system_prompt")
            params["system_prompt"] = system_prompt
        if description is not None:
            updates.append("description = :description")
            params["description"] = description
        if replace_base is not None:
            updates.append("replace_base = :replace_base")
            params["replace_base"] = 1 if replace_base else 0
        if metadata is not None:
            updates.append("metadata_json = :metadata_json")
            params["metadata_json"] = json.dumps(metadata)

        if not updates:
            return persona

        # Always update last_modified_at
        updates.append("last_modified_at = :last_modified_at")
        params["last_modified_at"] = datetime.now(UTC).isoformat()

        query = f"UPDATE personas SET {', '.join(updates)} WHERE id = :id"
        try:
            cursor = self._execute(query, params)
            if cursor.rowcount == 0:
                return None
            cursor.close()
            log.info(f"Updated persona: {persona_id}")
            return self.get_persona(persona_id)
        except sqlite3.IntegrityError as e:
            raise StorageError(f"Update failed (name conflict?): {e}") from e

    def delete_persona(self, persona_id: str) -> bool:
        """Delete a persona by ID.

        Args:
            persona_id: The persona's unique ID

        Returns:
            True if deleted, False if not found
        """
        persona = self.get_persona(persona_id)
        if not persona:
            log.warning(f"Attempted to delete non-existent persona: {persona_id}")
            return False

        cursor = self._execute("DELETE FROM personas WHERE id = ?", (persona_id,))
        deleted = cursor.rowcount > 0
        cursor.close()
        if deleted:
            log.info(f"Deleted persona: {persona_id}")
        return deleted

    def find_persona(self, identifier: str) -> Persona | None:
        """Find a persona by name, ID, or partial ID.

        Args:
            identifier: Name, full ID, or partial ID prefix

        Returns:
            Persona object or None if not found/ambiguous
        """
        # Try exact name match first
        persona = self.get_persona_by_name(identifier)
        if persona:
            return persona

        # Try exact ID match
        persona = self.get_persona(identifier)
        if persona:
            return persona

        # Try partial ID match
        all_personas = self.list_personas()
        matches = [p for p in all_personas if p.persona_id.startswith(identifier)]
        if len(matches) == 1:
            return matches[0]

        return None


# --- Singleton Access ---


@lru_cache(maxsize=1)
def get_persona_store() -> PersonaStore:
    """Get the persona store instance (singleton pattern)."""
    log.debug("Initializing persona store instance...")

    try:
        get_config().ensure_app_dir()
        instance = PersonaStore()
    except StorageError as e:
        log.error(f"Failed to initialize persona store: {e}")
        raise
    except Exception as e:
        log.exception("Unexpected error initializing persona store")
        raise StorageError(f"Failed to initialize persona store: {e}") from e

    return instance
