"""Fatness metrics - identify files/functions that exceed size thresholds."""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, TypedDict

if TYPE_CHECKING:
    from vibelexity.models import CallGraph, FileComplexity, FunctionComplexity


class FatFileInfo(TypedDict):
    path: str
    loc: int
    function_count: int
    reason: str


class FatFunctionInfo(TypedDict):
    path: str
    name: str
    line: int
    func_loc: int
    param_count: int
    call_count: int
    reason: str


@dataclass
class FatnessReport:
    fat_files: list[FatFileInfo] = field(default_factory=list)
    fat_functions: list[FatFunctionInfo] = field(default_factory=list)


_FILE_LOC_THRESHOLD = 500
_FILE_FUNC_COUNT_THRESHOLD = 20
_FUNCTION_LOC_THRESHOLD = 50
_FUNCTION_PARAM_COUNT_THRESHOLD = 5
_FUNCTION_CALL_COUNT_THRESHOLD = 7


def _get_call_counts(call_graph: CallGraph | None) -> dict[tuple[str, str], int]:
    """Count unique function calls per caller.

    Returns the number of unique functions called by each function.
    Multiple calls to the same callee are counted once.
    """
    if not call_graph:
        return {}

    unique_callees: dict[tuple[str, str], set[str]] = defaultdict(set)
    for call in call_graph.calls:
        if call.call_type == "external":
            continue
        caller_key = (call.caller_function, call.caller_file)
        callee_key = f"{call.callee_file}:{call.callee_function}"
        unique_callees[caller_key].add(callee_key)

    return {k: len(v) for k, v in unique_callees.items()}


def _build_file_fatness_reasons(
    file_result: FileComplexity, function_count: int
) -> str | None:
    is_fat_by_loc = file_result.loc > _FILE_LOC_THRESHOLD
    is_fat_by_funcs = function_count > _FILE_FUNC_COUNT_THRESHOLD

    if not is_fat_by_loc and not is_fat_by_funcs:
        return None

    reasons = []
    if is_fat_by_loc:
        reasons.append(f"{file_result.loc} LOC (threshold: {_FILE_LOC_THRESHOLD})")
    if is_fat_by_funcs:
        reasons.append(
            f"{function_count} functions (threshold: {_FILE_FUNC_COUNT_THRESHOLD})"
        )

    return ", ".join(reasons)


def _add_fat_file_if_needed(file_result: FileComplexity, report: FatnessReport) -> None:
    function_count = len(file_result.functions)
    reason = _build_file_fatness_reasons(file_result, function_count)

    if reason:
        report.fat_files.append(
            {
                "path": file_result.path,
                "loc": file_result.loc,
                "function_count": function_count,
                "reason": reason,
            }
        )


def _build_function_fatness_reasons(func, call_counts: dict, path: str) -> str | None:
    is_fat_by_func_loc = func.func_loc > _FUNCTION_LOC_THRESHOLD
    is_fat_by_param_count = func.param_count > _FUNCTION_PARAM_COUNT_THRESHOLD
    func_call_count = call_counts.get((func.name, path), 0)
    is_fat_by_call_count = func_call_count > _FUNCTION_CALL_COUNT_THRESHOLD

    if (
        not is_fat_by_func_loc
        and not is_fat_by_param_count
        and not is_fat_by_call_count
    ):
        return None

    reasons = []
    if is_fat_by_func_loc:
        reasons.append(f"{func.func_loc} LOC (threshold: {_FUNCTION_LOC_THRESHOLD})")
    if is_fat_by_param_count:
        reasons.append(
            f"{func.param_count} params (threshold: {_FUNCTION_PARAM_COUNT_THRESHOLD})"
        )
    if is_fat_by_call_count:
        reasons.append(
            f"{func_call_count} function calls (threshold: {_FUNCTION_CALL_COUNT_THRESHOLD})"
        )

    return ", ".join(reasons)


def _add_fat_function_if_needed(
    func, call_counts: dict, path: str, report: FatnessReport
) -> None:
    func_call_count = call_counts.get((func.name, path), 0)
    reason = _build_function_fatness_reasons(func, call_counts, path)

    if reason:
        report.fat_functions.append(
            {
                "path": path,
                "name": func.name,
                "line": func.line,
                "func_loc": func.func_loc,
                "param_count": func.param_count,
                "call_count": func_call_count,
                "reason": reason,
            }
        )


def compute_fatness(
    file_complexities: list[FileComplexity], call_graph: CallGraph | None = None
) -> FatnessReport:
    """Compute fatness metrics for files and functions."""
    report = FatnessReport()
    call_counts = _get_call_counts(call_graph)

    for file_result in file_complexities:
        _add_fat_file_if_needed(file_result, report)

        for func in file_result.functions:
            _add_fat_function_if_needed(func, call_counts, file_result.path, report)

    return report


__all__ = ["FatnessReport", "compute_fatness"]
