"""
bileshke/types.py — Core types for the Bileshke (Composite Convergence) Engine.

Phase D: Combines all 7 lens yakinlasma scores into a single composite.

Governing axioms and theorems:
  AX49:  Latifeler = {Akıl, Kalb, Nefs, Ruh, Sır, Hafî, Ahfâ} (+ Nefs)
  AX50:  Elvan-ı Seb'a — WhiteLight = ⊕₁⁷ Color_i
  AX51:  Quality Framework — 4 components Q-1..Q-4
  AX52:  Multiplicative gate — Gate(A) = ∏ᵢ 𝟙(bᵢ > 0)
  AX53:  Ahfâ permanently unmapped — □(φ_L(Ahfâ) = ⊥)
  AX54:  Axis independence (latife vs ortam)
  AX55:  Dual coverage gate — Gate(latife₆) ∧ Gate(medium₃)
  AX56:  Hakkalyakîn permanently inaccessible
  T6:    Convergence bound < 1.0
  T17:   Structural completeness ≤ 6/7
  T18:   Per-module diagnostic b ∈ {0,1}⁷, max = 6
  KV₄:   0 < Composite < 1; ≥ 0.95 → warning
  KV₇:   Independence — no shared state between lenses

Appendix C references:
  C.4:   bileshke: S_Text → [0, 1); yakinlasma: S_Mercek × S_Text → [0, 1)
  C.5:   Tescil partial bijection: Latife ↦ Lens (Ahfâ ↦ ∅)
"""

from __future__ import annotations

import enum
from collections import Counter
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple


# ========================================================================
# Utility
# ========================================================================

def clamp_score(value: float) -> float:
    """Clamp a score to [0, 0.9999]. T6/KV₄: composite < 1.0."""
    return min(max(value, 0.0), 0.9999)


# ========================================================================
# Enums
# ========================================================================

class Latife(enum.Enum):
    """
    AX49 — Seven subtle faculties (Latifeler).
    Epistemic organs of perception; each maps to at most one lens.
    """
    AKIL = "Akıl"
    KALB = "Kalb"
    NEFS = "Nefs"
    RUH = "Ruh"
    SIR = "Sır"
    HAFI = "Hafî"
    AHFA = "Ahfâ"  # AX53: permanently unmapped


class Ortam(enum.Enum):
    """
    Three epistemic media (Üç Ortam) — §5.2.
    Channels through which knowledge transmits.
    """
    NESIM = "Nesim"
    ZIYA = "Ziya"
    AB_I_HAYAT = "Ab-ı Hayat"


class EpistemicGrade(enum.Enum):
    """
    AX56 — Four epistemic grades in ascending order.
    Max achievable by formal instruments: İlmelyakîn.
    Hakkalyakîn permanently inaccessible.
    """
    TASAVVUR = "Tasavvur"        # Conceptual grasp
    TASDIK = "Tasdik"            # Affirmative judgment
    ILMELYAKIN = "İlmelyakîn"    # Knowledge-certainty (TARGET)
    HAKKALYAKIN = "Hakkalyakîn"  # Truth-certainty (INACCESSIBLE)

    @property
    def rank(self) -> int:
        """Numeric rank for ordering. 0 = lowest."""
        _ranks = {
            "Tasavvur": 0,
            "Tasdik": 1,
            "İlmelyakîn": 2,
            "Hakkalyakîn": 3,
        }
        return _ranks[self.value]

    @property
    def is_accessible(self) -> bool:
        """AX56: Hakkalyakîn is permanently inaccessible."""
        return self != EpistemicGrade.HAKKALYAKIN

    def __lt__(self, other: EpistemicGrade) -> bool:
        if not isinstance(other, EpistemicGrade):
            return NotImplemented
        return self.rank < other.rank

    def __le__(self, other: EpistemicGrade) -> bool:
        if not isinstance(other, EpistemicGrade):
            return NotImplemented
        return self.rank <= other.rank

    def __gt__(self, other: EpistemicGrade) -> bool:
        if not isinstance(other, EpistemicGrade):
            return NotImplemented
        return self.rank > other.rank

    def __ge__(self, other: EpistemicGrade) -> bool:
        if not isinstance(other, EpistemicGrade):
            return NotImplemented
        return self.rank >= other.rank


class LensId(enum.Enum):
    """
    The 7 formal lenses (Yedi Mercek) — §4.1.
    Each corresponds to a Python package in this project.
    """
    ONTOLOJI = "Ontoloji"              # kavram_sozlugu
    MEREOLOJI = "Mereoloji"            # mereoloji
    FOL = "FOL"                        # fol_formalizasyon
    BAYES = "Bayes"                    # bayes_analiz
    OYUN_TEORISI = "OyunTeorisi"       # oyun_teorisi
    KATEGORI_TEORISI = "KategoriTeorisi"  # kategori_teorisi
    TOPOLOJI_HOLOGRAFIK = "Topoloji + Holografik"  # holografik


# ========================================================================
# Constants & mappings
# ========================================================================

# Appendix C.5 — Tescil partial bijection: Latife ↦ LensId
# Ahfâ ↦ None (permanently unmapped, AX53)
TESCIL: Dict[Latife, Optional[LensId]] = {
    Latife.AKIL: LensId.ONTOLOJI,
    Latife.KALB: LensId.BAYES,
    Latife.NEFS: LensId.OYUN_TEORISI,
    Latife.RUH: LensId.TOPOLOJI_HOLOGRAFIK,
    Latife.SIR: LensId.KATEGORI_TEORISI,
    Latife.HAFI: LensId.TOPOLOJI_HOLOGRAFIK,
    Latife.AHFA: None,  # AX53
}

# Reverse mapping: LensId → set of Latife (for latife_vektor computation)
LENS_TO_LATIFE: Dict[LensId, Tuple[Latife, ...]] = {
    LensId.ONTOLOJI: (Latife.AKIL,),
    LensId.MEREOLOJI: (),               # No latife mapping (formal instrument)
    LensId.FOL: (),                      # No latife mapping (formal instrument)
    LensId.BAYES: (Latife.KALB,),
    LensId.OYUN_TEORISI: (Latife.NEFS,),
    LensId.KATEGORI_TEORISI: (Latife.SIR,),
    LensId.TOPOLOJI_HOLOGRAFIK: (Latife.RUH, Latife.HAFI),  # Dual faculty
}

# §4.1 — Lens to Python package mapping
LENS_PACKAGE: Dict[LensId, str] = {
    LensId.ONTOLOJI: "kavram_sozlugu",
    LensId.MEREOLOJI: "mereoloji",
    LensId.FOL: "fol_formalizasyon",
    LensId.BAYES: "bayes_analiz",
    LensId.OYUN_TEORISI: "oyun_teorisi",
    LensId.KATEGORI_TEORISI: "kategori_teorisi",
    LensId.TOPOLOJI_HOLOGRAFIK: "holografik",
}

# Canonical lens order (for array indexing)
LENS_ORDER: Tuple[LensId, ...] = (
    LensId.ONTOLOJI,
    LensId.MEREOLOJI,
    LensId.FOL,
    LensId.BAYES,
    LensId.OYUN_TEORISI,
    LensId.KATEGORI_TEORISI,
    LensId.TOPOLOJI_HOLOGRAFIK,
)

ALL_LATIFELER: Tuple[Latife, ...] = tuple(Latife)
ALL_ORTAM: Tuple[Ortam, ...] = tuple(Ortam)

NUM_LATIFELER = 7
NUM_ORTAM = 3
NUM_LENSES = 7
MAX_ACCESSIBLE_LATIFE = 6  # T17: Ahfâ permanently unmapped
MAX_COMPLETENESS_RATIO = 6 / 7  # T17


# ========================================================================
# Vectors
# ========================================================================

@dataclass(frozen=True)
class LatifeVektor:
    """
    {0,1}⁷ — Which of the 7 latifeler are engaged.

    AX53: Ahfâ bit is ALWAYS 0 (permanently unmapped).
    T17:  Maximum active count = 6.
    AX52: Gate(A) = ∏ᵢ 𝟙(bᵢ > 0) — but applied to accessible latifeler only.

    Order: [Akıl, Kalb, Nefs, Ruh, Sır, Hafî, Ahfâ]
    """
    bits: Tuple[int, ...]  # 7 elements, each 0 or 1

    def __post_init__(self) -> None:
        if len(self.bits) != NUM_LATIFELER:
            raise ValueError(
                f"LatifeVektor requires exactly {NUM_LATIFELER} bits, "
                f"got {len(self.bits)}"
            )
        for i, b in enumerate(self.bits):
            if b not in (0, 1):
                raise ValueError(
                    f"Bit {i} must be 0 or 1, got {b}"
                )
        # AX53: Ahfâ (index 6) must always be 0
        if self.bits[6] != 0:
            raise ValueError(
                "AX53 violation: Ahfâ (index 6) must be 0 "
                "(permanently unmapped)"
            )

    @property
    def active_count(self) -> int:
        """Number of active latifeler. Max = 6 (T17)."""
        return sum(self.bits)

    @property
    def completeness(self) -> float:
        """T17/T18: active / total. Max = 6/7 ≈ 0.857."""
        return self.active_count / NUM_LATIFELER

    @property
    def accessible_bits(self) -> Tuple[int, ...]:
        """The 6 accessible bits (excluding Ahfâ)."""
        return self.bits[:6]

    def gate_score(self) -> float:
        """
        AX52 multiplicative gate on accessible latifeler (6 bits).
        Gate = ∏ᵢ 𝟙(bᵢ > 0) for i in {0..5}.
        If ANY accessible bit is 0, gate collapses to 0.
        Note: Ahfâ is excluded since it's always 0 (AX53).
        """
        for b in self.accessible_bits:
            if b == 0:
                return 0.0
        return 1.0

    def is_engaged(self, latife: Latife) -> bool:
        """Check if a specific latife is engaged."""
        idx = list(Latife).index(latife)
        return self.bits[idx] == 1

    def to_dict(self) -> Dict[str, int]:
        """AX57: Transparency — which latifeler are active."""
        return {lat.value: bit for lat, bit in zip(Latife, self.bits)}

    @staticmethod
    def from_active_lenses(lenses: List[LensId]) -> LatifeVektor:
        """
        Compute latife_vektor from a list of active lenses.
        Uses LENS_TO_LATIFE mapping.
        """
        bits = [0] * NUM_LATIFELER
        latife_list = list(Latife)
        for lens in lenses:
            for latife in LENS_TO_LATIFE.get(lens, ()):
                idx = latife_list.index(latife)
                bits[idx] = 1
        # AX53: Force Ahfâ to 0
        bits[6] = 0
        return LatifeVektor(bits=tuple(bits))

    @staticmethod
    def full() -> LatifeVektor:
        """All accessible latifeler engaged (6/7). T17 maximum."""
        return LatifeVektor(bits=(1, 1, 1, 1, 1, 1, 0))

    @staticmethod
    def empty() -> LatifeVektor:
        """No latifeler engaged."""
        return LatifeVektor(bits=(0, 0, 0, 0, 0, 0, 0))


@dataclass(frozen=True)
class OrtamVektor:
    """
    {0,1}³ — Which of the 3 epistemic media are covered.

    AX54: latife and ortam axes are independent.
    AX55: PracticalGate = Gate(latife₆) ∧ Gate(medium₃).

    Order: [Nesim, Ziya, Ab-ı Hayat]
    """
    bits: Tuple[int, ...]  # 3 elements, each 0 or 1

    def __post_init__(self) -> None:
        if len(self.bits) != NUM_ORTAM:
            raise ValueError(
                f"OrtamVektor requires exactly {NUM_ORTAM} bits, "
                f"got {len(self.bits)}"
            )
        for i, b in enumerate(self.bits):
            if b not in (0, 1):
                raise ValueError(
                    f"Bit {i} must be 0 or 1, got {b}"
                )

    @property
    def active_count(self) -> int:
        return sum(self.bits)

    def gate_score(self) -> float:
        """AX52 multiplicative gate on all 3 ortam bits."""
        for b in self.bits:
            if b == 0:
                return 0.0
        return 1.0

    def to_dict(self) -> Dict[str, int]:
        return {ort.value: bit for ort, bit in zip(Ortam, self.bits)}

    @staticmethod
    def full() -> OrtamVektor:
        return OrtamVektor(bits=(1, 1, 1))

    @staticmethod
    def empty() -> OrtamVektor:
        return OrtamVektor(bits=(0, 0, 0))


# ========================================================================
# Pairwise lens correlation (KV₇ diagnostic)
# ========================================================================

@dataclass(frozen=True)
class LensCorrelation:
    """Pairwise correlation between two lenses across multiple analyses.

    Mirrors fidelity_funnel's independence heuristic (KV₇) but adapted
    for continuous lens scores with a threshold.

    The ratio P(j|i) / P(j) measures dependence:
      ratio ≈ 1.0 → independent
      ratio >> 1  → correlated (potential KV₇ violation)
      ratio > independence_threshold → KV₇ violation
    """
    lens_i: LensId
    lens_j: LensId
    p_j: float              # P(lens_j above threshold)
    p_j_given_i: float      # P(lens_j above threshold | lens_i above threshold)
    ratio: float             # p_j_given_i / p_j
    independent: bool        # ratio <= independence_threshold


# ========================================================================
# Funnel diagnostic (AX5 integration)
# ========================================================================

@dataclass
class FunnelDiagnostic:
    """Fidelity funnel applied to lens agreement patterns.

    Given N analyses (each a list of LensResults), compute how many
    analyses have exactly m lenses scoring above a threshold.
    This yields D(m) — the fidelity spectrum of the bileshke.

    The Diophantine Fidelity Funnel Conjecture predicts:
      If lenses are independent (KV₇), D(m) is monotonically non-increasing.

    Framework mapping:
      AX5:  Integration diagnostic — are the lenses working as a living system?
      AX22: Supremum (all 7 above threshold) may be unreachable
      AX52: Multiplicative gate — zero in any dimension collapses
      KV₇:  Independence prerequisite for valid convergence
      T6:   Convergence bound — composite < 1.0
    """
    threshold: float                        # Score threshold for "active"
    n_analyses: int                         # Number of analyses evaluated
    spectrum: Dict[int, int]                # D(m): count with exactly m active
    is_monotonic: bool                      # Funnel conjecture check
    violation_at: Optional[int]             # First m where D(m) > D(m-1)
    correlations: List[LensCorrelation]     # Pairwise independence
    kv7_satisfied: bool                     # All pairs independent?
    max_correlation_ratio: float            # Highest ratio observed

    def consecutive_ratios(self) -> List[Optional[float]]:
        """D(m-1)/D(m) for consecutive levels.

        Each ratio > 1 is consistent with the funnel conjecture.
        Ratio < 1 indicates a violation.
        Mirrors fidelity_funnel.FunnelResult.consecutive_ratios().
        """
        ratios: List[Optional[float]] = []
        max_m = max(self.spectrum.keys()) if self.spectrum else 0
        for m in range(1, max_m + 1):
            prev = self.spectrum.get(m - 1, 0)
            curr = self.spectrum.get(m, 0)
            if curr > 0:
                ratios.append(prev / curr)
            else:
                ratios.append(None)
        return ratios

    def to_dict(self) -> Dict[str, Any]:
        """AX57: Transparency — full diagnostic report."""
        return {
            "threshold": self.threshold,
            "n_analyses": self.n_analyses,
            "spectrum": self.spectrum,
            "is_monotonic": self.is_monotonic,
            "violation_at": self.violation_at,
            "kv7_satisfied": self.kv7_satisfied,
            "max_correlation_ratio": self.max_correlation_ratio,
            "consecutive_ratios": self.consecutive_ratios(),
            "correlations": [
                {
                    "lens_i": c.lens_i.value,
                    "lens_j": c.lens_j.value,
                    "p_j": c.p_j,
                    "p_j_given_i": c.p_j_given_i,
                    "ratio": c.ratio,
                    "independent": c.independent,
                }
                for c in self.correlations
            ],
        }


# ========================================================================
# Per-lens result
# ========================================================================

@dataclass(frozen=True)
class LensResult:
    """
    The output from a single lens's yakinlasma computation.
    Each lens produces a convergence score in [0, 1).
    """
    lens_id: LensId
    score: float                        # yakinlasma score, [0, 0.9999]
    grade: EpistemicGrade = EpistemicGrade.TASAVVUR
    checks_passed: int = 0
    checks_total: int = 0
    detail: str = ""

    def __post_init__(self) -> None:
        if not isinstance(self.lens_id, LensId):
            raise TypeError(f"lens_id must be LensId, got {type(self.lens_id)}")
        if not isinstance(self.score, (int, float)):
            raise TypeError(f"score must be numeric, got {type(self.score)}")
        if not isinstance(self.grade, EpistemicGrade):
            raise TypeError(f"grade must be EpistemicGrade, got {type(self.grade)}")
        # AX56: Cannot claim Hakkalyakîn
        if self.grade == EpistemicGrade.HAKKALYAKIN:
            raise ValueError(
                "AX56 violation: Hakkalyakîn is permanently inaccessible "
                "to formal instruments"
            )

    @property
    def clamped_score(self) -> float:
        """Score clamped to [0, 0.9999]."""
        return clamp_score(self.score)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "lens": self.lens_id.value,
            "score": self.clamped_score,
            "grade": self.grade.value,
            "checks_passed": self.checks_passed,
            "checks_total": self.checks_total,
            "detail": self.detail,
        }


# ========================================================================
# Quality Report (§5.5 schema)
# ========================================================================

@dataclass
class QualityReport:
    """
    §5.5 Quality Report Schema — AX57 mandatory transparency.

    {
      "composite_score": "[0, 1)",
      "coverage": {
        "latife": "{0,1}⁷",
        "medium": "{0,1}³"
      },
      "degree_distribution": "Map<Mercek, Mertebe>",
      "kavaid_checks": "Map<KV_id, Bool>",
      "completeness": "6/7 max"
    }
    """
    composite_score: float = 0.0
    latife_vektor: LatifeVektor = field(
        default_factory=LatifeVektor.empty
    )
    ortam_vektor: OrtamVektor = field(
        default_factory=OrtamVektor.empty
    )
    degree_distribution: Dict[str, str] = field(default_factory=dict)
    kavaid_checks: Dict[str, bool] = field(default_factory=dict)
    lens_results: List[LensResult] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)
    funnel_diagnostic: Optional[FunnelDiagnostic] = None

    @property
    def completeness(self) -> float:
        """T17: latife coverage ratio. Max = 6/7."""
        return self.latife_vektor.completeness

    @property
    def latife_gate(self) -> float:
        """AX52 multiplicative gate on accessible latifeler."""
        return self.latife_vektor.gate_score()

    @property
    def ortam_gate(self) -> float:
        """AX52 multiplicative gate on ortam."""
        return self.ortam_vektor.gate_score()

    @property
    def practical_gate(self) -> float:
        """AX55: PracticalGate = Gate(latife₆) ∧ Gate(medium₃)."""
        return self.latife_gate * self.ortam_gate

    @property
    def all_kavaid_pass(self) -> bool:
        """Q-3: All 8 kavaid must pass (multiplicative per AX52)."""
        if not self.kavaid_checks:
            return True
        return all(self.kavaid_checks.values())

    @property
    def has_kv4_warning(self) -> bool:
        """KV₄: composite ≥ 0.95 is suspicious."""
        return self.composite_score >= 0.95

    def to_dict(self) -> Dict[str, Any]:
        """AX57: Full transparency report."""
        return {
            "composite_score": clamp_score(self.composite_score),
            "coverage": {
                "latife": self.latife_vektor.to_dict(),
                "medium": self.ortam_vektor.to_dict(),
            },
            "degree_distribution": self.degree_distribution,
            "kavaid_checks": self.kavaid_checks,
            "completeness": self.completeness,
            "max_completeness": MAX_COMPLETENESS_RATIO,
            "latife_gate": self.latife_gate,
            "ortam_gate": self.ortam_gate,
            "practical_gate": self.practical_gate,
            "all_kavaid_pass": self.all_kavaid_pass,
            "lens_results": [r.to_dict() for r in self.lens_results],
            "warnings": self.warnings,
            "funnel_diagnostic": (
                self.funnel_diagnostic.to_dict()
                if self.funnel_diagnostic else None
            ),
        }
