//! Shared profiling harness: corpus builders, measurement helpers, reporting.

use luci_index::index::Index;
use luci_mapping::{FieldType,Mapping};
use serde_json::json;

use std::path::PathBuf;
use std::time::{Duration, Instant};

// --- Temp directory ---

pub fn profile_dir(name: &str) -> PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "luci_profile_{}_{name}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    dir
}

pub fn cleanup(path: &std::path::Path) {
    let _ = std::fs::remove_dir_all(path);
}

// --- Schemas ---

pub fn text_schema() -> Mapping {
    Mapping::builder()
        .field("title", FieldType::Text)
        .field("body", FieldType::Text)
        .field("tag", FieldType::Keyword)
        .field("price", FieldType::Float)
        .build()
}

pub fn vector_schema(dims: usize) -> Mapping {
    Mapping::builder()
        .field("title", FieldType::Text)
        .field("tag", FieldType::Keyword)
        .field("embedding", FieldType::DenseVector { dims })
        .build()
}

pub fn geo_schema() -> Mapping {
    Mapping::builder()
        .field("name", FieldType::Text)
        .field("city", FieldType::Keyword)
        .field("location", FieldType::GeoPoint)
        .build()
}

pub fn high_cardinality_schema() -> Mapping {
    Mapping::builder()
        .field("title", FieldType::Text)
        .field("category", FieldType::Keyword)
        .field("subcategory", FieldType::Keyword)
        .field("value", FieldType::Float)
        .build()
}

// --- Corpus builders ---

pub fn build_text_corpus(name: &str, doc_count: usize) -> (PathBuf, Index) {
    let path = profile_dir(name);
    let mut index = Index::create_with_mapping(&path, text_schema()).unwrap();

    let tags = ["tech", "science", "sports", "politics", "entertainment"];
    for i in 0..doc_count {
        let tag = tags[i % tags.len()];
        index.add(json!({
            "title": format!("document number {i} about {tag} topics"),
            "body": format!("this is the body text for document {i} covering {tag} related content and discussions"),
            "tag": tag,
            "price": (i as f64 % 100.0) + 0.99,
        })).unwrap();
    }
    index.commit().unwrap();
    (path, index)
}

pub fn build_multi_segment_corpus(name: &str, segments: usize, docs_per_segment: usize) -> (PathBuf, Index) {
    let path = profile_dir(name);
    let mut index = Index::create_with_mapping(&path, text_schema()).unwrap();

    let tags = ["tech", "science", "sports", "politics", "entertainment"];
    for seg in 0..segments {
        for i in 0..docs_per_segment {
            let global = seg * docs_per_segment + i;
            let tag = tags[global % tags.len()];
            index.add(json!({
                "title": format!("doc {global} segment {seg}"),
                "body": format!("body text for doc {global} about {tag}"),
                "tag": tag,
                "price": (global as f64 % 100.0) + 0.99,
            })).unwrap();
        }
        index.commit().unwrap();
    }
    (path, index)
}

pub fn build_vector_corpus(name: &str, doc_count: usize, dims: usize) -> (PathBuf, Index) {
    let path = profile_dir(name);
    let mut index = Index::create_with_mapping(&path, vector_schema(dims)).unwrap();

    let mut rng: u64 = 42;
    let tags = ["a", "b", "c", "d", "e"];
    for i in 0..doc_count {
        let mut vec = Vec::with_capacity(dims);
        for _ in 0..dims {
            rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
            vec.push((rng as f64 / u64::MAX as f64) * 2.0 - 1.0);
        }
        index.add(json!({
            "title": format!("vector doc {i}"),
            "tag": tags[i % tags.len()],
            "embedding": vec,
        })).unwrap();
    }
    index.commit().unwrap();
    (path, index)
}

pub fn build_geo_corpus(name: &str, doc_count: usize) -> (PathBuf, Index) {
    let path = profile_dir(name);
    let mut index = Index::create_with_mapping(&path, geo_schema()).unwrap();

    let mut rng: u64 = 99;
    let cities = ["nyc", "london", "paris", "tokyo", "sydney"];
    let base_coords: [(f64, f64); 5] = [
        (40.7, -74.0), (51.5, -0.1), (48.9, 2.3), (35.7, 139.7), (-33.9, 151.2),
    ];

    for i in 0..doc_count {
        let city_idx = i % cities.len();
        rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
        let jitter_lat = (rng as f64 / u64::MAX as f64 - 0.5) * 0.5;
        rng ^= rng << 13; rng ^= rng >> 7; rng ^= rng << 17;
        let jitter_lon = (rng as f64 / u64::MAX as f64 - 0.5) * 0.5;

        index.add(json!({
            "name": format!("place {i}"),
            "city": cities[city_idx],
            "location": {
                "lat": base_coords[city_idx].0 + jitter_lat,
                "lon": base_coords[city_idx].1 + jitter_lon,
            },
        })).unwrap();
    }
    index.commit().unwrap();
    (path, index)
}

pub fn build_high_cardinality_corpus(name: &str, doc_count: usize, unique_categories: usize) -> (PathBuf, Index) {
    let path = profile_dir(name);
    let mut index = Index::create_with_mapping(&path, high_cardinality_schema()).unwrap();

    for i in 0..doc_count {
        index.add(json!({
            "title": format!("doc {i}"),
            "category": format!("cat_{}", i % unique_categories),
            "subcategory": format!("sub_{}_{}", i % unique_categories, i % 10),
            "value": (i as f64 % 1000.0) + 0.5,
        })).unwrap();
    }
    index.commit().unwrap();
    (path, index)
}

// --- Measurement ---

pub struct Measurement {
    pub name: String,
    pub p50: Duration,
    pub p90: Duration,
    pub p95: Duration,
    pub p99: Duration,
    pub min: Duration,
    pub max: Duration,
    pub hits: u64,
}

fn percentile(sorted: &[(Duration, u64)], pct: f64) -> Duration {
    let idx = ((sorted.len() as f64 * pct / 100.0) as usize).min(sorted.len() - 1);
    sorted[idx].0
}

pub fn measure_query(
    name: &str,
    index: &mut Index,
    query: serde_json::Value,
    size: usize,
    warmup: usize,
    iterations: usize,
) -> Measurement {
    // Use at least 200 iterations for stable p99 (p99 of 200 = worst 2,
    // more robust against single-sample OS scheduling outliers).
    let iters = iterations.max(200);

    // Warmup
    for _ in 0..warmup {
        let _ = index.search(query.clone(), size);
    }

    // Measure
    let mut times: Vec<(Duration, u64)> = Vec::with_capacity(iters);
    for _ in 0..iters {
        let start = Instant::now();
        let results = index.search(query.clone(), size).unwrap();
        times.push((start.elapsed(), results.total_hits.value));
    }

    times.sort_by_key(|t| t.0);
    let hits = times[0].1;

    Measurement {
        name: name.to_string(),
        p50: percentile(&times, 50.0),
        p90: percentile(&times, 90.0),
        p95: percentile(&times, 95.0),
        p99: percentile(&times, 99.0),
        min: times[0].0,
        max: times[times.len() - 1].0,
        hits,
    }
}

pub fn print_measurement(m: &Measurement) {
    println!("{:<30} p50={:>8.1}  p90={:>8.1}  p95={:>8.1}  p99={:>8.1}  hits={}",
        m.name,
        m.p50.as_micros() as f64,
        m.p90.as_micros() as f64,
        m.p95.as_micros() as f64,
        m.p99.as_micros() as f64,
        m.hits,
    );
}

pub fn print_measurement_header() {
    println!("{:<30} {:>12}  {:>12}  {:>12}  {:>12}  {}",
        "query", "p50 (us)", "p90 (us)", "p95 (us)", "p99 (us)", "hits");
    println!("{}", "-".repeat(110));
}

// --- Regression assertions ---

impl Measurement {
    /// Assert that the p99 latency is under the given threshold in microseconds.
    ///
    /// Thresholds should be set at 3-5x the measured baseline to account for
    /// system load variance and CI environments, while still catching real
    /// regressions (e.g., algorithmic complexity changes).
    pub fn assert_p99_under_us(&self, threshold_us: u64) {
        let actual = self.p99.as_micros() as u64;
        assert!(
            actual < threshold_us,
            "REGRESSION: {} p99={actual}us exceeds threshold {threshold_us}us",
            self.name,
        );
    }

    /// Assert that the p50 latency is under the given threshold in microseconds.
    pub fn assert_p50_under_us(&self, threshold_us: u64) {
        let actual = self.p50.as_micros() as u64;
        assert!(
            actual < threshold_us,
            "REGRESSION: {} p50={actual}us exceeds threshold {threshold_us}us",
            self.name,
        );
    }

    /// Assert that the p99 latency is under the given threshold in milliseconds.
    pub fn assert_p99_under_ms(&self, threshold_ms: u64) {
        self.assert_p99_under_us(threshold_ms * 1000);
    }

    /// Assert p99 is within a band: [floor, ceiling].
    ///
    /// If p99 drops **below** the floor, it means a performance improvement
    /// occurred and the thresholds should be tightened to lock in the gain.
    /// This prevents thresholds from going stale as optimizations land.
    pub fn assert_p99_between_us(&self, floor_us: u64, ceiling_us: u64) {
        let actual = self.p99.as_micros() as u64;
        assert!(
            actual < ceiling_us,
            "REGRESSION: {} p99={actual}us exceeds ceiling {ceiling_us}us",
            self.name,
        );
        if actual < floor_us {
            panic!(
                "IMPROVEMENT: {} p99={actual}us is below floor {floor_us}us — \
                tighten the regression thresholds to lock in this gain",
                self.name,
            );
        }
    }

    /// Assert p99 is within a band in milliseconds.
    pub fn assert_p99_between_ms(&self, floor_ms: u64, ceiling_ms: u64) {
        self.assert_p99_between_us(floor_ms * 1000, ceiling_ms * 1000);
    }
}

// --- Memory ---

pub fn memory_usage_kb() -> usize {
    #[cfg(target_os = "macos")]
    {
        use std::process::Command;
        let output = Command::new("ps")
            .args(["-o", "rss=", "-p", &std::process::id().to_string()])
            .output();
        match output {
            Ok(o) => String::from_utf8_lossy(&o.stdout).trim().parse().unwrap_or(0),
            Err(_) => 0,
        }
    }
    #[cfg(not(target_os = "macos"))]
    { 0 }
}

pub fn disk_usage_bytes(path: &std::path::Path) -> u64 {
    let mut total = 0;
    if let Ok(entries) = std::fs::read_dir(path) {
        for entry in entries.flatten() {
            let meta = entry.metadata().unwrap();
            if meta.is_file() {
                total += meta.len();
            } else if meta.is_dir() {
                total += disk_usage_bytes(&entry.path());
            }
        }
    }
    total
}
