"""Inference adapter bridging the document extractor to the InferenceGateway.

Replaces direct use of llm_factory / VLLMClient / OllamaClient in the
extraction pipeline. Uses the shared InferenceGateway with extraction-
specific prompt construction and records actual endpoint, deployment,
model, and protocol lineage in the result.

Requirements: 2.12, 13.6
"""
from __future__ import annotations

import logging
import time
from dataclasses import dataclass, field

from services.extractor.client import (
    ExtractionAttempt,
    ExtractionResponse,
    _repair_json,
    _strip_markdown_fences,
)
from services.extractor.prompts import (
    build_extraction_prompt,
    get_json_schema,
    get_prompt_metadata,
)
from services.extractor.schemas import validate_extraction
from services.shared.inference.gateway import InferenceGateway
from services.shared.inference.lineage import ModelLineage, build_lineage_from_result
from services.shared.inference.models import (
    ChatMessage,
    InferenceResult,
    InferenceTarget,
    StructuredGenerationRequest,
)

logger = logging.getLogger("extractor.inference_adapter")


@dataclass
class ExtractionWithLineage:
    """Extraction result bundled with inference lineage metadata.

    The lineage records the actual endpoint, deployment, model, and
    protocol used — fixing the hardcoded ``model_provider = 'ollama'``.
    """

    response: ExtractionResponse
    lineage: ModelLineage
    raw_inference_results: list[InferenceResult] = field(default_factory=list)


async def extract_document(
    gateway: InferenceGateway,
    target: InferenceTarget,
    document_text: str,
    document_type: str = "article",
    document_id: str = "",
    known_tickers: list[str] | None = None,
    max_retries: int = 3,
    retry_base_delay: float = 2.0,
    retry_max_delay: float = 30.0,
    retry_backoff_multiplier: float = 2.0,
) -> ExtractionWithLineage:
    """Extract structured intelligence from a document via the InferenceGateway.

    This adapter:
    1. Builds extraction-specific prompts (same as current pipeline)
    2. Constructs a StructuredGenerationRequest
    3. Routes through the InferenceGateway (correct client per protocol)
    4. Parses / repairs JSON, validates against the extraction schema
    5. Records actual lineage (endpoint_id, deployment_id, model, protocol)

    Args:
        gateway: The shared InferenceGateway instance.
        target: Resolved inference target for extraction.
        document_text: The document to extract from.
        document_type: Type of document (article, filing, transcript, etc.).
        document_id: UUID of the source document.
        known_tickers: Optional list of tracked tickers for context.
        max_retries: Maximum number of retry attempts.
        retry_base_delay: Initial retry delay in seconds.
        retry_max_delay: Maximum retry delay in seconds.
        retry_backoff_multiplier: Backoff multiplier for retries.

    Returns:
        ExtractionWithLineage containing the extraction response and lineage.
    """
    import asyncio

    prompts = build_extraction_prompt(
        document_text=document_text,
        document_type=document_type,
        document_id=document_id,
        known_tickers=known_tickers,
    )
    json_schema = get_json_schema()
    prompt_meta = get_prompt_metadata()

    response = ExtractionResponse(
        prompt_metadata=prompt_meta,
        model=target.model,
    )
    inference_results: list[InferenceResult] = []
    last_lineage: ModelLineage | None = None

    total_start = time.monotonic()

    for attempt_num in range(max_retries + 1):
        # Build request
        request = StructuredGenerationRequest(
            messages=[
                ChatMessage(role="system", content=prompts["system"]),
                ChatMessage(role="user", content=prompts["user"]),
            ],
            json_schema=json_schema,
            max_output_tokens=target.extra_body.get("max_tokens", 4096),
            temperature=0.0,
            seed=0,
            timeout_seconds=target.timeout_seconds,
            trace_id=document_id,
        )

        # Call via gateway
        result = await gateway.generate(target, request)
        inference_results.append(result)
        last_lineage = build_lineage_from_result(result, trace_id=document_id)

        # Convert to ExtractionAttempt for compatibility
        attempt = _inference_result_to_attempt(result, target.model, document_text)
        response.attempts.append(attempt)

        if attempt.error is None and attempt.validation and attempt.validation.valid:
            response.success = True
            response.result = attempt.validation.parsed
            break

        # Determine if retryable
        retryable = _is_result_retryable(result)
        attempt.retryable = retryable

        if not retryable:
            logger.warning(
                "Non-retryable error for doc %s: %s — stopping retries",
                document_id or "unknown",
                attempt.error,
            )
            break

        if attempt_num < max_retries:
            delay = retry_base_delay * (retry_backoff_multiplier ** attempt_num)
            delay = min(delay, retry_max_delay)
            logger.warning(
                "Extraction attempt %d/%d failed for doc %s: %s — retrying in %.1fs",
                attempt_num + 1,
                max_retries + 1,
                document_id or "unknown",
                attempt.error or "validation failed",
                delay,
            )
            await asyncio.sleep(delay)

    response.total_duration_ms = int((time.monotonic() - total_start) * 1000)

    # Use actual lineage from last inference call
    lineage = last_lineage or ModelLineage(model=target.model, protocol=target.protocol)

    return ExtractionWithLineage(
        response=response,
        lineage=lineage,
        raw_inference_results=inference_results,
    )


def _inference_result_to_attempt(
    result: InferenceResult,
    model: str,
    document_text: str,
) -> ExtractionAttempt:
    """Convert an InferenceResult to the legacy ExtractionAttempt format.

    Applies the same markdown-fence stripping, JSON repair, and schema
    validation as the existing VLLMClient and OllamaClient.
    """
    attempt = ExtractionAttempt(model=model)
    attempt.duration_ms = result.latency_ms
    attempt.raw_output = result.content

    # Check for gateway-level errors
    if result.error:
        attempt.error = result.error
        attempt.retryable = _is_result_retryable(result)
        return attempt

    content = result.content
    if not content:
        attempt.error = "empty_model_response"
        return attempt

    # Strip markdown fences if present
    content = _strip_markdown_fences(content)

    # Repair malformed JSON
    content = _repair_json(content)

    # Validate against extraction schema
    attempt.validation = validate_extraction(content, document_text=document_text)
    if not attempt.validation.valid:
        attempt.error = "; ".join(attempt.validation.errors)

    return attempt


def _is_result_retryable(result: InferenceResult) -> bool:
    """Determine if an inference result error is retryable."""
    if result.error_category in (
        "timeout",
        "rate_limit",
        "server_error",
        "connection_error",
    ):
        return True
    if result.error and "empty" in result.error.lower():
        return True
    return False
