"""Context builder for mission generation.

Builds category-specific context strings from analysis results and briefing
for consumption by the LLM mission generator.
"""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from unfamiliar.constants import DEFAULT_TOKEN_BUDGET, InvestigationCategory
from unfamiliar.llm.context import CHARS_PER_TOKEN

if TYPE_CHECKING:
    from collections.abc import Callable

    from unfamiliar.analysis.models import AnalysisResult
    from unfamiliar.llm.briefing.generator import BriefingResult

logger = logging.getLogger(__name__)

# Token budget share per category (total budget / 7 categories)
_CATEGORIES_COUNT = 7


def build_mission_context(
    analysis: AnalysisResult,
    briefing_result: BriefingResult,
    category: InvestigationCategory,
    previous_missions: list[str],
    token_budget: int = DEFAULT_TOKEN_BUDGET,
) -> str:
    """Build category-focused context for mission generation.

    Args:
        analysis: Complete AnalysisResult from analyze_repository().
        briefing_result: Complete BriefingResult from generate_briefing().
        category: The investigation category to focus on.
        previous_missions: title_plain values from previously completed categories.
        token_budget: Total token budget (will be divided by 7).

    Returns:
        A structured text context focused on the specified category.
    """
    per_category_budget = token_budget // _CATEGORIES_COUNT
    sections: list[str] = [
        # Section 0: Valid file paths (CRITICAL - LLM must only use these)
        _build_valid_file_paths_section(analysis),
        # Section 1: Category-specific file analysis
        _build_category_files_section(analysis, category),
        # Section 2: Relevant module details
        _build_relevant_modules_section(analysis, briefing_result, category),
    ]

    # Section 3: Category-specific context
    category_context = _build_category_specific_context(
        analysis, category, per_category_budget
    )
    if category_context:
        sections.append(category_context)

    # Section 4: Previous missions for cross-category awareness
    if previous_missions:
        sections.append(_build_previous_missions_section(previous_missions))

    return "\n\n".join(sections)


def build_module_descriptions(briefing_result: BriefingResult) -> str:
    """Build module descriptions list with indices for mission linking.

    Args:
        briefing_result: Complete BriefingResult from generate_briefing().

    Returns:
        Formatted list of modules with indices for module_index field.
    """
    lines: list[str] = ["## Module Index (use in module_index field)"]

    for idx, module in enumerate(briefing_result.modules):
        lines.append(f"\n[{idx}] **{module.name}**")
        lines.append(f"    {module.description}")
        if module.security_relevant:
            lines.append("    *Security relevant*")

    return "\n".join(lines)


def _build_valid_file_paths_section(analysis: AnalysisResult) -> str:
    """Build section listing ALL valid file paths the LLM may reference.

    This is critical for preventing LLM hallucination of file paths.
    The LLM should ONLY use paths from this list in file_refs.
    """
    lines: list[str] = [
        "## VALID FILE PATHS (Use ONLY These Paths)",
        "\nThe following file paths exist in this repository. "
        "When creating file_refs, use ONLY paths from this list. "
        "Do NOT invent or guess file paths.",
    ]

    # Collect all unique file paths from analysis
    all_paths: set[str] = set()

    # From modules
    for module in analysis.modules:
        all_paths.update(module.files)

    # From parsed files
    all_paths.update(analysis.parsed_files.keys())

    # From scored files
    for scored in analysis.scored_files:
        all_paths.add(scored.path)

    # Sort and limit to prevent token explosion
    sorted_paths = sorted(all_paths)
    max_paths = 100  # Reasonable limit

    if len(sorted_paths) > max_paths:
        lines.append(f"\n*Showing {max_paths} of {len(sorted_paths)} files*")

    for path in sorted_paths[:max_paths]:
        lines.append(f"- `{path}`")

    return "\n".join(lines)


def _build_category_files_section(
    analysis: AnalysisResult,
    category: InvestigationCategory,
) -> str:
    """Build section listing files relevant to this category."""
    category_name = category.value.replace("_", " ").title()
    lines: list[str] = [f"## Files Relevant to {category_name}"]

    # Filter files by category hints
    relevant_files: list[str] = []
    for module in analysis.modules:
        if category in module.category_hints:
            relevant_files.extend(module.files[:5])  # Limit per module

    # Also check scored files for category relevance
    category_keywords = _get_category_keywords(category)
    for scored in analysis.scored_files[:20]:
        has_keyword = any(kw in scored.path.lower() for kw in category_keywords)
        if has_keyword and scored.path not in relevant_files:
            relevant_files.append(scored.path)

    if not relevant_files:
        # Fallback to top scored files if no category matches
        relevant_files = [sf.path for sf in analysis.scored_files[:15]]
        lines.append("\nNo specific category matches found. Top priority files:")
    else:
        lines.append(f"\n{len(relevant_files)} files identified as relevant:")

    # Limit output
    for path in sorted(set(relevant_files))[:20]:
        lines.append(f"- {path}")

    return "\n".join(lines)


def _build_relevant_modules_section(
    analysis: AnalysisResult,
    briefing_result: BriefingResult,
    category: InvestigationCategory,
) -> str:
    """Build section describing modules relevant to this category."""
    lines: list[str] = ["## Relevant Modules"]

    relevant_indices: list[int] = []
    for idx, module in enumerate(analysis.modules):
        if category in module.category_hints:
            relevant_indices.append(idx)

    if not relevant_indices:
        # Include all modules if none specifically match
        relevant_indices = list(range(min(5, len(analysis.modules))))

    for idx in relevant_indices[:5]:
        if idx < len(briefing_result.modules):
            mod_desc = briefing_result.modules[idx]
            has_analysis = idx < len(analysis.modules)
            analysis_mod = analysis.modules[idx] if has_analysis else None

            lines.append(f"\n### Module {idx}: {mod_desc.name}")
            lines.append(mod_desc.description)
            if mod_desc.security_relevant:
                lines.append("*This module handles security-sensitive operations.*")
            if analysis_mod:
                lines.append(f"Files: {', '.join(analysis_mod.files[:5])}")
                if len(analysis_mod.files) > 5:
                    lines.append(f"  ... and {len(analysis_mod.files) - 5} more")

    return "\n".join(lines)


def _build_category_specific_context(
    analysis: AnalysisResult,
    category: InvestigationCategory,
    token_budget: int,
) -> str:
    """Build category-specific context with file contents.

    Args:
        analysis: Complete analysis results.
        category: Target category.
        token_budget: Token budget for this section.

    Returns:
        Formatted context section, or empty string if nothing relevant.
    """
    lines: list[str] = []
    tokens_used = 0

    # Map categories to their builder functions
    builders: dict[InvestigationCategory, Callable[[AnalysisResult], str]] = {
        InvestigationCategory.TESTING: _build_testing_context,
        InvestigationCategory.DEPENDENCIES: _build_dependencies_context,
        InvestigationCategory.TECHNICAL_DEBT: _build_debt_context,
        InvestigationCategory.ARCHITECTURE: _build_architecture_context,
        InvestigationCategory.DATA_FLOW: _build_data_flow_context,
        InvestigationCategory.DOMAIN_BUSINESS_LOGIC: (
            _build_domain_business_logic_context
        ),
        InvestigationCategory.CONVENTIONS: _build_conventions_context,
    }

    # Build main category section with budget check
    builder = builders.get(category)
    if builder:
        section = builder(analysis)
        section_tokens = len(section) // CHARS_PER_TOKEN
        if tokens_used + section_tokens <= token_budget:
            lines.append(section)
            tokens_used += section_tokens
        else:
            logger.warning(
                "Category %s context (%d tokens) exceeds budget (%d tokens); skipping",
                category.value,
                section_tokens,
                token_budget,
            )

    # Add security context if relevant findings exist
    if analysis.security.secrets or analysis.security.vulnerabilities:
        security_section = _build_security_findings_context(analysis)
        security_tokens = len(security_section) // CHARS_PER_TOKEN
        if tokens_used + security_tokens <= token_budget:
            lines.append(security_section)

    return "\n\n".join(lines) if lines else ""


def _build_testing_context(analysis: AnalysisResult) -> str:
    """Build context for TESTING category."""
    lines: list[str] = ["## Testing Context"]

    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)[:10]:
            lines.append(f"- {tf}")
        if len(test_info.test_files) > 10:
            lines.append(f"- ... and {len(test_info.test_files) - 10} more")

    if test_info.coverage_summary:
        lines.append("\n### Coverage Summary")
        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}%")

    if test_info.untested_files:
        lines.append("\n### High-Priority Untested Files")
        # Show untested files that are in the top scored files
        untested_scored = [
            sf.path
            for sf in analysis.scored_files[:30]
            if sf.path in test_info.untested_files
        ]
        for path in untested_scored[:10]:
            lines.append(f"- {path}")

    return "\n".join(lines)


def _build_dependencies_context(analysis: AnalysisResult) -> str:
    """Build context for DEPENDENCIES category."""
    lines: list[str] = ["## Dependencies Context"]

    # Count imports to external packages
    external_imports: dict[str, int] = {}
    for parsed in analysis.parsed_files.values():
        for imp in parsed.imports:
            if not imp.is_relative and imp.target:
                # Extract package name
                pkg = imp.target.split(".")[0]
                external_imports[pkg] = external_imports.get(pkg, 0) + 1

    if external_imports:
        lines.append("\n### External Dependencies (by import frequency)")
        for pkg, count in sorted(
            external_imports.items(), key=lambda x: x[1], reverse=True
        )[:20]:
            lines.append(f"- {pkg}: {count} imports")

    if analysis.security.vulnerabilities:
        lines.append("\n### Known Vulnerabilities")
        for vuln in analysis.security.vulnerabilities[:10]:
            lines.append(
                f"- {vuln.package_name} {vuln.installed_version}: "
                f"{vuln.vulnerability_id} ({vuln.severity})"
            )

    return "\n".join(lines)


def _build_debt_context(analysis: AnalysisResult) -> str:
    """Build context for TECHNICAL_DEBT category."""
    lines: list[str] = ["## Technical Debt Context"]

    # High complexity files
    high_complexity = [
        (pf.path, pf.complexity)
        for pf in analysis.parsed_files.values()
        if pf.complexity > 20
    ]
    if high_complexity:
        lines.append("\n### High Complexity Files")
        for path, complexity in sorted(
            high_complexity, key=lambda x: x[1], reverse=True
        )[:10]:
            lines.append(f"- {path}: complexity {complexity}")

    # Large files
    large_files = [
        (pf.path, pf.line_count)
        for pf in analysis.parsed_files.values()
        if pf.line_count > 300
    ]
    if large_files:
        lines.append("\n### Large Files (>300 lines)")
        for path, lines_count in sorted(large_files, key=lambda x: x[1], reverse=True)[
            :10
        ]:
            lines.append(f"- {path}: {lines_count} lines")

    # Files with many definitions (potential god objects)
    many_defs = [
        (pf.path, len(pf.definitions))
        for pf in analysis.parsed_files.values()
        if len(pf.definitions) > 10
    ]
    if many_defs:
        lines.append("\n### Files with Many Definitions")
        for path, def_count in sorted(many_defs, key=lambda x: x[1], reverse=True)[:10]:
            lines.append(f"- {path}: {def_count} definitions")

    return "\n".join(lines)


def _build_architecture_context(analysis: AnalysisResult) -> str:
    """Build context for ARCHITECTURE category."""
    lines: list[str] = ["## Architecture Context"]

    # Entry points
    if analysis.entry_points:
        lines.append("\n### Entry Points")
        for ep in analysis.entry_points[:10]:
            lines.append(f"- {ep.file_path}:{ep.line} ({ep.entry_type})")
            lines.append(f"  {ep.description}")

    # Hub files
    hubs = sorted(
        analysis.import_graph.in_degree.items(),
        key=lambda x: x[1],
        reverse=True,
    )[:10]
    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")

    # Language distribution
    if analysis.languages:
        lines.append("\n### Language Distribution")
        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_data_flow_context(analysis: AnalysisResult) -> str:
    """Build context for DATA_FLOW category."""
    lines: list[str] = ["## Data Flow Context"]

    # Import chains (simplified view)
    edges = analysis.import_graph.edges
    if edges:
        lines.append("\n### Import Relationships (Sample)")
        count = 0
        for source, targets in edges.items():
            if count >= 20:
                break
            for target in list(targets)[:3]:
                lines.append(f"- {source} → {target}")
                count += 1

    # Files with data transformation indicators
    data_keywords = [
        "transform",
        "convert",
        "parse",
        "serialize",
        "validate",
        "process",
    ]
    data_files = [
        sf.path
        for sf in analysis.scored_files[:30]
        if any(kw in sf.path.lower() for kw in data_keywords)
    ]
    if data_files:
        lines.append("\n### Data Processing Files")
        for path in data_files[:10]:
            lines.append(f"- {path}")

    return "\n".join(lines)


def _build_domain_business_logic_context(analysis: AnalysisResult) -> str:
    """Build context for DOMAIN_BUSINESS_LOGIC category.

    Identifies files and structures that contain business rules, domain models, and core
    workflows. These represent "what the app does and why."
    """
    lines: list[str] = ["## Domain Business Logic Context"]

    # Directories that typically contain domain logic
    domain_dirs = [
        "models",
        "domain",
        "entities",
        "services",
        "workflows",
        "schemas",
        "core",
    ]
    domain_files: list[str] = []
    for sf in analysis.scored_files[:50]:
        path_parts = sf.path.lower().split("/")
        if any(d in path_parts for d in domain_dirs):
            domain_files.append(sf.path)

    if domain_files:
        lines.append("\n### Domain Business Logic Files")
        for path in domain_files[:15]:
            lines.append(f"- {path}")

    # Classes with many methods (likely domain entities or services)
    rich_classes = []
    for pf in analysis.parsed_files.values():
        class_defs = [d for d in pf.definitions if d.kind == "class"]
        for cls in class_defs:
            # Count methods by looking for definitions within class scope
            # Note: this is approximate based on line proximity
            method_count = sum(
                1
                for d in pf.definitions
                if d.kind == "function"
                and cls.line_start < d.line_start < (cls.line_end or 10_000)
            )
            if method_count >= 3:
                rich_classes.append((pf.path, cls.name, method_count))

    if rich_classes:
        lines.append("\n### Rich Domain Classes (3+ methods)")
        for path, name, count in sorted(rich_classes, key=lambda x: x[2], reverse=True)[
            :10
        ]:
            lines.append(f"- {path}.{name}: {count} methods")

    # Entry points (show business workflows)
    if analysis.entry_points:
        lines.append("\n### Entry Points (Business Workflows)")
        for ep in analysis.entry_points[:8]:
            lines.append(f"- {ep.file_path}:{ep.line} ({ep.description})")

    # Modules with domain hints
    domain_modules = [
        m
        for m in analysis.modules
        if InvestigationCategory.DOMAIN_BUSINESS_LOGIC in m.category_hints
    ]
    if domain_modules:
        lines.append("\n### Domain Modules (Business Logic)")
        for mod in domain_modules[:5]:
            lines.append(f"- {mod.primary_path} ({mod.file_count} files)")

    return "\n".join(lines)


def _build_conventions_context(analysis: AnalysisResult) -> str:
    """Build context for CONVENTIONS category.

    Identifies patterns, idioms, naming conventions, and organizational style.
    These represent "how things work around here."
    """
    lines: list[str] = ["## Conventions Context"]

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

    # Import style: relative vs absolute
    relative_imports = 0
    absolute_imports = 0
    for pf in analysis.parsed_files.values():
        for imp in pf.imports:
            if imp.is_relative:
                relative_imports += 1
            elif imp.target and not imp.target.startswith(("unfamiliar", "tests")):
                # External absolute import
                continue
            elif imp.target:
                absolute_imports += 1

    if relative_imports or absolute_imports:
        lines.append("\n### Import Style")
        total = relative_imports + absolute_imports
        if total > 0:
            rel_pct = (relative_imports / total) * 100
            lines.append(f"- Relative imports: {relative_imports} ({rel_pct:.0f}%)")
            lines.append(f"- Absolute imports: {absolute_imports}")

    # Naming patterns in function definitions
    func_prefixes: dict[str, int] = {}
    for pf in analysis.parsed_files.values():
        for d in pf.definitions:
            if d.kind == "function":
                # Extract common prefixes
                name = d.name
                for prefix in [
                    "get_",
                    "set_",
                    "handle_",
                    "process_",
                    "validate_",
                    "build_",
                    "create_",
                    "update_",
                    "delete_",
                    "_",
                ]:
                    if name.startswith(prefix):
                        func_prefixes[prefix] = func_prefixes.get(prefix, 0) + 1
                        break

    if func_prefixes:
        lines.append("\n### Common Function Prefixes")
        for prefix, count in sorted(
            func_prefixes.items(), key=lambda x: x[1], reverse=True
        )[:8]:
            display = prefix if prefix != "_" else "_ (private)"
            lines.append(f"- {display}: {count}")

    # Test naming patterns (if testing detected)
    test_patterns: dict[str, int] = {}
    for tf in analysis.test_info.test_files:
        if "test_" in tf:
            test_patterns["test_*"] = test_patterns.get("test_*", 0) + 1
        elif "_test" in tf:
            test_patterns["*_test"] = test_patterns.get("*_test", 0) + 1
        elif ".spec." in tf:
            test_patterns["*.spec.*"] = test_patterns.get("*.spec.*", 0) + 1

    if test_patterns:
        lines.append("\n### Test File Naming")
        for pattern, count in sorted(
            test_patterns.items(), key=lambda x: x[1], reverse=True
        ):
            lines.append(f"- {pattern}: {count} files")

    # File organization patterns
    top_dirs: dict[str, int] = {}
    for sf in analysis.scored_files:
        parts = sf.path.split("/")
        if len(parts) > 1:
            top_dir = parts[0]
            top_dirs[top_dir] = top_dirs.get(top_dir, 0) + 1

    if top_dirs:
        lines.append("\n### Top-Level Organization")
        for dir_name, count in sorted(
            top_dirs.items(), key=lambda x: x[1], reverse=True
        )[:6]:
            lines.append(f"- {dir_name}/: {count} files")

    return "\n".join(lines)


def _build_security_findings_context(analysis: AnalysisResult) -> str:
    """Build context for security findings."""
    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[:10]:
            lines.append(
                f"- {secret.file_path}:{secret.line_number} - {secret.secret_type}"
            )

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

    return "\n".join(lines)


def _build_previous_missions_section(previous_missions: list[str]) -> str:
    """Build section listing previous mission titles."""
    lines: list[str] = [
        "## Previously Generated Missions",
        "\nThese missions were generated for earlier categories. "
        "Reference for awareness only - do NOT add as prerequisites.",
    ]

    for title in previous_missions:
        lines.append(f"- {title}")

    return "\n".join(lines)


def _get_category_keywords(category: InvestigationCategory) -> list[str]:
    """Get keywords for identifying category-relevant files."""
    keywords_map: dict[InvestigationCategory, list[str]] = {
        InvestigationCategory.ARCHITECTURE: [
            "main",
            "app",
            "server",
            "config",
            "settings",
            "init",
        ],
        InvestigationCategory.DOMAIN_BUSINESS_LOGIC: [
            "service",
            "handler",
            "controller",
            "processor",
            "manager",
            "logic",
            "business",
            "domain",
        ],
        InvestigationCategory.DATA_FLOW: [
            "transform",
            "convert",
            "parse",
            "serialize",
            "validate",
            "pipeline",
            "flow",
            "stream",
        ],
        InvestigationCategory.DEPENDENCIES: [
            "requirements",
            "package",
            "setup",
            "pyproject",
            "cargo",
            "gem",
            "composer",
        ],
        InvestigationCategory.CONVENTIONS: [
            "style",
            "lint",
            "format",
            "convention",
            "pattern",
        ],
        InvestigationCategory.TESTING: [
            "test",
            "spec",
            "conftest",
            "mock",
            "fixture",
        ],
        InvestigationCategory.TECHNICAL_DEBT: [
            "todo",
            "fixme",
            "hack",
            "deprecated",
            "legacy",
        ],
    }
    return keywords_map.get(category, [])
