# retoor <retoor@molodetz.nl>
from __future__ import annotations

import hashlib
import math
import re
from dataclasses import dataclass

from devplacepy.services.jobs.isslop.analysis.languages import extract_comments

COMPLEXITY_TOKENS = re.compile(
    r"\b(if|elif|else if|for|while|case|when|catch|except|and|or)\b|&&|\|\||\?\s"
)
FUNCTION_PATTERNS: dict[str, re.Pattern[str]] = {
    "python": re.compile(r"^\s*(?:async\s+)?def\s+(\w+)", re.MULTILINE),
    "javascript": re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)|^\s*(\w+)\s*\([^)]*\)\s*{", re.MULTILINE),
    "typescript": re.compile(r"^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)|^\s*(\w+)\s*\([^)]*\)\s*{", re.MULTILINE),
    "go": re.compile(r"^\s*func\s+(?:\([^)]+\)\s*)?(\w+)", re.MULTILINE),
    "java": re.compile(r"^\s*(?:public|private|protected|static|\s)+[\w<>\[\]]+\s+(\w+)\s*\(", re.MULTILINE),
    "php": re.compile(r"^\s*(?:public|private|protected|static|\s)*function\s+(\w+)", re.MULTILINE),
    "ruby": re.compile(r"^\s*def\s+(\w+)", re.MULTILINE),
    "rust": re.compile(r"^\s*(?:pub\s+)?(?:async\s+)?fn\s+(\w+)", re.MULTILINE),
}
DOCSTRING_PATTERN = re.compile(r"def\s+\w+[^:]*:\s*\n\s*(?:\"\"\"|''')", re.MULTILINE)
TOKEN_PATTERN = re.compile(r"[A-Za-z_]\w*|\d+|[^\sA-Za-z0-9_]")
DUPLICATION_WINDOW: int = 6
MAINTAINABILITY_CEILING: float = 171.0


@dataclass(frozen=True)
class FileMetrics:
    total_lines: int
    sloc: int
    blank_lines: int
    comment_lines: int
    blank_ratio: float
    comment_ratio: float
    avg_line_length: float
    max_line_length: int
    avg_leading_spaces: float
    avg_leading_tabs: float
    indent_variance: float
    function_count: int
    docstring_count: int
    max_function_length: int
    cyclomatic_total: int
    cyclomatic_max_estimate: int
    duplication_ratio: float
    maintainability_index: float
    import_count: int


def shannon_entropy(value: str) -> float:
    if not value:
        return 0.0
    counts: dict[str, int] = {}
    for char in value:
        counts[char] = counts.get(char, 0) + 1
    total = len(value)
    return -sum((count / total) * math.log2(count / total) for count in counts.values())


def _leading_whitespace(lines: list[str]) -> tuple[float, float, float]:
    spaces: list[int] = []
    tabs: list[int] = []
    for line in lines:
        if not line.strip():
            continue
        space_count = len(line) - len(line.lstrip(" "))
        tab_count = len(line) - len(line.lstrip("\t"))
        spaces.append(space_count)
        tabs.append(tab_count)
    if not spaces:
        return 0.0, 0.0, 0.0
    mean_spaces = sum(spaces) / len(spaces)
    mean_tabs = sum(tabs) / len(tabs)
    variance = sum((value - mean_spaces) ** 2 for value in spaces) / len(spaces)
    return mean_spaces, mean_tabs, math.sqrt(variance)


def _duplication_ratio(lines: list[str]) -> float:
    normalized = [re.sub(r"\s+", " ", line.strip()) for line in lines if line.strip()]
    if len(normalized) < DUPLICATION_WINDOW * 2:
        return 0.0
    seen: dict[str, int] = {}
    duplicated = 0
    windows = 0
    for index in range(len(normalized) - DUPLICATION_WINDOW + 1):
        window = "\n".join(normalized[index:index + DUPLICATION_WINDOW])
        if len(window) < 60:
            continue
        digest = hashlib.sha1(window.encode("utf-8")).hexdigest()
        windows += 1
        if digest in seen:
            duplicated += 1
        seen[digest] = index
    return duplicated / windows if windows else 0.0


def _function_lengths(language: str, lines: list[str]) -> tuple[int, int]:
    pattern = FUNCTION_PATTERNS.get(language)
    if pattern is None:
        return 0, 0
    starts: list[int] = []
    for number, line in enumerate(lines):
        if pattern.match(line):
            starts.append(number)
    if not starts:
        return 0, 0
    lengths: list[int] = []
    for index, start in enumerate(starts):
        end = starts[index + 1] if index + 1 < len(starts) else len(lines)
        lengths.append(end - start)
    return len(starts), max(lengths)


def _maintainability(sloc: int, cyclomatic: int, tokens: list[str]) -> float:
    if sloc <= 0 or not tokens:
        return 100.0
    unique = len(set(tokens))
    volume = len(tokens) * math.log2(max(unique, 2))
    raw = MAINTAINABILITY_CEILING - 5.2 * math.log(max(volume, 1.0)) - 0.23 * cyclomatic - 16.2 * math.log(max(sloc, 1))
    return max(0.0, raw * 100.0 / MAINTAINABILITY_CEILING)


def compute_metrics(language: str, text: str) -> FileMetrics:
    lines = text.splitlines()
    total = len(lines)
    blank = sum(1 for line in lines if not line.strip())
    comments = extract_comments(language, lines)
    comment_lines = len(comments)
    sloc = max(0, total - blank - comment_lines)
    lengths = [len(line) for line in lines if line.strip()]
    avg_length = sum(lengths) / len(lengths) if lengths else 0.0
    max_length = max(lengths) if lengths else 0
    mean_spaces, mean_tabs, indent_deviation = _leading_whitespace(lines)
    function_count, max_function_length = _function_lengths(language, lines)
    docstring_count = len(DOCSTRING_PATTERN.findall(text)) if language == "python" else 0
    cyclomatic = len(COMPLEXITY_TOKENS.findall(text))
    per_function = cyclomatic // function_count if function_count else cyclomatic
    tokens = TOKEN_PATTERN.findall(text)[:20000]
    import_count = len(re.findall(r"^\s*(?:import|from|require|use|#include)\b", text, re.MULTILINE))
    return FileMetrics(
        total_lines=total,
        sloc=sloc,
        blank_lines=blank,
        comment_lines=comment_lines,
        blank_ratio=blank / total if total else 0.0,
        comment_ratio=comment_lines / total if total else 0.0,
        avg_line_length=avg_length,
        max_line_length=max_length,
        avg_leading_spaces=mean_spaces,
        avg_leading_tabs=mean_tabs,
        indent_variance=indent_deviation,
        function_count=function_count,
        docstring_count=docstring_count,
        max_function_length=max_function_length,
        cyclomatic_total=cyclomatic,
        cyclomatic_max_estimate=per_function,
        duplication_ratio=_duplication_ratio(lines),
        maintainability_index=_maintainability(sloc, per_function, tokens),
        import_count=import_count,
    )
