"""Code and session metrics for the engineer reports.

Python files get AST-derived metrics (functions, classes, cyclomatic
complexity, nesting depth, imports); any file gets size metrics. Cyclomatic
complexity is the classic approximation: 1 per function plus 1 per branch
point (if/elif/for/while/except/assert/bool-op/comprehension-if/ternary).
"""

from __future__ import annotations

import ast
from dataclasses import dataclass, field

_BRANCHES = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.ExceptHandler,
             ast.Assert, ast.IfExp)


@dataclass
class FileMetrics:
    path: str
    loc: int = 0            # physical lines
    sloc: int = 0           # non-blank, non-comment lines
    chars: int = 0
    is_python: bool = False
    functions: int = 0
    classes: int = 0
    complexity: int = 0     # sum over functions (module counts as one unit)
    max_complexity: int = 0
    max_depth: int = 0      # deepest statement nesting
    imports: list[str] = field(default_factory=list)
    avg_function_len: float = 0.0
    parse_error: bool = False


def file_metrics(path: str, content: str) -> FileMetrics:
    lines = content.splitlines()
    m = FileMetrics(
        path=path,
        loc=len(lines),
        sloc=sum(1 for l in lines if l.strip() and not l.strip().startswith("#")),
        chars=len(content),
        is_python=path.endswith(".py"),
    )
    if not m.is_python:
        return m
    try:
        tree = ast.parse(content)
    except SyntaxError:
        m.parse_error = True
        return m

    fn_lens: list[int] = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            m.functions += 1
            fn_complexity = 1 + _branch_count(node)
            m.max_complexity = max(m.max_complexity, fn_complexity)
            if node.end_lineno:
                fn_lens.append(node.end_lineno - node.lineno + 1)
        elif isinstance(node, ast.ClassDef):
            m.classes += 1
        elif isinstance(node, ast.Import):
            m.imports += [a.name.split(".")[0] for a in node.names]
        elif isinstance(node, ast.ImportFrom) and node.module:
            m.imports.append(node.module.split(".")[0])
    m.imports = sorted(set(m.imports))
    m.complexity = 1 + _branch_count(tree)
    m.max_depth = _max_depth(tree)
    m.avg_function_len = round(sum(fn_lens) / len(fn_lens), 1) if fn_lens else 0.0
    return m


def _branch_count(node: ast.AST) -> int:
    count = 0
    for child in ast.walk(node):
        if isinstance(child, _BRANCHES):
            count += 1
        elif isinstance(child, ast.BoolOp):
            count += len(child.values) - 1
        elif isinstance(child, (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)):
            count += sum(len(gen.ifs) + 1 for gen in child.generators)
    return count


def _max_depth(tree: ast.AST) -> int:
    deepest = 0

    def walk(node: ast.AST, depth: int) -> None:
        nonlocal deepest
        for child in ast.iter_child_nodes(node):
            if isinstance(child, (ast.If, ast.For, ast.AsyncFor, ast.While, ast.With,
                                  ast.AsyncWith, ast.Try, ast.FunctionDef,
                                  ast.AsyncFunctionDef, ast.ClassDef)):
                deepest = max(deepest, depth + 1)
                walk(child, depth + 1)
            else:
                walk(child, depth)

    walk(tree, 0)
    return deepest


def aggregate(files: list[FileMetrics]) -> dict:
    py = [f for f in files if f.is_python and not f.parse_error]
    return {
        "files": len(files),
        "loc": sum(f.loc for f in files),
        "sloc": sum(f.sloc for f in files),
        "chars": sum(f.chars for f in files),
        "functions": sum(f.functions for f in py),
        "classes": sum(f.classes for f in py),
        "complexity": sum(f.complexity for f in py),
        "max_complexity": max((f.max_complexity for f in py), default=0),
        "max_depth": max((f.max_depth for f in py), default=0),
        "imports": sorted({i for f in py for i in f.imports}),
        "avg_function_len": round(
            sum(f.avg_function_len * f.functions for f in py) /
            max(1, sum(f.functions for f in py)), 1),
        "parse_errors": sum(1 for f in files if f.parse_error),
    }


def session_metrics(events: list[dict]) -> dict:
    actions: dict[str, int] = {}
    for e in events:
        if e["event"] == "action":
            kind = e.get("action", "?")
            actions[kind] = actions.get(kind, 0) + 1
    completions = sum(1 for e in events if e["event"] == "completion")
    return {
        # older sessions have no per-turn completion events; fall back to action counts
        "model_calls": completions or sum(
            1 for e in events if e["event"] in ("action", "bad_action")),
        "actions": actions,
        "bad_actions": sum(1 for e in events if e["event"] == "bad_action"),
        "test_runs": sum(1 for e in events if e["event"] == "run_tests"),
        "tokens_out": sum(e.get("tokens", 0) for e in events if e["event"] == "completion"),
        "tokens_in": sum(e.get("tokens_in", 0) for e in events if e["event"] == "completion"),
        "turns": max((e.get("turn", 0) for e in events), default=0),
    }
