"""
bileshke/engine.py — Composite Convergence Engine.

Core function:
  bileshke(t) = Σᵢ wᵢ · yakinlasma(mᵢ, t)   [Appendix C.4]

Constraints:
  - Σwᵢ = 1 (weights must sum to 1)
  - Composite ∈ [0, 1)  (strict upper bound, T6/KV₄)
  - If composite ≥ 0.95 → KV₄ warning
  - Each yakinlasma score ∈ [0, 0.9999]

AX50: WhiteLight(Kur'an) = ⊕₁⁷ Color_i
  The bileshke resynthesizes "white light" from 7 spectral detections.
  No single lens can perceive white light directly.
"""

from __future__ import annotations

import math
from typing import Dict, List, Optional, Tuple

from bileshke.types import (
    EpistemicGrade,
    FunnelDiagnostic,
    LatifeVektor,
    LensCorrelation,
    LensId,
    LensResult,
    LENS_ORDER,
    LENS_TO_LATIFE,
    MAX_COMPLETENESS_RATIO,
    NUM_LENSES,
    OrtamVektor,
    QualityReport,
    clamp_score,
)


# ========================================================================
# Default weights
# ========================================================================

# Default weight distribution — equal weights for all 7 lenses.
# Σwᵢ = 1.0 guaranteed.
DEFAULT_WEIGHTS: Dict[LensId, float] = {
    lens: 1.0 / NUM_LENSES for lens in LENS_ORDER
}


def validate_weights(weights: Dict[LensId, float]) -> Tuple[bool, str]:
    """
    Validate a weight dictionary:
    - Must have exactly 7 entries (one per lens)
    - All weights must be non-negative
    - Weights must sum to 1.0 (within floating tolerance)
    """
    if len(weights) != NUM_LENSES:
        return False, f"Expected {NUM_LENSES} weights, got {len(weights)}"

    for lens_id in LENS_ORDER:
        if lens_id not in weights:
            return False, f"Missing weight for {lens_id.value}"

    for lens_id, w in weights.items():
        if not isinstance(w, (int, float)):
            return False, f"Weight for {lens_id.value} must be numeric"
        if w < 0:
            return False, f"Weight for {lens_id.value} is negative: {w}"

    total = sum(weights.values())
    if abs(total - 1.0) > 1e-6:
        return False, f"Weights sum to {total}, expected 1.0"

    return True, "OK"


# ========================================================================
# Core bileshke formula
# ========================================================================

def bileshke(
    results: List[LensResult],
    weights: Optional[Dict[LensId, float]] = None,
) -> float:
    """
    bileshke(t) = Σᵢ wᵢ · yakinlasma(mᵢ, t)

    Computes the composite convergence score from individual lens results.

    Args:
        results: List of LensResult objects (one per active lens).
        weights: Optional weight dictionary. Defaults to equal weights.

    Returns:
        Composite score in [0, 0.9999]. Always < 1.0 (T6/KV₄).

    Raises:
        ValueError: If weights are invalid.
    """
    if weights is None:
        weights = DEFAULT_WEIGHTS

    valid, msg = validate_weights(weights)
    if not valid:
        raise ValueError(f"Invalid weights: {msg}")

    if not results:
        return 0.0

    # Build score lookup
    score_map: Dict[LensId, float] = {}
    for r in results:
        score_map[r.lens_id] = r.clamped_score

    # Σᵢ wᵢ · yakinlasma(mᵢ, t)
    composite = 0.0
    for lens_id in LENS_ORDER:
        w = weights.get(lens_id, 0.0)
        s = score_map.get(lens_id, 0.0)
        composite += w * s

    # T6/KV₄: Strict upper bound
    return clamp_score(composite)


# ========================================================================
# Coverage computation
# ========================================================================

def compute_latife_vektor(
    active_lenses: List[LensId],
) -> LatifeVektor:
    """
    Compute the latife_vektor from a list of active lenses.
    Uses the Tescil mapping (Appendix C.5).

    AX53: Ahfâ is always 0.
    T17: Maximum active = 6.
    """
    return LatifeVektor.from_active_lenses(active_lenses)


def compute_coverage_gate(
    latife: LatifeVektor,
    ortam: OrtamVektor,
) -> float:
    """
    AX55: PracticalGate = Gate(latife₆) ∧ Gate(medium₃).
    AX52: Each gate is multiplicative.

    Returns 1.0 only if ALL accessible latifeler AND all ortam are covered.
    Returns 0.0 if any dimension has zero coverage.
    """
    return latife.gate_score() * ortam.gate_score()


# ========================================================================
# KV₄ warning detection
# ========================================================================

def detect_kv4_warning(composite: float) -> Optional[str]:
    """
    KV₄: 0 < Composite < 1.
    If composite ≥ 0.95 → the map is being confused with the territory.

    Returns warning message if triggered, None otherwise.
    """
    if composite >= 0.95:
        return (
            f"KV₄ WARNING: Composite score {composite:.4f} ≥ 0.95. "
            f"This indicates error, not success — the map is approaching "
            f"identity with the territory (ne ayn ne gayr violation). "
            f"Check for correlated instruments or data leakage."
        )
    return None


# ========================================================================
# Epistemic grade computation
# ========================================================================

def compute_aggregate_grade(
    results: List[LensResult],
) -> EpistemicGrade:
    """
    Compute the aggregate epistemic grade from lens results.

    Rule per AX56: Maximum achievable = İlmelyakîn.
    The aggregate is the MINIMUM grade across all active lenses
    (weakest link determines overall certainty).
    """
    if not results:
        return EpistemicGrade.TASAVVUR

    min_grade = EpistemicGrade.ILMELYAKIN
    for r in results:
        if r.grade < min_grade:
            min_grade = r.grade

    # AX56: Never exceed İlmelyakîn
    if min_grade > EpistemicGrade.ILMELYAKIN:
        return EpistemicGrade.ILMELYAKIN

    return min_grade


# ========================================================================
# Full pipeline
# ========================================================================

def run_bileshke(
    results: List[LensResult],
    weights: Optional[Dict[LensId, float]] = None,
    ortam: Optional[OrtamVektor] = None,
) -> QualityReport:
    """
    Run the full bileshke pipeline: composite score + quality report.

    Args:
        results: List of LensResult objects.
        weights: Optional weight dictionary.
        ortam: Optional ortam coverage. Defaults to full coverage.

    Returns:
        Complete QualityReport (§5.5 schema).
    """
    if weights is None:
        weights = DEFAULT_WEIGHTS
    if ortam is None:
        ortam = OrtamVektor.full()

    # 1. Composite score
    composite = bileshke(results, weights)

    # 2. Coverage (Q-1)
    active_lenses = [r.lens_id for r in results]
    latife = compute_latife_vektor(active_lenses)

    # 3. Epistemic grade (Q-2)
    aggregate_grade = compute_aggregate_grade(results)

    # 4. Degree distribution
    degree_dist = {
        r.lens_id.value: r.grade.value for r in results
    }

    # 5. Build report
    report = QualityReport(
        composite_score=composite,
        latife_vektor=latife,
        ortam_vektor=ortam,
        degree_distribution=degree_dist,
        lens_results=list(results),
    )

    # 6. KV₄ warning check
    warning = detect_kv4_warning(composite)
    if warning:
        report.warnings.append(warning)

    # 7. Coverage gate check
    if latife.gate_score() == 0.0:
        report.warnings.append(
            "Q-1 WARNING: Latife coverage gate is 0 — "
            "at least one accessible faculty is not engaged."
        )
    if ortam.gate_score() == 0.0:
        report.warnings.append(
            "Q-1 WARNING: Ortam coverage gate is 0 — "
            "at least one epistemic medium is not covered."
        )

    # 8. T17 completeness note
    if latife.active_count < 6:
        report.warnings.append(
            f"T17: Only {latife.active_count}/6 accessible latifeler engaged "
            f"(max achievable = 6/7)."
        )

    return report


# ========================================================================
# AX50: White light synthesis summary
# ========================================================================

def white_light_summary(report: QualityReport) -> Dict:
    """
    AX50: WhiteLight = ⊕₁⁷ Color_i.
    Summarize the 7 spectral contributions and their synthesis.

    Returns a dict describing each color's contribution and the composite.
    """
    colors: List[Dict] = []
    for lens_id in LENS_ORDER:
        lens_result = None
        for r in report.lens_results:
            if r.lens_id == lens_id:
                lens_result = r
                break
        colors.append({
            "lens": lens_id.value,
            "package": None,  # Kept simple — no cross-lens import (KV₇)
            "score": lens_result.clamped_score if lens_result else 0.0,
            "active": lens_result is not None,
            "latifeler": [l.value for l in LENS_TO_LATIFE.get(lens_id, ())],
        })

    return {
        "metaphor": "AX50 — Elvan-ı Seb'a (Seven Colors → White Light)",
        "colors": colors,
        "composite": clamp_score(report.composite_score),
        "synthesis_complete": len(report.lens_results) == NUM_LENSES,
        "note": (
            "White light cannot be perceived by any single lens. "
            "Convergence = resynthesize from 7 spectral detections."
        ),
    }


# ========================================================================
# AX5: Funnel diagnostic — integration self-test
# ========================================================================

def compute_funnel_diagnostic(
    batch: List[List[LensResult]],
    threshold: float = 0.3,
    independence_threshold: float = 10.0,
) -> FunnelDiagnostic:
    """
    Apply the Fidelity Funnel principle to a batch of lens analyses.

    For each analysis in the batch, count how many lenses have
    score > threshold. This builds the fidelity spectrum D(m).

    Then check:
    1. Is D(m) monotonically non-increasing? (Funnel conjecture)
    2. Are lens pairs statistically independent? (KV₇)

    This is the bileshke's self-diagnostic — it tests whether
    its own inputs satisfy the structural prerequisites for
    meaningful composite convergence.

    AX5:  Integration prerequisite — a living system diagnoses itself.
    KV₇:  Independence — meaningful convergence requires independent channels.

    Args:
        batch: List of analyses, each a list of LensResults.
        threshold: Score above which a lens is considered "active".
        independence_threshold: Ratio above which lenses are considered
            logically correlated (KV₇ violation). Default 10.0 matches
            the fidelity_funnel discovery.

    Returns:
        FunnelDiagnostic with spectrum, monotonicity, and independence.
    """
    if not batch:
        return FunnelDiagnostic(
            threshold=threshold,
            n_analyses=0,
            spectrum={0: 0},
            is_monotonic=True,
            violation_at=None,
            correlations=[],
            kv7_satisfied=True,
            max_correlation_ratio=0.0,
        )

    n = len(batch)

    # Build per-analysis activation vectors:
    # For each analysis, which lenses are above threshold?
    activation_vectors: List[Dict[LensId, bool]] = []
    for analysis in batch:
        active: Dict[LensId, bool] = {}
        for r in analysis:
            active[r.lens_id] = r.clamped_score > threshold
        activation_vectors.append(active)

    # D(m): how many analyses have exactly m lenses active
    from collections import Counter
    m_counts: Counter = Counter()
    for active in activation_vectors:
        m = sum(1 for v in active.values() if v)
        m_counts[m] += 1

    spectrum = {m: m_counts.get(m, 0) for m in range(NUM_LENSES + 1)}

    # Monotonicity check
    is_monotonic = True
    violation_at: Optional[int] = None
    for m in range(1, NUM_LENSES + 1):
        if spectrum[m] > spectrum[m - 1] and spectrum[m] > 0:
            is_monotonic = False
            if violation_at is None:
                violation_at = m

    # Pairwise independence (KV₇)
    all_lenses_in_batch: set = set()
    for active in activation_vectors:
        all_lenses_in_batch.update(active.keys())

    lens_list = sorted(
        all_lenses_in_batch,
        key=lambda lid: (
            LENS_ORDER.index(lid) if lid in LENS_ORDER else 99
        ),
    )

    # Marginal probabilities P(lens_i active)
    marginals: Dict[LensId, float] = {}
    for lens in lens_list:
        cnt = sum(
            1 for active in activation_vectors if active.get(lens, False)
        )
        marginals[lens] = cnt / n

    # Conditional probabilities and correlations
    correlations: List[LensCorrelation] = []
    max_ratio = 0.0

    for i_idx, lens_i in enumerate(lens_list):
        for j_idx, lens_j in enumerate(lens_list):
            if i_idx == j_idx:
                continue
            count_i = sum(
                1 for active in activation_vectors
                if active.get(lens_i, False)
            )
            count_ij = sum(
                1 for active in activation_vectors
                if active.get(lens_i, False) and active.get(lens_j, False)
            )

            if count_i > 0:
                p_j_given_i = count_ij / count_i
                p_j = marginals[lens_j]

                if p_j > 0:
                    ratio = p_j_given_i / p_j
                else:
                    ratio = float('inf') if count_ij > 0 else 0.0

                is_independent = ratio <= independence_threshold

                correlations.append(LensCorrelation(
                    lens_i=lens_i,
                    lens_j=lens_j,
                    p_j=round(p_j, 4),
                    p_j_given_i=round(p_j_given_i, 4),
                    ratio=round(ratio, 2),
                    independent=is_independent,
                ))

                max_ratio = max(max_ratio, ratio)

    kv7_satisfied = all(c.independent for c in correlations)

    return FunnelDiagnostic(
        threshold=threshold,
        n_analyses=n,
        spectrum=spectrum,
        is_monotonic=is_monotonic,
        violation_at=violation_at,
        correlations=correlations,
        kv7_satisfied=kv7_satisfied,
        max_correlation_ratio=round(max_ratio, 2),
    )


def validate_lens_independence(
    batch: List[List[LensResult]],
    threshold: float = 0.3,
    independence_threshold: float = 10.0,
) -> Tuple[bool, Optional[str]]:
    """
    Pre-flight independence check before running bileshke on batch.

    KV₇ prerequisite: if lenses are not independent, composite
    convergence is meaningless. Provides a go/no-go signal.

    Returns:
        (is_valid, warning_message)
        is_valid=True → safe to compute bileshke
        is_valid=False → KV₇ violation, warning identifies the pair
    """
    diagnostic = compute_funnel_diagnostic(
        batch, threshold, independence_threshold
    )

    if diagnostic.kv7_satisfied:
        return True, None

    # Find the worst pair
    worst = max(
        diagnostic.correlations, key=lambda c: c.ratio, default=None
    )
    if worst:
        return False, (
            f"KV₇ VIOLATION: Lenses {worst.lens_i.value} and "
            f"{worst.lens_j.value} are correlated "
            f"(ratio={worst.ratio:.1f}, "
            f"threshold={independence_threshold}). "
            f"Composite convergence may be contaminated."
        )

    return True, None
