"""Text output formatting for vibelexity."""

from __future__ import annotations

from collections import defaultdict
from dataclasses import dataclass
from vibelexity import fmt, thresholds
from vibelexity.analysis import (
    _get_metric_val,
    _compute_stats,
    _build_candidate_lists,
    _find_refactor_candidates,
    _compute_duplication_stats,
    _compute_cycle_stats,
    _aggregate_cognitive,
    _aggregate_npath,
    _aggregate_dtd,
    _aggregate_cyclomatic,
    _aggregate_mi,
    _aggregate_ldi,
)


_METRIC_SECTIONS = [
    (
        "Cognitive complexity",
        "Cognitive",
        _aggregate_cognitive,
        "complexity",
        thresholds.COGNITIVE_COMPLEXITY_THRESHOLD,
        "higher",
    ),
    (
        "NPath complexity",
        "NPath",
        _aggregate_npath,
        "npath",
        thresholds.NPATH_THRESHOLD,
        "higher",
    ),
    (
        "Data Transformation Density (DTD)",
        "DTD",
        _aggregate_dtd,
        "dtd",
        thresholds.DTD_THRESHOLD,
        "higher",
    ),
    (
        "Cyclomatic complexity",
        "Cyclomatic",
        _aggregate_cyclomatic,
        "cyclomatic",
        thresholds.CYCLOMATIC_THRESHOLD,
        "higher",
    ),
    (
        "Maintainability Index (MI)",
        "MI",
        _aggregate_mi,
        "mi",
        thresholds.MI_RED_THRESHOLD,
        "lower",
    ),
    (
        "Logic Density Index (LDI)",
        "LDI",
        _aggregate_ldi,
        "ldi",
        thresholds.LDI_THRESHOLD,
        "higher",
    ),
]


@dataclass
class StatsLineConfig:
    lbl: str
    stats: dict
    use_compact: bool = False
    metric_name: str | None = None
    threshold: int = 0
    threshold_mode: str = "higher"


@dataclass
class TextSummaryConfig:
    results: list
    all_funcs: list
    duplicates: list
    cycle_stats: dict
    dup_stats: dict
    total_loc: int


def _print_text_summary(config: TextSummaryConfig):
    """Print the module/function count and line-metric summary."""
    lines = [
        f"\n{fmt.header('Analyzed')} {fmt.colored_int(len(config.results))} modules, {fmt.colored_int(len(config.all_funcs))} functions",
        f"  {fmt.label('Total lines:')} {fmt.colored_int(config.total_loc)}",
        f"  {fmt.label('Total functions:')} {fmt.colored_int(len(config.all_funcs))}",
        fmt.header("Lines per module:"),
        _format_stats_line(
            StatsLineConfig("LOC", _compute_stats([r.loc for r in config.results]))
        ),
        _format_stats_line(
            StatsLineConfig("LLOC", _compute_stats([r.lloc for r in config.results]))
        ),
        _format_stats_line(
            StatsLineConfig("SLOC", _compute_stats([r.sloc for r in config.results]))
        ),
    ]
    for line in lines:
        print(line)


def _format_value_with_threshold(value, threshold, mode="higher", use_compact=False):
    """Format a value with threshold warning if violated.

    Args:
        value: The value to format
        threshold: The threshold value
        mode: "higher" (violates if > threshold) or "lower" (violates if < threshold)
        use_compact: Whether to use compact formatting for large numbers
    """
    if threshold == 0:
        return value

    if mode == "higher":
        violates = value > threshold
    else:
        violates = value < threshold

    if violates:
        op = ">" if mode == "higher" else "<"
        val_fmt = fmt.fmt_compact(value) if use_compact else fmt.fmt_number(value)
        thresh_fmt = (
            fmt.fmt_compact(threshold) if use_compact else fmt.fmt_number(threshold)
        )
        return fmt.warn(f"{val_fmt} ({op}{thresh_fmt})")
    else:
        val_formatter = fmt.colored_compact if use_compact else fmt.colored_num
        return val_formatter(value)


def _format_stats_line(config: StatsLineConfig):
    if config.metric_name and config.threshold != 0:
        p99_formatter = lambda v: _format_value_with_threshold(
            v, config.threshold, config.threshold_mode, config.use_compact
        )
        p95_formatter = lambda v: _format_value_with_threshold(
            v, config.threshold, config.threshold_mode, config.use_compact
        )
    else:
        p99_formatter = None
        p95_formatter = None

    val_formatter = fmt.colored_compact if config.use_compact else fmt.colored_num

    return (
        f"  {config.lbl:<51}"
        f"{fmt.rpad(config.stats['average'], 12, val_formatter)}"
        f"  {fmt.rpad(config.stats['median'], 12, val_formatter)}"
        f"  {fmt.rpad(config.stats['p95'], 12, p95_formatter or val_formatter)}"
        f"  {fmt.rpad(config.stats['p99'], 12, p99_formatter or val_formatter)}"
    )


def _print_text_distributions(results, all_funcs):
    """Print complexity, NPath, DTD, cyclomatic, MI, and LDI distributions."""
    print(f"\n{fmt.header('Statistics')}")
    print(
        f"                                                          Average        Median           P95           P99"
    )
    for title, lbl, agg_fn, metric_name, threshold, threshold_mode in _METRIC_SECTIONS:
        agg = agg_fn(results)
        use_compact = agg_fn == _aggregate_npath
        print(
            _format_stats_line(
                StatsLineConfig(
                    lbl=title,
                    stats=agg["stats"],
                    use_compact=use_compact,
                    metric_name=metric_name,
                    threshold=threshold,
                    threshold_mode=threshold_mode,
                )
            )
        )


def _print_text_refactor_candidates(candidate_lists):
    """Print top 10 refactor candidates."""
    candidates, func_lookup, thresholds = _find_refactor_candidates(candidate_lists)
    print(f"\n{fmt.header('Top 10 Refactor Candidates:')}")
    if not candidates:
        print(f"  {fmt.dim('No refactor candidates found')}")
        return

    for key, names in candidates[:10]:
        fpath, func = func_lookup[key]
        violations = _build_violation_list(func, names, thresholds)
        func_col = f"{fpath}:{func.line}"
        print(f"{fmt.func_name(func.name):<45} {func_col:<70} {', '.join(violations)}")


def _build_violation_list(func, names, thresholds):
    """Build list of metric violations for display."""
    violations = []
    for n in names:
        val = _get_metric_val(func, n)
        thresh = thresholds.get(n, 0)
        op = "<" if n == "mi" else ">"
        val_fmt = fmt.fmt_compact(val) if n == "npath" else fmt.fmt_number(val)
        violations.append(
            f"{n}: {fmt.warn(val_fmt)} {fmt.dim(f'({op} {fmt.fmt_number(thresh)})')}"
        )
    return violations


def _print_text_all_functions(all_funcs):
    """Print all functions with full metric details."""
    print(f"\n{fmt.header('All functions:')}")
    for fpath, func in sorted(all_funcs, key=lambda x: x[1].complexity, reverse=True):
        effort = func.halstead.effort if func.halstead else 0
        loc = f"{fmt.path(fpath)}:{func.line}"
        vals = (
            f"{fmt.label('complexity:')} {fmt.colored_num(func.complexity)}, "
            f"{fmt.label('effort:')} {fmt.colored_num(effort)}, "
            f"{fmt.label('npath:')} {fmt.colored_compact(func.npath if func.npath is not None else 0)}, "
            f"{fmt.label('dtd:')} {fmt.colored_num(func.dtd if func.dtd is not None else 0)}, "
            f"{fmt.label('cyclomatic:')} {fmt.colored_num(func.cyclomatic if func.cyclomatic is not None else 0)}, "
            f"{fmt.label('mi:')} {fmt.colored_num(func.mi if func.mi is not None else 0)}, "
            f"{fmt.label('ldi:')} {fmt.colored_num(func.ldi if func.ldi is not None else 0)}"
        )
        print(f"  {loc} {fmt.func_name(func.name)} ({vals})")


def _print_text_patterns(results, show_bad_patterns=False):
    """Print pattern violations."""
    all_violations = []
    for result in results:
        for violation in result.pattern_violations:
            all_violations.append(
                (result.path, violation.line, violation.type, violation.description)
            )

    if not all_violations:
        print(f"\n{fmt.header('Pattern violations:')}")
        print(f"  {fmt.dim('No pattern violations found')}")
        return

    all_violations.sort(key=lambda x: (x[2], x[1]))

    violations_by_type = defaultdict(list)
    for path, line, v_type, description in all_violations:
        violations_by_type[v_type].append((path, line, description))

    type_counts = {
        v_type: len(violations) for v_type, violations in violations_by_type.items()
    }

    if show_bad_patterns:
        print(f"\n{fmt.header('Pattern violations:')}")

        for v_type in sorted(type_counts.keys()):
            label = v_type.replace("_", " ").title()
            print(
                f"\n  {fmt.label(f'{label}:')} {fmt.colored_int(type_counts[v_type])}"
            )

            for path, line, description in violations_by_type[v_type]:
                loc = f"{fmt.path(path)}:{line}"
                print(f"    {loc:<70} {description}")
    else:
        print(f"\n{fmt.header('Pattern violations:')}")
        print(
            f"  {fmt.label('Total violations:')} {fmt.colored_int(len(all_violations))}"
        )
        for v_type in sorted(type_counts.keys()):
            label = v_type.replace("_", " ").title()
            print(
                f"  {fmt.label(f'{label}:')}    {fmt.colored_int(type_counts[v_type])}"
            )
        print(f"  {fmt.dim('Use --show-bad-patterns to see the full list')}")


def _print_text_duplicates(
    duplicates, show_dupes=False, dup_stats=None, all_funcs=None, total_loc=None
):
    """Print duplication clusters."""
    type1 = [c for c in duplicates if c.dup_type == 1]
    type2 = [c for c in duplicates if c.dup_type == 2]
    print(f"\n{fmt.header('Code duplication:')}")
    lines = [
        f"  {fmt.label('Type 1 (exact clones):')}      {fmt.colored_int(len(type1))} cluster(s)",
        f"  {fmt.label('Type 2 (parametric clones):')} {fmt.colored_int(len(type2))} cluster(s)",
    ]
    for line in lines:
        print(line)
    if dup_stats and all_funcs and total_loc is not None:
        lines = [
            f"  {fmt.label('Duplicate lines:')} {fmt.colored_int(dup_stats['duplicate_lines'])}/{fmt.colored_int(total_loc)} ({dup_stats['duplicate_lines_pct']:.1f}%)",
            f"  {fmt.label('Duplicate functions:')} {fmt.colored_int(dup_stats['duplicate_functions'])}/{fmt.colored_int(len(all_funcs))} ({dup_stats['duplicate_functions_pct']:.1f}%)",
        ]
        for line in lines:
            print(line)
    if show_dupes and duplicates:
        for cluster in duplicates:
            kind = "Type 1 (exact)" if cluster.dup_type == 1 else "Type 2 (parametric)"
            print(
                f"\n  {fmt.label(kind)} - {fmt.colored_int(len(cluster.members))} functions:"
            )
            for fpath, func in cluster.members:
                print(f"    {fmt.path(fpath)}:{func.line} {fmt.func_name(func.name)}")
    elif not show_dupes:
        print(f"  {fmt.dim('Use --show-dupes to see the full list')}")


def _print_text_cycle_stats(cycle_stats, show_circulars=False):
    """Print circular import cycle statistics."""
    num_cycles = cycle_stats.get("num_cycles", 0)

    stats = cycle_stats.get("cycle_length_stats", _compute_stats([]))

    lines = [f"\n{fmt.header('Circular import cycles:')}"]
    lines.append(
        f"  {fmt.label('Number of cycles detected:')} {fmt.colored_int(num_cycles)}"
    )
    if num_cycles > 0:
        lines.append(f"  {fmt.label('Cycle length statistics (nodes per cycle):')}")
        lines.append(_format_stats_line(StatsLineConfig(lbl="  Stats", stats=stats)))
    for line in lines:
        print(line)

    if show_circulars and num_cycles > 0:
        sum_cycles_per_file = cycle_stats.get("sum_cycles_per_file", {})
        if sum_cycles_per_file:
            print(f"\n  {fmt.label('Cycles per file:')}")
            for file_path, count in sorted(sum_cycles_per_file.items()):
                print(f"    {fmt.path(file_path)}: {fmt.colored_int(count)}")
    elif not show_circulars:
        print(f"  {fmt.dim('Use --show-circulars to see cycle details')}")


def _print_text_dead_code_summary(dead_code_violations, show_dead=False):
    """Print dead code violations summary by type."""
    type_counts = defaultdict(int)
    for v in dead_code_violations:
        type_counts[v.type] += 1

    labels = {
        "unused_import": "Unused imports",
        "dead_function": "Dead functions",
        "dead_class": "Dead classes",
        "unused_local_var": "Unused local variables",
    }

    total = len(dead_code_violations)
    print(f"\n{fmt.header('Dead code violations:')}")
    print(f"  {fmt.label('Total violations:')} {fmt.colored_int(total)}")
    for v_type in sorted(type_counts.keys()):
        label = labels.get(v_type, v_type.replace("_", " ").title())
        print(f"  {fmt.label(f'{label}:')}    {fmt.colored_int(type_counts[v_type])}")
    if not show_dead:
        print(f"  {fmt.dim('Use --show-dead to see the full list')}")


def _print_text_fatness(fatness_report, show_fatties=False):
    """Print fatness metrics - files/functions that exceed size thresholds."""
    wide_functions = [f for f in fatness_report.fat_functions if f["param_count"] > 5]
    save_world_functions = [
        f for f in fatness_report.fat_functions if f["call_count"] > 7
    ]

    items = [
        ("Fat files", len(fatness_report.fat_files)),
        ("Fat functions", len(fatness_report.fat_functions)),
        ("Wide functions (>5 params)", len(wide_functions)),
        ("Save the world functions (>7 calls)", len(save_world_functions)),
    ]
    print(f"\n{fmt.header('Fatness metrics:')}")
    for label, count in items:
        print(f"  {fmt.label(f'{label}:')} {fmt.colored_int(count)}")

    if not show_fatties:
        print(f"  {fmt.dim('Use --show-fatties to see the full list')}")

    if show_fatties:
        if fatness_report.fat_files:
            print(f"\n  Fat files (>500 LOC or >20 functions):")
            for file_info in fatness_report.fat_files:
                print(f"    {fmt.path(file_info['path'])} - {file_info['reason']}")

        if fatness_report.fat_functions:
            print(f"\n  Fat functions (>50 LOC, >5 params, or >7 calls):")
            for func_info in fatness_report.fat_functions:
                print(
                    f"    {fmt.path(func_info['path'])}:{func_info['line']} {fmt.func_name(func_info['name'])} - {func_info['reason']}"
                )
