"""
fw_server/grade_map.py — Map verification pass-counts to EpistemicGrade.

Governing axioms:
  AX56: Max achievable grade = İlmelyakîn; Hakkalyakîn permanently inaccessible.
  T15:  Structural claims (külliyat) > detail claims (tafsîlât).
  §5.3: Epistemic degree scale: Tasavvur < Tasdik < İlmelyakîn.

The mapping is deliberately conservative: the framework must NOT
over-claim certainty.  The thresholds are calibrated so that even
a perfect formal instrument never crosses into Hakkalyakîn.
"""

from __future__ import annotations

from bileshke.types import EpistemicGrade


# ── Threshold boundaries (inclusive lower bound) ──
# 0.00 – 0.60  → TASAVVUR   (conceptual grasp)
# 0.61 – 0.85  → TASDIK     (affirmative judgment)
# 0.86 – 1.00  → ILMELYAKIN (knowledge-certainty — TARGET grade)
# Hakkalyakîn is NEVER returned (AX56).

_GRADE_THRESHOLDS: list[tuple[float, EpistemicGrade]] = [
    (0.86, EpistemicGrade.ILMELYAKIN),
    (0.61, EpistemicGrade.TASDIK),
    (0.00, EpistemicGrade.TASAVVUR),
]


def score_to_grade(score: float) -> EpistemicGrade:
    """Convert a yakinlasma score ∈ [0, 1) to an EpistemicGrade.

    >>> score_to_grade(0.92)
    <EpistemicGrade.ILMELYAKIN: 'İlmelyakîn'>
    >>> score_to_grade(0.70)
    <EpistemicGrade.TASDIK: 'Tasdik'>
    >>> score_to_grade(0.30)
    <EpistemicGrade.TASAVVUR: 'Tasavvur'>
    """
    clamped = min(max(score, 0.0), 0.9999)
    for threshold, grade in _GRADE_THRESHOLDS:
        if clamped >= threshold:
            return grade
    return EpistemicGrade.TASAVVUR


def pass_ratio_to_grade(passed: int, total: int) -> EpistemicGrade:
    """Convert a checks-passed / checks-total ratio to an EpistemicGrade.

    >>> pass_ratio_to_grade(9, 10)
    <EpistemicGrade.ILMELYAKIN: 'İlmelyakîn'>
    >>> pass_ratio_to_grade(7, 10)
    <EpistemicGrade.TASDIK: 'Tasdik'>
    >>> pass_ratio_to_grade(3, 10)
    <EpistemicGrade.TASAVVUR: 'Tasavvur'>
    >>> pass_ratio_to_grade(0, 0)
    <EpistemicGrade.TASAVVUR: 'Tasavvur'>
    """
    if total <= 0:
        return EpistemicGrade.TASAVVUR
    ratio = passed / total
    return score_to_grade(ratio)


def grade_to_string(grade: EpistemicGrade) -> str:
    """Get the display string for an EpistemicGrade.

    >>> grade_to_string(EpistemicGrade.ILMELYAKIN)
    'İlmelyakîn'
    """
    return grade.value


# ═══════════════════════════════════════════════════════════════════════
# Phase 3: Adaptive Grading (WATERFALL_PLAN §7)
# Blind spots: BS-07 (Hardcoded thresholds), BS-08 (Grade inflation),
#              BS-15 (Flat grade distribution)
# ═══════════════════════════════════════════════════════════════════════

from dataclasses import dataclass
from statistics import mean as _mean


@dataclass(frozen=True)
class GradeInput:
    """Content-sensitive inputs for adaptive epistemic grading.

    Replaces the single-score approach with a three-factor weighted model:
    composite (0.50) + kavaid (0.25) + evidence_quality (0.25).

    Fields
    ------
    composite_score   : bileshke composite ∈ [0, 0.9999]
    kavaid_pass_ratio : passed / total kavaid ∈ [0, 1]
    structured_ratio  : fraction of lenses with structured params ∈ [0, 1]
    lens_agreement    : 1 − σ(scores); higher = more agreement ∈ [0, 1]
    evidence_density  : vocab hits per 100 words (raw, unnormalized)
    path              : classified path (P0–P4)
    """

    composite_score: float
    kavaid_pass_ratio: float
    structured_ratio: float
    lens_agreement: float
    evidence_density: float
    path: str


# Path-adjusted thresholds: (ilmelyakin_threshold, tasdik_threshold)
# P2 requires higher evidence (epistemic evaluation demands rigor)
# P4 retains higher-than-default thresholds (theological claims need care)
# but calibrated for text-mode analysis where per-lens caps limit composites.
# Full kavaid battery (KV₁–KV₈) provides the real P4 rigour, not the threshold.
_PATH_THRESHOLDS: dict[str, tuple[float, float]] = {
    "P0": (0.75, 0.50),
    "P1": (0.75, 0.50),
    "P2": (0.82, 0.60),
    "P3": (0.75, 0.50),
    "P4": (0.78, 0.50),
}
_DEFAULT_THRESHOLDS: tuple[float, float] = (0.75, 0.50)


def adaptive_score_to_grade(inp: GradeInput) -> EpistemicGrade:
    """Three-factor weighted grading with path-adjusted thresholds.

    Governing axioms:
      AX56: Max grade = İlmelyakîn — Hakkalyakîn NEVER returned.
      T15:  Structural claims weighted above detail claims.
      KV₄:  0 < composite < 1.

    Weights: composite 0.50, kavaid 0.25, evidence_quality 0.25.
    Evidence quality = mean(structured_ratio, lens_agreement,
                           normalized_evidence_density).

    >>> adaptive_score_to_grade(GradeInput(0.90, 1.0, 0.9, 0.9, 4.0, "P1"))
    <EpistemicGrade.ILMELYAKIN: 'İlmelyakîn'>
    >>> adaptive_score_to_grade(GradeInput(0.0, 0.0, 0.0, 0.0, 0.0, "P1"))
    <EpistemicGrade.TASAVVUR: 'Tasavvur'>
    """
    # Clamp composite to [0, 0.9999] — mirrors score_to_grade behavior
    composite = min(max(inp.composite_score, 0.0), 0.9999)

    # Clamp kavaid to [0, 1]
    kavaid = min(max(inp.kavaid_pass_ratio, 0.0), 1.0)

    # Evidence quality: three normalized signals averaged
    norm_sr = min(max(inp.structured_ratio, 0.0), 1.0)
    norm_la = min(max(inp.lens_agreement, 0.0), 1.0)
    norm_ed = min(max(inp.evidence_density, 0.0) / 5.0, 1.0)
    evidence_quality = _mean([norm_sr, norm_la, norm_ed])

    # Three-factor weighted score
    weighted = 0.50 * composite + 0.25 * kavaid + 0.25 * evidence_quality

    # Path-adjusted thresholds
    ilm_thresh, tasdik_thresh = _PATH_THRESHOLDS.get(
        inp.path, _DEFAULT_THRESHOLDS
    )

    # Grade determination — AX56: cap at İlmelyakîn
    if weighted >= ilm_thresh:
        return EpistemicGrade.ILMELYAKIN
    if weighted >= tasdik_thresh:
        return EpistemicGrade.TASDIK
    return EpistemicGrade.TASAVVUR
