"""Service layer for managing actor configurations."""

from __future__ import annotations

import os
from datetime import datetime
from typing import TYPE_CHECKING, Any

import structlog

from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import (
    BusinessRuleViolation,
    NotFoundError,
    ValidationError,
)
from cleveragents.domain.models.core import Actor
from cleveragents.infrastructure.database.unit_of_work import UnitOfWork
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 ActorService:
    """Coordinate actor persistence and default selection rules."""

    def __init__(
        self,
        settings: Settings,
        unit_of_work: UnitOfWork,
        event_bus: EventBus | None = None,
    ):
        self.settings = settings
        self.unit_of_work = unit_of_work
        self._event_bus = event_bus

    def _normalize_name(self, name: str, *, allow_built_in: bool = False) -> str:
        normalized = name.strip()
        if not normalized:
            raise ValidationError("Actor name cannot be empty")
        if "/" not in normalized:
            normalized = f"local/{normalized}"
        if normalized.count("/") != 1:
            raise ValidationError("Actor names must include exactly one '/' separator")

        prefix, identifier = normalized.split("/", 1)
        if not prefix or not identifier:
            raise ValidationError("Actor names must include both prefix and identifier")
        if prefix != "local" and not allow_built_in:
            raise ValidationError(
                "Custom actors must use the 'local/<id>' naming pattern"
            )
        return normalized

    def list_actors(self) -> list[Actor]:
        """Return all actors ordered by name."""

        with self.unit_of_work.transaction() as ctx:
            return ctx.actors.list_all()

    def get_actor(self, name: str) -> Actor:
        """Retrieve an actor by name or raise when missing."""

        normalized = self._normalize_name(name, allow_built_in=True)
        with self.unit_of_work.transaction() as ctx:
            actor = ctx.actors.get_by_name(normalized)
            if not actor:
                raise NotFoundError(resource_type="actor", resource_id=normalized)
            return actor

    def upsert_actor(
        self,
        *,
        name: str,
        provider: str,
        model: str,
        config_blob: dict[str, Any] | None = None,
        graph_descriptor: dict[str, Any] | None = None,
        unsafe: bool = False,
        set_default: bool = False,
        is_built_in: bool = False,
        yaml_text: str | None = None,
        schema_version: str | None = None,
        compiled_metadata: dict[str, Any] | None = None,
    ) -> Actor:
        """Create or update an actor configuration."""

        normalized = self._normalize_name(name, allow_built_in=True)
        blob = config_blob or {}
        config_hash = Actor.compute_hash(blob)
        now = datetime.now()

        with self.unit_of_work.transaction() as ctx:
            existing = ctx.actors.get_by_name(normalized)
            if existing and existing.is_built_in and not is_built_in:
                raise BusinessRuleViolation(
                    "Cannot overwrite built-in actor with custom entry"
                )

            prefix, _ = normalized.split("/", 1)
            if prefix != "local" and not is_built_in:
                raise ValidationError(
                    "Custom actors must use the 'local/<id>' naming pattern"
                )

            should_be_default = set_default or (
                existing.is_default if existing else False
            )
            actor = Actor(
                id=existing.id if existing else None,
                name=normalized,
                provider=provider,
                model=model,
                config_blob=blob,
                config_hash=config_hash,
                graph_descriptor=graph_descriptor,
                yaml_text=yaml_text,
                schema_version=schema_version or "1.0",
                compiled_metadata=compiled_metadata,
                unsafe=unsafe,
                is_built_in=is_built_in
                or (existing.is_built_in if existing else False),
                is_default=should_be_default,
                created_at=existing.created_at if existing else now,
                updated_at=now,
            )

            saved = ctx.actors.upsert(actor)
            if set_default:
                saved = ctx.actors.set_default(saved.name)
            return saved

    def remove_actor(self, name: str) -> None:
        """Remove a custom actor if allowed."""

        normalized = self._normalize_name(name, allow_built_in=False)
        with self.unit_of_work.transaction() as ctx:
            actor = ctx.actors.get_by_name(normalized)
            if not actor:
                raise NotFoundError(resource_type="actor", resource_id=normalized)

            try:
                ctx.actors.delete(normalized)
            except ValueError as exc:  # Built-in or default guard
                raise BusinessRuleViolation(str(exc)) from exc
        if self._event_bus is not None:
            try:
                self._event_bus.emit(
                    DomainEvent(
                        event_type=EventType.ENTITY_DELETED,
                        details={
                            "entity_type": "actor",
                            "entity_name": normalized,
                        },
                    )
                )
            except Exception:
                _logger.warning("audit_emit_failed", event_type="ENTITY_DELETED")

    def set_default_actor(self, name: str) -> Actor:
        """Mark a built-in or custom actor as the default entry."""

        normalized = self._normalize_name(name, allow_built_in=True)
        with self.unit_of_work.transaction() as ctx:
            actor = ctx.actors.get_by_name(normalized)
            if not actor:
                raise NotFoundError(resource_type="actor", resource_id=normalized)
            return ctx.actors.set_default(normalized)

    def get_default_actor(self) -> Actor | None:
        """Return the current default actor if present."""

        with self.unit_of_work.transaction() as ctx:
            return ctx.actors.get_default()

    def ensure_default_mock_actor(self, *, force: bool = False) -> Actor | None:
        """Ensure a default mock actor exists in testing mode.

        This is idempotent and only runs when CLEVERAGENTS_TESTING_USE_MOCK_AI is true
        unless ``force`` is set to True (used by tests to provision a fallback actor).
        """
        if not force and os.environ.get(
            "CLEVERAGENTS_TESTING_USE_MOCK_AI", ""
        ).lower() not in (
            "1",
            "true",
            "yes",
        ):
            return None

        with self.unit_of_work.transaction() as ctx:
            existing_default = ctx.actors.get_default()
            if existing_default:
                return existing_default

        # No default yet: create or promote a mock actor
        now = datetime.now()
        mock_name = "local/mock-default"
        mock_actor = Actor(
            id=None,
            name=mock_name,
            provider="MockProvider",
            model="mock-gpt-4",
            config_blob={},
            config_hash=Actor.compute_hash({}),
            graph_descriptor=None,
            unsafe=True,
            is_built_in=True,
            is_default=True,
            created_at=now,
            updated_at=now,
        )

        with self.unit_of_work.transaction() as ctx:
            existing = ctx.actors.get_by_name(mock_name)
            if existing:
                if not existing.is_default:
                    ctx.actors.set_default(existing.name)
                return ctx.actors.get_default()

            saved = ctx.actors.upsert(mock_actor)
            ctx.actors.set_default(saved.name)
            return ctx.actors.get_default()
