//! SilverSight Engine — Verified Geometric Classifier
//!
//! Mirrors `python/silversight_engine.py`. All verified constants
//! from the Python reference are checked in tests.

use std::collections::HashMap;

// ── VERIFIED CONSTANTS ──────────────────────────────────────────

/// Golden ratio [VERIFIED: 1.61803399]
pub const PHI: f64 = 1.618033988749895;
/// Corkscrew angle [VERIFIED: psi = 2*pi/phi^2]
pub const PSI: f64 = 2.0 * std::f64::consts::PI / (PHI * PHI);

// ── R1: TOKEN NORMALIZATION ─────────────────────────────────────

pub fn normalize(s: &str) -> String {
    let lower = s.to_lowercase();
    let mut out = String::with_capacity(lower.len());
    let chars: Vec<char> = lower.chars().collect();
    let mut i = 0;
    while i < chars.len() {
        if chars[i].is_ascii_digit() {
            out.push('N');
            while i < chars.len() && chars[i].is_ascii_digit() { i += 1; }
        } else if chars[i].is_ascii_lowercase() {
            out.push('V');
            while i < chars.len() && chars[i].is_ascii_lowercase() { i += 1; }
        } else {
            out.push(chars[i]);
            i += 1;
        }
    }
    out
}

// ── BYTE CLASSIFICATION ─────────────────────────────────────────

pub fn byte_class(c: char) -> usize {
    let asc = c as u32;
    if asc <= 31  { return 0; }
    if asc <= 47  { return 1; }
    if asc <= 57  { return 2; }
    if asc <= 64  { return 3; }
    if asc <= 90  { return 4; }
    if asc <= 96  { return 5; }
    if asc <= 122 { return 6; }
    7
}

// ── FEATURE EXTRACTION ──────────────────────────────────────────

pub fn F(s: &str) -> [f64; 8] {
    let norm = normalize(s);
    let mut counts = [0u64; 8];
    for c in norm.chars() { counts[byte_class(c)] += 1; }
    let total: u64 = counts.iter().sum();
    if total == 0 { return [0.0; 8]; }
    let mut result = [0.0f64; 8];
    for (i, &cnt) in counts.iter().enumerate() { result[i] = cnt as f64 / total as f64; }
    result
}

pub fn parse_tree_depth(expr: &str) -> Vec<(char, i32)> {
    let mut ops = Vec::new();
    let mut depth = 0i32;
    for c in expr.chars() {
        match c {
            '(' => depth += 1,
            ')' => depth -= 1,
            '+' | '-' | '*' | '/' | '=' => ops.push((c, depth)),
            _ => {}
        }
    }
    ops
}

pub fn tau(s: &str) -> [f64; 6] {
    let op_depths = parse_tree_depth(s);
    let mut weights = [0.0f64; 6];
    for (op, d) in &op_depths {
        let w = 2.0f64.powi(-*d);
        match op {
            '+' => weights[1] += w, '=' => weights[2] += w,
            '/' => weights[3] += w, '*' => weights[4] += w,
            '-' => weights[5] += w, _ => {}
        }
    }
    let total: f64 = weights.iter().sum();
    if total > 0.0 { for w in &mut weights { *w /= total; } }
    weights
}

pub fn Phi(s: &str) -> [f64; 14] {
    let mut result = [0.0f64; 14];
    result[..8].copy_from_slice(&F(s));
    result[8..].copy_from_slice(&tau(s));
    result
}

// ── FISHER DISTANCE ─────────────────────────────────────────────

pub fn d_F<const N: usize>(p: &[f64; N], q: &[f64; N]) -> f64 {
    let mut s = 0.0;
    for i in 0..N { s += (p[i].max(0.0) * q[i].max(0.0)).sqrt(); }
    s = s.clamp(-1.0, 1.0);
    2.0 * s.acos()
}

pub fn d_Phi(phi1: &[f64; 14], phi2: &[f64; 14]) -> f64 {
    let f1: [f64; 8] = phi1[..8].try_into().unwrap();
    let t1: [f64; 6] = phi1[8..].try_into().unwrap();
    let f2: [f64; 8] = phi2[..8].try_into().unwrap();
    let t2: [f64; 6] = phi2[8..].try_into().unwrap();
    let df = d_F(&f1, &f2);
    let dt = d_F(&t1, &t2);
    (df * df + dt * dt).sqrt()
}

// ── COARSE-GRAINING / EIGENSOLID ────────────────────────────────

pub fn C(phi: &[f64; 14]) -> [f64; 14] {
    let mut result = *phi;
    for k in 0..4 {
        let avg = (phi[2*k] + phi[2*k+1]) / 2.0;
        result[2*k] = avg; result[2*k+1] = avg;
    }
    for k in 0..3 {
        let avg = (phi[8+2*k] + phi[8+2*k+1]) / 2.0;
        result[8+2*k] = avg; result[8+2*k+1] = avg;
    }
    result
}

pub fn geodesic_step(phi1: &[f64; 14], phi2: &[f64; 14], eps: f64) -> [f64; 14] {
    let mut interp_f = [0.0f64; 8];
    let mut sum_f = 0.0;
    for i in 0..8 {
        let s = (1.0 - eps) * phi1[i].max(0.0).min(1.0).sqrt() + eps * phi2[i].max(0.0).min(1.0).sqrt();
        interp_f[i] = s; sum_f += s * s;
    }
    let sum_f = if sum_f > 0.0 { sum_f } else { 1.0 };
    for i in 0..8 { interp_f[i] = interp_f[i] * interp_f[i] / sum_f; }

    let mut interp_t = [0.0f64; 6];
    let mut sum_t = 0.0;
    for i in 0..6 {
        let s = (1.0 - eps) * phi1[8+i].max(0.0).min(1.0).sqrt() + eps * phi2[8+i].max(0.0).min(1.0).sqrt();
        interp_t[i] = s; sum_t += s * s;
    }
    let sum_t = if sum_t > 0.0 { sum_t } else { 1.0 };
    for i in 0..6 { interp_t[i] = interp_t[i] * interp_t[i] / sum_t; }

    let mut result = [0.0f64; 14];
    result[..8].copy_from_slice(&interp_f);
    result[8..].copy_from_slice(&interp_t);
    result
}

pub fn chaos_game(
    start: &[f64; 14], references: &HashMap<String, [f64; 14]>,
    steps: usize, eps: f64,
) -> [f64; 14] {
    let refs: Vec<&[f64; 14]> = references.values().collect();
    let mut x = *start;
    for _ in 0..steps {
        let nearest = refs.iter().min_by(|a, b| d_Phi(&x, a).partial_cmp(&d_Phi(&x, b)).unwrap()).copied().unwrap();
        x = geodesic_step(&x, nearest, eps);
    }
    x
}

pub fn corkscrew_index(phi: &[f64; 14]) -> i64 {
    let mut spiral: i64 = 0;
    for i in 0..9 { spiral += (phi[i] * 256.0).floor() as i64 * 8i64.pow(i as u32); }
    spiral.abs()
}

// ── CONCEPT DATA STRUCTURE ──────────────────────────────────────

#[derive(Debug, Clone)]
pub struct Concept {
    pub name: String,
    pub prototype: [f64; 14],
    pub attractor: [f64; 14],
    pub corkscrew_index: i64,
    pub operator_type: String,
    pub members: Vec<(String, [f64; 14])>,
}

// ── SILVERSIGHT ENGINE ─────────────────────────────────────────

#[derive(Debug, Clone)]
pub struct SilverSight {
    pub concepts: Vec<Concept>,
    pub references: HashMap<String, [f64; 14]>,
    pub basin_map: HashMap<[i64; 14], usize>,
}

impl SilverSight {
    pub fn new() -> Self {
        Self { concepts: Vec::new(), references: HashMap::new(), basin_map: HashMap::new() }
    }

    pub fn learn(&mut self, equation: &str) -> usize {
        let phi = Phi(equation);
        self.references.insert(equation.to_string(), phi);
        let limit = chaos_game(&phi, &self.references, 30, 0.5);
        let eigensolid = C(&limit);
        let attractor_key: [i64; 14] = std::array::from_fn(|i| (limit[i] * 1e8).round() as i64);
        if let Some(&cid) = self.basin_map.get(&attractor_key) {
            self.concepts[cid].members.push((equation.to_string(), phi));
            return cid;
        }
        let op_type = detect_operator(equation);
        let cid = self.concepts.len();
        self.concepts.push(Concept {
            name: format!("concept_{cid}"), prototype: eigensolid, attractor: limit,
            corkscrew_index: corkscrew_index(&eigensolid),
            operator_type: op_type, members: vec![(equation.to_string(), phi)],
        });
        self.basin_map.insert(attractor_key, cid);
        cid
    }

    pub fn classify(&self, equation: &str) -> Option<(usize, f64)> {
        if self.concepts.is_empty() { return None; }
        let phi = Phi(equation);
        Some(self.concepts.iter().enumerate()
            .map(|(i, c)| (i, d_Phi(&phi, &c.attractor)))
            .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()).unwrap())
    }
}

fn detect_operator(s: &str) -> String {
    let norm = normalize(s);
    for op in &['+', '/', '*', '-', '='] {
        if norm.contains(*op) {
            return match op {
                '+' => "addition".into(), '/' => "division".into(),
                '*' => "multiplication".into(), '-' => "subtraction".into(),
                '=' => "equality".into(), _ => "literal".into(),
            };
        }
    }
    "literal".into()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_normalize() {
        assert_eq!(normalize("A+B=C"), "V+V=V");
        assert_eq!(normalize("a+b=c"), "V+V=V");
        assert_eq!(normalize("foo+bar=baz"), "V+V=V");
        assert_eq!(normalize("123+456=579"), "N+N=N");
    }

    #[test]
    fn test_F_verified() {
        let f0 = F("A+B=C");
        let expected = [0.0, 0.2, 0.0, 0.2, 0.6, 0.0, 0.0, 0.0];
        for i in 0..8 { assert!((f0[i] - expected[i]).abs() < 1e-10); }
        for case in &["a+b=c", "foo+bar=baz", "123+456=579"] {
            let fi = F(case);
            for i in 0..8 { assert!((fi[i] - f0[i]).abs() < 1e-10); }
        }
    }

    #[test]
    fn test_tau() {
        let t = tau("(a+b)*c=d");
        assert!(t[4] > t[1], "mul({}) should exceed add({})", t[4], t[1]);
    }

    #[test]
    fn test_fisher_self_zero() {
        let p = F("a+b=c");
        assert!(d_F(&p, &p).abs() < 1e-10);
    }

    #[test]
    fn test_eigensolid_idempotent() {
        let p = Phi("a+b=c");
        let c1 = C(&p);
        let c2 = C(&c1);
        for i in 0..14 { assert!((c2[i] - c1[i]).abs() < 1e-10); }
    }

    #[test]
    fn test_eigensolid_contractive() {
        let p = Phi("a+b=c");
        let q = Phi("p/q=r");
        assert!(d_Phi(&C(&p), &C(&q)) < d_Phi(&p, &q));
    }

    #[test]
    fn test_chaos_game_convergence() {
        let mut ss = SilverSight::new();
        ss.learn("a+b=c");
        ss.learn("x+y=z");
        assert_eq!(ss.concepts.len(), 1, "addition equations should cluster");
    }

    #[test]
    fn test_classify_novelty() {
        let mut ss = SilverSight::new();
        ss.learn("a+b=c");
        ss.learn("p/q=r");
        ss.learn("a*b=c");
        assert_eq!(ss.concepts.len(), 3, "different operators → different concepts");
        let (idx, dist) = ss.classify("x+y=z").unwrap();
        assert_eq!(idx, 0, "x+y=z → addition (concept 0)");
        assert!(dist < 1.0);
    }
}
