use std::env;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, UNIX_EPOCH};

use hinoirisetr::notify::InitializedNotificationSystem;
use hinoirisetr::{
    Config, ConfigError, GammaBackend, TempBackend, apply_gamma, apply_temp, compute_settings,
    debug, error, info, reset_cache, trace, warn,
};
use time::OffsetDateTime;
use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::net::UnixListener;
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::{Notify, RwLock};
use tokio::time::sleep;

fn get_socket_path() -> PathBuf {
    let username = env::var("USER").expect("USER environment variable not set");

    let mut socket_path = PathBuf::from("/tmp");
    socket_path.push(format!("hinoirisetr-{}.sock", username));
    socket_path
}

static CONFIG: OnceLock<Arc<RwLock<Config>>> = OnceLock::new();
static LAST_MODIFIED: AtomicU64 = AtomicU64::new(0);

enum NotifyState {
    Enabled(InitializedNotificationSystem),
    Disabled,
}

async fn write_response<W: AsyncWrite + Unpin>(
    writer: &mut W,
    response: &str,
) -> Result<(), Box<dyn std::error::Error>> {
    writer.write_all(response.as_bytes()).await?;
    writer.shutdown().await?;
    Ok(())
}

fn get_status_string(
    now: OffsetDateTime,
    config: &Config,
    disabled_temp: &AtomicBool,
    disabled_gamma: &AtomicBool,
) -> String {
    let (cur_temp, cur_gamma) = compute_settings(now, config);
    format!(
        "dimming - {}{}{}",
        if config.temp_backend == TempBackend::None {
            "".to_string()
        } else {
            format!(
                "temp: {}",
                if disabled_temp.load(Ordering::SeqCst) {
                    "disabled".to_string()
                } else {
                    format!("{cur_temp}K")
                }
            )
        },
        if config.temp_backend != TempBackend::None && config.gamma_backend != GammaBackend::None {
            ", "
        } else {
            ""
        },
        if config.gamma_backend == GammaBackend::None {
            "".to_string()
        } else {
            format!(
                "gamma: {}",
                if disabled_gamma.load(Ordering::SeqCst) {
                    "disabled".to_string()
                } else {
                    format!("{cur_gamma}%")
                }
            )
        }
    )
}

async fn config_reloader(notify: Arc<Notify>) {
    debug!("config reloader started");
    // Config polling every 5 seconds
    loop {
        trace!("config poll tick");
        let config_path = get_config_path();
        if config_path.exists() {
            if let Ok(current_modified) = std::fs::metadata(&config_path)
                .and_then(|m| m.modified())
                .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs())
            {
                let last: u64 = LAST_MODIFIED.load(Ordering::SeqCst);
                if 0 != last {
                    if current_modified > last {
                        trace!("{current_modified}");
                        trace!("{last}");
                        debug!("Config file modified, reloading...");
                        let _ = reload_config(Arc::clone(&notify)).await;
                    }
                } else {
                    // File is new, reload to capture initial state
                    debug!("Config file detected, reloading...");
                    let _ = reload_config(Arc::clone(&notify)).await;
                }
            }
        }
        sleep(Duration::from_secs(5)).await;
    }
}

#[allow(clippy::too_many_arguments)]
async fn handle_flag_update<F, G, W>(
    disabled_temp: &AtomicBool,
    disabled_gamma: &AtomicBool,
    set_temp: F,
    set_gamma: G,
    status: &str,
    #[allow(unused_variables)]
    trace_msg: &str,
    notify: &Notify,
    writer: &mut W,
) where
    F: FnOnce(&AtomicBool),
    G: FnOnce(&AtomicBool),
    W: AsyncWrite + Unpin,
{
    trace!("{}", trace_msg);
    set_temp(disabled_temp);
    set_gamma(disabled_gamma);
    notify.notify_one();
    info!("{}", status);
    if let Err(e) = write_response(writer, status).await {
        error!("Failed to respond over socket: {e}");
    }
}

macro_rules! toggle_flag {
    ($flag:expr, $notify:expr, $writer:expr, $name:expr, $trace_msg:expr) => {{
        use std::sync::atomic::Ordering::SeqCst;
        trace!("{}", $trace_msg);
        let old = $flag.load(SeqCst);
        let new_val = !old;
        $flag.store(new_val, SeqCst);
        $notify.notify_one();
        let status = format!(
            "{} dimming is {}",
            $name,
            if new_val { "disabled" } else { "enabled" }
        );
        info!("{}", status);
        if let Err(e) = write_response(&mut $writer, &status).await {
            error!("Failed to respond over socket: {e}");
        }
    }};
}

async fn socket_server(
    disabled_temp: Arc<AtomicBool>,
    disabled_gamma: Arc<AtomicBool>,
    notify: Arc<Notify>,
) -> ! {
    let listener = UnixListener::bind(get_socket_path()).expect("Failed to bind socket");

    match std::fs::set_permissions(get_socket_path(), std::fs::Permissions::from_mode(0o600)) {
        Ok(_) => {
            trace!("socket file permissions set");
        }
        Err(e) => {
            error!("Failed to set socket file permissions: {e}");
        }
    };

    trace!("socket server bound");
    let notification: NotifyState =
        match hinoirisetr::notify::InitializedNotificationSystem::new("hinoirisetr") {
            Ok(not) => NotifyState::Enabled(not),
            Err(_) => {
                info!("libnotify not found, disabling 'status_notify' command");
                NotifyState::Disabled
            }
        };

    loop {
        let (mut stream, _) = listener.accept().await.unwrap();
        let (reader, mut writer) = stream.split();
        let mut lines = BufReader::new(reader).lines();

        trace!("socket server accepted connection");

        if let Ok(Some(line)) = lines.next_line().await {
            match line.trim() {
                "disable_temp" => {
                    handle_flag_update(
                        &disabled_temp,
                        &disabled_gamma,
                        |flag| flag.store(true, Ordering::SeqCst),
                        |_| {},
                        "temp dimming is disabled",
                        "disable_temp dispatched",
                        &notify,
                        &mut writer,
                    )
                    .await;
                }
                "disable_gamma" => {
                    handle_flag_update(
                        &disabled_temp,
                        &disabled_gamma,
                        |_| {},
                        |flag| flag.store(true, Ordering::SeqCst),
                        "gamma dimming is disabled",
                        "disable_gamma dispatched",
                        &notify,
                        &mut writer,
                    )
                    .await;
                }
                "disable" => {
                    handle_flag_update(
                        &disabled_temp,
                        &disabled_gamma,
                        |flag| flag.store(true, Ordering::SeqCst),
                        |flag| flag.store(true, Ordering::SeqCst),
                        "dimming is disabled",
                        "disable dispatched",
                        &notify,
                        &mut writer,
                    )
                    .await;
                }
                "enable_temp" => {
                    handle_flag_update(
                        &disabled_temp,
                        &disabled_gamma,
                        |flag| flag.store(false, Ordering::SeqCst),
                        |_| {},
                        "temp dimming is enabled",
                        "enable_temp dispatched",
                        &notify,
                        &mut writer,
                    )
                    .await;
                }
                "enable_gamma" => {
                    handle_flag_update(
                        &disabled_temp,
                        &disabled_gamma,
                        |_| {},
                        |flag| flag.store(false, Ordering::SeqCst),
                        "gamma dimming is enabled",
                        "enable_gamma dispatched",
                        &notify,
                        &mut writer,
                    )
                    .await;
                }
                "enable" => {
                    handle_flag_update(
                        &disabled_temp,
                        &disabled_gamma,
                        |flag| flag.store(false, Ordering::SeqCst),
                        |flag| flag.store(false, Ordering::SeqCst),
                        "dimming is enabled",
                        "enable dispatched",
                        &notify,
                        &mut writer,
                    )
                    .await;
                }
                "toggle_temp" => {
                    toggle_flag!(
                        disabled_temp,
                        notify,
                        writer,
                        "temp",
                        "toggle_temp dispatched"
                    );
                }
                "toggle_gamma" => {
                    toggle_flag!(
                        disabled_gamma,
                        notify,
                        writer,
                        "gamma",
                        "toggle_gamma dispatched"
                    );
                }
                "toggle" => {
                    let old = disabled_temp.load(Ordering::SeqCst);
                    let new_val = !old;
                    disabled_temp.store(new_val, Ordering::SeqCst);
                    disabled_gamma.store(new_val, Ordering::SeqCst);
                    notify.notify_one();
                    let status = format!(
                        "dimming is now {}",
                        if new_val { "disabled" } else { "enabled" }
                    );
                    info!("{}", status);
                    if let Err(e) = write_response(&mut writer, &status).await {
                        error!("Failed to respond over socket: {e}");
                    }
                }
                "status" => {
                    trace!("status dispatched");
                    let now = get_time();
                    let config = get_config().await;
                    let status = get_status_string(now, &config, &disabled_temp, &disabled_gamma);
                    info!("{}", status);
                    if let Err(e) = write_response(&mut writer, &status).await {
                        error!("Failed to respond over socket: {e}");
                    }
                }
                "reload" => {
                    trace!("reload dispatched");

                    let result = reload_config(notify.clone()).await;
                    let status = match result {
                        Ok(_) => "reloaded config".into(),
                        Err(e) => format!("failed to reload config: {e:?}"),
                    };

                    if let Err(e) = write_response(&mut writer, &status).await {
                        error!("Failed to respond over socket: {e}");
                    }

                    trace!("reload completed");
                }
                "status_notify" => {
                    trace!("status_notify dispatched");

                    let now = get_time();
                    let config = get_config().await;
                    let status = get_status_string(now, &config, &disabled_temp, &disabled_gamma);

                    let command_status = match notification {
                        NotifyState::Enabled(ref not) => {
                            let timeout = config.notification_timeout;
                            match not.show_notification(
                                "Sunsetting",
                                &status,
                                "notification-icon",
                                timeout as i32,
                            ) {
                                Ok(_) => "notify notification sent".into(),
                                Err(e) => {
                                    error!("Failed to show notification: {e:?}");
                                    format!("Failed to show notification: {e:?}")
                                }
                            }
                        }
                        NotifyState::Disabled => "notify notification disabled".into(),
                    };

                    if let Err(e) = write_response(&mut writer, &command_status).await {
                        error!("Failed to respond over socket: {e}");
                    }
                }
                _ => error!("unknown command: {}", line.trim()),
            }
        }
    }
}

async fn send_command(command: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut stream = tokio::net::UnixStream::connect(get_socket_path()).await?;
    let (reader, mut writer) = stream.split();
    let mut lines = BufReader::new(reader).lines();

    writer.write_all(command.as_bytes()).await?;
    writer.shutdown().await?;

    println!("{}", lines.next_line().await?.unwrap_or("".to_string()));

    Ok(())
}

#[tokio::main]
async fn main() {
    let args: Vec<String> = std::env::args().collect();

    match env::var("RUST_LOG") {
        Ok(val) => match val.parse::<hinoirisetr::log::LogLevel>() {
            Ok(level) => hinoirisetr::log::set_log_level(level),
            Err(err) => error!("Failed to parse RUST_LOG: {err}"),
        },
        Err(_) => {
            if cfg!(debug_assertions) {
                hinoirisetr::log::set_log_level(hinoirisetr::log::LogLevel::Debug);
            } else {
                hinoirisetr::log::set_log_level(hinoirisetr::log::LogLevel::Info);
            }
        }
    }

    if args.len() >= 2 {
        match args[1].as_str() {
            "--version" | "-v" => {
                println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
            }
            cmd => {
                let error_prefix = match cmd {
                    "reload" => Some("Failed to reload config: "),
                    "status_notify" => Some("Failed to send notification: "),
                    "status" | "toggle" | "toggle_gamma" | "toggle_temp" | "enable"
                    | "enable_gamma" | "enable_temp" | "disable" | "disable_gamma"
                    | "disable_temp" => Some("Failed to send status: "),
                    _ => None,
                };

                if let Some(prefix) = error_prefix {
                    if let Err(e) = send_command(cmd).await {
                        eprintln!("{}{}", prefix, e);
                    }
                } else {
                    eprintln!("Unknown command: {}", cmd);
                }
            }
        }
        return;
    }

    info!("starting the daemon");

    let disabled_temp = Arc::new(AtomicBool::new(false));
    let disabled_gamma = Arc::new(AtomicBool::new(false));
    let notify = Arc::new(Notify::new());

    // load config
    let config_path = get_config_path();
    let cfg: Config = if config_path.exists() {
        debug!("Config file found, loading...");
        LAST_MODIFIED.store(
            std::fs::metadata(&config_path)
                .and_then(|m| m.modified())
                .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs())
                .unwrap_or(0),
            SeqCst,
        );
        match Config::load(&config_path) {
            Ok(cfg) => cfg,
            Err(err) => {
                error!("Failed to load config: {err:?}");
                warn!("Using default config.");
                Config::default()
            }
        }
    } else {
        warn!("Config file not found, using default config.");
        warn!("Config path {}", get_config_path().display());
        Config::default()
    };

    fn check_binary(binary: &str) {
        if !is_binary_available(binary) {
            error!("{binary} is not available, exiting.");
            std::process::exit(1);
        }
    }

    match cfg.gamma_backend {
        GammaBackend::Hyprctl => check_binary("hyprctl"),
        GammaBackend::Ddcutil => check_binary("ddcutil"),
        GammaBackend::Xsct => check_binary("xsct"),
        GammaBackend::Redshift => check_binary("redshift"),
        GammaBackend::Gammastep => check_binary("gammastep"),
        GammaBackend::None => {}
    }

    match cfg.temp_backend {
        TempBackend::Hyprctl => check_binary("hyprctl"),
        TempBackend::Gammastep => check_binary("gammastep"),
        TempBackend::Xsct => check_binary("xsct"),
        TempBackend::Redshift => check_binary("redshift"),
        TempBackend::None => {}
    }

    if cfg.temp_backend == TempBackend::None && cfg.gamma_backend == GammaBackend::None {
        error!("No backends selected, exiting.");
        std::process::exit(1);
    }

    CONFIG.set(Arc::new(RwLock::new(cfg))).unwrap();

    if std::path::Path::new(&get_socket_path()).exists() {
        match std::os::unix::net::UnixStream::connect(get_socket_path()) {
            Ok(_) => {
                error!("Another instance is running.");
                std::process::exit(1);
            }
            Err(_) => {
                warn!("Stale socket found, removing.");
                let _ = std::fs::remove_file(get_socket_path());
            }
        }
    }

    // Spawn control socket server
    {
        let disabled_temp = Arc::clone(&disabled_temp);
        let disabled_gamma = Arc::clone(&disabled_gamma);
        let notify = Arc::clone(&notify);
        tokio::spawn(async move {
            socket_server(disabled_temp, disabled_gamma, notify).await;
        });
    }

    // Spawn config reloader
    {
        let notify = Arc::clone(&notify);
        tokio::spawn(async move {
            config_reloader(notify).await;
        });
    }

    // Spawn timer
    {
        let notify = Arc::clone(&notify);
        tokio::spawn(async move {
            loop {
                sleep(Duration::from_secs(300)).await;
                notify.notify_one();
            }
        });
    }

    // Signal handling
    let mut sigint = signal(SignalKind::interrupt()).unwrap();
    let mut sigterm = signal(SignalKind::terminate()).unwrap();

    // set initial settings
    {
        let now = get_time();
        let (temp, gamma) = compute_settings(now, &get_config().await);
        apply_temp(temp, &get_config().await);
        apply_gamma(gamma, &get_config().await);
        trace!("initial settings applied: {temp}K, {gamma}%");
    }

    // Main loop with shutdown support
    tokio::select! {
        _ = async {
            loop {
                let now = get_time();
                let (temp, gamma) = compute_settings(now, &get_config().await);
                if disabled_temp.load(Ordering::SeqCst) {
                    apply_temp(
                        get_config().await.temp_day,
                        &get_config().await,
                    );
                } else {
                    apply_temp(temp, &get_config().await);
                }
                if disabled_gamma.load(Ordering::SeqCst) {
                    apply_gamma(
                        get_config().await.gamma_day,
                        &get_config().await,
                    );
                } else {
                    apply_gamma(gamma, &get_config().await);
                }

                notify.notified().await;
        }} => {},
        _ = sigint.recv() => {
            info!("Received SIGINT, shutting down...");
        },
        _ = sigterm.recv() => {
            info!("Received SIGTERM, shutting down...");
        },
    }

    // Cleanup the socket file on shutdown
    if std::path::Path::new(&get_socket_path()).exists() {
        match std::fs::remove_file(get_socket_path()) {
            Ok(_) => info!("Socket file {} removed.", get_socket_path().display()),
            Err(e) => warn!("Failed to remove socket file {}: {e}", get_socket_path().display()),
        }
    }
}

// Function to handle config reloading
async fn reload_config(notify: Arc<Notify>) -> Result<(), ConfigError> {
    trace!("reload_config called");
    let config_handle = config_handle();
    let mut config = config_handle.write().await;
    let config_path = get_config_path();
    match Config::load(&config_path) {
        Ok(cfg) => {
            debug!("Config file reloaded successfully");

            fn check_binary(binary: &str) -> String {
                if !is_binary_available(binary) {
                    error!("{binary} is not available, exiting.");
                    return binary.to_string();
                }
                "".to_string()
            }

            let gamma_check = match cfg.gamma_backend {
                GammaBackend::Hyprctl => check_binary("hyprctl"),
                GammaBackend::Ddcutil => check_binary("ddcutil"),
                GammaBackend::Xsct => check_binary("xsct"),
                GammaBackend::Redshift => check_binary("redshift"),
                GammaBackend::Gammastep => check_binary("gammastep"),
                GammaBackend::None => "".to_string(),
            };

            let temp_check = match cfg.temp_backend {
                TempBackend::Hyprctl => check_binary("hyprctl"),
                TempBackend::Gammastep => check_binary("gammastep"),
                TempBackend::Xsct => check_binary("xsct"),
                TempBackend::Redshift => check_binary("redshift"),
                TempBackend::None => "".to_string(),
            };

            if !gamma_check.is_empty() || !temp_check.is_empty() {
                error!("One or more binaries are not available, retaining old config.");
                return Err(ConfigError::UnavailableCommand(format!(
                    "binaries {gamma_check} {temp_check} are not available"
                )));
            }

            *config = cfg;
            reset_cache();

            let new_modified = std::fs::metadata(&config_path)
                .and_then(|m| m.modified())
                .map(|t| t.duration_since(UNIX_EPOCH).unwrap().as_secs())
                .unwrap_or(0);
            trace!("new_modified: {new_modified:?}");
            LAST_MODIFIED.store(new_modified, SeqCst);
            notify.notify_one();
            Ok(())
        }
        Err(err) => {
            error!("Failed to reload config: {err:?}");
            warn!("Retaining current config");
            Err(err)
        }
    }
}

fn is_binary_available(binary_name: &str) -> bool {
    use std::fs;
    if let Ok(paths) = env::var("PATH") {
        for path in env::split_paths(&paths) {
            let full_path = path.join(binary_name);
            if full_path.exists()
                && fs::metadata(&full_path)
                    .map(|m| m.is_file())
                    .unwrap_or(false)
            {
                return true;
            }
        }
    }
    false
}

#[inline]
fn get_config_path() -> PathBuf {
        let xdg_config_home = env::var("XDG_CONFIG_HOME").ok();
        let home = env::var("HOME").ok();
        let user = env::var("USER").unwrap_or_else(|_| "default".to_string());

        xdg_config_home
            .map(|x| PathBuf::from(format!("{x}/hinoirisetr.toml")))
            .or_else(|| home.map(|h| PathBuf::from(format!("{h}/.config/hinoirisetr.toml"))))
            .unwrap_or_else(|| PathBuf::from(format!("/home/{user}/.config/hinoirisetr.toml")))
}

async fn get_config() -> Config {
    *CONFIG.get().expect("config not init").read().await
}

fn config_handle() -> Arc<RwLock<Config>> {
    CONFIG.get().expect("config not init").clone()
}

// async fn get_config() -> Config {
//     let lock = CONFIG.get().expect("config not initialized").read().await;
//     lock.clone()
// }

fn get_time() -> OffsetDateTime {
    OffsetDateTime::now_local().expect("Failed to get local time")
}
