"""
Service generator - generates domain service stubs from ServiceSpec.

This module creates service classes that implement domain operations.
Services handle business logic and can be customized by users.
"""

from __future__ import annotations  # required: forward reference

import asyncio
import builtins
import logging
from abc import ABC, abstractmethod
from collections.abc import Callable
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, Generic, TypeVar
from uuid import UUID, uuid4

from pydantic import BaseModel

from dazzle_back.specs.entity import EntitySpec, StateMachineSpec
from dazzle_back.specs.service import (
    ServiceSpec,
)

if TYPE_CHECKING:
    from dazzle_back.runtime.repository import Repository

logger = logging.getLogger("dazzle.service")

# =============================================================================
# Type Variables
# =============================================================================

T = TypeVar("T", bound=BaseModel)
CreateT = TypeVar("CreateT", bound=BaseModel)
UpdateT = TypeVar("UpdateT", bound=BaseModel)


# =============================================================================
# Base Service Classes
# =============================================================================


class BaseService(ABC, Generic[T]):
    """
    Abstract base service class.

    Provides the interface for all services. Subclasses implement
    specific domain operations.
    """

    @abstractmethod
    async def execute(self, **kwargs: Any) -> Any:
        """Execute the service operation."""
        ...


# Callback type for entity lifecycle events
EntityEventCallback = Callable[
    [str, str, dict[str, Any], dict[str, Any] | None],  # entity_name, entity_id, data, old_data
    Any,  # Return type (usually Awaitable[list[str]] for process IDs)
]


class CRUDService(BaseService[T], Generic[T, CreateT, UpdateT]):
    """
    Generic CRUD service implementation.

    Provides standard create, read, update, delete, and list operations.
    Supports both in-memory storage (for testing) and database persistence
    via the repository pattern.

    Entity lifecycle events (v0.24.0):
    Services can register callbacks to be notified when entities are
    created, updated, or deleted. This enables process triggering.
    """

    def __init__(
        self,
        entity_name: str,
        model_class: type[T],
        create_schema: type[CreateT],
        update_schema: type[UpdateT],
        repository: Repository[T] | None = None,
        state_machine: StateMachineSpec | None = None,
        entity_spec: EntitySpec | None = None,
    ):
        self.entity_name = entity_name
        self.model_class = model_class
        self.create_schema = create_schema
        self.update_schema = update_schema
        self.state_machine = state_machine
        self.entity_spec = entity_spec

        # Repository for database persistence
        self._repository = repository

        # In-memory store as fallback (for testing without database)
        self._store: dict[UUID, T] = {}

        # Lifecycle event callbacks (v0.24.0)
        self._on_created_callbacks: list[EntityEventCallback] = []
        self._on_updated_callbacks: list[EntityEventCallback] = []
        self._on_deleted_callbacks: list[EntityEventCallback] = []

        # Pre-operation hooks (v0.29.0 — can modify data or cancel operations)
        self._pre_create_hooks: list[Callable[..., Any]] = []
        self._pre_update_hooks: list[Callable[..., Any]] = []
        self._pre_delete_hooks: list[Callable[..., Any]] = []

    def on_created(self, callback: EntityEventCallback) -> None:
        """Register a callback to be called after entity creation."""
        self._on_created_callbacks.append(callback)

    def on_updated(self, callback: EntityEventCallback) -> None:
        """Register a callback to be called after entity update."""
        self._on_updated_callbacks.append(callback)

    def on_deleted(self, callback: EntityEventCallback) -> None:
        """Register a callback to be called after entity deletion."""
        self._on_deleted_callbacks.append(callback)

    def add_pre_create_hook(self, hook: Callable[..., Any]) -> None:
        """Register a pre-create hook. Hook receives (entity_name, data) → data."""
        self._pre_create_hooks.append(hook)

    def add_pre_update_hook(self, hook: Callable[..., Any]) -> None:
        """Register a pre-update hook. Hook receives (entity_name, id, data, old_data) → data."""
        self._pre_update_hooks.append(hook)

    def add_pre_delete_hook(self, hook: Callable[..., Any]) -> None:
        """Register a pre-delete hook. Hook receives (entity_name, id, data) → bool."""
        self._pre_delete_hooks.append(hook)

    async def _notify_callbacks(
        self,
        callbacks: list[EntityEventCallback],
        entity_id: str,
        entity_data: dict[str, Any],
        old_data: dict[str, Any] | None,
        event_label: str,
    ) -> None:
        """Invoke lifecycle callbacks, logging but not raising on failure."""
        for callback in callbacks:
            try:
                result = callback(self.entity_name, entity_id, entity_data, old_data)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as e:
                logger.warning(
                    "Entity %s callback failed for %s: %s", event_label, self.entity_name, e
                )

    async def _notify_created(self, entity_id: str, entity_data: dict[str, Any]) -> None:
        """Notify all on_created callbacks."""
        await self._notify_callbacks(
            self._on_created_callbacks, entity_id, entity_data, None, "created"
        )

    async def _notify_updated(
        self, entity_id: str, entity_data: dict[str, Any], old_data: dict[str, Any]
    ) -> None:
        """Notify all on_updated callbacks."""
        await self._notify_callbacks(
            self._on_updated_callbacks, entity_id, entity_data, old_data, "updated"
        )

    async def _notify_deleted(self, entity_id: str, entity_data: dict[str, Any]) -> None:
        """Notify all on_deleted callbacks."""
        await self._notify_callbacks(
            self._on_deleted_callbacks, entity_id, entity_data, None, "deleted"
        )

    def set_repository(self, repository: Repository[T]) -> None:
        """
        Set the repository for persistent storage.

        Args:
            repository: Repository instance
        """
        self._repository = repository

    async def execute(self, **kwargs: Any) -> Any:
        """Route to the appropriate operation."""
        operation: str = kwargs.pop("operation", "")
        operations: dict[str, Callable[..., Any]] = {
            "create": self.create,
            "read": self.read,
            "update": self.update,
            "delete": self.delete,
            "list": self.list,
        }
        if operation not in operations:
            raise ValueError(f"Unknown operation: {operation}")
        return await operations[operation](**kwargs)

    async def create(self, data: CreateT) -> T:
        """
        Create a new entity.

        Validates:
        - Invariants are satisfied
        - Foreign key references exist

        Applies:
        - Default values for missing fields
        """
        from dazzle_back.runtime.invariant_evaluator import (
            InvariantViolationError,
            check_invariants_for_create,
        )

        # Sanitize string/text input fields (v0.25 - #135)
        self._sanitize_fields(data)

        # Generate ID
        entity_id = uuid4()

        # Run pre-create hooks (v0.29.0)
        data_dict = data.model_dump()
        for hook in self._pre_create_hooks:
            try:
                result = hook(self.entity_name, data_dict)
                if asyncio.iscoroutine(result):
                    result = await result
                if isinstance(result, dict):
                    data_dict = result
            except Exception as e:
                logger.warning("Pre-create hook failed for %s: %s", self.entity_name, e)

        # Build entity data
        entity_data = {"id": entity_id, **data_dict}

        # Inject auto_add timestamps (v0.25 - #132)
        if self.entity_spec:
            now = datetime.now(UTC)
            for field in self.entity_spec.fields:
                if field.auto_add:
                    entity_data[field.name] = now

        # Apply default values for fields not provided (v0.14.2)
        if self.entity_spec:
            from dazzle.core.ir.params import ParamRef

            for field in self.entity_spec.fields:
                if field.name not in entity_data or entity_data[field.name] is None:
                    if field.default is not None:
                        # ParamRef: use the declared default, not the ref object (#641)
                        default = field.default
                        if isinstance(default, ParamRef):
                            default = default.default
                        if default is not None:
                            entity_data[field.name] = default

        # Validate invariants (v0.14.2)
        if self.entity_spec and self.entity_spec.invariants:
            try:
                check_invariants_for_create(self.entity_spec.invariants, entity_data)
            except InvariantViolationError:
                raise  # Re-raise as-is

        # Validate foreign key references (v0.14.2)
        if self.entity_spec and self._repository:
            await self._validate_references(entity_data)

        # Use repository if available
        if self._repository:
            entity = await self._repository.create(entity_data)
        else:
            # Fallback to in-memory
            entity = self.model_class(**entity_data)
            self._store[entity_id] = entity

        # Notify callbacks (v0.24.0 - process triggering)
        await self._notify_created(str(entity_id), entity_data)

        return entity

    async def read(self, id: UUID, include: list[str] | None = None) -> T | dict[str, Any] | None:
        """Read an entity by ID, optionally eager-loading relations."""
        if self._repository:
            return await self._repository.read(id, include=include)
        return self._store.get(id)

    async def update(
        self,
        id: UUID,
        data: UpdateT,
        user_roles: list[str] | None = None,
        current_user: str | None = None,
        is_superuser: bool = False,
    ) -> T | dict[str, Any] | None:
        """
        Update an existing entity.

        Validates:
        - State machine transitions are valid
        - Invariants are satisfied after update
        - Foreign key references exist

        Args:
            id: Entity ID to update
            data: Update data
            user_roles: User's roles for state machine role guard checks
            current_user: Current user identifier for field comparison guards

        Returns:
            Updated entity or None if not found

        Raises:
            TransitionError: If state machine transition is invalid
            InvariantViolationError: If invariant constraint is violated
            ReferenceNotFoundError: If referenced entity doesn't exist
        """
        from dazzle_back.runtime.invariant_evaluator import (
            InvariantViolationError,
            check_invariants_for_update,
        )
        from dazzle_back.runtime.state_machine import (
            validate_status_update,
        )

        # Sanitize string/text input fields (v0.25 - #135)
        self._sanitize_fields(data)

        # Get update data, excluding None values
        update_data = {k: v for k, v in data.model_dump().items() if v is not None}

        # Inject auto_update timestamps (v0.25 - #132)
        if self.entity_spec:
            now = datetime.now(UTC)
            for field in self.entity_spec.fields:
                if field.auto_update:
                    update_data[field.name] = now

        # Read current entity for state machine validation
        current = await self.read(id)
        if current is None:
            return None

        current_data = current.model_dump() if hasattr(current, "model_dump") else dict(current)

        # Run pre-update hooks (v0.29.0)
        for hook in self._pre_update_hooks:
            try:
                result = hook(self.entity_name, str(id), update_data, current_data)
                if asyncio.iscoroutine(result):
                    result = await result
                if isinstance(result, dict):
                    update_data = result
            except Exception as e:
                logger.warning("Pre-update hook failed for %s: %s", self.entity_name, e)

        # Validate state machine transition if entity has a state machine
        if self.state_machine:
            # Construct GrantStore for has_grant() guards if DB is available
            _grant_store = None
            if self._repository:
                try:
                    from dazzle_back.runtime.grant_store import GrantStore

                    # db.connection() is a context manager — must enter it
                    _grant_conn = self._repository.db.connection()
                    _conn = _grant_conn.__enter__()
                    _grant_store = GrantStore(_conn)
                except Exception:
                    logger.debug(
                        "GrantStore init failed — has_grant() will return False", exc_info=True
                    )
            try:
                result = validate_status_update(
                    self.state_machine,
                    current_data,
                    update_data,
                    user_roles,
                    current_user,
                    is_superuser=is_superuser,
                    grant_store=_grant_store,
                )
                if result is not None and not result.is_valid:
                    raise result.error  # type: ignore
            finally:
                if _grant_store is not None:
                    try:
                        _grant_conn.__exit__(None, None, None)
                    except Exception:
                        pass

        # Validate invariants after update (v0.14.2)
        if self.entity_spec and self.entity_spec.invariants:
            try:
                check_invariants_for_update(
                    self.entity_spec.invariants,
                    current_data,
                    update_data,
                )
            except InvariantViolationError:
                raise  # Re-raise as-is

        # Validate foreign key references (v0.14.2)
        if self.entity_spec and self._repository:
            await self._validate_references(update_data)

        if self._repository:
            updated = await self._repository.update(id, update_data)
        else:
            # Fallback to in-memory
            existing = self._store.get(id)
            if not existing:
                return None

            # Merge with existing
            merged_data = {**existing.model_dump(), **update_data}

            # Create updated instance
            updated = self.model_class(**merged_data)
            self._store[id] = updated

        if updated is not None:
            # Get updated entity data for notification
            updated_data = updated.model_dump() if hasattr(updated, "model_dump") else dict(updated)
            # Notify callbacks (v0.24.0 - process triggering)
            await self._notify_updated(str(id), updated_data, current_data)

        return updated

    async def delete(self, id: UUID) -> bool:
        """Delete an entity by ID."""
        # Read entity data before deletion for notification
        entity = await self.read(id)
        if entity is None:
            return False

        entity_data = entity.model_dump() if hasattr(entity, "model_dump") else dict(entity)

        # Run pre-delete hooks (v0.29.0) — can cancel deletion
        for hook in self._pre_delete_hooks:
            try:
                result = hook(self.entity_name, str(id), entity_data)
                if asyncio.iscoroutine(result):
                    result = await result
                if result is False:
                    logger.info("Pre-delete hook cancelled deletion of %s %s", self.entity_name, id)
                    return False
            except Exception as e:
                logger.warning("Pre-delete hook failed for %s: %s", self.entity_name, e)

        if self._repository:
            deleted = await self._repository.delete(id)
        else:
            if id in self._store:
                del self._store[id]
                deleted = True
            else:
                deleted = False

        if deleted:
            # Notify callbacks (v0.24.0 - process triggering)
            await self._notify_deleted(str(id), entity_data)

        return deleted

    async def list(
        self,
        page: int = 1,
        page_size: int = 20,
        filters: dict[str, Any] | None = None,
        sort: list[str] | None = None,
        search: str | None = None,
        select_fields: list[str] | None = None,
        include: list[str] | None = None,
        search_fields: list[str] | None = None,
    ) -> dict[str, Any]:
        """
        List entities with pagination and filtering.

        Args:
            page: Page number (1-indexed)
            page_size: Items per page
            filters: Optional filter criteria
            sort: Optional sort fields (prefix with '-' for descending)
            search: Optional full-text search query
            select_fields: Optional field projection (SELECT only these columns)
            include: Optional list of relation names to eager-load
            search_fields: Fields to search across (from surface config)

        Returns:
            Dictionary with items, total, page, and page_size
        """
        if self._repository:
            return await self._repository.list(
                page,
                page_size,
                filters,
                sort=sort,
                search=search,
                select_fields=select_fields,
                include=include,
                search_fields=search_fields,
            )

        # Fallback to in-memory
        items = list(self._store.values())

        # Apply filters if provided
        if filters:
            items = self._apply_filters(items, filters)

        # Calculate pagination
        total = len(items)
        start = (page - 1) * page_size
        end = start + page_size
        paginated_items = items[start:end]

        return {
            "items": paginated_items,
            "total": total,
            "page": page,
            "page_size": page_size,
        }

    def _apply_filters(self, items: builtins.list[T], filters: dict[str, Any]) -> builtins.list[T]:
        """Apply filters to a list of items."""
        filtered = []
        for item in items:
            item_dict = item.model_dump()
            match = True
            for key, value in filters.items():
                if key in item_dict and item_dict[key] != value:
                    match = False
                    break
            if match:
                filtered.append(item)
        return filtered

    def _sanitize_fields(self, data: BaseModel) -> None:
        """Sanitize string and text fields to prevent XSS via the API.

        Mutates the model in-place: str fields get all tags stripped,
        text fields get only dangerous tags/attributes stripped.
        """
        if not self.entity_spec:
            return

        from dazzle_back.runtime.sanitizer import strip_dangerous_tags, strip_html_tags
        from dazzle_back.specs.entity import ScalarType

        field_type_map = {f.name: f.type for f in self.entity_spec.fields}

        for field_name, field_type in field_type_map.items():
            value = getattr(data, field_name, None)
            if not isinstance(value, str):
                continue

            if field_type.kind == "scalar" and field_type.scalar_type == ScalarType.TEXT:
                # Text fields: strip dangerous tags only
                sanitized = strip_dangerous_tags(value)
            elif field_type.kind == "scalar" and field_type.scalar_type in (
                ScalarType.STR,
                ScalarType.EMAIL,
                ScalarType.URL,
            ):
                # Plain string fields: strip all HTML tags
                sanitized = strip_html_tags(value)
            elif field_type.kind == "enum":
                # Enum values should not contain HTML
                sanitized = strip_html_tags(value)
            else:
                continue

            if sanitized != value:
                # Pydantic models may be frozen — use model internals to set
                object.__setattr__(data, field_name, sanitized)

    async def _validate_references(self, data: dict[str, Any]) -> None:
        """
        Validate that all foreign key references point to existing entities.

        v0.14.2: Added to ensure referential integrity.

        Args:
            data: Entity data to validate

        Raises:
            ReferenceNotFoundError: If a referenced entity doesn't exist
        """
        if not self.entity_spec or not self._repository:
            return

        # Get ref fields from entity spec
        for field in self.entity_spec.fields:
            if field.type.kind == "ref" and field.type.ref_entity:
                ref_value = data.get(field.name)
                if ref_value is not None:
                    # Need to check if the referenced entity exists
                    # Import repository factory to get the right repo

                    # Get the database manager from our repository
                    db = self._repository.db

                    # Check if referenced entity exists
                    ref_entity = field.type.ref_entity
                    ref_id = str(ref_value) if not isinstance(ref_value, str) else ref_value

                    with db.connection() as conn:
                        from dazzle_back.runtime.query_builder import quote_identifier

                        table = quote_identifier(ref_entity)
                        ph = db.placeholder
                        sql = f'SELECT 1 FROM {table} WHERE "id" = {ph} LIMIT 1'
                        cursor = conn.execute(sql, (ref_id,))  # nosemgrep
                        exists = cursor.fetchone() is not None

                    if not exists:
                        from dazzle_back.runtime.invariant_evaluator import (
                            InvariantViolationError,
                        )

                        raise InvariantViolationError(
                            f"Referenced {ref_entity} with ID '{ref_id}' not found "
                            f"(field: {field.name})"
                        )


class CustomService(BaseService[T]):
    """
    Custom service for non-CRUD operations.

    Provides a flexible base for implementing custom domain logic.
    """

    def __init__(
        self,
        service_name: str,
        handler: Callable[..., Any] | None = None,
    ):
        self.service_name = service_name
        self._handler = handler

    async def execute(self, **kwargs: Any) -> Any:
        """Execute the custom service operation."""
        if self._handler:
            return await self._handler(**kwargs)
        else:
            # Default stub implementation
            return {"status": "ok", "service": self.service_name, "message": "Not implemented"}

    def set_handler(self, handler: Callable[..., Any]) -> None:
        """Set the handler function for this service."""
        self._handler = handler


# =============================================================================
# Service Factory
# =============================================================================


class ServiceFactory:
    """
    Factory for creating services from ServiceSpec.

    Creates appropriate service implementations based on the service specification.
    """

    def __init__(
        self,
        models: dict[str, type[BaseModel]],
        state_machines: dict[str, StateMachineSpec] | None = None,
        entity_specs: dict[str, EntitySpec] | None = None,
    ):
        """
        Initialize the service factory.

        Args:
            models: Dictionary mapping entity names to Pydantic models
            state_machines: Dictionary mapping entity names to state machine specs
            entity_specs: Dictionary mapping entity names to full entity specs (v0.14.2)
        """
        self.models = models
        self.state_machines = state_machines or {}
        self.entity_specs = entity_specs or {}
        self._services: dict[str, BaseService[Any]] = {}

    def create_service(
        self,
        spec: ServiceSpec,
        create_schema: type[BaseModel] | None = None,
        update_schema: type[BaseModel] | None = None,
    ) -> BaseService[Any]:
        """
        Create a service from a ServiceSpec.

        Args:
            spec: Service specification
            create_schema: Optional create schema (for CRUD services)
            update_schema: Optional update schema (for CRUD services)

        Returns:
            Service instance
        """
        if spec.is_crud and spec.target_entity:
            entity_name = spec.target_entity
            model = self.models.get(entity_name)

            if not model:
                raise ValueError(f"No model found for entity: {entity_name}")

            # Use provided schemas or create defaults
            if not create_schema:
                create_schema = model
            if not update_schema:
                update_schema = model

            # Get state machine for this entity
            state_machine = self.state_machines.get(entity_name)

            # Get full entity spec for validation (v0.14.2)
            entity_spec = self.entity_specs.get(entity_name)

            service: BaseService[Any] = CRUDService(
                entity_name=entity_name,
                model_class=model,
                create_schema=create_schema,
                update_schema=update_schema,
                state_machine=state_machine,
                entity_spec=entity_spec,
            )
        else:
            service = CustomService(service_name=spec.name)

        self._services[spec.name] = service
        return service

    def get_service(self, name: str) -> BaseService[Any] | None:
        """Get a service by name."""
        return self._services.get(name)

    def create_all_services(
        self,
        specs: list[ServiceSpec],
        schemas: dict[str, dict[str, type[BaseModel]]] | None = None,
    ) -> dict[str, BaseService[Any]]:
        """
        Create services for all specifications.

        Args:
            specs: List of service specifications
            schemas: Optional dictionary mapping entity names to
                     {"create": schema, "update": schema}

        Returns:
            Dictionary mapping service names to service instances
        """
        schemas = schemas or {}

        for spec in specs:
            entity_name = spec.target_entity
            entity_schemas = schemas.get(entity_name or "", {})

            self.create_service(
                spec,
                create_schema=entity_schemas.get("create"),
                update_schema=entity_schemas.get("update"),
            )

        return self._services


# =============================================================================
# Service Execution Context
# =============================================================================


class ServiceContext:
    """
    Execution context for services.

    Holds request-scoped data like current user, tenant, and trace ID.
    """

    def __init__(
        self,
        user_id: UUID | None = None,
        tenant_id: UUID | None = None,
        trace_id: str | None = None,
        permissions: list[str] | None = None,
    ):
        self.user_id = user_id
        self.tenant_id = tenant_id
        self.trace_id = trace_id or str(uuid4())
        self.permissions = permissions or []

    def has_permission(self, permission: str) -> bool:
        """Check if context has a specific permission."""
        return "*" in self.permissions or permission in self.permissions
