mod cli;
mod config;
mod desktop;
mod logs;
mod process;
#[cfg(test)]
mod tests;
mod tray;
mod ui;

use crate::cli::{
    parse_cli_args, prepare_run_startup, should_expose_configuration, tray_tooltip,
    validate_runtime_mode,
};
use crate::config::{
    normalize_saved_display_name, reconcile_startup_runtime_state, ProfileLockHandle,
};
use crate::desktop::{
    app_id_for_profile, create_desktop_file_from_cli, ensure_hidden_identity_desktop_file,
    load_window_icon_pixbuf, resolve_tray_icon,
};
use crate::logs::{build_logs_window, setup_log_receiver, setup_logs_handlers};
use crate::process::start_command;
use crate::tray::TrayHandle;
use crate::ui::{
    build_about_window, build_config_window, install_css, install_log_filters,
    refresh_desktop_toggles, setup_config_handlers, setup_menu_polling, setup_process_watcher,
};
use gtk::prelude::*;
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::process::{exit, Child};
use std::rc::Rc;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct RuntimeOwnershipState {
    pub pid: u32,
    pub pgid: u32,
    #[serde(alias = "started_at_unix_ms")]
    pub started_at_clock_ticks: u64,
    pub command_label: String,
    pub profile_name: Option<String>,
    pub ephemeral: bool,
}

impl RuntimeOwnershipState {
    pub fn validate(&self) -> Result<(), String> {
        if self.pid == 0 {
            return Err("pid must be greater than 0".to_string());
        }
        if self.pgid == 0 {
            return Err("pgid must be greater than 0".to_string());
        }
        if self.started_at_clock_ticks == 0 {
            return Err("started_at_clock_ticks must be greater than 0".to_string());
        }
        if self.command_label.len() > MAX_COMMAND_LENGTH {
            return Err(format!(
                "command_label exceeds maximum length of {} bytes",
                MAX_COMMAND_LENGTH
            ));
        }
        Ok(())
    }
}
const APP_NAME: &str = "givetray";
const DEFAULT_PROFILE: &str = "default";
const DEFAULT_COMMAND: &str = "echo configure command";
const MAX_LOG_LINES: usize = 5000;
const UI_EVENT_CHANNEL_CAPACITY: usize = 750;
const MAX_UNDO: usize = 200;
const MAX_COMMAND_LENGTH: usize = 8192;
const MAX_PROFILE_LENGTH: usize = 128;
const MAX_DISPLAY_NAME_LENGTH: usize = 128;
const ICON_FILE_NAME: &str = "icon.png";
const BUNDLED_ICON_FILE_NAME: &str = "default-icon.png";
const BG_CHILD_ENV: &str = "GIVETRAY_BG_CHILD";
const LOG_LINK_TAG_NAME: &str = "log-link";
const LOG_LINK_CLICK_SLOP: f64 = 4.0;
const RUNTIME_RESTORED_MESSAGE: &str = "restored active run from previous session";
const RUNTIME_STALE_CLEARED_MESSAGE: &str = "cleared stale runtime state from previous session";
const RUNTIME_INVALID_CLEARED_MESSAGE: &str = "cleared invalid runtime state from previous session";
const RUNTIME_STOP_FAILED_MESSAGE: &str =
    "failed to stop managed process group; process may still be running";
const RUNTIME_ALREADY_OPEN_MESSAGE: &str = "profile already open";

pub(crate) fn coalesced_log_overflow_message(count: usize) -> String {
    let suffix = if count == 1 { "" } else { "s" };
    format!("log queue saturated; dropped {count} line{suffix}")
}

#[derive(Debug, Clone)]
struct CliOptions {
    run_target: CliRunTarget,
    command_override: Option<String>,
    name_override: Option<String>,
    icon_source: Option<PathBuf>,
    log_file: Option<PathBuf>,
    mode: CliMode,
}

impl CliOptions {
    fn persistent_profile(&self) -> Option<&str> {
        match &self.run_target {
            CliRunTarget::PersistentProfile { profile } => Some(profile),
            CliRunTarget::EphemeralArgv { .. } => None,
        }
    }
}

#[derive(Debug, Clone)]
enum CliRequest {
    Run(CliOptions),
    PrintHelp,
    PrintVersion,
}

#[derive(Debug, Clone)]
enum CliRunTarget {
    PersistentProfile { profile: String },
    EphemeralArgv { argv: Vec<String> },
}

#[derive(Debug, Clone)]
enum CliMode {
    Run,
    DesktopFile {
        output_dir: Option<PathBuf>,
        autostart: bool,
    },
}

#[derive(Debug, Serialize, Deserialize, Clone)]
struct Config {
    command: String,
    #[serde(default)]
    autostart: bool,
    #[serde(default)]
    name: Option<String>,
    #[serde(default)]
    icon_path: Option<String>,
    #[serde(default)]
    log_to_file: bool,
    #[serde(default)]
    log_file_path: Option<String>,
}

enum UiEvent {
    AppendLog(String),
    ProcessExited(Option<i32>),
    SetRunning(bool),
    ClearRuntimeState,
}

struct AppState {
    run_target: CliRunTarget,
    tray: TrayHandle,
    persistent_config_access: Option<PersistentConfigAccess>,
    display_name: Option<String>,
    command: String,
    saved_command: String,
    saved_autostart: bool,
    saved_name: Option<String>,
    saved_icon_path: Option<String>,
    saved_log_to_file: bool,
    saved_log_file_path: Option<String>,
    child: Option<Child>,
    owned_pgid: Option<i32>,
    owned_pid: Option<u32>,
    process_exit_reported: bool,
    runtime_state_path: Option<PathBuf>,
    restored_running: bool,
    owns_profile_lock: bool,
    profile_lock: Option<ProfileLockHandle>,
    log_lines: VecDeque<String>,
    log_links: VecDeque<Vec<LogLink>>,
    log_file_path: Option<PathBuf>,
    log_file_writer: Option<std::io::LineWriter<std::fs::File>>,
    logs_window: gtk::Window,
    logs_view: gtk::TextView,
    logs_buffer: gtk::TextBuffer,
    logs_clear_button: gtk::Button,
    logs_copy_button: gtk::Button,
    logs_start_button: gtk::Button,
    logs_stop_button: gtk::Button,
    logs_restart_button: gtk::Button,
    logs_status_label: gtk::Label,
    about_window: gtk::Window,
    config_window: gtk::Window,
    config_view: gtk::TextView,
    config_buffer: gtk::TextBuffer,
    config_name_entry: gtk::Entry,
    config_autostart: gtk::CheckButton,
    config_log_to_file: gtk::CheckButton,
    config_applications: gtk::CheckButton,
    config_system_autostart: gtk::CheckButton,
    config_save_button: gtk::Button,
    config_status_label: gtk::Label,
    config_saved_applications: bool,
    config_saved_system_autostart: bool,
    config_undo: Vec<String>,
    config_redo: Vec<String>,
    config_last: String,
    config_ignore: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct LogLink {
    start_char: i32,
    end_char: i32,
    uri: String,
}

#[derive(Debug, Clone)]
struct PendingLogLink {
    uri: String,
    x: f64,
    y: f64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct PersistentConfigAccess {
    profile: String,
    config_path: PathBuf,
}

struct StartupState {
    profile_label: String,
    display_name: Option<String>,
    persistent_config_access: Option<PersistentConfigAccess>,
    config: Config,
    log_file_path: Option<PathBuf>,
    launch_on_startup: bool,
    runtime_state_path: Option<PathBuf>,
    runtime_ownership: Option<RuntimeOwnershipState>,
    restored_running: bool,
    owns_profile_lock: bool,
    profile_lock: Option<ProfileLockHandle>,
    startup_message: Option<String>,
}

enum ConfigCloseAction {
    Save,
    Discard,
    Cancel,
}

fn should_launch_on_startup(startup: &StartupState) -> bool {
    startup.launch_on_startup && !startup.restored_running
}

pub(crate) fn ensure_profile_identity_desktop_if_owner(
    profile: &str,
    config: &Config,
    owns_profile_lock: bool,
) -> Option<Result<PathBuf, String>> {
    if !owns_profile_lock {
        return None;
    }

    Some(ensure_hidden_identity_desktop_file(profile, config))
}

fn main() {
    install_log_filters();

    let cli = match parse_cli_args() {
        Ok(cli) => cli,
        Err(err) => {
            eprintln!("{err}");
            exit(2);
        }
    };

    if let Err(err) = validate_runtime_mode(&cli) {
        eprintln!("{err}");
        exit(1);
    }

    match cli.mode.clone() {
        CliMode::DesktopFile {
            output_dir,
            autostart,
        } => {
            if let Err(err) = create_desktop_file_from_cli(&cli, output_dir, autostart) {
                eprintln!("{err}");
                exit(1);
            }
            return;
        }
        CliMode::Run => {}
    }

    let startup = match prepare_run_startup(&cli) {
        Ok(startup) => startup,
        Err(err) => {
            eprintln!("failed to prepare app startup: {err}");
            exit(1);
        }
    };

    let startup = reconcile_startup_runtime_state(startup);

    if let Some(profile) = cli.persistent_profile() {
        if let Some(Err(err)) =
            ensure_profile_identity_desktop_if_owner(profile, &startup.config, startup.owns_profile_lock)
        {
            eprintln!("warning: failed to ensure hidden desktop identity for {profile}: {err}");
        }

        let app_id = app_id_for_profile(profile);
        gtk::glib::set_prgname(Some(&app_id));
    }

    gtk::glib::set_application_name(APP_NAME);

    gtk::init().expect("failed to initialize GTK");

    if let Some(profile) = cli.persistent_profile() {
        let app_id = app_id_for_profile(profile);
        gtk::gdk::set_program_class(&app_id);
    }

    install_css();

    let window_icon = load_window_icon_pixbuf(&startup.config);
    if let Some(icon) = window_icon.as_ref() {
        gtk::Window::set_default_icon(icon);
    }

    let (
        logs_window,
        logs_view,
        logs_buffer,
        logs_clear_button,
        logs_copy_button,
        logs_start_button,
        logs_stop_button,
        logs_restart_button,
        logs_status_label,
    ) = build_logs_window(startup.display_name.as_deref());
    let initial_saved_name = normalize_saved_display_name(startup.config.name.as_deref());

    let (
        config_window,
        config_view,
        config_buffer,
        config_name_entry,
        config_autostart,
        config_log_to_file,
        config_applications,
        config_system_autostart,
        config_save_button,
        config_status_label,
    ) = build_config_window(
        &startup.profile_label,
        &startup.config.command,
        initial_saved_name.as_deref(),
        startup.config.autostart,
        startup.config.log_to_file,
    );
    let about_window = build_about_window(window_icon.as_ref());

    if let Some(icon) = window_icon.as_ref() {
        logs_window.set_icon(Some(icon));
        config_window.set_icon(Some(icon));
        about_window.set_icon(Some(icon));
    }

    let (ui_tx, ui_rx) = async_channel::bounded::<UiEvent>(UI_EVENT_CHANNEL_CAPACITY);

    let tooltip = tray_tooltip(&cli.run_target, startup.display_name.as_deref());
    let resolved_tray_icon = resolve_tray_icon(&startup.config).expect("failed to load tray icon");
    let tray_icon = resolved_tray_icon
        .to_tray_icon()
        .expect("failed to build tray icon");
    let tray = crate::tray::build_tray(
        &tooltip,
        should_expose_configuration(startup.persistent_config_access.as_ref()),
        startup.restored_running,
        &cli.run_target,
        startup.owns_profile_lock,
        tray_icon,
        Some(&resolved_tray_icon),
    )
    .expect("failed to create tray icon");
    let should_launch = should_launch_on_startup(&startup);
    let startup_message = startup.startup_message.clone();

    let state = Rc::new(RefCell::new(AppState {
        run_target: cli.run_target.clone(),
        tray,
        persistent_config_access: startup.persistent_config_access,
        display_name: startup.display_name.clone(),
        command: startup.config.command.clone(),
        saved_command: startup.config.command.clone(),
        saved_autostart: startup.config.autostart,
        saved_name: initial_saved_name,
        saved_icon_path: startup.config.icon_path.clone(),
        saved_log_to_file: startup.config.log_to_file,
        saved_log_file_path: startup.config.log_file_path.clone(),
        child: None,
        owned_pgid: startup.runtime_ownership.as_ref().map(|o| o.pgid as i32),
        owned_pid: startup.runtime_ownership.as_ref().map(|o| o.pid),
        process_exit_reported: false,
        runtime_state_path: startup.runtime_state_path,
        restored_running: startup.restored_running,
        owns_profile_lock: startup.owns_profile_lock,
        profile_lock: startup.profile_lock,
        log_lines: VecDeque::new(),
        log_links: VecDeque::new(),
        log_file_path: startup.log_file_path,
        log_file_writer: None,
        logs_window,
        logs_view,
        logs_buffer,
        logs_clear_button,
        logs_copy_button,
        logs_start_button,
        logs_stop_button,
        logs_restart_button,
        logs_status_label,
        about_window,
        config_window,
        config_view,
        config_buffer,
        config_name_entry,
        config_autostart,
        config_log_to_file,
        config_applications,
        config_system_autostart,
        config_save_button,
        config_status_label,
        config_saved_applications: false,
        config_saved_system_autostart: false,
        config_undo: Vec::new(),
        config_redo: Vec::new(),
        config_last: startup.config.command,
        config_ignore: false,
    }));

    {
        let app = state.borrow();
        let _ = app.display_name.as_deref();
        debug_assert!(app.owns_profile_lock || app.profile_lock.is_none());
    }

    if let Some(message) = startup_message {
        logs::append_log(&mut state.borrow_mut(), message);
    }

    if state.borrow().persistent_config_access.is_some() {
        let (apps_toggle, system_autostart_toggle) = {
            let app = state.borrow();
            (
                app.config_applications.clone(),
                app.config_system_autostart.clone(),
            )
        };
        refresh_desktop_toggles(state.clone(), &apps_toggle, &system_autostart_toggle);
        setup_config_handlers(state.clone());
    }
    setup_logs_handlers(state.clone(), ui_tx.clone());
    setup_log_receiver(state.clone(), ui_rx);
    setup_menu_polling(state.clone(), ui_tx.clone());
    setup_process_watcher(state.clone(), ui_tx.clone());

    if should_launch {
        start_command(state.clone(), ui_tx);
    }

    gtk::main();
}
