"""Specialist extraction engine — wraps GLiNER2 or a mock for testing."""

from __future__ import annotations

import logging
import os
from typing import Any

from services.specialist.batching import DynamicBatcher
from services.specialist.models import (
    MODEL_VERSION,
    SCHEMA_VERSION,
    ClassificationResult,
    EntityResult,
    RelationResult,
    StructuredResult,
)

logger = logging.getLogger(__name__)


class SpecialistEngine:
    """CPU-first extraction engine backed by GLiNER2 Large or a mock.

    In test mode (SPECIALIST_TEST_MODE=1), uses a mock engine that produces
    deterministic results without downloading model weights.

    Integrates a DynamicBatcher for each extraction type to collect concurrent
    requests and process them as batches.
    """

    def __init__(
        self,
        model_name: str = "urchade/gliner_large-v2.1",
        max_batch_size: int = 32,
        max_wait_ms: float = 50.0,
        max_queue_size: int = 256,
    ) -> None:
        self.model_name = model_name
        self.max_batch_size = max_batch_size
        self.max_wait_ms = max_wait_ms
        self.max_queue_size = max_queue_size
        self._model: Any = None
        self._test_mode = os.environ.get("SPECIALIST_TEST_MODE", "1") == "1"
        self._ready = False

        # Dynamic batchers (created but not started until load_model)
        self._entity_batcher: DynamicBatcher | None = None
        self._classification_batcher: DynamicBatcher | None = None
        self._relation_batcher: DynamicBatcher | None = None
        self._structured_batcher: DynamicBatcher | None = None

    @property
    def is_ready(self) -> bool:
        return self._ready

    @property
    def batcher_metrics(self) -> dict[str, Any]:
        """Return aggregated metrics from all batchers."""
        metrics: dict[str, Any] = {
            "total_batches": 0,
            "total_items": 0,
            "total_rejections": 0,
            "queue_depth": 0,
        }
        for batcher in [
            self._entity_batcher,
            self._classification_batcher,
            self._relation_batcher,
            self._structured_batcher,
        ]:
            if batcher:
                metrics["total_batches"] += batcher.total_batches_processed
                metrics["total_items"] += batcher.total_items_processed
                metrics["total_rejections"] += batcher.total_rejections
                metrics["queue_depth"] += batcher.queue_size
        return metrics

    def load_model(self, model_name: str | None = None) -> None:
        """Load the specialist model or mock engine."""
        if model_name:
            self.model_name = model_name

        if self._test_mode:
            logger.info("Specialist engine starting in TEST mode (mock)")
            self._model = _MockGLiNER()
            self._ready = True
            return

        try:
            from gliner import GLiNER  # type: ignore[import-untyped]

            logger.info("Loading GLiNER model: %s", self.model_name)
            self._model = GLiNER.from_pretrained(self.model_name)
            self._ready = True
            logger.info("GLiNER model loaded successfully")
        except ImportError:
            logger.warning(
                "gliner package not available — falling back to mock engine"
            )
            self._model = _MockGLiNER()
            self._ready = True
        except Exception:
            logger.exception("Failed to load GLiNER model")
            self._model = _MockGLiNER()
            self._ready = True

    def warm_up(self) -> None:
        """Run a dummy inference to warm up model weights and caches."""
        if not self._ready:
            self.load_model()
        dummy_text = "Apple Inc reported Q3 revenue of $81.4 billion."
        dummy_labels = ["company", "financial_metric", "date"]
        self.extract_entities([dummy_text], dummy_labels)
        logger.info("Specialist engine warm-up complete")

    def extract_entities(
        self, texts: list[str], labels: list[str]
    ) -> list[list[EntityResult]]:
        """Extract entities from a batch of texts."""
        if not self._ready:
            raise RuntimeError("Engine not initialized — call load_model() first")

        results: list[list[EntityResult]] = []
        for text in texts:
            entities = self._predict_entities(text, labels)
            results.append(entities)
        return results

    def classify_texts(
        self, texts: list[str], labels: list[str]
    ) -> list[list[ClassificationResult]]:
        """Classify texts against the given labels."""
        if not self._ready:
            raise RuntimeError("Engine not initialized — call load_model() first")

        results: list[list[ClassificationResult]] = []
        for text in texts:
            classifications = self._predict_classification(text, labels)
            results.append(classifications)
        return results

    def extract_relations(
        self, texts: list[str], labels: list[str]
    ) -> list[list[RelationResult]]:
        """Extract relations from a batch of texts."""
        if not self._ready:
            raise RuntimeError("Engine not initialized — call load_model() first")

        results: list[list[RelationResult]] = []
        for text in texts:
            relations = self._predict_relations(text, labels)
            results.append(relations)
        return results

    def extract_structured(
        self, texts: list[str], labels: list[str]
    ) -> list[list[StructuredResult]]:
        """Extract structured key-value facts from texts."""
        if not self._ready:
            raise RuntimeError("Engine not initialized — call load_model() first")

        results: list[list[StructuredResult]] = []
        for text in texts:
            structured = self._predict_structured(text, labels)
            results.append(structured)
        return results

    # ------------------------------------------------------------------
    # Internal prediction methods
    # ------------------------------------------------------------------

    def _predict_entities(self, text: str, labels: list[str]) -> list[EntityResult]:
        """Run entity prediction for a single text."""
        if self._test_mode or isinstance(self._model, _MockGLiNER):
            return self._model.predict_entities(text, labels)

        # Real GLiNER inference
        raw_entities = self._model.predict_entities(text, labels)
        return [
            EntityResult(
                text=ent["text"],
                entity_type=ent["label"],
                start_char=ent["start"],
                end_char=ent["end"],
                score=round(float(ent["score"]), 4),
                model_version=MODEL_VERSION,
                schema_version=SCHEMA_VERSION,
            )
            for ent in raw_entities
        ]

    def _predict_classification(
        self, text: str, labels: list[str]
    ) -> list[ClassificationResult]:
        """Run classification for a single text using zero-shot NER heuristic."""
        if self._test_mode or isinstance(self._model, _MockGLiNER):
            return self._model.predict_classification(text, labels)

        # Use entity extraction as a proxy for classification
        raw_entities = self._model.predict_entities(text, labels)
        # Group by label and take the highest score per label
        label_scores: dict[str, float] = {}
        for ent in raw_entities:
            lbl = ent["label"]
            score = float(ent["score"])
            if lbl not in label_scores or score > label_scores[lbl]:
                label_scores[lbl] = score

        return [
            ClassificationResult(
                text=text[:200],
                label=lbl,
                score=round(score, 4),
                model_version=MODEL_VERSION,
                schema_version=SCHEMA_VERSION,
            )
            for lbl, score in label_scores.items()
        ]

    def _predict_relations(
        self, text: str, labels: list[str]
    ) -> list[RelationResult]:
        """Run relation extraction for a single text."""
        if self._test_mode or isinstance(self._model, _MockGLiNER):
            return self._model.predict_relations(text, labels)

        # Real GLiNER does not natively support relations — use mock pattern
        return self._model.predict_relations(text, labels)

    def _predict_structured(
        self, text: str, labels: list[str]
    ) -> list[StructuredResult]:
        """Run structured fact extraction for a single text."""
        if self._test_mode or isinstance(self._model, _MockGLiNER):
            return self._model.predict_structured(text, labels)

        # Real GLiNER structured extraction uses entity spans as key-value pairs
        raw_entities = self._model.predict_entities(text, labels)
        return [
            StructuredResult(
                text=ent["text"],
                field=ent["label"],
                value=ent["text"],
                start_char=ent["start"],
                end_char=ent["end"],
                score=round(float(ent["score"]), 4),
                model_version=MODEL_VERSION,
                schema_version=SCHEMA_VERSION,
            )
            for ent in raw_entities
        ]


class _MockGLiNER:
    """Mock GLiNER model for testing without downloading weights."""

    def predict_entities(self, text: str, labels: list[str]) -> list[EntityResult]:
        """Produce deterministic mock entities based on simple heuristics."""
        results: list[EntityResult] = []

        # Simple keyword-based mock extraction
        _MOCK_ENTITIES = {
            "company": ["Apple", "Google", "Microsoft", "Tesla", "Amazon"],
            "person": ["Elon Musk", "Tim Cook", "Satya Nadella"],
            "financial_metric": ["revenue", "earnings", "EPS", "profit"],
            "date": ["Q1", "Q2", "Q3", "Q4", "2024", "2025"],
            "currency": ["$", "€", "£"],
            "percentage": ["%"],
        }

        for label in labels:
            keywords = _MOCK_ENTITIES.get(label, [])
            for keyword in keywords:
                start = text.find(keyword)
                if start >= 0:
                    results.append(
                        EntityResult(
                            text=keyword,
                            entity_type=label,
                            start_char=start,
                            end_char=start + len(keyword),
                            score=0.85,
                            model_version=MODEL_VERSION,
                            schema_version=SCHEMA_VERSION,
                        )
                    )
        return results

    def predict_classification(
        self, text: str, labels: list[str]
    ) -> list[ClassificationResult]:
        """Produce deterministic mock classification results."""
        results: list[ClassificationResult] = []
        # Assign first label with high score, rest with decreasing
        for i, label in enumerate(labels):
            score = max(0.3, 0.9 - i * 0.2)
            results.append(
                ClassificationResult(
                    text=text[:200],
                    label=label,
                    score=round(score, 4),
                    model_version=MODEL_VERSION,
                    schema_version=SCHEMA_VERSION,
                )
            )
        return results

    def predict_relations(
        self, text: str, labels: list[str]
    ) -> list[RelationResult]:
        """Produce deterministic mock relation results."""
        results: list[RelationResult] = []

        # Simple mock: if text mentions two companies, create a relation
        companies = ["Apple", "Google", "Microsoft", "Tesla", "Amazon"]
        found: list[tuple[str, int]] = []
        for company in companies:
            idx = text.find(company)
            if idx >= 0:
                found.append((company, idx))

        if len(found) >= 2 and labels:
            subj_name, subj_start = found[0]
            obj_name, obj_start = found[1]
            results.append(
                RelationResult(
                    subject=subj_name,
                    subject_type="company",
                    subject_start=subj_start,
                    subject_end=subj_start + len(subj_name),
                    relation=labels[0],
                    object=obj_name,
                    object_type="company",
                    object_start=obj_start,
                    object_end=obj_start + len(obj_name),
                    score=0.78,
                    model_version=MODEL_VERSION,
                    schema_version=SCHEMA_VERSION,
                )
            )
        return results

    def predict_structured(
        self, text: str, labels: list[str]
    ) -> list[StructuredResult]:
        """Produce deterministic mock structured results."""
        results: list[StructuredResult] = []

        # Extract number-like patterns as structured facts
        import re

        for label in labels:
            # Find dollar amounts
            pattern = r"\$[\d,]+\.?\d*\s*(?:billion|million|thousand)?"
            matches = list(re.finditer(pattern, text))
            for match in matches:
                results.append(
                    StructuredResult(
                        text=match.group(),
                        field=label,
                        value=match.group(),
                        start_char=match.start(),
                        end_char=match.end(),
                        score=0.82,
                        model_version=MODEL_VERSION,
                        schema_version=SCHEMA_VERSION,
                    )
                )
                break  # one per label for mock
        return results
