use std::collections::{HashMap, VecDeque};
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{Instant, SystemTime, UNIX_EPOCH};

use maxminddb::Reader;
use serde::{Deserialize, Serialize};

use crate::GeoLevel;

// ─── Ring buffer limits ───
const MAX_MINUTES: usize = 60;
const MAX_HOURS: usize = 24;
const MAX_DAYS: usize = 30;

// ─── Public stats handle ───

pub struct Stats {
    pub tcp_total: AtomicU64,
    pub relays_total: AtomicU64,
    pub relays_active: AtomicU64,
    pub relays_completed: AtomicU64,
    pub relays_failed: AtomicU64,
    pub bytes_up: AtomicU64,
    pub bytes_down: AtomicU64,
    pub http_requests: AtomicU64,
    pub challenges_failed: AtomicU64,
    pub started: Instant,
    geo: Mutex<GeoTracker>,
    history: Mutex<History>,
    stats_file: Option<PathBuf>,
}

// ─── History ───

#[derive(Serialize, Deserialize, Clone, Default)]
struct History {
    minutes: VecDeque<HistoryPoint>,
    hours: VecDeque<HistoryPoint>,
    days: VecDeque<HistoryPoint>,
    months: Vec<HistoryPoint>,
    #[serde(default)]
    last_hour_ts: u64,
    #[serde(default)]
    last_day_ts: u64,
    #[serde(default)]
    last_month: u32, // year*12 + month (0-indexed)
}

#[derive(Serialize, Deserialize, Clone)]
pub struct HistoryPoint {
    pub ts: u64,
    pub tcp_total: u64,
    pub relays_total: u64,
    pub completed: u64,
    pub failed: u64,
    pub bytes_up: u64,
    pub bytes_down: u64,
}

/// On-disk format
#[derive(Serialize, Deserialize, Default)]
struct Persisted {
    counters: PersistedCounters,
    history: History,
}

#[derive(Serialize, Deserialize, Default, Clone)]
struct PersistedCounters {
    tcp_total: u64,
    relays_total: u64,
    completed: u64,
    failed: u64,
    bytes_up: u64,
    bytes_down: u64,
    http_requests: u64,
    challenges_failed: u64,
}

// ─── Geo tracking (unchanged) ───

struct GeoTracker {
    reader: Option<Reader<Vec<u8>>>,
    level: GeoLevel,
    sources: HashMap<String, GeoEntry>,
    destinations: HashMap<String, GeoEntry>,
}

struct GeoEntry {
    count: u64,
    lat: f64,
    lon: f64,
}

// ─── JSON snapshot types ───

#[derive(Serialize)]
pub struct Snapshot {
    pub uptime_secs: u64,
    pub connections: ConnectionCounts,
    pub bytes: ByteCounts,
    pub http_requests: u64,
    pub challenges_failed: u64,
    pub geo: GeoSnapshot,
    pub history: HistorySnapshot,
}

#[derive(Serialize)]
pub struct ConnectionCounts {
    pub tcp_total: u64,
    pub relays_total: u64,
    pub active: u64,
    pub completed: u64,
    pub failed: u64,
}

#[derive(Serialize)]
pub struct ByteCounts {
    pub up: u64,
    pub down: u64,
}

#[derive(Serialize)]
pub struct GeoSnapshot {
    pub sources: Vec<GeoLocation>,
    pub destinations: Vec<GeoLocation>,
}

#[derive(Serialize)]
pub struct GeoLocation {
    pub label: String,
    pub lat: f64,
    pub lon: f64,
    pub count: u64,
}

#[derive(Serialize)]
pub struct HistorySnapshot {
    pub minutes: Vec<HistoryPoint>,
    pub hours: Vec<HistoryPoint>,
    pub days: Vec<HistoryPoint>,
    pub months: Vec<HistoryPoint>,
}

// ─── Implementation ───

impl Stats {
    pub fn new(
        geoip_db: Option<&str>,
        geo_level: GeoLevel,
        stats_file: Option<&str>,
    ) -> anyhow::Result<Self> {
        let reader = match geoip_db {
            Some(path) => {
                let r = Reader::open_readfile(path)?;
                eprintln!("layover: loaded GeoIP database from {path}");
                Some(r)
            }
            None => None,
        };

        let stats_path = stats_file.map(PathBuf::from);
        let persisted = stats_path
            .as_ref()
            .and_then(|p| load_persisted(p))
            .unwrap_or_default();

        let c = &persisted.counters;
        if stats_path.is_some() && (c.tcp_total > 0 || c.relays_total > 0) {
            eprintln!(
                "layover: restored stats ({} tcp, {} relays, {} up, {} down)",
                c.tcp_total, c.relays_total, c.bytes_up, c.bytes_down,
            );
        }

        Ok(Self {
            tcp_total: AtomicU64::new(c.tcp_total),
            relays_total: AtomicU64::new(c.relays_total),
            relays_active: AtomicU64::new(0),
            relays_completed: AtomicU64::new(c.completed),
            relays_failed: AtomicU64::new(c.failed),
            bytes_up: AtomicU64::new(c.bytes_up),
            bytes_down: AtomicU64::new(c.bytes_down),
            http_requests: AtomicU64::new(c.http_requests),
            challenges_failed: AtomicU64::new(c.challenges_failed),
            started: Instant::now(),
            geo: Mutex::new(GeoTracker {
                reader,
                level: geo_level,
                sources: HashMap::new(),
                destinations: HashMap::new(),
            }),
            history: Mutex::new(persisted.history),
            stats_file: stats_path,
        })
    }

    pub fn record_source_ip(&self, ip: IpAddr) {
        let mut geo = self.geo.lock().unwrap();
        geo.record_ip(ip, true);
    }

    pub fn record_dest_ip(&self, ip: IpAddr) {
        let mut geo = self.geo.lock().unwrap();
        geo.record_ip(ip, false);
    }

    /// Called every 60s by the background task. Records history and persists.
    pub fn tick(&self) {
        let now = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs();

        let point = HistoryPoint {
            ts: now,
            tcp_total: self.tcp_total.load(Ordering::Relaxed),
            relays_total: self.relays_total.load(Ordering::Relaxed),
            completed: self.relays_completed.load(Ordering::Relaxed),
            failed: self.relays_failed.load(Ordering::Relaxed),
            bytes_up: self.bytes_up.load(Ordering::Relaxed),
            bytes_down: self.bytes_down.load(Ordering::Relaxed),
        };

        let mut hist = self.history.lock().unwrap();

        // Minutes: always push
        hist.minutes.push_back(point.clone());
        if hist.minutes.len() > MAX_MINUTES {
            hist.minutes.pop_front();
        }

        // Hours: push if we crossed an hour boundary
        let current_hour = now / 3600;
        if current_hour != hist.last_hour_ts {
            hist.hours.push_back(point.clone());
            if hist.hours.len() > MAX_HOURS {
                hist.hours.pop_front();
            }
            hist.last_hour_ts = current_hour;
        }

        // Days: push if we crossed a day boundary
        let current_day = now / 86400;
        if current_day != hist.last_day_ts {
            hist.days.push_back(point.clone());
            if hist.days.len() > MAX_DAYS {
                hist.days.pop_front();
            }
            hist.last_day_ts = current_day;
        }

        // Months: push if we crossed a month boundary
        let current_month = {
            let secs = now;
            // Approximate: good enough for month boundaries
            let days_since_epoch = secs / 86400;
            let years = days_since_epoch / 365;
            let month_approx = (days_since_epoch % 365) / 30;
            (years * 12 + month_approx) as u32
        };
        if current_month != hist.last_month {
            hist.months.push(point);
            hist.last_month = current_month;
        }

        // Persist to disk
        if let Some(path) = &self.stats_file {
            let persisted = Persisted {
                counters: PersistedCounters {
                    tcp_total: self.tcp_total.load(Ordering::Relaxed),
                    relays_total: self.relays_total.load(Ordering::Relaxed),
                    completed: self.relays_completed.load(Ordering::Relaxed),
                    failed: self.relays_failed.load(Ordering::Relaxed),
                    bytes_up: self.bytes_up.load(Ordering::Relaxed),
                    bytes_down: self.bytes_down.load(Ordering::Relaxed),
                    http_requests: self.http_requests.load(Ordering::Relaxed),
                    challenges_failed: self.challenges_failed.load(Ordering::Relaxed),
                },
                history: hist.clone(),
            };
            drop(hist); // release lock before I/O
            save_persisted(path, &persisted);
        }
    }

    pub fn snapshot(&self) -> Snapshot {
        let geo = self.geo.lock().unwrap();
        let hist = self.history.lock().unwrap();
        Snapshot {
            uptime_secs: self.started.elapsed().as_secs(),
            connections: ConnectionCounts {
                tcp_total: self.tcp_total.load(Ordering::Relaxed),
                relays_total: self.relays_total.load(Ordering::Relaxed),
                active: self.relays_active.load(Ordering::Relaxed),
                completed: self.relays_completed.load(Ordering::Relaxed),
                failed: self.relays_failed.load(Ordering::Relaxed),
            },
            bytes: ByteCounts {
                up: self.bytes_up.load(Ordering::Relaxed),
                down: self.bytes_down.load(Ordering::Relaxed),
            },
            http_requests: self.http_requests.load(Ordering::Relaxed),
            challenges_failed: self.challenges_failed.load(Ordering::Relaxed),
            geo: GeoSnapshot {
                sources: entries_to_vec(&geo.sources),
                destinations: entries_to_vec(&geo.destinations),
            },
            history: HistorySnapshot {
                minutes: hist.minutes.iter().cloned().collect(),
                hours: hist.hours.iter().cloned().collect(),
                days: hist.days.iter().cloned().collect(),
                months: hist.months.clone(),
            },
        }
    }
}

fn load_persisted(path: &Path) -> Option<Persisted> {
    let data = std::fs::read_to_string(path).ok()?;
    match serde_json::from_str(&data) {
        Ok(p) => Some(p),
        Err(e) => {
            eprintln!("layover: warning: failed to parse {}: {e}", path.display());
            None
        }
    }
}

fn save_persisted(path: &Path, data: &Persisted) {
    let json = match serde_json::to_string(data) {
        Ok(j) => j,
        Err(e) => {
            eprintln!("layover: warning: failed to serialize stats: {e}");
            return;
        }
    };
    // Write atomically via temp file
    let tmp = path.with_extension("tmp");
    if let Err(e) = std::fs::write(&tmp, &json) {
        eprintln!("layover: warning: failed to write {}: {e}", tmp.display());
        return;
    }
    if let Err(e) = std::fs::rename(&tmp, path) {
        eprintln!("layover: warning: failed to rename {} -> {}: {e}", tmp.display(), path.display());
    }
}

fn entries_to_vec(map: &HashMap<String, GeoEntry>) -> Vec<GeoLocation> {
    let mut v: Vec<_> = map
        .iter()
        .map(|(label, e)| GeoLocation {
            label: label.clone(),
            lat: e.lat,
            lon: e.lon,
            count: e.count,
        })
        .collect();
    v.sort_by(|a, b| b.count.cmp(&a.count));
    v
}

impl GeoTracker {
    fn record_ip(&mut self, ip: IpAddr, is_source: bool) {
        let reader = match &self.reader {
            Some(r) => r,
            None => return,
        };

        let lookup: Result<maxminddb::geoip2::City, _> = reader.lookup(ip);
        let city_data = match lookup {
            Ok(data) => data,
            Err(_) => return,
        };

        let country_code = city_data
            .country
            .as_ref()
            .and_then(|c| c.iso_code)
            .unwrap_or("??");

        let location = city_data.location.as_ref();
        let lat = location.and_then(|l| l.latitude).unwrap_or(0.0);
        let lon = location.and_then(|l| l.longitude).unwrap_or(0.0);

        let label = match self.level {
            GeoLevel::Country => country_code.to_string(),
            GeoLevel::City => {
                let city_name = city_data
                    .city
                    .as_ref()
                    .and_then(|c| c.names.as_ref())
                    .and_then(|n| n.get("en"))
                    .copied();
                match city_name {
                    Some(city) => format!("{city}, {country_code}"),
                    None => country_code.to_string(),
                }
            }
        };

        let map = if is_source {
            &mut self.sources
        } else {
            &mut self.destinations
        };

        let entry = map.entry(label).or_insert(GeoEntry { count: 0, lat, lon });
        entry.count += 1;
    }
}
