"""Context builder for transforming AnalysisResult into LLM-ready text.

Produces a structured text document from static analysis results that
can be consumed by the LLM for briefing generation.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

from unfamiliar.constants import DEFAULT_TOKEN_BUDGET

if TYPE_CHECKING:
    from unfamiliar.analysis.models import AnalysisResult

logger = logging.getLogger(__name__)

# Token estimation: approximately 4 characters per token for English/code
CHARS_PER_TOKEN = 4

# Maximum file size to include in full (larger files get truncated)
MAX_FILE_CHARS = 10_000  # ~2500 tokens max per file


def build_briefing_context(
    analysis: AnalysisResult,
    token_budget: int = DEFAULT_TOKEN_BUDGET,
) -> str:
    """Transform static analysis results into a structured text context.

    Args:
        analysis: Complete AnalysisResult from analyze_repository().
        token_budget: Maximum tokens for the entire context.

    Returns:
        A structured text document suitable for LLM consumption.
    """
    sections: list[str] = []
    max_chars = token_budget * CHARS_PER_TOKEN

    # Section 1: Repository Overview
    sections.append(_build_repository_overview(analysis))

    # Section 2: Entry Points
    if analysis.entry_points:
        sections.append(_build_entry_points_section(analysis))

    # Section 3: Module Structure
    if analysis.modules:
        sections.append(_build_modules_section(analysis))

    # Section 4: Import Graph Summary
    sections.append(_build_import_graph_section(analysis))

    # Section 5: Test Coverage Summary
    sections.append(_build_test_coverage_section(analysis))

    # Section 6: Security Findings
    if analysis.security.secrets or analysis.security.vulnerabilities:
        sections.append(_build_security_section(analysis))

    # Calculate remaining budget for file contents
    current_chars = sum(len(s) for s in sections)
    remaining_budget = max(
        0, token_budget - (current_chars // CHARS_PER_TOKEN) - 500
    )  # Reserve 500 for formatting

    # Section 7: Key File Contents (budget-aware)
    key_content, signatures = _build_file_contents_section(analysis, remaining_budget)
    sections.append(key_content)
    if signatures:
        sections.append(signatures)

    # Combine and enforce hard cap (but keep at least repo overview)
    result = "\n\n".join(sections)
    if len(result) > max_chars and max_chars > 500:
        result = result[:max_chars] + "\n\n... [context truncated to fit token budget]"
        logger.warning(
            "Context truncated from %d to %d chars to fit %d token budget",
            len("\n\n".join(sections)),
            max_chars,
            token_budget,
        )

    return result


def _build_repository_overview(analysis: AnalysisResult) -> str:
    """Build the repository overview section."""
    lines: list[str] = ["# Repository Overview"]

    # Basic stats
    total_files = len(analysis.files)
    lines.append(f"- Total files: {total_files}")
    lines.append(f"- Repository path: {analysis.repo_path}")

    # Language distribution
    if analysis.languages:
        lines.append("\n## Languages")
        for lang, count in sorted(
            analysis.languages.items(), key=lambda x: x[1], reverse=True
        ):
            lines.append(f"- {lang}: {count} files")

    return "\n".join(lines)


def _build_entry_points_section(analysis: AnalysisResult) -> str:
    """Build the entry points section."""
    lines: list[str] = ["# Entry Points"]

    # Group by file for cleaner output
    entry_by_file: dict[str, list[str]] = {}
    for ep in analysis.entry_points:
        if ep.file_path not in entry_by_file:
            entry_by_file[ep.file_path] = []
        entry_by_file[ep.file_path].append(
            f"  - Line {ep.line}: {ep.entry_type} - {ep.description}"
        )

    for file_path, entries in sorted(entry_by_file.items()):
        lines.append(f"\n## {file_path}")
        lines.extend(entries)

    return "\n".join(lines)


def _build_modules_section(analysis: AnalysisResult) -> str:
    """Build the module structure section.

    Only multi-file modules are listed in detail. Singletons (1-file "modules")
    are summarised as a count to avoid bloating the context with hundreds of
    individual-file entries that carry no structural information.
    """
    lines: list[str] = ["# Module Structure"]
    total = len(analysis.modules)

    multi_file = [(i, m) for i, m in enumerate(analysis.modules) if m.file_count > 1]
    singleton_count = total - len(multi_file)

    lines.append(
        f"\nDetected {total} logical module(s) from import clustering "
        f"({len(multi_file)} multi-file, {singleton_count} single-file):"
    )

    for i, module in multi_file:
        category_hints = (
            ", ".join(str(h) for h in module.category_hints)
            if module.category_hints
            else "none"
        )
        lines.append(f"\n## Module {i + 1}: {module.primary_path or 'unnamed'}")
        lines.append(f"- Files: {module.file_count}")
        lines.append(f"- Confidence: {module.confidence:.2f}")
        lines.append(f"- Category hints: {category_hints}")

        if module.files:
            display_files = module.files[:5]
            for f in display_files:
                lines.append(f"  - {f}")
            if len(module.files) > 5:
                lines.append(f"  - ... and {len(module.files) - 5} more files")

    if singleton_count:
        lines.append(
            f"\nPlus {singleton_count} single-file modules (not listed individually)."
        )

    return "\n".join(lines)


def _build_import_graph_section(analysis: AnalysisResult) -> str:
    """Build the import graph summary section."""
    lines: list[str] = ["# Import Graph Summary"]

    # Count edges
    total_edges = sum(len(targets) for targets in analysis.import_graph.edges.values())
    lines.append(f"- Total import relationships: {total_edges}")

    # Hub files (high in-degree)
    hubs = sorted(
        analysis.import_graph.in_degree.items(),
        key=lambda x: x[1],
        reverse=True,
    )[:5]

    if hubs:
        lines.append("\n## Hub Files (most imported)")
        for path, degree in hubs:
            if degree > 0:
                lines.append(f"- {path}: imported by {degree} files")

    return "\n".join(lines)


def _build_test_coverage_section(analysis: AnalysisResult) -> str:
    """Build the test coverage summary section."""
    lines: list[str] = ["# Test Coverage Summary"]

    test_info = analysis.test_info
    lines.append(f"- Test files: {len(test_info.test_files)}")
    lines.append(f"- Untested files: {len(test_info.untested_files)}")
    lines.append(f"- Detection tier: {test_info.tier}")

    if test_info.test_files:
        lines.append("\n## Test Files")
        for tf in sorted(test_info.test_files)[:8]:
            lines.append(f"- {tf}")
        if len(test_info.test_files) > 8:
            lines.append(f"- ... and {len(test_info.test_files) - 8} more")

    if test_info.coverage_summary and test_info.tier == "enhanced":
        lines.append("\n## Coverage Highlights")
        # Show files with coverage data
        covered = sorted(
            test_info.coverage_summary.items(),
            key=lambda x: x[1],
            reverse=True,
        )[:10]
        for path, coverage in covered:
            lines.append(f"- {path}: {coverage * 100:.1f}%")

    return "\n".join(lines)


def _build_security_section(analysis: AnalysisResult) -> str:
    """Build the security findings section."""
    lines: list[str] = ["# Security Findings"]

    if analysis.security.secrets:
        lines.append(f"\n## Potential Secrets ({len(analysis.security.secrets)})")
        for secret in analysis.security.secrets[:5]:
            lines.append(
                f"- {secret.file_path}:{secret.line_number} - {secret.secret_type}"
            )
        if len(analysis.security.secrets) > 5:
            lines.append(
                f"- ... and {len(analysis.security.secrets) - 5} more findings"
            )

    if analysis.security.vulnerabilities:
        vuln_count = len(analysis.security.vulnerabilities)
        lines.append(f"\n## Dependency Vulnerabilities ({vuln_count})")
        for vuln in analysis.security.vulnerabilities[:5]:
            lines.append(
                f"- {vuln.package_name} {vuln.installed_version}: "
                f"{vuln.vulnerability_id} ({vuln.severity})"
            )
        if len(analysis.security.vulnerabilities) > 5:
            lines.append(f"- ... and {len(analysis.security.vulnerabilities) - 5} more")

    if analysis.security.scan_errors:
        lines.append("\n## Scan Errors")
        for error in analysis.security.scan_errors:
            lines.append(f"- {error}")

    return "\n".join(lines)


def _build_file_contents_section(
    analysis: AnalysisResult,
    token_budget: int,
) -> tuple[str, str]:
    """Build key file contents and remaining file signatures.

    Returns:
        Tuple of (key_contents_section, signatures_section).
    """
    key_lines: list[str] = ["# Key File Contents"]
    sig_lines: list[str] = ["# Other Files (Signatures)"]

    # Estimate overhead from other sections (rough)
    # We'll allocate most of the budget to file contents
    content_budget = token_budget - 2000  # Reserve ~2000 tokens for structure

    if content_budget < 0:
        content_budget = 500  # Minimum budget

    tokens_used = 0
    included_paths: set[str] = set()

    # Process scored files in priority order
    for scored in analysis.scored_files:
        # Check if we have budget
        if tokens_used >= content_budget:
            break

        # Get file content
        file_path = Path(analysis.repo_path) / scored.path
        try:
            content = file_path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError) as e:
            logger.debug("Could not read %s: %s", scored.path, e)
            continue

        # Truncate large files
        if len(content) > MAX_FILE_CHARS:
            content = content[:MAX_FILE_CHARS] + "\n... [file truncated]"
            file_tokens = MAX_FILE_CHARS // CHARS_PER_TOKEN
        else:
            file_tokens = len(content) // CHARS_PER_TOKEN

        # Check if this file alone exceeds remaining budget
        remaining = content_budget - tokens_used
        if file_tokens > remaining:
            # Truncate to fit
            max_chars = remaining * CHARS_PER_TOKEN
            content = content[:max_chars] + "\n... [truncated]"

        # Add to key contents
        key_lines.append(f"\n## {scored.path}")
        key_lines.append(f"Priority score: {scored.score}")
        if scored.reasons:
            key_lines.append(f"Reasons: {', '.join(scored.reasons)}")
        key_lines.append("```")
        key_lines.append(content)
        key_lines.append("```")

        included_paths.add(scored.path)
        tokens_used += min(file_tokens, remaining)

    # Build signatures for remaining files
    sig_count = 0
    for scored in analysis.scored_files:
        if scored.path in included_paths:
            continue

        # Get parsed file for signatures
        parsed = analysis.parsed_files.get(scored.path)
        if parsed:
            defs = [f"{d.kind} {d.name}" for d in parsed.definitions[:5]]
            sig = f"- {scored.path}"
            if defs:
                sig += f": {', '.join(defs)}"
                if len(parsed.definitions) > 5:
                    sig += f" (+{len(parsed.definitions) - 5} more)"
            sig_lines.append(sig)
            sig_count += 1

            # Limit signatures to avoid huge output
            if sig_count >= 100:
                remaining = len(analysis.scored_files) - len(included_paths) - sig_count
                if remaining > 0:
                    sig_lines.append(f"- ... and {remaining} more files")
                break

    return "\n".join(key_lines), "\n".join(sig_lines)
