import json
import re
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4

from pydantic import BaseModel, field_validator
from sinapsis_core.data_containers.model_types.llm_types import LLMMessage

from sinapsis_chat_history.helpers.schemas import (
    CHAT_HISTORY_FILTERABLE_COLUMNS,
    ChatHistoryColumn,
    ChatHistoryFilterValue,
    ChatHistoryOrder,
    ChatHistoryOrderBy,
)

DEFAULT_TABLE_NAME = "chat_history"
SQL_IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")


class DatabaseConfig(BaseModel):
    """Common provider configuration for chat history backends.

    Attributes:
        table (str): Storage table name used by the selected provider.
    """

    table: str = DEFAULT_TABLE_NAME

    @field_validator("table")
    @classmethod
    def validate_table_name(cls, value: str) -> str:
        """Ensure the storage table name is a safe SQL identifier.

        Args:
            value (str): Candidate table name.

        Returns:
            str: Validated table name.
        """
        if not SQL_IDENTIFIER_PATTERN.fullmatch(value):
            raise ValueError(
                f"Invalid table name '{value}'. Table names must match {SQL_IDENTIFIER_PATTERN.pattern!r}."
            )
        return value


class BaseStorageProvider(ABC):
    """Abstract base provider for chat history backends."""

    def __init__(self, config: DatabaseConfig) -> None:
        self.config = config
        self.initialize()

    @staticmethod
    def _validate_scope(user_id: str | None = None, session_id: str | None = None) -> None:
        """Require at least one supported scoping identifier.

        Args:
            user_id (str | None): Optional user scope.
            session_id (str | None): Optional session scope.
        """
        if not user_id and not session_id:
            raise ValueError("At least one of 'user_id' or 'session_id' must be provided.")

    @staticmethod
    def _normalize_metadata(metadata: dict[str, Any] | None = None) -> dict[str, Any]:
        """Normalize metadata to a dictionary.

        Args:
            metadata (dict[str, Any] | None): Raw metadata value.

        Returns:
            dict[str, Any]: Normalized metadata dictionary.
        """
        if metadata is None:
            return {}
        if not isinstance(metadata, dict):
            raise TypeError("Message metadata must be a dictionary when provided.")
        return metadata

    @staticmethod
    def _serialize_content(content: Any) -> str:
        """Serialize message content into the stored text format.

        Args:
            content (Any): Raw message content.

        Returns:
            str: Serialized content.
        """
        if isinstance(content, str):
            return content
        return json.dumps(content, ensure_ascii=True, default=str)

    def _normalize_messages(
        self,
        messages: list[dict[str, Any]],
        metadata: dict[str, Any] | None = None,
        user_id: str | None = None,
        session_id: str | None = None,
    ) -> list[dict[str, Any]]:
        """Normalize template message payloads into stored chat history rows.

        Args:
            messages (list[dict[str, Any]]): Raw message payloads coming from
                the templates.
            metadata (dict[str, Any] | None): Shared metadata merged into every
                stored message.
            user_id (str | None): Optional user scope.
            session_id (str | None): Optional session scope.

        Returns:
            list[dict[str, Any]]: Normalized rows ready for backend
                persistence.
        """
        self._validate_scope(user_id=user_id, session_id=session_id)
        if not messages:
            raise ValueError("'messages' must contain at least one entry.")

        shared_metadata = self._normalize_metadata(metadata)
        normalized_rows: list[dict[str, Any]] = []
        for message in messages:
            role = message.get("role")
            if role is None:
                raise ValueError("Each message must include 'role'.")

            row_metadata = self._normalize_metadata(message.get("metadata"))
            if shared_metadata:
                row_metadata.update(shared_metadata)

            normalized_rows.append(
                {
                    ChatHistoryColumn.ID.value: str(uuid4()),
                    ChatHistoryColumn.ROLE.value: str(role),
                    ChatHistoryColumn.CONTENT.value: self._serialize_content(message.get("content")),
                    ChatHistoryColumn.METADATA.value: row_metadata,
                    ChatHistoryColumn.CREATED_AT.value: datetime.now(timezone.utc),
                }
            )
        return normalized_rows

    @classmethod
    def _build_scoped_filters(
        cls,
        filters: dict[str, ChatHistoryFilterValue] | None = None,
        user_id: str | None = None,
        session_id: str | None = None,
    ) -> dict[str, ChatHistoryFilterValue]:
        """Merge explicit scope identifiers into validated provider filters.

        Args:
            filters (dict[str, ChatHistoryFilterValue] | None): Optional
                message-level filters.
            user_id (str | None): Optional explicit user scope.
            session_id (str | None): Optional explicit session scope.

        Returns:
            dict[str, ChatHistoryFilterValue]: Validated filters with scope
                values merged in.
        """
        normalized_filters = {key: value for key, value in (filters or {}).items() if value is not None}
        invalid_filters = set(normalized_filters).difference(column.value for column in CHAT_HISTORY_FILTERABLE_COLUMNS)
        if invalid_filters:
            invalid = ", ".join(sorted(invalid_filters))
            raise ValueError(f"Unsupported chat history filters: {invalid}")

        if user_id:
            normalized_filters[ChatHistoryColumn.USER_ID.value] = user_id
        if session_id:
            normalized_filters[ChatHistoryColumn.SESSION_ID.value] = session_id

        cls._validate_scope(
            user_id=normalized_filters.get(ChatHistoryColumn.USER_ID.value),
            session_id=normalized_filters.get(ChatHistoryColumn.SESSION_ID.value),
        )
        return normalized_filters

    @staticmethod
    def _validate_pagination(
        limit: int,
        offset: int,
    ) -> None:
        """Validate shared pagination parameters.

        Args:
            limit (int): Maximum number of rows to return. Use `-1` to fetch
                all matching rows.
            offset (int): Number of rows to skip before returning results.
        """
        if limit != -1 and limit < 1:
            raise ValueError("'limit' must be greater than 0 or equal to -1 to fetch all rows.")
        if offset < 0:
            raise ValueError("'offset' must be greater than or equal to 0.")

    @abstractmethod
    def initialize(self) -> None:
        """Prepare the provider backend for chat history operations."""

    @abstractmethod
    def save_messages(
        self,
        messages: list[dict[str, Any]],
        metadata: dict[str, Any],
        user_id: str | None = None,
        session_id: str | None = None,
    ) -> int:
        """Persist chat messages and return the inserted row count.

        Args:
            messages (list[dict[str, Any]]): Messages to persist.
            metadata (dict[str, Any]): Shared metadata merged into stored
                messages.
            user_id (str | None): Optional user scope.
            session_id (str | None): Optional session scope.

        Returns:
            int: Number of inserted messages.
        """

    @abstractmethod
    def fetch_messages(
        self,
        limit: int,
        offset: int,
        order: ChatHistoryOrder,
        order_by: ChatHistoryOrderBy,
        filters: dict[str, Any],
        user_id: str | None = None,
        session_id: str | None = None,
    ) -> list[LLMMessage]:
        """Fetch chat history messages in packet-compatible format.

        Args:
            limit (int): Maximum number of messages to return.
            offset (int): Number of messages to skip.
            order (ChatHistoryOrder): Sort direction.
            order_by (ChatHistoryOrderBy): Field used for ordering results.
            filters (dict[str, Any]): Additional message-level filters.
            user_id (str | None): Optional user scope.
            session_id (str | None): Optional session scope.

        Returns:
            list[LLMMessage]: Normalized messages ready to be attached to a
                conversation packet.
        """

    @abstractmethod
    def remove_messages(
        self,
        filters: dict[str, Any],
        user_id: str | None = None,
        session_id: str | None = None,
    ) -> int:
        """Delete matching chat history messages and return the affected row count.

        Args:
            filters (dict[str, Any]): Additional message-level filters.
            user_id (str | None): Optional user scope.
            session_id (str | None): Optional session scope.

        Returns:
            int: Number of deleted messages.
        """

    @abstractmethod
    def reset(self) -> None:
        """Reset all persisted chat history data for the provider."""
