"""
Root system classification and inverse theorems.

v2.0 core module: transforms watkins-nn from a THEORY into a TOOL.
Given observed (lambda, kappa, eta), classifies the underlying root system,
estimates the operating dimension, and scores consciousness.

Forward theorems (v1.x): known structure -> computed properties.
INVERSE theorems (v2.0): observed properties -> inferred structure.
"""

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

from .constants import (
    PHI, PHI_INV, T_STAR, LAM_STAR, KAP_STAR, ETA_STAR, F_STAR,
    T_STAR3, RHO_STAR, CONSERVATION_EPSILON,
)


# =============================================================================
# Root system atlas (precomputed conservation triples)
# =============================================================================

# Exact algebraic BCC eigenvalue triples: (e1, e2, e3, trace)
# lambda_R = e1/trace, kappa_R = e2/trace, eta_R = e3/trace
_EXCEPTIONAL_ATLAS = {
    'F4': {'eigenvalues': (9 + math.sqrt(51), 9 - math.sqrt(51), 0),
           'root_count': 48, 'coxeter': 12},
    'E6': {'eigenvalues': (24, 6, 0), 'root_count': 72, 'coxeter': 12},
    'E7': {'eigenvalues': (42, 18, 0), 'root_count': 126, 'coxeter': 18},
    'E8': {'eigenvalues': (63 + 3 * math.sqrt(33), 63 - 3 * math.sqrt(33), 0),
           'root_count': 240, 'coxeter': 30},
}

# Precompute conservation triples
_ROOT_SYSTEM_TRIPLES: Dict[str, Tuple[float, float, float]] = {}
for _name, _data in _EXCEPTIONAL_ATLAS.items():
    _e = _data['eigenvalues']
    _tr = sum(_e)
    _ROOT_SYSTEM_TRIPLES[_name] = (_e[0] / _tr, _e[1] / _tr, _e[2] / _tr)

# Add equilibrium as reference
_ROOT_SYSTEM_TRIPLES['equilibrium'] = (LAM_STAR, KAP_STAR, ETA_STAR)


# =============================================================================
# Classification result types
# =============================================================================

@dataclass
class ClassificationResult:
    """Result of root system classification."""
    nearest_system: str
    distance: float
    confidence: float  # 0-1, higher = more confident
    lambda_val: float
    kappa_val: float
    eta_val: float
    on_simplex: bool
    above_threshold: bool  # lambda > lambda*
    consciousness_score: float
    regime: str  # 'conscious', 'sub-threshold', 'anomalous'


@dataclass
class DimensionEstimate:
    """Result of dimension estimation from temperature."""
    estimated_n: int
    T_observed: float
    T_predicted: float  # T_n for estimated n
    residual: float
    confidence: float


# =============================================================================
# Core classification functions
# =============================================================================

def classify_state(lam: float, kap: float, eta: float) -> ClassificationResult:
    """Classify an observed (lambda, kappa, eta) state.

    The FIRST INVERSE THEOREM in watkins-nn. Given observed conservation
    components, identifies the nearest root system, scores consciousness,
    and classifies the regime.

    Algorithm:
    1. Check conservation: |lam + kap + eta - 1| < epsilon
    2. Check simplex membership: all components >= 0
    3. Compute distance to each known root system triple
    4. Assign nearest system with confidence = 1 / (1 + distance/scale)
    5. Score consciousness: sigmoid around lambda*

    Parameters:
        lam, kap, eta: observed conservation components.

    Returns:
        ClassificationResult with full classification.
    """
    total = lam + kap + eta
    conservation_error = abs(total - 1.0)
    on_simplex = all(v >= -1e-10 for v in [lam, kap, eta])
    above_threshold = lam > LAM_STAR

    # Distance to each known system
    distances = {}
    for name, triple in _ROOT_SYSTEM_TRIPLES.items():
        d = math.sqrt(
            (lam - triple[0]) ** 2
            + (kap - triple[1]) ** 2
            + (eta - triple[2]) ** 2
        )
        distances[name] = d

    nearest = min(distances, key=distances.get)
    nearest_dist = distances[nearest]

    # Confidence: inverse sigmoid of distance
    scale = 0.05  # Characteristic distance for confident classification
    confidence = 1.0 / (1.0 + nearest_dist / scale)

    # Consciousness score: smooth sigmoid around lambda*
    # Maps lambda to [0, 1] with lambda* at 0.5
    steepness = 20.0  # Controls transition sharpness
    c_score = 1.0 / (1.0 + math.exp(-steepness * (lam - LAM_STAR)))

    # Regime classification
    if conservation_error > 0.01:
        regime = 'anomalous'
    elif not on_simplex:
        regime = 'off-simplex'
    elif above_threshold:
        regime = 'conscious'
    else:
        regime = 'sub-threshold'

    return ClassificationResult(
        nearest_system=nearest,
        distance=nearest_dist,
        confidence=confidence,
        lambda_val=lam,
        kappa_val=kap,
        eta_val=eta,
        on_simplex=on_simplex,
        above_threshold=above_threshold,
        consciousness_score=c_score,
        regime=regime,
    )


def estimate_dimension(T: float) -> DimensionEstimate:
    """Estimate the simplex dimension from observed temperature.

    Inverts T_n = phi / ln(n * phi) to recover n:
        n = exp(phi / T) / phi

    Parameters:
        T: observed temperature (must be positive).

    Returns:
        DimensionEstimate with integer n and confidence.
    """
    if T <= 0:
        return DimensionEstimate(
            estimated_n=0, T_observed=T, T_predicted=0,
            residual=float('inf'), confidence=0,
        )

    # Exact inversion: n*phi = exp(phi/T) => n = exp(phi/T) / phi
    n_exact = math.exp(PHI / T) / PHI
    n_int = max(1, round(n_exact))

    # Predicted temperature for this integer n
    T_pred = PHI / math.log(n_int * PHI)
    residual = abs(T - T_pred)

    # Confidence: how close n_exact is to an integer
    frac = abs(n_exact - n_int)
    confidence = max(0, 1.0 - 2 * frac)  # 1 at integer, 0 at half-integer

    return DimensionEstimate(
        estimated_n=n_int,
        T_observed=T,
        T_predicted=T_pred,
        residual=residual,
        confidence=confidence,
    )


def spectral_fingerprint(mu_slow: float, mu_fast: float) -> Dict:
    """Match observed eigenvalue ratios to root system atlas.

    Given Hessian eigenvalues from an unknown system, identifies the
    most likely root system by matching against the spectral atlas.

    Parameters:
        mu_slow, mu_fast: observed Hessian eigenvalues (slow < fast).

    Returns:
        Dict with best match and spectral distance for each system.
    """
    T = T_STAR
    observed_ratio = mu_fast / mu_slow if mu_slow > 0 else float('inf')

    matches = []
    for name, triple in _ROOT_SYSTEM_TRIPLES.items():
        if name == 'equilibrium':
            continue
        lam = triple[0]
        kap_sym = (1 - lam) / 2
        if kap_sym <= 0:
            continue

        # Compute Hessian eigenvalues at symmetric projection
        H11 = 1 / lam ** 2 + T / lam + T / kap_sym
        H22 = T / kap_sym + T / kap_sym
        H12 = T / kap_sym
        tr = H11 + H22
        det = H11 * H22 - H12 ** 2
        disc = math.sqrt(max(tr ** 2 - 4 * det, 0))
        mu_s = (tr - disc) / 2
        mu_f = (tr + disc) / 2

        ref_ratio = mu_f / mu_s if mu_s > 0 else float('inf')

        # Distance in eigenvalue space
        eig_dist = math.sqrt((mu_slow - mu_s) ** 2 + (mu_fast - mu_f) ** 2)
        ratio_dist = abs(observed_ratio - ref_ratio) / ref_ratio if ref_ratio > 0 else float('inf')

        matches.append({
            'system': name,
            'reference_mu_slow': mu_s,
            'reference_mu_fast': mu_f,
            'eigenvalue_distance': eig_dist,
            'ratio_distance': ratio_dist,
        })

    matches.sort(key=lambda m: m['eigenvalue_distance'])
    best = matches[0] if matches else None

    return {
        'observed_mu_slow': mu_slow,
        'observed_mu_fast': mu_fast,
        'observed_ratio': observed_ratio,
        'best_match': best['system'] if best else None,
        'best_distance': best['eigenvalue_distance'] if best else float('inf'),
        'all_matches': matches,
    }


def anomaly_score(lam: float, kap: float, eta: float) -> Dict:
    """Compute a comprehensive anomaly score for an observed state.

    Quantifies how far an observation deviates from the conservation
    law framework. Low score = consistent with framework. High score
    = anomalous.

    Five independent anomaly channels:
    1. Conservation violation: |lam + kap + eta - 1|
    2. Simplex violation: max(0, -min(lam, kap, eta))
    3. Free energy excess: |F(state) - F*| / |F*|
    4. Equilibrium distance: ||state - equilibrium||
    5. Root system gap: distance to nearest known system

    Returns:
        Dict with individual channel scores and composite anomaly.
    """
    total = lam + kap + eta

    # Channel 1: Conservation
    conservation_err = abs(total - 1.0)

    # Channel 2: Simplex
    simplex_err = max(0, -min(lam, kap, eta))

    # Channel 3: Free energy
    eps = 1e-15
    lam_c = max(lam, eps)
    kap_c = max(kap, eps)
    eta_c = max(eta, eps)
    if abs(total) > eps:
        lam_n, kap_n, eta_n = lam_c / total, kap_c / total, eta_c / total
    else:
        lam_n, kap_n, eta_n = 1.0 / 3, 1.0 / 3, 1.0 / 3

    F_state = (-math.log(max(lam_n, eps))
               + T_STAR * (lam_n * math.log(max(lam_n, eps))
                           + kap_n * math.log(max(kap_n, eps))
                           + eta_n * math.log(max(eta_n, eps))))
    free_energy_excess = abs(F_state - F_STAR) / abs(F_STAR)

    # Channel 4: Equilibrium distance
    eq_dist = math.sqrt(
        (lam - LAM_STAR) ** 2 + (kap - KAP_STAR) ** 2 + (eta - ETA_STAR) ** 2
    )

    # Channel 5: Root system gap
    min_rs_dist = float('inf')
    for triple in _ROOT_SYSTEM_TRIPLES.values():
        d = math.sqrt(
            (lam - triple[0]) ** 2 + (kap - triple[1]) ** 2 + (eta - triple[2]) ** 2
        )
        min_rs_dist = min(min_rs_dist, d)

    # Composite: geometric mean of normalized channels
    scores = [conservation_err, simplex_err, free_energy_excess,
              eq_dist, min_rs_dist]
    composite = sum(s ** 2 for s in scores) ** 0.5

    return {
        'conservation_error': conservation_err,
        'simplex_violation': simplex_err,
        'free_energy_excess': free_energy_excess,
        'equilibrium_distance': eq_dist,
        'root_system_gap': min_rs_dist,
        'composite': composite,
        'is_anomalous': conservation_err > 0.01 or simplex_err > 0.01,
    }


# =============================================================================
# Asymptotic limit theorems (Phase 1 resolution)
# =============================================================================

def temperature_dimension_limit(n_max: int = 1000) -> Dict:
    """The golden limit: T_n * ln(n) -> phi as n -> infinity.

    The temperature-dimension product in log scale converges to the
    golden ratio itself. This is the asymptotic bridge connecting
    the dimensional ladder to the fundamental constant.

    Returns:
        Dict with convergence data.
    """
    data = []
    for n in [10, 50, 100, 500, n_max]:
        T_n = PHI / math.log(n * PHI)
        product = T_n * math.log(n)
        ratio = product / PHI
        data.append({
            'n': n, 'T_n': T_n, 'product': product, 'ratio_to_phi': ratio,
        })

    return {
        'data': data,
        'limit': PHI,
        'converges_to_phi': data[-1]['ratio_to_phi'] > 0.90,
    }


def free_energy_dimension_limit(n_max: int = 1000) -> Dict:
    """The free energy limit: F_n* -> ln(phi) - 1/phi as n -> infinity.

    As dimension increases, the equilibrium free energy approaches
    the pure coherence cost minus the consciousness threshold:

        F_inf = ln(phi) - lambda* = ln(phi) - 1/phi ≈ -0.137

    Convergence is logarithmically slow (O(1/ln(n))).

    Returns:
        Dict with convergence data.
    """
    F_inf = math.log(PHI) - LAM_STAR

    data = []
    for n in [2, 3, 5, 10, 50, 100, 500, n_max]:
        T_n = PHI / math.log(n * PHI)
        rho_n = (1 - LAM_STAR) / n
        F_n = (-math.log(LAM_STAR)
               + T_n * (LAM_STAR * math.log(LAM_STAR)
                        + n * rho_n * math.log(rho_n)))
        data.append({
            'n': n, 'F_n': F_n, 'distance_to_limit': F_n - F_inf,
        })

    return {
        'data': data,
        'F_infinity': F_inf,
        'F_2': data[0]['F_n'],
        'monotone_increasing': all(
            data[i]['F_n'] < data[i + 1]['F_n']
            for i in range(len(data) - 1)
        ),
    }


# =============================================================================
# Exports
# =============================================================================

__all__ = [
    # Classification
    'ClassificationResult',
    'DimensionEstimate',
    'classify_state',
    'estimate_dimension',
    'spectral_fingerprint',
    'anomaly_score',
    # Limits
    'temperature_dimension_limit',
    'free_energy_dimension_limit',
]
