"""Briefing generator that orchestrates the LLM call.

Transforms analysis results into structured briefings via a single LLM call
with retry handling for transient errors.
"""

from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from unfamiliar.constants import DEFAULT_TOKEN_BUDGET
from unfamiliar.llm.briefing.prompts import BRIEFING_PROMPT
from unfamiliar.llm.briefing.schemas import BriefingResponse, ModuleDescription
from unfamiliar.llm.context import CHARS_PER_TOKEN, build_briefing_context
from unfamiliar.llm.exceptions import AuthenticationError, BriefingGenerationError
from unfamiliar.llm.retry import with_llm_retry
from unfamiliar.llm.structured import create_structured_chain

if TYPE_CHECKING:
    from langchain_core.language_models.chat_models import BaseChatModel

    from unfamiliar.analysis.models import AnalysisResult

logger = logging.getLogger(__name__)


@dataclass
class BriefingResult:
    """Complete output of briefing generation.

    Contains the structured briefing content, module descriptions,
    and metadata about the generation process.
    """

    briefing: BriefingResponse
    """The structured briefing with summary, tech_stack, and security_posture."""

    modules: list[ModuleDescription]
    """Named descriptions for each detected module cluster."""

    context_tokens_estimate: int
    """Approximate number of tokens in the context sent to the LLM."""


def generate_briefing(
    analysis: AnalysisResult,
    model: BaseChatModel,
    token_budget: int = DEFAULT_TOKEN_BUDGET,
    provider_name: str | None = None,
) -> BriefingResult:
    """Generate a structured codebase briefing via LLM.

    Makes a single LLM call (plus retries on transient failure) to produce
    both the briefing content and module naming.

    Args:
        analysis: Complete AnalysisResult from analyze_repository().
        model: A configured BaseChatModel instance.
        token_budget: Maximum tokens for key file contents in context.
        provider_name: Provider name for retry delay (e.g., "glm" or "anthropic").
            "glm" → 30s base delay, "anthropic" → 1s, None → 1s default.

    Returns:
        BriefingResult containing the structured briefing and module descriptions.

    Raises:
        AuthenticationError: If the LLM API returns a 401/403 error.
        BriefingGenerationError: If generation fails after max retries.
    """
    logger.info("Generating briefing for %s", analysis.repo_path)

    # Build the context string
    context = build_briefing_context(analysis, token_budget)
    context_tokens = len(context) // CHARS_PER_TOKEN
    logger.info(
        "Built context with %d estimated tokens from %d files",
        context_tokens,
        len(analysis.files),
    )

    # Cap LLM module count — asking for 100+ descriptions in one call
    # produces timeouts and garbage. The padding logic below fills in the rest.
    _MAX_LLM_MODULES = 15
    total_modules = len(analysis.modules)
    llm_module_count = min(total_modules, _MAX_LLM_MODULES)
    if llm_module_count < total_modules:
        logger.info(
            "Capping LLM module descriptions at %d (of %d total). "
            "Remaining %d will use path-based defaults.",
            llm_module_count,
            total_modules,
            total_modules - llm_module_count,
        )

    # Create the structured output chain (provider-agnostic)

    chain = create_structured_chain(
        model, BRIEFING_PROMPT, BriefingResponse, provider_name=provider_name
    )

    # Prepare input
    input_data = {
        "context": context,
        "module_count": llm_module_count,
    }

    # Call LLM with retry handling
    attempts = 0
    try:

        def invoke_chain() -> BriefingResponse:
            nonlocal attempts
            attempts += 1
            result = chain.invoke(input_data)
            if not isinstance(result, BriefingResponse):
                raise TypeError(
                    f"Expected BriefingResponse, got {type(result).__name__}"
                )
            return result

        response = with_llm_retry(
            invoke_chain, max_attempts=5, provider_name=provider_name
        )

    except AuthenticationError as exc:
        logger.error("Briefing generation failed after %d attempts: %s", attempts, exc)
        raise
    except Exception as exc:
        raise BriefingGenerationError(
            f"Failed to generate briefing: {exc}",
            attempts=attempts,
        ) from exc

    # Validate module count and pad to cover all analysis modules
    modules = response.modules
    if len(modules) != total_modules:
        logger.warning(
            "Have %d module descriptions, need %d. Padding with defaults.",
            len(modules),
            total_modules,
        )
        # Pad with default names based on module primary paths
        while len(modules) < total_modules:
            idx = len(modules)
            primary_path = (
                analysis.modules[idx].primary_path
                if idx < len(analysis.modules)
                else f"module_{idx}"
            )
            modules.append(
                ModuleDescription(
                    name=primary_path or f"Module {idx + 1}",
                    description="Description not provided by LLM.",
                    security_relevant=False,
                )
            )

    # Log warnings for empty tech_stack
    if not response.tech_stack:
        logger.warning("LLM returned empty tech_stack")

    logger.info(
        "Generated briefing with %d modules and %d tech items",
        len(modules),
        len(response.tech_stack),
    )

    return BriefingResult(
        briefing=response,
        modules=modules,
        context_tokens_estimate=context_tokens,
    )
