"""
parsing.broker — Provider chain orchestrator.

Tries parsing providers in priority order, short-circuits on high
confidence, and returns the best result. The engineer never knows
which engine won — they just get structured data.
"""

from __future__ import annotations

import asyncio
import logging
import re
import time
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional

from .config import ParseConfig
from .providers import (
    OllamaProvider,
    ParseProvider,
    ProviderResult,
    TextFSMProvider,
    TTPProvider,
)

logger = logging.getLogger("nterm.parsing.broker")


# ── Request / Response Models ───────────────────────────────

@dataclass
class ParseOptions:
    """Per-request overrides from the client."""
    providers: Optional[List[str]] = None    # restrict to these providers
    max_wait_ms: int = 5000                  # total timeout
    force_provider: Optional[str] = None     # skip chain, use only this one


@dataclass
class ParseResponse:
    """Full response returned to /api/parse."""
    provider: Optional[str] = None
    template: Optional[str] = None
    score: int = 0
    confidence: str = "none"
    records: List[Dict[str, Any]] = field(default_factory=list)
    record_count: int = 0
    fields: List[str] = field(default_factory=list)
    parse_time_ms: int = 0
    competing: Dict[str, Any] = field(default_factory=dict)
    chain_time_ms: int = 0
    short_circuited: bool = False
    error: Optional[str] = None

    def to_dict(self) -> dict:
        d = {
            "provider": self.provider,
            "template": self.template,
            "score": self.score,
            "confidence": self.confidence,
            "records": self.records,
            "record_count": self.record_count,
            "fields": self.fields,
            "parse_time_ms": self.parse_time_ms,
            "competing": self.competing,
            "chain_time_ms": self.chain_time_ms,
            "short_circuited": self.short_circuited,
        }
        if self.error:
            d["error"] = self.error
        return d


# ── Broker ──────────────────────────────────────────────────

class ParseBroker:
    """
    Tries parsing providers in priority order, returns best result.

    Short-circuits when a provider scores above the high-confidence
    threshold. Falls through to LLM only when templates can't help.
    Handles missing or unavailable providers gracefully.
    """

    def __init__(self, config: ParseConfig):
        self.config = config
        self.providers: List[ParseProvider] = []
        self._provider_map: Dict[str, ParseProvider] = {}

        self._init_providers()

    def _init_providers(self):
        """
        Instantiate providers based on config.

        Order follows config.broker.provider_order.
        Each provider that fails to initialize is logged and skipped.
        """
        cfg = self.config
        registry: Dict[str, ParseProvider] = {}

        # Build available providers
        if cfg.tfsm.enabled:
            p = TextFSMProvider(db_path=cfg.tfsm.resolved_db_path, module_path=cfg.tfsm.resolved_module_path)
            registry["tfsm"] = p

        if cfg.ttp.enabled:
            p = TTPProvider(db_path=cfg.ttp.resolved_db_path, module_path=cfg.ttp.resolved_module_path)
            registry["ttp"] = p

        if cfg.ollama.enabled:
            p = OllamaProvider(
                base_url=cfg.ollama.base_url,
                model=cfg.ollama.model,
                timeout_s=cfg.ollama.timeout_s,
                max_output_chars=cfg.ollama.max_output_chars,
                health_check_interval_s=cfg.ollama.health_check_interval_s,
            )
            # Initial health check — non-blocking, just marks availability
            p.check_availability()
            registry["ollama"] = p

        # Order per config
        for name in cfg.broker.provider_order:
            if name in registry:
                self.providers.append(registry[name])
                self._provider_map[name] = registry[name]

        # Catch any enabled providers not in the order list
        for name, prov in registry.items():
            if name not in self._provider_map:
                self.providers.append(prov)
                self._provider_map[name] = prov

        available = [p.name for p in self.providers if p.available]
        unavailable = [p.name for p in self.providers if not p.available]

        logger.info(
            f"ParseBroker initialized — "
            f"available: {available or '(none)'}, "
            f"unavailable: {unavailable or '(none)'}"
        )

    # ── Main Parse Method ───────────────────────────────────

    async def parse(
        self,
        command: str,
        output: str,
        vendor_hint: Optional[str] = None,
        options: Optional[ParseOptions] = None,
    ) -> ParseResponse:
        """
        Run the provider chain and return the best parse result.

        Provider chain logic:
          1. Build filter string from command + vendor_hint
          2. Try each provider in order
          3. Short-circuit if score > high_confidence_threshold
          4. If no provider clears high threshold, take best score
          5. If best score < partial_confidence_threshold, try Ollama
          6. Return best result (or empty response if all fail)
        """
        filter_string = self._build_filter(command, vendor_hint)
        ordered = self._ordered_providers(options)

        competing: Dict[str, Any] = {}
        best_result: Optional[ProviderResult] = None
        short_circuited = False
        chain_start = time.monotonic()

        high_thresh = self.config.broker.high_confidence_threshold
        partial_thresh = self.config.broker.partial_confidence_threshold

        for provider in ordered:
            # Re-check availability (Ollama may come/go)
            if not provider.check_availability():
                competing[provider.name] = {"error": "not available", "score": 0}
                continue

            try:
                result = await asyncio.to_thread(
                    provider.parse, output, filter_string
                )
                competing[provider.name] = result.summary()

                if best_result is None or result.score > best_result.score:
                    best_result = result

                # Short-circuit on high confidence
                if result.score > high_thresh:
                    short_circuited = True
                    break

            except Exception as e:
                logger.error(f"Provider {provider.name} raised: {e}")
                competing[provider.name] = {"error": str(e), "score": 0}

        chain_ms = int((time.monotonic() - chain_start) * 1000)

        return self._build_response(
            best_result, competing, chain_ms, short_circuited
        )

    # ── Filter String Construction ──────────────────────────

    @staticmethod
    def _build_filter(command: str, vendor_hint: Optional[str] = None) -> str:
        """
        Build the SQLite LIKE filter from command + vendor hint.

        'show ip bgp summary' + 'arista_eos'
        → 'arista_eos_show_ip_bgp_summary'
        """
        cmd_slug = re.sub(r"[^a-zA-Z0-9]+", "_", command).strip("_")
        if vendor_hint:
            return f"{vendor_hint}_{cmd_slug}"
        return cmd_slug

    # ── Provider Ordering ───────────────────────────────────

    def _ordered_providers(
        self, options: Optional[ParseOptions] = None
    ) -> List[ParseProvider]:
        """Apply per-request provider filters/ordering."""
        if options and options.force_provider:
            return [
                p for p in self.providers if p.name == options.force_provider
            ]
        if options and options.providers:
            return [
                p for p in self.providers if p.name in options.providers
            ]
        return self.providers

    # ── Response Builder ────────────────────────────────────

    def _build_response(
        self,
        best: Optional[ProviderResult],
        competing: Dict[str, Any],
        chain_ms: int,
        short_circuited: bool,
    ) -> ParseResponse:
        """Build the full API response from the best provider result."""
        if not best or best.score == 0:
            return ParseResponse(
                competing=competing,
                chain_time_ms=chain_ms,
                error="All providers failed or unavailable"
                if not best
                else (best.error or "No template matched"),
            )

        return ParseResponse(
            provider=best.provider,
            template=best.template,
            score=best.score,
            confidence=self._score_to_confidence(best),
            records=best.records,
            record_count=len(best.records),
            fields=best.fields,
            parse_time_ms=best.parse_time_ms,
            competing=competing,
            chain_time_ms=chain_ms,
            short_circuited=short_circuited,
        )

    def _score_to_confidence(self, result: ProviderResult) -> str:
        """Map score + provider to a confidence label for the UI."""
        if result.provider == "ollama":
            return "llm"
        if result.score > self.config.broker.high_confidence_threshold:
            return "high"
        if result.score > self.config.broker.partial_confidence_threshold:
            return "partial"
        if result.score > 0:
            return "low"
        return "none"

    # ── Diagnostics ─────────────────────────────────────────

    def get_provider_status(self) -> List[dict]:
        """Status summary for all registered providers."""
        return [
            {
                "name": p.name,
                "available": p.available,
            }
            for p in self.providers
        ]