// SPDX-License-Identifier: GPL-3.0-or-later
// Temperlux — brightness controller for external and internal displays
// Based on Luminance by sidevesh (https://github.com/sidevesh/luminance)

use anyhow::Result;
use clap::Parser;

use temperlux::cli::{Cli, ScheduleCommand, SyncModeArg};
use temperlux::display::backlight::RealBacklightProvider;
use temperlux::display::ddc_ffi::DdcService;
use temperlux::display::manager::DisplayManager;
use temperlux::i18n::{tr, tr_args};
use temperlux::schedule;
use temperlux::schedule::model::ScheduleTrigger;
use temperlux::settings::Settings;

fn main() -> Result<()> {
    env_logger::init();
    temperlux::i18n::init();
    let cli = Cli::parse();
    let settings = Settings::load().unwrap_or_default();

    // Schedule commands don't need display detection
    if let Cli::Schedule { command } = cli {
        return handle_schedule(command, &settings);
    }

    // Settings-only commands — no display detection needed
    match &cli {
        Cli::Sync { mode } => {
            let mut s = Settings::load()?;
            s.sync_mode = match mode {
                SyncModeArg::Off => temperlux::settings::SyncMode::Off,
                SyncModeArg::Same => temperlux::settings::SyncMode::SameValue,
                SyncModeArg::Proportional => temperlux::settings::SyncMode::Proportional,
            };
            s.proportional_sync = matches!(mode, SyncModeArg::Proportional);
            s.save()?;
            println!("{}", tr_args("msg.sync_set", &[("mode", &format!("{:?}", s.sync_mode))]));
            return Ok(());
        }
        Cli::Autostart { enabled } => {
            let mut s = Settings::load()?;
            s.autostart = *enabled;
            s.save()?;
            if *enabled {
                temperlux::autostart::enable()?;
                println!("{}", tr("msg.autostart_enabled"));
            } else {
                temperlux::autostart::disable()?;
                println!("{}", tr("msg.autostart_disabled"));
            }
            return Ok(());
        }
        Cli::Config { show_numbers, use_24h, show_range_labels } => {
            let mut s = Settings::load()?;
            let mut changed = false;
            if let Some(v) = show_numbers {
                s.show_display_numbers = *v;
                changed = true;
                println!("{}", tr_args("msg.show_numbers", &[("val", &v.to_string())]));
            }
            if let Some(v) = use_24h {
                s.use_24h_time = *v;
                changed = true;
                println!("{}", tr_args("msg.use_24h", &[("val", &v.to_string())]));
            }
            if let Some(v) = show_range_labels {
                s.show_range_labels = *v;
                changed = true;
                println!("{}", tr_args("msg.show_range", &[("val", &v.to_string())]));
            }
            if changed {
                s.save()?;
            } else {
                println!("{}", tr_args("msg.current_settings", &[
                    ("sn", &s.show_display_numbers.to_string()),
                    ("t24", &s.use_24h_time.to_string()),
                    ("sr", &s.show_range_labels.to_string()),
                ]));
            }
            return Ok(());
        }
        _ => {}
    }

    // Use DdcService for detection (same path as GUI — reliable handle management)
    let (ddc_service, ffi_displays) = match DdcService::start() {
        Ok((svc, info)) => (Some(svc), info),
        Err(e) => {
            log::warn!("DdcService start failed: {}, continuing without DDC", e);
            (None, Vec::new())
        }
    };

    let backlight = RealBacklightProvider;
    let mut mgr = DisplayManager::detect_with_ffi(&ffi_displays, &backlight, &settings)?;

    let result = match cli {
        Cli::List { json } => mgr.list(json),
        Cli::Get { display } => mgr.get(display),
        Cli::Set { display, percent } => {
            mgr.set_with_service(display, percent, ddc_service.as_ref())?;
            apply_sync(display, percent, &settings, &mut mgr, ddc_service.as_ref())?;
            Ok(())
        }
        Cli::Increase { display, percent } => {
            mgr.increase_with_service(display, percent, ddc_service.as_ref())?;
            if let Some(idx) = display {
                let current = mgr.brightness_at(idx).unwrap_or(percent);
                apply_sync(display, current, &settings, &mut mgr, ddc_service.as_ref())?;
            }
            Ok(())
        }
        Cli::Decrease { display, percent } => {
            mgr.decrease_with_service(display, percent, ddc_service.as_ref())?;
            if let Some(idx) = display {
                let current = mgr.brightness_at(idx).unwrap_or(percent);
                apply_sync(display, current, &settings, &mut mgr, ddc_service.as_ref())?;
            }
            Ok(())
        }
        Cli::Rename { display, name } => mgr.rename(display, name),
        Cli::Caps { display, min, max } => {
            let key = mgr.key_at(display)
                .ok_or_else(|| anyhow::anyhow!("{}", tr_args("msg.error_display_not_found", &[("num", &display.to_string())])))?;
            let mut s = Settings::load()?;
            let mut ds = s.get_display(&key);
            if let Some(min_val) = min {
                if !(0.0..=100.0).contains(&min_val) {
                    anyhow::bail!("{}", tr_args("msg.error_min_range", &[("val", &min_val.to_string())]));
                }
                ds.min_brightness = min_val;
            }
            if let Some(max_val) = max {
                if !(0.0..=100.0).contains(&max_val) {
                    anyhow::bail!("{}", tr_args("msg.error_max_range", &[("val", &max_val.to_string())]));
                }
                ds.max_brightness = max_val;
            }
            if ds.min_brightness > ds.max_brightness {
                anyhow::bail!("{}", tr_args("msg.error_min_exceeds_max", &[
                    ("min", &ds.min_brightness.to_string()),
                    ("max", &ds.max_brightness.to_string()),
                ]));
            }
            s.set_display(&key, ds.clone());
            s.save()?;
            println!("{}", tr_args("msg.caps_set", &[
                ("num", &display.to_string()),
                ("min", &format!("{:.0}", ds.min_brightness)),
                ("max", &format!("{:.0}", ds.max_brightness)),
            ]));
            Ok(())
        }
        Cli::Order { display, up, down } => {
            if up == down {
                anyhow::bail!("{}", tr("msg.error_specify_direction"));
            }
            let mut s = Settings::load()?;
            let key = mgr.key_at(display)
                .ok_or_else(|| anyhow::anyhow!("{}", tr_args("msg.error_display_not_found", &[("num", &display.to_string())])))?;

            if s.display_order.is_empty() {
                s.display_order = mgr.all_keys();
            }

            let pos = s.display_order.iter().position(|k| k == &key)
                .ok_or_else(|| anyhow::anyhow!("{}", tr_args("msg.error_display_not_found", &[("num", &display.to_string())])))?;
            let swap_with = if up {
                pos.checked_sub(1)
            } else {
                (pos + 1 < s.display_order.len()).then_some(pos + 1)
            };
            let target = swap_with
                .ok_or_else(|| anyhow::anyhow!("{}", tr_args("msg.error_display_not_found", &[("num", &display.to_string())])))?;
            s.display_order.swap(pos, target);
            s.save()?;
            println!("{}", tr_args(if up { "msg.moved_up" } else { "msg.moved_down" }, &[("num", &display.to_string())]));
            Ok(())
        }
        Cli::Completions { shell } => temperlux::cli::print_completions(shell),
        Cli::Sync { .. } | Cli::Autostart { .. } | Cli::Config { .. } => unreachable!(),
        Cli::Schedule { .. } => unreachable!(),
    };

    // Shut down DdcService cleanly (after all operations complete)
    drop(mgr);
    if let Some(mut svc) = ddc_service {
        svc.shutdown();
    }

    result
}

fn handle_schedule(cmd: ScheduleCommand, settings: &Settings) -> Result<()> {
    match cmd {
        ScheduleCommand::List { json } => {
            let profiles = schedule::list_profiles()?;
            if json {
                println!("{}", serde_json::to_string_pretty(&profiles)?);
            } else if profiles.is_empty() {
                println!("{}", tr("msg.no_profiles"));
            } else {
                for p in &profiles {
                    let status = if p.enabled { "✓" } else { "✗" };
                    println!("{} {} [{}]", status, p.name, p.id);
                    println!("   Triggers: {}", p.trigger_summary());
                    if !p.entries.is_empty() {
                        let entries: Vec<String> = p.entries
                            .iter()
                            .map(|e| {
                                let name = settings.get_custom_name(&e.display_key)
                                    .unwrap_or_else(|| e.display_key.clone());
                                format!("{}={:.0}%", name, e.brightness)
                            })
                            .collect();
                        println!("   Displays: {}", entries.join(", "));
                    }
                    println!();
                }
            }
        }

        ScheduleCommand::Add { name, monitors, time, after_sunrise, after_sunset } => {
            let mut profile = schedule::create_profile(name);

            // Parse monitor entries
            for m in &monitors {
                let entry = parse_monitor_entry(m)?;
                profile.set_entry(entry.0, entry.1);
            }

            // Add triggers
            if let Some(t) = time {
                validate_time(&t)?;
                profile.add_trigger(ScheduleTrigger::Absolute { time: t });
            }
            if let Some(offset) = after_sunrise {
                profile.add_trigger(ScheduleTrigger::Sunrise { offset_minutes: offset });
            }
            if let Some(offset) = after_sunset {
                profile.add_trigger(ScheduleTrigger::Sunset { offset_minutes: offset });
            }

            if profile.triggers.is_empty() {
                anyhow::bail!("{}", tr("msg.error_trigger_required"));
            }

            let id = profile.id.clone();
            schedule::save_profile(profile)?;
            let name = schedule::list_profiles()?
                .iter().find(|p| p.id == id).map(|p| p.name.clone()).unwrap_or_else(|| "?".into());
            println!("{}", tr_args("msg.profile_created", &[("name", &name), ("id", &id)]));
        }

        ScheduleCommand::Edit { id, name, monitors, time, after_sunrise, after_sunset, remove_trigger, enabled } => {
            let mut profile = schedule::get_profile(&id)?
                .ok_or_else(|| anyhow::anyhow!("{}", tr_args("msg.error_profile_not_found", &[("id", &id)])))?;

            if let Some(n) = name {
                profile.name = n;
            }

            if let Some(en) = enabled {
                profile.enabled = en;
            }

            if let Some(monitors) = monitors {
                // Clear existing entries and set new ones
                profile.entries.clear();
                for m in &monitors {
                    let entry = parse_monitor_entry(m)?;
                    profile.set_entry(entry.0, entry.1);
                }
            }

            if let Some(idx) = remove_trigger {
                profile.remove_trigger(idx);
            }

            if let Some(t) = time {
                validate_time(&t)?;
                profile.add_trigger(ScheduleTrigger::Absolute { time: t });
            }
            if let Some(offset) = after_sunrise {
                profile.add_trigger(ScheduleTrigger::Sunrise { offset_minutes: offset });
            }
            if let Some(offset) = after_sunset {
                profile.add_trigger(ScheduleTrigger::Sunset { offset_minutes: offset });
            }

            schedule::save_profile(profile)?;
            println!("{}", tr_args("msg.profile_updated", &[("id", &id)]));
        }

        ScheduleCommand::Delete { id } => {
            if schedule::delete_profile(&id)? {
                println!("{}", tr_args("msg.profile_deleted", &[("id", &id)]));
            } else {
                anyhow::bail!("{}", tr_args("msg.error_profile_not_found", &[("id", &id)]));
            }
        }

        ScheduleCommand::Apply { profile: specific_id } => {
            let ctx = schedule::trigger::TriggerContext::new(settings.latitude, settings.longitude);

            // If --profile given, apply that specific profile directly (no time check)
            let profiles: Vec<_> = if let Some(id) = specific_id {
                match schedule::get_profile(&id) {
                    Ok(Some(p)) => vec![p],
                    Ok(None) => anyhow::bail!("{}", tr_args("msg.error_profile_not_found", &[("id", &id)])),
                    Err(e) => anyhow::bail!("Error loading profile: {}", e),
                }
            } else {
                schedule::check_due_profiles(&ctx)?
            };

            if profiles.is_empty() {
                println!("{}", tr("msg.no_schedules_due"));
                return Ok(());
            }

            // Need display access to apply
            let (ddc_service, ffi_displays) = match DdcService::start() {
                Ok((svc, info)) => (Some(svc), info),
                Err(e) => {
                    anyhow::bail!("DdcService start failed: {}", e);
                }
            };
            let backlight = RealBacklightProvider;
            let mut mgr = DisplayManager::detect_with_ffi(&ffi_displays, &backlight, settings)?;

            for profile in &profiles {
                println!("{}", tr_args("msg.applying", &[("name", &profile.name)]));
                for entry in &profile.entries {
                    if let Some(idx) = mgr.index_by_key(&entry.display_key) {
                        mgr.set_with_service(Some(idx), entry.brightness, ddc_service.as_ref())?;
                        let name = settings.get_custom_name(&entry.display_key)
                            .unwrap_or_else(|| entry.display_key.clone());
                        println!("  {} → {:.0}%", name, entry.brightness);
                    } else {
                        log::warn!("Display '{}' not found, skipping", entry.display_key);
                    }
                }
            }

            drop(mgr);
            if let Some(mut svc) = ddc_service {
                svc.shutdown();
            }
        }

        ScheduleCommand::ShowTimes => {
            let profiles = schedule::list_profiles()?;
            let ctx = schedule::trigger::TriggerContext::new(settings.latitude, settings.longitude);
            let now = chrono::Local::now();

            if profiles.is_empty() {
                println!("{}", tr("msg.no_profiles_short"));
                return Ok(());
            }

            println!("{}\n", tr_args("msg.current_time", &[("time", &now.format("%H:%M:%S").to_string())]));
            for p in &profiles {
                let status = if p.enabled { "enabled" } else { "disabled" };
                println!("{} [{}] ({})", p.name, p.id, status);
                for t in &p.triggers {
                    match schedule::trigger::resolve_trigger_time(t, &ctx, now.date_naive()) {
                        Some(resolved) => {
                            println!("  {} → resolves to {}", t.description(), resolved.format("%H:%M"));
                        }
                        None => {
                            println!("  {} → cannot resolve (polar region?)", t.description());
                        }
                    }
                }
                println!();
            }
        }

        ScheduleCommand::GenerateTimers => {
            let ctx = schedule::trigger::TriggerContext::new(settings.latitude, settings.longitude);
            let generated = schedule::systemd::generate_timers(&ctx)?;
            if generated.is_empty() {
                println!("{}", tr("msg.no_timers"));
            } else {
                println!("{}", tr_args("msg.generated_timers", &[("count", &generated.len().to_string())]));
                for g in &generated {
                    println!("  {}", g);
                }
                println!("\nRun to activate:");
                println!("  systemctl --user daemon-reload");
                println!("  systemctl --user enable --now temperlux-schedule-*.timer.target");
            }
        }
        ScheduleCommand::Location { lat, lon } => {
            let mut s = temperlux::settings::Settings::load()?;
            s.latitude = lat;
            s.longitude = lon;
            s.save()?;
            println!("{}", tr_args("msg.location_set", &[("lat", &lat.to_string()), ("lon", &lon.to_string())]));
        }
        ScheduleCommand::DetectLocation => {
            let output = std::process::Command::new("curl")
                .args(["-s", "--max-time", "5", "https://ipapi.co/json/"])
                .output()
                .map_err(|e| anyhow::anyhow!("curl failed: {}", e))?;

            if !output.status.success() {
                anyhow::bail!("{}", tr("msg.error_detection_failed"));
            }

            let json_str = String::from_utf8_lossy(&output.stdout);
            let json: serde_json::Value = serde_json::from_str(&json_str)
                .map_err(|e| anyhow::anyhow!("Parse error: {}", e))?;

            let lat = json["latitude"].as_f64().ok_or_else(|| anyhow::anyhow!("Missing latitude in response"))?;
            let lon = json["longitude"].as_f64().ok_or_else(|| anyhow::anyhow!("Missing longitude in response"))?;
            let city = json["city"].as_str().unwrap_or("Unknown");

            let mut s = Settings::load()?;
            s.latitude = lat;
            s.longitude = lon;
            s.save()?;
            println!("{}", tr_args("msg.detected", &[("city", city), ("lat", &lat.to_string()), ("lon", &lon.to_string())]));
        }
    }
    Ok(())
}

fn parse_monitor_entry(s: &str) -> Result<(String, f64)> {
    let parts: Vec<&str> = s.split('=').collect();
    if parts.len() != 2 {
        anyhow::bail!("{}", tr_args("msg.error_invalid_monitor", &[("val", s)]));
    }
    let key = parts[0].trim().to_string();
    let brightness: f64 = parts[1].trim().parse()
        .map_err(|_| anyhow::anyhow!("{}", tr_args("msg.error_invalid_brightness", &[("val", parts[1].trim())])))?;
    if !(0.0..=100.0).contains(&brightness) {
        anyhow::bail!("{}", tr_args("msg.error_brightness_range", &[("val", &brightness.to_string())]));
    }
    Ok((key, brightness))
}

fn validate_time(time: &str) -> Result<()> {
    if ScheduleTrigger::parse_absolute_time(time).is_none() {
        anyhow::bail!("{}", tr_args("msg.error_invalid_time", &[("time", time)]));
    }
    Ok(())
}

/// Propagate brightness to other displays based on sync_mode setting.
/// Only applies when a single display was changed (not "all displays").
fn apply_sync(
    changed_display: Option<usize>,
    target_percent: f64,
    settings: &Settings,
    mgr: &mut DisplayManager,
    service: Option<&DdcService>,
) -> Result<()> {
    use temperlux::settings::SyncMode;

    let idx = match changed_display {
        Some(i) => i,
        None => return Ok(()), // "all displays" mode — no sync needed
    };

    match settings.sync_mode {
        SyncMode::Off => {}
        SyncMode::SameValue => {
            // Set all other displays to the same percentage
            let total = mgr.all_keys().len();
            for other in 1..=total {
                if other != idx {
                    mgr.set_with_service(Some(other), target_percent, service)?;
                }
            }
        }
        SyncMode::Proportional => {
            // Map the source position proportionally across each display's min/max range
            let src_key = mgr.key_at(idx)
                .ok_or_else(|| anyhow::anyhow!("Cannot find source display"))?;
            let src_ds = settings.get_display(&src_key);
            let src_min = src_ds.min_brightness;
            let src_max = src_ds.max_brightness;
            let src_range = (src_max - src_min).max(0.01);
            let src_pos = (target_percent - src_min) / src_range; // 0.0 to 1.0

            let total = mgr.all_keys().len();
            for other in 1..=total {
                if other != idx {
                    if let Some(other_key) = mgr.key_at(other) {
                        let ds = settings.get_display(&other_key);
                        let other_val = ds.min_brightness + src_pos * (ds.max_brightness - ds.min_brightness);
                        mgr.set_with_service(Some(other), other_val, service)?;
                    }
                }
            }
        }
    }
    Ok(())
}
