"""Named Entity Recognition application service.

Orchestrates NER extraction, caching, and persistence following hexagonal architecture:
    gRPC mixin → NerService (application) → NerEngine (infrastructure)
"""

from __future__ import annotations

import asyncio
from dataclasses import dataclass
from typing import TYPE_CHECKING

from noteflow.config.constants import ERROR_MSG_MEETING_PREFIX
from noteflow.config.settings import get_feature_flags
from noteflow.domain.entities.named_entity import NamedEntity
from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence
    from uuid import UUID

    from noteflow.domain.ports.ner import NerPort
    from noteflow.domain.value_objects import MeetingId
    from noteflow.infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork

    UoWFactory = Callable[[], SqlAlchemyUnitOfWork]

logger = get_logger(__name__)


@dataclass
class ExtractionResult:
    """Result of entity extraction.

    Attributes:
        entities: List of extracted named entities.
        cached: Whether the result came from cache.
        total_count: Total number of entities.
    """

    entities: Sequence[NamedEntity]
    cached: bool
    total_count: int


class NerService:
    """Application service for Named Entity Recognition.

    Provides a clean interface for NER operations, abstracting away
    the infrastructure details (spaCy engine, database persistence).

    Orchestrates:
    - Feature flag checking
    - Cache lookup (return existing entities if available)
    - Extraction via NerPort adapter
    - Persistence via UnitOfWork
    """

    def __init__(
        self,
        ner_engine: NerPort,
        uow_factory: UoWFactory,
    ) -> None:
        """Initialize NER service.

        Args:
            ner_engine: NER engine implementation (infrastructure adapter).
            uow_factory: Factory for creating Unit of Work instances.
        """
        self._ner_engine = ner_engine
        self._uow_factory = uow_factory
        self._extraction_lock = asyncio.Lock()
        self._model_load_lock = asyncio.Lock()

    async def extract_entities(
        self,
        meeting_id: MeetingId,
        force_refresh: bool = False,
    ) -> ExtractionResult:
        """Extract named entities from a meeting's transcript.

        Checks for cached results first, unless force_refresh is True.
        Persists new extractions to the database.

        Args:
            meeting_id: Meeting to extract entities from.
            force_refresh: If True, re-extract even if cached results exist.

        Returns:
            ExtractionResult with entities and metadata.

        Raises:
            ValueError: If meeting not found or has no segments.
            RuntimeError: If NER feature is disabled.
        """
        self._check_feature_enabled()

        # Check cache and get segments in one transaction
        cached_or_segments = await self._get_cached_or_segments(meeting_id, force_refresh)
        if isinstance(cached_or_segments, ExtractionResult):
            return cached_or_segments

        segments = cached_or_segments

        # Extract and persist
        entities = await self._extract_with_lock(segments)
        for entity in entities:
            entity.meeting_id = meeting_id

        await self._persist_entities(meeting_id, entities, force_refresh)

        logger.info(
            "Extracted %d entities from meeting %s (%d segments)",
            len(entities),
            meeting_id,
            len(segments),
        )
        return ExtractionResult(entities=entities, cached=False, total_count=len(entities))

    def _check_feature_enabled(self) -> None:
        """Raise if NER feature is disabled."""
        if not get_feature_flags().ner_enabled:
            raise RuntimeError("NER extraction is disabled by feature flag")

    async def _get_cached_or_segments(
        self,
        meeting_id: MeetingId,
        force_refresh: bool,
    ) -> ExtractionResult | list[tuple[int, str]]:
        """Check cache and return cached result or segments for extraction."""
        async with self._uow_factory() as uow:
            if not force_refresh:
                cached = await uow.entities.get_by_meeting(meeting_id)
                if cached:
                    logger.debug("Returning %d cached entities for meeting %s", len(cached), meeting_id)
                    return ExtractionResult(entities=cached, cached=True, total_count=len(cached))

            meeting = await uow.meetings.get(meeting_id)
            if not meeting:
                raise ValueError(f"{ERROR_MSG_MEETING_PREFIX}{meeting_id} not found")

            # Load segments separately (not eagerly loaded on meeting)
            segments = await uow.segments.get_by_meeting(meeting_id)
            if not segments:
                logger.debug("Meeting %s has no segments", meeting_id)
                return ExtractionResult(entities=[], cached=False, total_count=0)

            return [(s.segment_id, s.text) for s in segments]

    async def _persist_entities(
        self,
        meeting_id: MeetingId,
        entities: list[NamedEntity],
        force_refresh: bool,
    ) -> None:
        """Persist extracted entities to database."""
        async with self._uow_factory() as uow:
            if force_refresh:
                await uow.entities.delete_by_meeting(meeting_id)
            await uow.entities.save_batch(entities)
            await uow.commit()

    async def _extract_with_lock(
        self,
        segments: list[tuple[int, str]],
    ) -> list[NamedEntity]:
        """Extract entities with concurrency control.

        Ensures only one extraction runs at a time and handles
        lazy model loading safely.

        Args:
            segments: List of (segment_id, text) tuples.

        Returns:
            List of extracted entities.
        """
        async with self._extraction_lock:
            # Ensure model is loaded (thread-safe)
            if not self._ner_engine.is_ready():
                async with self._model_load_lock:
                    if not self._ner_engine.is_ready():
                        # Warm up model with a simple extraction
                        loop = asyncio.get_running_loop()
                        await loop.run_in_executor(
                            None,
                            lambda: self._ner_engine.extract("warm up"),
                        )

            # Extract entities in executor (CPU-bound)
            loop = asyncio.get_running_loop()
            return await loop.run_in_executor(
                None,
                self._ner_engine.extract_from_segments,
                segments,
            )

    async def get_entities(self, meeting_id: MeetingId) -> Sequence[NamedEntity]:
        """Get cached entities for a meeting (no extraction).

        Args:
            meeting_id: Meeting UUID.

        Returns:
            List of entities (empty if not extracted yet).
        """
        async with self._uow_factory() as uow:
            return await uow.entities.get_by_meeting(meeting_id)

    async def pin_entity(self, entity_id: UUID, is_pinned: bool = True) -> bool:
        """Mark an entity as user-verified (pinned).

        Args:
            entity_id: Entity UUID.
            is_pinned: New pinned status.

        Returns:
            True if entity was found and updated.
        """
        async with self._uow_factory() as uow:
            result = await uow.entities.update_pinned(entity_id, is_pinned)
            if result:
                await uow.commit()
            return result

    async def clear_entities(self, meeting_id: MeetingId) -> int:
        """Delete all entities for a meeting.

        Args:
            meeting_id: Meeting UUID.

        Returns:
            Number of deleted entities.
        """
        async with self._uow_factory() as uow:
            count = await uow.entities.delete_by_meeting(meeting_id)
            await uow.commit()
            logger.info("Cleared %d entities for meeting %s", count, meeting_id)
            return count

    async def has_entities(self, meeting_id: MeetingId) -> bool:
        """Check if a meeting has extracted entities.

        Args:
            meeting_id: Meeting UUID.

        Returns:
            True if at least one entity exists.
        """
        async with self._uow_factory() as uow:
            return await uow.entities.exists_for_meeting(meeting_id)

    def is_engine_ready(self) -> bool:
        """Check if NER engine model is loaded and ready for extraction.

        Verifies both that the spaCy model is loaded and that the NER
        feature is enabled via configuration.

        Returns:
            True if the engine is ready and NER is enabled.
        """
        return get_feature_flags().ner_enabled and self._ner_engine.is_ready()
