"""Core pretok pipeline."""

from __future__ import annotations

import logging
import time
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

from pretok.capability import get_default_registry
from pretok.detection import DetectionError, DetectionResult
from pretok.detection.base import BaseDetector
from pretok.detection.composite import create_detector
from pretok.pipeline.cache import Cache, MemoryCache, make_cache_key
from pretok.segment import Segment, SegmentType, lex_prompt
from pretok.segment.types import segments_to_text
from pretok.translation import TranslationError, TranslationResult
from pretok.translation.base import BaseTranslator

if TYPE_CHECKING:
    from pretok.config import PretokConfig

logger = logging.getLogger(__name__)

# Hook type aliases
HookFn = Callable[..., Any]


@dataclass
class PipelineStats:
    """Pipeline processing statistics.

    Attributes:
        total_segments: Total number of segments parsed
        translated_segments: Number of segments translated
        skipped_segments: Number of segments skipped (below confidence, same language, etc.)
        detection_time_ms: Time spent on language detection in milliseconds
        translation_time_ms: Time spent on translation in milliseconds
        total_time_ms: Total processing time in milliseconds
        cache_hit: Whether the result was served from cache
    """

    total_segments: int = 0
    translated_segments: int = 0
    skipped_segments: int = 0
    detection_time_ms: float = 0.0
    translation_time_ms: float = 0.0
    total_time_ms: float = 0.0
    cache_hit: bool = False


@dataclass
class PipelineResult:
    """Result from pretok pipeline processing.

    Attributes:
        original_text: Original input text
        processed_text: Processed/translated text
        segments: List of segments with their details
        detections: Language detection results
        translations: Translation results
        from_cache: Whether result was from cache
        metadata: Additional processing metadata
        stats: Pipeline processing statistics
    """

    original_text: str
    processed_text: str
    segments: list[Segment] = field(default_factory=list)
    detections: list[DetectionResult] = field(default_factory=list)
    translations: list[TranslationResult] = field(default_factory=list)
    from_cache: bool = False
    metadata: dict[str, Any] = field(default_factory=dict)
    stats: PipelineStats = field(default_factory=PipelineStats)

    @property
    def was_modified(self) -> bool:
        """Check if text was modified."""
        return self.original_text != self.processed_text


class Pretok:
    """Universal pre-token language adaptation layer.

    Pretok processes input text to optimize it for LLM tokenization:
    1. Parses prompt structure (roles, code, text)
    2. Detects languages in text segments
    3. Translates non-optimal languages to model's preferred language
    4. Reconstructs the prompt preserving structure

    Example:
        >>> from pretok import Pretok
        >>> pretok = Pretok(target_language="en")
        >>> result = pretok.process("Hello World")  # noqa: RUF002
        >>> result.processed_text
        'Hello World'

    With configuration:
        >>> from pretok import Pretok
        >>> from pretok.config import load_config
        >>> config = load_config("pretok.yaml")
        >>> pretok = Pretok(config=config)
    """

    def __init__(
        self,
        config: PretokConfig | None = None,
        *,
        target_language: str | None = None,
        model_id: str | None = None,
        detector: BaseDetector | None = None,
        translator: BaseTranslator | None = None,
        cache: Cache | None = None,
    ) -> None:
        """Initialize Pretok pipeline.

        Args:
            config: Full configuration object
            target_language: Target language for translation (overrides config)
            model_id: Model ID for capability lookup
            detector: Custom detector instance
            translator: Custom translator instance
            cache: Custom cache instance
        """
        self._config = config
        self._model_id = model_id

        # Determine target language
        if target_language:
            self._target_language = target_language
        elif model_id:
            capability = get_default_registry().get(model_id)
            self._target_language = capability.primary_language
        else:
            self._target_language = "en"

        # Initialize components
        self._detector = detector or self._create_detector()
        self._translator = translator
        self._cache = cache or self._create_cache()

        # Get model capability
        self._capability = get_default_registry().get(model_id) if model_id else None

        # Pipeline settings from config
        self._confidence_threshold = (
            config.pipeline.confidence_threshold if config else 0.8
        )
        self._strict_mode = config.pipeline.strict_mode if config else False
        self._fallback_to_original = (
            config.pipeline.fallback_to_original if config else True
        )

        # Hooks
        self._hooks: dict[str, list[HookFn]] = {
            "pre_detection": [],
            "post_detection": [],
            "pre_translation": [],
            "post_translation": [],
        }

        logger.info(
            "Initialized Pretok: target_language=%s, model_id=%s",
            self._target_language,
            model_id,
        )

    def _create_detector(self) -> BaseDetector:
        """Create detector from config or defaults."""
        if self._config:
            backend = self._config.pipeline.default_detector
            return create_detector(backend)

        # Default to langdetect
        return create_detector("langdetect")

    def _create_cache(self) -> Cache | None:
        """Create cache from config or defaults."""
        if self._config and not self._config.pipeline.cache_enabled:
            return None

        if self._config:
            backend = self._config.pipeline.cache_backend

            if backend == "redis":
                try:
                    from pretok.pipeline.redis_cache import RedisCache

                    redis_config = self._config.cache.redis
                    return RedisCache(
                        url=redis_config.url,
                        prefix=redis_config.prefix,
                        ttl=redis_config.ttl,
                    )
                except ImportError:
                    logger.warning(
                        "Redis cache requested but redis package not installed. "
                        "Falling back to memory cache."
                    )

            if backend == "sqlite":
                from pretok.pipeline.sqlite_cache import SQLiteCache

                sqlite_config = self._config.cache.sqlite
                return SQLiteCache(
                    path=sqlite_config.path,
                    ttl=sqlite_config.ttl,
                )

            # Default: memory cache
            cache_config = self._config.cache.memory
            return MemoryCache(
                max_size=cache_config.max_size,
                ttl=cache_config.ttl,
            )

        return MemoryCache()

    @property
    def target_language(self) -> str:
        """Return target language."""
        return self._target_language

    @property
    def detector(self) -> BaseDetector:
        """Return detector instance."""
        return self._detector

    @property
    def translator(self) -> BaseTranslator | None:
        """Return translator instance."""
        return self._translator

    def set_translator(self, translator: BaseTranslator) -> None:
        """Set translator instance.

        Args:
            translator: Translator to use
        """
        self._translator = translator

    def process(
        self,
        text: str,
        *,
        target_language: str | None = None,
        translate: bool = True,
        detect_only: bool = False,
    ) -> PipelineResult:
        """Process text through the pretok pipeline.

        Args:
            text: Input text to process
            target_language: Override target language for this call
            translate: Whether to perform translation
            detect_only: Only detect languages, don't translate

        Returns:
            PipelineResult with processed text and metadata

        Raises:
            DetectionError: If strict_mode is True and detection fails
            TranslationError: If strict_mode is True and translation fails
        """
        start_time = time.monotonic()
        stats = PipelineStats()

        if not text:
            stats.total_time_ms = (time.monotonic() - start_time) * 1000
            return PipelineResult(
                original_text=text,
                processed_text=text,
                stats=stats,
            )

        target = target_language or self._target_language

        # Check cache
        if self._cache and self._translator:
            cache_key = make_cache_key(text, None, target, self._translator.name)
            cached = self._cache.get(cache_key)
            if cached is not None:
                logger.debug("Cache hit for translation")
                stats.cache_hit = True
                stats.total_time_ms = (time.monotonic() - start_time) * 1000
                return PipelineResult(
                    original_text=text,
                    processed_text=cached,
                    from_cache=True,
                    stats=stats,
                )

        # Parse into segments
        segments = lex_prompt(text)
        stats.total_segments = len(segments)

        # Run pre-detection hooks
        for hook in self._hooks["pre_detection"]:
            hook(segments=segments, text=text)

        # Detect languages for text segments
        detection_start = time.monotonic()
        detections: list[DetectionResult] = []
        for i, seg in enumerate(segments):
            if seg.type == SegmentType.TEXT and seg.content.strip():
                try:
                    detection = self._detector.detect(seg.content)
                    detections.append(detection)
                    # Apply confidence threshold
                    if detection.confidence >= self._confidence_threshold:
                        segments[i] = seg.with_language(detection.language)
                    else:
                        logger.debug(
                            "Detection confidence %.2f below threshold %.2f, skipping segment",
                            detection.confidence,
                            self._confidence_threshold,
                        )
                        stats.skipped_segments += 1
                except DetectionError as e:
                    if self._strict_mode:
                        raise
                    logger.warning("Detection failed for segment: %s", e)
        stats.detection_time_ms = (time.monotonic() - detection_start) * 1000

        # Run post-detection hooks
        for hook in self._hooks["post_detection"]:
            hook(segments=segments, detections=detections)

        if detect_only:
            stats.total_time_ms = (time.monotonic() - start_time) * 1000
            return PipelineResult(
                original_text=text,
                processed_text=text,
                segments=segments,
                detections=detections,
                stats=stats,
            )

        # Run pre-translation hooks
        for hook in self._hooks["pre_translation"]:
            hook(segments=segments, target_language=target)

        # Translate segments if needed
        translation_start = time.monotonic()
        translations: list[TranslationResult] = []
        processed_segments = []

        for seg in segments:
            if self._should_translate(seg, target) and translate and self._translator:
                try:
                    result = self._translator.translate(
                        seg.content,
                        target,
                        seg.language,
                    )
                    translations.append(result)
                    processed_segments.append(seg.with_content(result.translated_text))
                    stats.translated_segments += 1
                except TranslationError as e:
                    if self._strict_mode:
                        raise
                    logger.warning("Translation failed: %s", e)
                    processed_segments.append(seg)
            else:
                processed_segments.append(seg)
        stats.translation_time_ms = (time.monotonic() - translation_start) * 1000

        # Run post-translation hooks
        for hook in self._hooks["post_translation"]:
            hook(segments=processed_segments, translations=translations)

        # Reconstruct text
        processed_text = segments_to_text(processed_segments)

        # Cache result
        if self._cache and self._translator and translations:
            cache_key = make_cache_key(text, None, target, self._translator.name)
            self._cache.set(cache_key, processed_text)

        stats.total_time_ms = (time.monotonic() - start_time) * 1000

        return PipelineResult(
            original_text=text,
            processed_text=processed_text,
            segments=processed_segments,
            detections=detections,
            translations=translations,
            metadata={
                "target_language": target,
                "segments_count": len(segments),
                "translated_count": len(translations),
            },
            stats=stats,
        )

    def _should_translate(self, segment: Segment, target_language: str) -> bool:
        """Determine if a segment should be translated.

        Args:
            segment: Segment to check
            target_language: Target language

        Returns:
            True if segment should be translated
        """
        # Non-translatable segments
        if not segment.translatable:
            return False

        # No language detected
        if not segment.language:
            return False

        # Already in target language
        if segment.language.lower() == target_language.lower():
            return False

        # Check model capability if available
        if self._capability:
            return self._capability.needs_translation(segment.language)

        return True

    def add_hook(self, event: str, hook: HookFn) -> None:
        """Register a hook function for a pipeline event.

        Available events:
        - ``pre_detection``: Called before language detection. Receives ``segments``, ``text``.
        - ``post_detection``: Called after detection. Receives ``segments``, ``detections``.
        - ``pre_translation``: Called before translation. Receives ``segments``, ``target_language``.
        - ``post_translation``: Called after translation. Receives ``segments``, ``translations``.

        Args:
            event: Hook event name
            hook: Callable to invoke

        Raises:
            ValueError: If event name is not recognized
        """
        if event not in self._hooks:
            valid = ", ".join(sorted(self._hooks))
            raise ValueError(f"Unknown hook event '{event}'. Valid events: {valid}")
        self._hooks[event].append(hook)

    def remove_hook(self, event: str, hook: HookFn) -> bool:
        """Remove a previously registered hook.

        Args:
            event: Hook event name
            hook: The callable to remove

        Returns:
            True if the hook was found and removed
        """
        if event not in self._hooks:
            return False
        try:
            self._hooks[event].remove(hook)
            return True
        except ValueError:
            return False

    def process_batch(
        self,
        texts: list[str],
        *,
        target_language: str | None = None,
        translate: bool = True,
    ) -> list[PipelineResult]:
        """Process multiple texts through the pipeline.

        Args:
            texts: List of input texts to process
            target_language: Override target language for this call
            translate: Whether to perform translation

        Returns:
            List of PipelineResult for each input text
        """
        return [
            self.process(text, target_language=target_language, translate=translate)
            for text in texts
        ]

    def detect(self, text: str) -> DetectionResult:
        """Detect the language of text.

        Convenience method for single-text detection.

        Args:
            text: Text to detect

        Returns:
            DetectionResult with language and confidence
        """
        return self._detector.detect(text)

    def translate(
        self,
        text: str,
        target_language: str | None = None,
        source_language: str | None = None,
    ) -> TranslationResult:
        """Translate text to target language.

        Convenience method for single-text translation.

        Args:
            text: Text to translate
            target_language: Target language (uses default if not specified)
            source_language: Source language (auto-detect if not specified)

        Returns:
            TranslationResult with translated text

        Raises:
            ValueError: If no translator is configured
        """
        if not self._translator:
            raise ValueError("No translator configured")

        target = target_language or self._target_language
        return self._translator.translate(text, target, source_language)


def create_pretok(
    config: PretokConfig | None = None,
    *,
    target_language: str | None = None,
    model_id: str | None = None,
) -> Pretok:
    """Create a Pretok instance.

    Factory function for creating configured Pretok instances.

    Args:
        config: Configuration object
        target_language: Target language (uses model's primary if not specified)
        model_id: Model ID for capability lookup

    Returns:
        Configured Pretok instance
    """
    return Pretok(
        config=config,
        target_language=target_language,
        model_id=model_id,
    )
