"""Shared helpers for cognitive complexity analyzers (TypeScript & Go)."""

from __future__ import annotations

import math
from collections import Counter
from dataclasses import dataclass
from typing import TYPE_CHECKING, Callable

if TYPE_CHECKING:
    from tree_sitter import Node

from .models import HalsteadMetrics


@dataclass
class HalsteadRawCounts:
    n1: int
    n2: int
    N1: int
    N2: int
    n: int
    N: int


def _halstead_derived(
    counts: HalsteadRawCounts,
) -> tuple[float, float, float, float, float]:
    """Compute derived Halstead measures from base counts."""
    volume = counts.N * math.log2(counts.n)
    difficulty = (counts.n1 / 2) * (counts.N2 / counts.n2)
    effort = difficulty * volume
    return (
        round(volume, 2),
        round(difficulty, 2),
        round(effort, 2),
        round(effort / 18, 2),
        round(effort ** (2 / 3) / 3000, 4),
    )


def compute_halstead(operators: list[str], operands: list[str]) -> HalsteadMetrics:
    """Compute Halstead metrics from raw operator and operand lists."""
    op_counts = Counter(operators)
    od_counts = Counter(operands)

    n1 = len(op_counts)
    n2 = len(od_counts)
    N1 = sum(op_counts.values())
    N2 = sum(od_counts.values())

    n = n1 + n2  # vocabulary
    N = N1 + N2  # length

    if n == 0 or n2 == 0:
        return HalsteadMetrics(n1=n1, n2=n2, N1=N1, N2=N2, vocabulary=n, length=N)

    counts = HalsteadRawCounts(n1=n1, n2=n2, N1=N1, N2=N2, n=n, N=N)
    volume, difficulty, effort, time, bugs = _halstead_derived(counts)
    return HalsteadMetrics(
        n1=n1,
        n2=n2,
        N1=N1,
        N2=N2,
        vocabulary=n,
        length=N,
        volume=volume,
        difficulty=difficulty,
        effort=effort,
        time=time,
        bugs=bugs,
    )


# ---------------------------------------------------------------------------
# Line-metric helpers (LOC / SLOC) -- shared across all languages
# ---------------------------------------------------------------------------


def _compute_loc(source: str) -> int:
    """Count lines of code (non-blank lines)."""
    return sum(1 for line in source.splitlines() if line.strip())


def _collect_comment_nodes(node: Node, acc: list[Node]) -> None:
    """Recursively collect all comment nodes from the AST."""
    if node.type == "comment":
        acc.append(node)
    for child in node.children:
        _collect_comment_nodes(child, acc)


def _compute_sloc(source: str, root_node: Node) -> int:
    """Count source lines of code (non-blank, non-comment-only lines)."""
    lines = source.splitlines()
    comment_only: set[int] = set()

    comments: list = []
    _collect_comment_nodes(root_node, comments)

    for comment in comments:
        start_line = comment.start_point[0]
        end_line = comment.end_point[0]
        start_col = comment.start_point[1]
        end_col = comment.end_point[1]

        for line_idx in range(start_line, end_line + 1):
            if line_idx >= len(lines):
                continue
            line = lines[line_idx]
            if start_line == end_line:
                # Single-line comment -- comment-only if nothing else on the line
                if not line[:start_col].strip() and not line[end_col:].strip():
                    comment_only.add(line_idx)
            elif line_idx == start_line:
                if not line[:start_col].strip():
                    comment_only.add(line_idx)
            elif line_idx == end_line:
                if not line[end_col:].strip():
                    comment_only.add(line_idx)
            else:
                # Interior line of a multi-line comment
                comment_only.add(line_idx)

    return sum(
        1 for i, line in enumerate(lines) if line.strip() and i not in comment_only
    )


def _unwrap_parens(node: Node) -> Node:
    """Unwrap parenthesized_expression nodes to get the inner expression."""
    while node.type == "parenthesized_expression":
        for child in node.children:
            if child.type not in ("(", ")"):
                node = child
                break
        else:
            break
    return node


def _is_logical_binary(node: Node) -> bool:
    """Check if a node (possibly parenthesized) is a binary_expression with && or ||."""
    node = _unwrap_parens(node)
    if node.type != "binary_expression":
        return False
    for child in node.children:
        if child.type in ("&&", "||"):
            return True
    return False


def _flatten_boolean_ops(
    node: Node, ops: list[str], right_subtrees: list[Node]
) -> None:
    """Flatten a left-associative binary_expression tree of logical operators."""
    node = _unwrap_parens(node)
    if not _is_logical_binary(node):
        return
    children = node.children
    if len(children) >= 3:
        left = children[0]
        operator = children[1]
        right = children[2]
        if _is_logical_binary(left):
            _flatten_boolean_ops(left, ops, right_subtrees)
        ops.append(operator.type)
        if _is_logical_binary(right):
            right_subtrees.append(_unwrap_parens(right))


def _count_bool_ops(node: Node) -> int:
    """Count logical binary operators (&& / ||) in an expression subtree."""
    unwrapped = _unwrap_parens(node)
    if unwrapped.type == "binary_expression":
        is_logical = False
        for child in unwrapped.children:
            if child.type in ("&&", "||"):
                is_logical = True
                break
        count = 1 if is_logical else 0
        # Recurse into the unwrapped node's children to avoid double-counting
        for child in unwrapped.children:
            if child.type not in ("&&", "||"):
                count += _count_bool_ops(child)
        return count
    # For non-binary nodes, recurse normally
    count = 0
    for child in unwrapped.children:
        count += _count_bool_ops(child)
    return count


def compute_maintainability_index(
    halstead_volume: float, cyclomatic: int, loc: int
) -> float:
    """Compute the VSCode Maintainability Index for a single function.

    MI = max(0, (171 - 5.2*ln(HV) - 0.23*CC - 16.2*ln(LOC)) * 100 / 171)
    Scale: 0-100, higher = more maintainable.
    """
    if halstead_volume <= 0 or loc <= 0:
        return 100.0
    mi = (
        171 - 5.2 * math.log(halstead_volume) - 0.23 * cyclomatic - 16.2 * math.log(loc)
    )
    return round(max(0.0, mi * 100 / 171), 2)


def compute_ldi(volume: float, vocabulary: int, n2: int, difficulty: float) -> float:
    """Compute the Logic Density Index for a single function.

    LDI = min(100, (vocabulary/100 * 40) + (difficulty/60 * 40) + (n2/volume * 200))
    Scale: 0-100, higher = more complex logic.
    """
    if volume <= 0:
        return 0.0
    ldi = (vocabulary / 100 * 40) + (difficulty / 60 * 40) + (n2 / volume * 200)
    return round(min(100.0, ldi), 2)


def _score_boolean_chain(
    node: Node, flatten_fn: Callable[[Node, list[str], list[Node]], None] | None = None
) -> int:
    """Score a boolean chain in binary_expression nodes using && and ||.

    Rules:
    - +1 for the first boolean operator in a sequence
    - +1 for each switch between operator types (&& -> ||, || -> &&)
    - Same consecutive operators don't add extra cost

    If *flatten_fn* is provided it replaces the default `_flatten_boolean_ops`.
    """
    if flatten_fn is None:
        flatten_fn = _flatten_boolean_ops
    ops = []
    right_subtrees = []
    flatten_fn(node, ops, right_subtrees)

    score = 0
    if ops:
        score = 1
        for i in range(1, len(ops)):
            if ops[i] != ops[i - 1]:
                score += 1

    for subtree in right_subtrees:
        score += _score_boolean_chain(subtree, flatten_fn=flatten_fn)

    return score
