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

import math
import re
from dataclasses import dataclass, field, replace
from typing import Any

from devplacepy.services.jobs.isslop.analysis.domsignals.aggregate import DomEvidence
from devplacepy.services.jobs.isslop.analysis.metrics import FileMetrics
from devplacepy.services.jobs.isslop.analysis.signals import (
    AXIS_ORIGIN,
    SEVERITY_FACTORS,
    SEVERITY_MEDIUM,
    SEVERITY_STRONG,
    Signal,
)
from devplacepy.services.jobs.isslop.config import DOM_AI_WEIGHT

CATEGORY_SLOP: str = "ai-slop"
CATEGORY_SOPHISTICATED: str = "sophisticated-ai"
CATEGORY_HUMAN_CLEAN: str = "human-clean"
CATEGORY_HUMAN_MESSY: str = "human-messy"
CATEGORY_UNCERTAIN: str = "uncertain"

ORIGIN_HIGH_THRESHOLD: float = 55.0
ORIGIN_LOW_THRESHOLD: float = 45.0
QUALITY_SLOP_THRESHOLD: float = 50.0
QUALITY_CLEAN_THRESHOLD: float = 30.0
QUALITY_SATURATION: float = 14.0
ORIGIN_SIGNAL_CAP: float = 45.0
ORIGIN_SIGNAL_SCALE: float = 4.5

CRITICAL_PATH_PATTERN = re.compile(r"auth|login|password|payment|billing|crypto|security|token|session|admin", re.IGNORECASE)
CRITICALITY_WEIGHT: float = 1.5

GRADE_BANDS: tuple[tuple[float, str], ...] = ((15.0, "A"), (30.0, "B"), (50.0, "C"), (70.0, "D"), (100.0, "F"))

AI_SCORE_WEIGHT: float = 0.68
QUALITY_SCORE_WEIGHT: float = 0.32

AI_RAMP_LOW: float = 35.0
AI_RAMP_HIGH: float = 65.0

IMAGE_TEXT_AI_WEIGHT: float = 0.75
IMAGE_AI_WEIGHT: float = 0.25


def compose_slop_score(ai_percent: float, quality_deficit: float) -> float:
    return round(AI_SCORE_WEIGHT * ai_percent + QUALITY_SCORE_WEIGHT * quality_deficit, 1)


def ai_fraction(origin: float) -> float:
    if origin <= AI_RAMP_LOW:
        return 0.0
    if origin >= AI_RAMP_HIGH:
        return 1.0
    position = (origin - AI_RAMP_LOW) / (AI_RAMP_HIGH - AI_RAMP_LOW)
    return position * position * (3.0 - 2.0 * position)

HUMAN_MARKER_PATTERN = re.compile(r"\b(HACK|FIXME|XXX)\b.*[A-Z]{2,}-\d+|\bFIXME\(\w+\)", re.IGNORECASE)


@dataclass
class FileScore:
    relative: str
    language: str
    sloc: int
    origin_score: float
    quality_deficit: float
    category: str
    criticality: float
    signals: list[Signal] = field(default_factory=list)


@dataclass(frozen=True)
class RepoScores:
    origin_score: float
    quality_deficit: float
    slop_score: float
    grade: str
    category: str
    human_percent: float
    ai_percent: float
    confidence: str
    strong_signal_count: int
    medium_signal_count: int
    files_scored: int


def quality_deficit_score(signals: list[Signal], metrics: FileMetrics) -> float:
    weighted = sum(
        signal.weight * SEVERITY_FACTORS.get(signal.severity, 0.3)
        for signal in signals
        if signal.axis != AXIS_ORIGIN
    )
    if metrics.maintainability_index < 40.0:
        weighted += 2.0
    elif metrics.maintainability_index < 65.0:
        weighted += 1.0
    return round(100.0 * (1.0 - math.exp(-weighted / QUALITY_SATURATION)), 1)


FINGERPRINT_ORIGIN_BASE: float = 50.0
FINGERPRINT_ORIGIN_SCALE: float = 7.0


def fingerprint_origin_score(signals: list[Signal]) -> float:
    origin_weight = sum(
        signal.weight * SEVERITY_FACTORS.get(signal.severity, 0.3)
        for signal in signals
        if signal.axis == AXIS_ORIGIN
    )
    if origin_weight <= 0.0:
        return FINGERPRINT_ORIGIN_BASE
    score = FINGERPRINT_ORIGIN_BASE + origin_weight * FINGERPRINT_ORIGIN_SCALE
    if any(signal.code in ("AI_BUILDER_FINGERPRINT", "VIBE_STACK") for signal in signals):
        score += 12.0
    return round(min(100.0, score), 1)


def origin_score(text: str, metrics: FileMetrics, signals: list[Signal]) -> float:
    score = 0.0
    if metrics.sloc >= 40:
        if metrics.indent_variance < 0.45:
            score += 16.0
        elif metrics.indent_variance < 1.2:
            score += 8.0
        elif metrics.indent_variance > 4.0:
            score -= 14.0
    if 0.10 <= metrics.blank_ratio <= 0.22 and metrics.total_lines >= 60:
        score += 6.0
    if metrics.comment_ratio >= 0.22 and metrics.sloc >= 40:
        score += 8.0
    if metrics.function_count >= 4 and metrics.docstring_count >= metrics.function_count:
        score += 10.0
    origin_weight = sum(
        signal.weight * SEVERITY_FACTORS.get(signal.severity, 0.3)
        for signal in signals
        if signal.axis == AXIS_ORIGIN
    )
    score += min(ORIGIN_SIGNAL_CAP, origin_weight * ORIGIN_SIGNAL_SCALE)
    if any(signal.code == "AI_SIGNATURE" for signal in signals):
        score += 22.0
    if any(signal.code == "NAMING_MIXED" for signal in signals):
        score -= 8.0
    if HUMAN_MARKER_PATTERN.search(text):
        score -= 10.0
    score += 24.0
    return round(min(100.0, max(0.0, score)), 1)


def categorize(origin: float, quality: float) -> str:
    if origin >= ORIGIN_HIGH_THRESHOLD and quality >= QUALITY_SLOP_THRESHOLD:
        return CATEGORY_SLOP
    if origin >= ORIGIN_HIGH_THRESHOLD and quality < QUALITY_CLEAN_THRESHOLD:
        return CATEGORY_SOPHISTICATED
    if origin < ORIGIN_LOW_THRESHOLD:
        return CATEGORY_HUMAN_MESSY if quality >= QUALITY_SLOP_THRESHOLD else CATEGORY_HUMAN_CLEAN
    return CATEGORY_UNCERTAIN


def criticality_for(relative: str) -> float:
    return CRITICALITY_WEIGHT if CRITICAL_PATH_PATTERN.search(relative) else 1.0


def grade_for(slop: float) -> str:
    for ceiling, grade in GRADE_BANDS:
        if slop <= ceiling:
            return grade
    return "F"


def _confidence(strong: int, medium: int, files: int) -> str:
    if files < 3:
        return "low"
    if strong >= 3:
        return "high"
    if strong >= 1 or medium >= 5:
        return "medium"
    return "low"


def aggregate(files: list[FileScore]) -> RepoScores:
    scored = [entry for entry in files if entry.sloc > 0]
    if not scored:
        return RepoScores(0.0, 0.0, 0.0, "A", CATEGORY_UNCERTAIN, 50.0, 50.0, "low", 0, 0, 0)
    total_weight = sum(entry.sloc * entry.criticality for entry in scored)
    origin = sum(entry.origin_score * entry.sloc * entry.criticality for entry in scored) / total_weight
    quality = sum(entry.quality_deficit * entry.sloc * entry.criticality for entry in scored) / total_weight
    ai_mass = sum(entry.sloc * entry.criticality * ai_fraction(entry.origin_score) for entry in scored)
    ai_percent = round(100.0 * ai_mass / total_weight, 1)
    slop = compose_slop_score(ai_percent, quality)
    strong = sum(1 for entry in scored for signal in entry.signals if signal.severity == SEVERITY_STRONG)
    medium = sum(1 for entry in scored for signal in entry.signals if signal.severity == SEVERITY_MEDIUM)
    return RepoScores(
        origin_score=round(origin, 1),
        quality_deficit=round(quality, 1),
        slop_score=round(slop, 1),
        grade=grade_for(slop),
        category=categorize(origin, quality),
        human_percent=round(100.0 - ai_percent, 1),
        ai_percent=ai_percent,
        confidence=_confidence(strong, medium, len(scored)),
        strong_signal_count=strong,
        medium_signal_count=medium,
        files_scored=len(scored),
    )


TEMPLATE_CONFIDENT_SCORE: float = 35.0
TEMPLATE_STRONG_SCORE: float = 70.0
TEMPLATE_INFLUENCE: float = 0.7
TEMPLATE_STRONG_INFLUENCE: float = 0.85

DOM_BUILDER_CONFIDENT_THRESHOLD: float = 0.75
DOM_BUILDER_FLOOR: float = 70.0


def _force_slop_category(scores: RepoScores, floor: float) -> RepoScores:
    ai_percent = max(scores.ai_percent, floor)
    origin = max(scores.origin_score, floor)
    slop = compose_slop_score(ai_percent, scores.quality_deficit)
    return replace(
        scores,
        origin_score=origin,
        ai_percent=ai_percent,
        human_percent=round(100.0 - ai_percent, 1),
        slop_score=slop,
        grade=grade_for(slop),
        category=CATEGORY_SLOP,
    )


def adjust_for_template(scores: RepoScores, template_score: float) -> RepoScores:
    if template_score < TEMPLATE_CONFIDENT_SCORE:
        return scores
    if template_score >= TEMPLATE_STRONG_SCORE:
        floor = round(TEMPLATE_STRONG_INFLUENCE * template_score, 1)
        return _force_slop_category(scores, floor)
    floor = round(TEMPLATE_INFLUENCE * template_score, 1)
    ai_percent = max(scores.ai_percent, floor)
    origin = max(scores.origin_score, floor)
    slop = compose_slop_score(ai_percent, scores.quality_deficit)
    category = categorize(origin, scores.quality_deficit)
    if category in (CATEGORY_HUMAN_CLEAN, CATEGORY_HUMAN_MESSY):
        category = CATEGORY_UNCERTAIN
    return replace(
        scores,
        origin_score=origin,
        ai_percent=ai_percent,
        human_percent=round(100.0 - ai_percent, 1),
        slop_score=slop,
        grade=grade_for(slop),
        category=category,
    )


def adjust_for_dom_signals(scores: RepoScores, dom: DomEvidence) -> RepoScores:
    if dom.detected_builder is not None and dom.builder_confidence >= DOM_BUILDER_CONFIDENT_THRESHOLD:
        return _force_slop_category(scores, DOM_BUILDER_FLOOR)
    if dom.score <= 0:
        return scores
    ai_percent = round((1.0 - DOM_AI_WEIGHT) * scores.ai_percent + DOM_AI_WEIGHT * dom.score, 1)
    slop = compose_slop_score(ai_percent, scores.quality_deficit)
    return replace(
        scores,
        ai_percent=ai_percent,
        human_percent=round(100.0 - ai_percent, 1),
        slop_score=slop,
        grade=grade_for(slop),
    )


def adjust_for_images(scores: RepoScores, mean_image_ai: float) -> RepoScores:
    ai_percent = round(
        IMAGE_TEXT_AI_WEIGHT * scores.ai_percent + IMAGE_AI_WEIGHT * mean_image_ai, 1
    )
    slop = compose_slop_score(ai_percent, scores.quality_deficit)
    return replace(
        scores,
        ai_percent=ai_percent,
        human_percent=round(100.0 - ai_percent, 1),
        slop_score=slop,
        grade=grade_for(slop),
    )


def repo_scores_to_dict(scores: RepoScores) -> dict[str, Any]:
    return {
        "origin_score": scores.origin_score,
        "quality_deficit": scores.quality_deficit,
        "slop_score": scores.slop_score,
        "grade": scores.grade,
        "category": scores.category,
        "human_percent": scores.human_percent,
        "ai_percent": scores.ai_percent,
        "confidence": scores.confidence,
        "strong_signal_count": scores.strong_signal_count,
        "medium_signal_count": scores.medium_signal_count,
        "files_scored": scores.files_scored,
    }
