use chrono::{Datelike, Local, Timelike};
use std::collections::HashMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::{Duration, SystemTime};

fn played_breaks_path() -> PathBuf {
    dirs::home_dir().unwrap().join(".gradio/data/tmp/played_breaks")
}

fn is_break_played(hour: u32, minute: u32) -> bool {
    let today = chrono::Local::now().format("%Y%m%d").to_string();
    let key = format!("{} {:02}:{:02}", today, hour, minute);
    let path = played_breaks_path();
    if let Ok(content) = fs::read_to_string(&path) {
        return content.lines().any(|l| l.trim() == key);
    }
    false
}

// Cache de validación (path + mtime → playable). Evita re-decodificar el
// mismo comercial/evento cada minuto.
static VALIDATION_CACHE: OnceLock<Mutex<HashMap<(PathBuf, SystemTime), bool>>> = OnceLock::new();
fn validation_cache() -> &'static Mutex<HashMap<(PathBuf, SystemTime), bool>> {
    VALIDATION_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}

fn is_url(s: &str) -> bool {
    s.starts_with("http://") || s.starts_with("https://")
}

fn is_special_token(s: &str) -> bool {
    s.eq_ignore_ascii_case("hora")
}

fn is_playable_cached(path: &Path) -> bool {
    let path_str = path.to_string_lossy();
    if is_url(&path_str) || is_special_token(&path_str) {
        return true;
    }
    let mtime = match fs::metadata(path).and_then(|m| m.modified()) {
        Ok(t) => t,
        Err(_) => return false,
    };
    let key = (path.to_path_buf(), mtime);
    {
        let cache = validation_cache().lock().unwrap();
        if let Some(&v) = cache.get(&key) { return v; }
    }
    let ok = grpautaje::audio_probe::is_playable(path);
    if !ok {
        eprintln!("[sched] audio inválido, descartado: {}", path.display());
    }
    validation_cache().lock().unwrap().insert(key, ok);
    ok
}

fn main() {
    loop {
        wait_until_second_58();

        let now = Local::now();
        let target = now + chrono::Duration::seconds(2);
        let h = target.hour();
        let m = target.minute();
        let dow = target.weekday().number_from_monday();
        eprintln!("[sched] === {:02}:{:02}:{:02} — procesando pauta {}:{:02} (dow={}) ===",
            now.hour(), now.minute(), now.second(), h, m, dow);

        schedule_comerciales(h, m);
        schedule_eventos_espera(h, m);
        schedule_eventos(h, m);

        thread::sleep(Duration::from_millis(1500));
    }
}

/// Si la ruta es una carpeta (termina en /*, / o es un directorio existente),
/// elige un audio aleatorio de ella. Si es un archivo concreto lo devuelve tal cual.
/// Retorna None si la carpeta está vacía o no existe.
fn resolve_path(raw: &str) -> Option<String> {
    let raw = raw.trim().trim_end_matches('\r');

    if is_url(raw) {
        return Some(raw.to_string());
    }

    // Normalizar: quitar /* y / al final
    let normalized = raw
        .trim_end_matches('/')
        .trim_end_matches('*')
        .trim_end_matches('/');

    let p = Path::new(normalized);

    if p.is_dir() {
        // Elegir audio aleatorio de la carpeta
        let mut files: Vec<PathBuf> = std::fs::read_dir(p)
            .ok()?
            .filter_map(|e| e.ok())
            .map(|e| e.path())
            .filter(|f| f.is_file() && is_audio(f))
            .collect();

        if files.is_empty() {
            eprintln!("[sched] SKIP carpeta vacía: {}", normalized);
            return None;
        }

        // Mezcla simple con índice pseudoaleatorio basado en tiempo
        let idx = (std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .subsec_nanos() as usize)
            % files.len();

        files.sort(); // orden determinista antes de elegir
        // Rotar por el índice para distribuir mejor
        let chosen = &files[idx % files.len()];
        let path_str = chosen.to_string_lossy().to_string();
        eprintln!("[sched] carpeta→random: {} → {}", normalized,
            chosen.file_name().unwrap_or_default().to_string_lossy());
        Some(path_str)
    } else if p.exists() && is_audio(p) {
        Some(normalized.to_string())
    } else if p.exists() {
        // Existe pero no es audio ni carpeta (ej. "Hora", streams)
        Some(normalized.to_string())
    } else {
        // Podría ser "Hora" u otro token especial sin ruta real
        Some(raw.to_string())
    }
}

fn is_audio(p: &Path) -> bool {
    p.extension()
        .and_then(|e| e.to_str())
        .map(|e| matches!(e.to_ascii_lowercase().as_str(),
            "mp3" | "wav" | "ogg" | "flac" | "m4a"))
        .unwrap_or(false)
}

fn wait_until_second_58() {
    loop {
        let now = Local::now();
        let sec = now.second();

        if sec == 58 {
            return;
        }

        let ms_current = sec as i64 * 1000 + now.timestamp_subsec_millis() as i64;
        let ms_target  = 58 * 1000i64;

        let ms_to_wait = if ms_current < ms_target {
            ms_target - ms_current
        } else {
            60 * 1000 - ms_current + ms_target
        };

        thread::sleep(Duration::from_millis(ms_to_wait.max(10) as u64));
    }
}

fn schedule_comerciales(hour: u32, minute: u32) {
    // ── Verificar si esta tanda ya fue reproducida manualmente ──────────────
    if is_break_played(hour, minute) {
        eprintln!("[sched] comerciales {hour}:{minute:02} — YA REPRODUCIDO MANUALMENTE, salteando");
        return;
    }

    let target_time = Local::now() + chrono::Duration::seconds(2);
    let dow = target_time.weekday().number_from_monday();

    let comercial_path = comercial_file(hour, minute);
    if !comercial_path.exists() { return; }

    let out_path = dirs::home_dir().unwrap().join(".gradio/data/tmp/comercialeslist4");
    let mut out = OpenOptions::new().create(true).append(true).open(&out_path).unwrap();
    let file = match File::open(&comercial_path) { Ok(f) => f, Err(_) => return };

    let mut loaded = 0u32;
    let mut skipped_day = 0u32;
    let mut skipped_date = 0u32;

    for line in BufReader::new(file).lines() {
        let line = match line { Ok(l) => l, Err(_) => continue };
        let line = line.trim().to_string();
        if line.is_empty() { continue; }

        let parts: Vec<&str> = line.split('|').collect();
        if parts.len() != 4 {
            eprintln!("[sched] comerciales {hour}:{minute:02} — línea inválida: {line}");
            continue;
        }

        let path      = parts[0].trim();
        let days_mask = parts[1].trim();
        let start_date = parts[2].trim();
        let end_date   = parts[3].trim();

        if !valid_day(dow, days_mask) {
            eprintln!("[sched] SKIP día  — mask={days_mask} hoy=dow{dow} — {path}");
            skipped_day += 1;
            continue;
        }
        if !in_date_range(target_time, start_date, end_date) {
            eprintln!("[sched] SKIP fecha — {start_date}~{end_date} hoy={} — {path}",
                target_time.format("%Y%m%d"));
            skipped_date += 1;
            continue;
        }

        // Resolver carpeta → audio concreto
        match resolve_path(path) {
            Some(resolved) => {
                if !is_playable_cached(Path::new(&resolved)) { continue; }
                writeln!(out, "{}", resolved).unwrap();
                loaded += 1;
            }
            None => {
                eprintln!("[sched] SKIP sin audio — {path}");
            }
        }
    }
    eprintln!("[sched] comerciales {hour}:{minute:02} → cargados={loaded} skip_dia={skipped_day} skip_fecha={skipped_date}");
}

fn comercial_file(hour: u32, minute: u32) -> PathBuf {
    dirs::home_dir().unwrap()
        .join(".gradio/data/comerciales")
        .join(format!("{}", hour))
        .join(format!("{}.com", minute))
}

fn schedule_eventos_espera(hour: u32, minute: u32) {
    let target_time = Local::now() + chrono::Duration::seconds(2);
    let dow = target_time.weekday().number_from_monday();

    let evento_path = evento_espera_file(hour, minute);
    if !evento_path.exists() { return; }

    let out_path = dirs::home_dir().unwrap().join(".gradio/data/tmp/eventos-esperalist");
    let mut out = OpenOptions::new().create(true).append(true).open(out_path).unwrap();

    let mut loaded = 0u32;
    let mut skipped_day = 0u32;
    let mut skipped_date = 0u32;

    for line in BufReader::new(File::open(&evento_path).unwrap()).lines() {
        let line = match line { Ok(l) => l, Err(_) => continue };
        let line = line.trim().to_string();
        if line.is_empty() { continue; }

        let parts: Vec<&str> = line.split('|').collect();
        if parts.len() != 4 { continue; }

        let path      = parts[0].trim();
        let days_mask = parts[1].trim();
        let start_date = parts[2].trim();
        let end_date   = parts[3].trim();

        if !valid_day(dow, days_mask) {
            eprintln!("[sched] SKIP día  ev-espera — mask={days_mask} hoy=dow{dow} — {path}");
            skipped_day += 1;
            continue;
        }
        if !in_date_range(target_time, start_date, end_date) {
            eprintln!("[sched] SKIP fecha ev-espera — {start_date}~{end_date} — {path}");
            skipped_date += 1;
            continue;
        }

        match resolve_path(path) {
            Some(resolved) => {
                if !is_playable_cached(Path::new(&resolved)) { continue; }
                writeln!(out, "{}", resolved).unwrap();
                loaded += 1;
            }
            None => { eprintln!("[sched] SKIP sin audio ev-espera — {path}"); }
        }
    }
    eprintln!("[sched] ev-espera {hour}:{minute:02} → cargados={loaded} skip_dia={skipped_day} skip_fecha={skipped_date}");
}

fn evento_espera_file(hour: u32, minute: u32) -> PathBuf {
    dirs::home_dir().unwrap()
        .join(".gradio/data/eventos-espera")
        .join(format!("{}", hour))
        .join(format!("{}.com", minute))
}

fn schedule_eventos(hour: u32, minute: u32) {
    let target_time = Local::now() + chrono::Duration::seconds(2);
    let dow = target_time.weekday().number_from_monday();

    let evento_path = evento_file(hour, minute);
    if !evento_path.exists() { return; }

    let out_path = dirs::home_dir().unwrap().join(".gradio/data/tmp/eventoslist");
    let mut out = OpenOptions::new().create(true).append(true).open(out_path).unwrap();

    let mut loaded = 0u32;
    let mut skipped_day = 0u32;
    let mut skipped_date = 0u32;

    for line in BufReader::new(File::open(&evento_path).unwrap()).lines() {
        let line = match line { Ok(l) => l, Err(_) => continue };
        let line = line.trim().to_string();
        if line.is_empty() { continue; }

        let parts: Vec<&str> = line.split('|').collect();
        if parts.len() != 4 { continue; }

        let path      = parts[0].trim();
        let days_mask = parts[1].trim();
        let start_date = parts[2].trim();
        let end_date   = parts[3].trim();

        if !valid_day(dow, days_mask) {
            eprintln!("[sched] SKIP día  eventos — mask={days_mask} hoy=dow{dow} — {path}");
            skipped_day += 1;
            continue;
        }
        if !in_date_range(target_time, start_date, end_date) {
            eprintln!("[sched] SKIP fecha eventos — {start_date}~{end_date} — {path}");
            skipped_date += 1;
            continue;
        }

        match resolve_path(path) {
            Some(resolved) => {
                if !is_playable_cached(Path::new(&resolved)) { continue; }
                writeln!(out, "{}", resolved).unwrap();
                loaded += 1;
            }
            None => { eprintln!("[sched] SKIP sin audio eventos — {path}"); }
        }
    }
    eprintln!("[sched] eventos {hour}:{minute:02} → cargados={loaded} skip_dia={skipped_day} skip_fecha={skipped_date}");
}

fn evento_file(hour: u32, minute: u32) -> PathBuf {
    dirs::home_dir().unwrap()
        .join(".gradio/data/eventos")
        .join(format!("{}", hour))
        .join(format!("{}.com", minute))
}

fn valid_day(today: u32, mask: &str) -> bool {
    if mask == "0" || mask.is_empty() { return true; }
    mask.chars().any(|c| c.to_digit(10) == Some(today))
}

fn in_date_range(now: chrono::DateTime<Local>, start: &str, end: &str) -> bool {
    let today = now.date_naive();
    let start_ok = start == "0" || start.is_empty() ||
        chrono::NaiveDate::parse_from_str(start, "%Y%m%d")
            .map(|d| today >= d).unwrap_or(true);
    let end_ok = end == "0" || end.is_empty() ||
        chrono::NaiveDate::parse_from_str(end, "%Y%m%d")
            .map(|d| today <= d).unwrap_or(true);
    start_ok && end_ok
}
