use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RunMetadata {
    pub scenario: String,
    pub frames: u64,
    pub profile: String,
    pub target_os: String,
    pub target_arch: String,
    pub started_unix_ms: u128,
}

impl RunMetadata {
    pub fn new(scenario: impl Into<String>, frames: u64) -> Self {
        Self {
            scenario: scenario.into(),
            frames,
            profile: if cfg!(debug_assertions) {
                "debug".into()
            } else {
                "release".into()
            },
            target_os: std::env::consts::OS.into(),
            target_arch: std::env::consts::ARCH.into(),
            started_unix_ms: SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or(Duration::ZERO)
                .as_millis(),
        }
    }
}

#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct SampleSeries {
    pub name: String,
    pub unit: String,
    pub samples: Vec<f64>,
}

impl SampleSeries {
    pub fn new(name: impl Into<String>, unit: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            unit: unit.into(),
            samples: Vec::new(),
        }
    }

    pub fn push(&mut self, sample: f64) {
        if sample.is_finite() {
            self.samples.push(sample);
        }
    }

    pub fn summary(&self) -> SampleSummary {
        SampleSummary::from_samples(&self.name, &self.unit, &self.samples)
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SampleSummary {
    pub name: String,
    pub unit: String,
    pub count: usize,
    pub avg: f64,
    pub p50: f64,
    pub p95: f64,
    pub max: f64,
}

impl SampleSummary {
    fn from_samples(name: &str, unit: &str, samples: &[f64]) -> Self {
        if samples.is_empty() {
            return Self {
                name: name.into(),
                unit: unit.into(),
                count: 0,
                avg: 0.0,
                p50: 0.0,
                p95: 0.0,
                max: 0.0,
            };
        }

        let mut sorted = samples.to_vec();
        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
        let sum = sorted.iter().sum::<f64>();
        Self {
            name: name.into(),
            unit: unit.into(),
            count: sorted.len(),
            avg: sum / sorted.len() as f64,
            p50: percentile(&sorted, 0.50),
            p95: percentile(&sorted, 0.95),
            max: *sorted.last().unwrap_or(&0.0),
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PerfReport {
    pub metadata: RunMetadata,
    pub summaries: Vec<SampleSummary>,
    pub counters: BTreeMap<String, f64>,
    pub notes: Vec<String>,
}

#[derive(Clone, Debug)]
pub struct PerfReportBuilder {
    metadata: RunMetadata,
    series: BTreeMap<String, SampleSeries>,
    counters: BTreeMap<String, f64>,
    notes: Vec<String>,
}

impl PerfReportBuilder {
    pub fn new(metadata: RunMetadata) -> Self {
        Self {
            metadata,
            series: BTreeMap::new(),
            counters: BTreeMap::new(),
            notes: Vec::new(),
        }
    }

    pub fn record_ms(&mut self, name: impl Into<String>, value: f64) {
        self.record(name, "ms", value);
    }

    pub fn record(&mut self, name: impl Into<String>, unit: impl Into<String>, value: f64) {
        let name = name.into();
        let unit = unit.into();
        self.series
            .entry(name.clone())
            .or_insert_with(|| SampleSeries::new(name, unit))
            .push(value);
    }

    pub fn add_counter(&mut self, name: impl Into<String>, value: impl Into<f64>) {
        *self.counters.entry(name.into()).or_insert(0.0) += value.into();
    }

    pub fn set_counter(&mut self, name: impl Into<String>, value: impl Into<f64>) {
        self.counters.insert(name.into(), value.into());
    }

    pub fn note(&mut self, note: impl Into<String>) {
        self.notes.push(note.into());
    }

    pub fn finish(self) -> PerfReport {
        PerfReport {
            metadata: self.metadata,
            summaries: self.series.values().map(SampleSeries::summary).collect(),
            counters: self.counters,
            notes: self.notes,
        }
    }
}

impl PerfReport {
    pub fn write_json(&self, path: impl AsRef<Path>) -> io::Result<()> {
        let path = path.as_ref();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
        fs::write(path, json)?;
        Ok(())
    }

    pub fn default_output_path(&self) -> PathBuf {
        PathBuf::from("target")
            .join("ruin-perf")
            .join(format!("{}.json", self.metadata.scenario))
    }

    pub fn print_summary(&self) {
        println!(
            "scenario={} frames={} profile={} target={}/{}",
            self.metadata.scenario,
            self.metadata.frames,
            self.metadata.profile,
            self.metadata.target_os,
            self.metadata.target_arch
        );
        println!(
            "{:<32} {:>8} {:>10} {:>10} {:>10} {:>10}",
            "metric", "count", "avg", "p50", "p95", "max"
        );
        for summary in &self.summaries {
            println!(
                "{:<32} {:>8} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {}",
                summary.name,
                summary.count,
                summary.avg,
                summary.p50,
                summary.p95,
                summary.max,
                summary.unit
            );
        }
        if !self.counters.is_empty() {
            println!();
            println!("{:<32} {:>12}", "counter", "value");
            for (name, value) in &self.counters {
                println!("{name:<32} {value:>12.3}");
            }
        }
    }
}

fn percentile(sorted: &[f64], percentile: f64) -> f64 {
    if sorted.is_empty() {
        return 0.0;
    }
    let index = ((sorted.len() - 1) as f64 * percentile).round() as usize;
    sorted[index.min(sorted.len() - 1)]
}
