"""Concrete session service implementation.

Extends the ``SessionService`` ABC from the domain layer with database-backed
persistence using ``SessionRepository`` and ``SessionMessageRepository``.

Based on ADR-007 (Repository Pattern) and the session domain model.
"""

from __future__ import annotations

import hashlib
import json
from datetime import datetime
from typing import TYPE_CHECKING, Any

import structlog
from ulid import ULID

from cleveragents.application.services.prompt_sanitizer import PromptSanitizer
from cleveragents.domain.models.core.session import (
    EXPORT_SCHEMA_VERSION,
    MessageRole,
    Session,
    SessionImportError,
    SessionMessage,
    SessionNotFoundError,
    SessionService,
    SessionTokenUsage,
)
from cleveragents.infrastructure.database.repositories import (
    SessionMessageRepository,
    SessionRepository,
)
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType

if TYPE_CHECKING:
    from cleveragents.infrastructure.events.protocol import EventBus

_logger = structlog.get_logger(__name__)


class PersistentSessionService(SessionService):
    """Database-backed session service.

    Implements the ``SessionService`` ABC using ``SessionRepository`` and
    ``SessionMessageRepository`` for persistence.  All operations delegate
    to the repositories which flush but do not commit; callers must manage
    transactions via the UnitOfWork pattern.
    """

    def __init__(
        self,
        session_repo: SessionRepository,
        message_repo: SessionMessageRepository,
        event_bus: EventBus | None = None,
    ) -> None:
        """Initialise with repository instances.

        Args:
            session_repo: Repository for session CRUD.
            message_repo: Repository for message operations.
            event_bus: Optional EventBus for domain event emission.
        """
        self._session_repo = session_repo
        self._message_repo = message_repo
        self._sanitizer = PromptSanitizer()
        self._event_bus = event_bus

    def create(self, actor_name: str | None = None) -> Session:
        """Create a new session.

        Args:
            actor_name: Optional namespaced actor to bind to the session.

        Returns:
            A newly created Session with a fresh ULID.
        """
        session = Session(
            session_id=str(ULID()),
            actor_name=actor_name,
        )
        self._session_repo.create(session)
        if self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.SESSION_CREATED,
                        session_id=session.session_id,
                        actor_name=actor_name,
                        details={
                            "session_id": session.session_id,
                            "actor_name": actor_name,
                        },
                    )
                )
            except Exception:
                _logger.warning("audit_emit_failed", event_type="SESSION_CREATED")
        return session

    def get(self, session_id: str) -> Session:
        """Retrieve a session by ID.

        Args:
            session_id: The ULID of the session to retrieve.

        Returns:
            The Session with the given ID.

        Raises:
            SessionNotFoundError: If no session with the given ID exists.
        """
        result = self._session_repo.get_by_id(session_id)
        if result is None:
            raise SessionNotFoundError(f"Session '{session_id}' not found")
        return result  # type: ignore[return-value]

    def list(self) -> list[Session]:
        """List all sessions.

        Returns:
            A list of all sessions, ordered by creation time.
        """
        return self._session_repo.list_all()  # type: ignore[return-value]

    def delete(self, session_id: str) -> None:
        """Delete a session by ID.

        Args:
            session_id: The ULID of the session to delete.

        Raises:
            SessionNotFoundError: If no session with the given ID exists.
        """
        deleted = self._session_repo.delete(session_id)
        if not deleted:
            raise SessionNotFoundError(f"Session '{session_id}' not found")
        if self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.ENTITY_DELETED,
                        session_id=session_id,
                        details={
                            "entity_type": "session",
                            "entity_name": session_id,
                        },
                    )
                )
            except Exception:
                _logger.warning("audit_emit_failed", event_type="ENTITY_DELETED")

    def append_message(
        self,
        session_id: str,
        role: MessageRole,
        content: str,
        metadata: dict[str, Any] | None = None,
    ) -> SessionMessage:
        """Append a message to a session.

        Args:
            session_id: The ULID of the target session.
            role: The message role.
            content: The message content.
            metadata: Optional metadata dict.

        Returns:
            The newly created SessionMessage.

        Raises:
            SessionNotFoundError: If no session with the given ID exists.
        """
        session = self._session_repo.get_by_id(session_id)
        if session is None:
            raise SessionNotFoundError(f"Session '{session_id}' not found")

        # Sanitize user-provided content before storage (mechanism 1)
        if role == MessageRole.USER:
            result = self._sanitizer.sanitize_user_input(content)
            content = result.sanitized

        # Determine next sequence number
        count = self._message_repo.count_for_session(session_id)

        message = SessionMessage(
            message_id=str(ULID()),
            role=role,
            content=content,
            sequence=count,
            timestamp=datetime.now(),
            metadata=metadata or {},
        )

        self._message_repo.append(session_id, message)

        # Update session timestamp
        session.updated_at = datetime.now()
        self._session_repo.update(session)

        # Emit SESSION_MESSAGE_SENT event
        if self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.SESSION_MESSAGE_SENT,
                        session_id=session_id,
                        details={
                            "session_id": session_id,
                            "message_id": message.message_id,
                            "role": role.value if hasattr(role, "value") else str(role),
                        },
                    )
                )
            except Exception:
                _logger.warning("audit_emit_failed", event_type="SESSION_MESSAGE_SENT")

        return message

    def export_session(self, session_id: str) -> dict[str, Any]:
        """Export a session as a JSON-serializable dict.

        Args:
            session_id: The ULID of the session to export.

        Returns:
            A dict containing the full session data with checksum.

        Raises:
            SessionNotFoundError: If no session with the given ID exists.
        """
        session = self._session_repo.get_by_id(session_id)
        if session is None:
            raise SessionNotFoundError(f"Session '{session_id}' not found")

        # Load all messages
        messages = self._message_repo.get_for_session(session_id)
        session.messages = messages

        return session.as_export_dict()

    def import_session(self, data: dict[str, Any]) -> Session:
        """Import a session from an export dict.

        Args:
            data: A dict previously produced by export_session.

        Returns:
            The imported Session object.

        Raises:
            SessionImportError: If import fails (bad schema, corrupt data).
        """
        # Validate schema version
        schema_version = data.get("schema_version")
        if schema_version != EXPORT_SCHEMA_VERSION:
            raise SessionImportError(
                f"Unsupported schema version: {schema_version} "
                f"(expected {EXPORT_SCHEMA_VERSION})"
            )

        # Validate checksum
        raw_checksum = data.get("checksum")
        if raw_checksum is None:
            raise SessionImportError("Missing checksum in import data")
        checksum = "sha256:" + raw_checksum

        # Recompute checksum
        data_without_checksum = {k: v for k, v in data.items() if k != "checksum"}
        canonical = json.dumps(data_without_checksum, sort_keys=True, default=str)
        expected_checksum = "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()
        if checksum != expected_checksum:
            raise SessionImportError("Checksum verification failed")

        try:
            # Parse token usage
            token_data = data.get("token_usage", {})
            token_usage = SessionTokenUsage(
                input_tokens=token_data.get("input_tokens", 0),
                output_tokens=token_data.get("output_tokens", 0),
                estimated_cost=token_data.get("estimated_cost", 0.0),
            )

            # Parse messages (generate new ULIDs for imported messages)
            messages: list[SessionMessage] = []
            for msg_data in data.get("messages", []):
                messages.append(
                    SessionMessage(
                        message_id=str(ULID()),
                        role=MessageRole(msg_data["role"]),
                        content=msg_data["content"],
                        sequence=msg_data["sequence"],
                        timestamp=datetime.fromisoformat(msg_data["timestamp"]),
                        metadata=msg_data.get("metadata", {}),
                        tool_call_id=msg_data.get("tool_call_id"),
                    )
                )

            # Create session with new ULID (imported sessions get fresh IDs)
            session = Session(
                session_id=str(ULID()),
                actor_name=data.get("actor_name"),
                namespace=data.get("namespace", "local"),
                linked_plan_ids=data.get("linked_plan_ids", []),
                token_usage=token_usage,
                metadata=data.get("metadata", {}),
                messages=messages,
            )

            # Persist session (without messages initially)
            self._session_repo.create(
                Session(
                    session_id=session.session_id,
                    actor_name=session.actor_name,
                    namespace=session.namespace,
                    linked_plan_ids=session.linked_plan_ids,
                    token_usage=session.token_usage,
                    metadata=session.metadata,
                )
            )

            # Persist messages
            for msg in messages:
                self._message_repo.append(session.session_id, msg)

            return session

        except (KeyError, ValueError, TypeError) as exc:
            raise SessionImportError(f"Invalid import data: {exc}") from exc

    def update_token_usage(
        self,
        session_id: str,
        input_tokens: int,
        output_tokens: int,
        cost: float,
    ) -> None:
        """Update token usage counters for a session.

        Args:
            session_id: The ULID of the session to update.
            input_tokens: Number of input tokens to add.
            output_tokens: Number of output tokens to add.
            cost: Estimated cost to add (USD).

        Raises:
            SessionNotFoundError: If no session with the given ID exists.
        """
        session = self._session_repo.get_by_id(session_id)
        if session is None:
            raise SessionNotFoundError(f"Session '{session_id}' not found")

        session.token_usage.input_tokens += input_tokens
        session.token_usage.output_tokens += output_tokens
        session.token_usage.estimated_cost += cost
        session.updated_at = datetime.now()

        self._session_repo.update(session)
