use super::ImageIcon;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::widgets::widget::BarWidget;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::Margin;
use eframe::egui::RichText;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::vec2;
use komorebi_client::PathExt;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
use tracing;
use which::which;

/// Minimum interval between consecutive application launches to prevent accidental spamming.
const MIN_LAUNCH_INTERVAL: Duration = Duration::from_millis(800);

#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget configuration
pub struct ApplicationsConfig {
    /// Enables or disables the applications widget.
    pub enable: bool,
    /// Whether to show the launch command on hover (optional).
    /// Could be overridden per application. Defaults to `false` if not set.
    pub show_command_on_hover: Option<bool>,
    /// Horizontal spacing between application buttons.
    pub spacing: Option<f32>,
    /// Default display format for all applications (optional).
    /// Could be overridden per application. Defaults to `Icon`.
    pub display: Option<ApplicationsDisplayFormat>,
    /// List of configured applications to display.
    pub items: Vec<AppConfig>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Application button configuration
pub struct AppConfig {
    /// Whether to enable this application button (optional).
    /// Inherits from the global `Applications` setting if omitted.
    pub enable: Option<bool>,
    /// Whether to show the launch command on hover (optional).
    /// Inherits from the global `Applications` setting if omitted.
    pub show_command_on_hover: Option<bool>,
    /// Display name of the application.
    pub name: String,
    /// Optional icon: a path to an image or a text-based glyph (e.g., from Nerd Fonts).
    /// If not set, and if the `command` is a path to an executable, an icon might be extracted from it.
    /// Note: glyphs require a compatible `font_family`.
    pub icon: Option<String>,
    /// Command to execute (e.g. path to the application or shell command).
    pub command: String,
    /// Display format for this application button (optional). Overrides global format if set.
    pub display: Option<ApplicationsDisplayFormat>,
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Applications widget display format
pub enum ApplicationsDisplayFormat {
    /// Show only the application icon.
    #[default]
    Icon,
    /// Show only the application name as text.
    Text,
    /// Show both the application icon and name.
    IconAndText,
}

#[derive(Clone, Debug)]
pub struct Applications {
    /// Whether the applications widget is enabled.
    pub enable: bool,
    /// Horizontal spacing between application buttons.
    pub spacing: Option<f32>,
    /// Applications to be rendered in the UI.
    pub items: Vec<App>,
}

impl BarWidget for Applications {
    fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        if !self.enable {
            return;
        }

        let icon_config = IconConfig {
            font_id: config.icon_font_id.clone(),
            size: config.icon_font_id.size,
            color: ctx.style().visuals.selection.stroke.color,
        };

        if let Some(spacing) = self.spacing {
            ui.spacing_mut().item_spacing.x = spacing;
        }

        config.apply_on_widget(false, ui, |ui| {
            for app in &mut self.items {
                app.render(ctx, ui, &icon_config);
            }
        });
    }
}

impl From<&ApplicationsConfig> for Applications {
    fn from(applications_config: &ApplicationsConfig) -> Self {
        let items = applications_config
            .items
            .iter()
            .enumerate()
            .map(|(index, config)| {
                let command = UserCommand::new(&config.command);

                App {
                    enable: config.enable.unwrap_or(applications_config.enable),
                    #[allow(clippy::obfuscated_if_else)]
                    name: config
                        .name
                        .is_empty()
                        .then(|| format!("App {}", index + 1))
                        .unwrap_or_else(|| config.name.clone()),
                    icon: Icon::try_from_path(config.icon.as_deref())
                        .or_else(|| Icon::try_from_command(&command)),
                    command,
                    display: config
                        .display
                        .or(applications_config.display)
                        .unwrap_or_default(),
                    show_command_on_hover: config
                        .show_command_on_hover
                        .or(applications_config.show_command_on_hover)
                        .unwrap_or(false),
                }
            })
            .collect();

        Self {
            enable: applications_config.enable,
            items,
            spacing: applications_config.spacing,
        }
    }
}

/// A single resolved application entry used at runtime.
#[derive(Clone, Debug)]
pub struct App {
    /// Whether this application is enabled.
    pub enable: bool,
    /// Display name of the application. Defaults to "App N" if not set.
    pub name: String,
    /// Icon to display for this application, if available.
    pub icon: Option<Icon>,
    /// Command to execute when the application is launched.
    pub command: UserCommand,
    /// Display format (icon, text, or both).
    pub display: ApplicationsDisplayFormat,
    /// Whether to show the launch command on hover.
    pub show_command_on_hover: bool,
}

impl App {
    /// Renders the application button in the provided `Ui` context with a given icon size.
    #[inline]
    pub fn render(&mut self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
        if self.enable
            && SelectableFrame::new(false)
                .show(ui, |ui| {
                    ui.spacing_mut().item_spacing = Vec2::splat(4.0);

                    match self.display {
                        ApplicationsDisplayFormat::Icon => self.draw_icon(ctx, ui, icon_config),
                        ApplicationsDisplayFormat::Text => self.draw_name(ui),
                        ApplicationsDisplayFormat::IconAndText => {
                            self.draw_icon(ctx, ui, icon_config);
                            self.draw_name(ui);
                        }
                    }

                    // Add hover text with command information
                    let response = ui.response();
                    if self.show_command_on_hover {
                        response.on_hover_text(format!("Launch: {}", self.command.as_ref()));
                    }
                })
                .clicked()
        {
            // Launch the application when clicked
            self.command.launch_if_ready();
        }
    }

    /// Draws the application's icon within the UI if available,
    /// or falls back to a default placeholder icon.
    #[inline]
    fn draw_icon(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
        if let Some(icon) = &self.icon {
            icon.draw(ctx, ui, icon_config);
        } else {
            Icon::draw_fallback(ui, Vec2::splat(icon_config.size));
        }
    }

    /// Displays the application's name as a non-selectable label within the UI.
    #[inline]
    fn draw_name(&self, ui: &mut Ui) {
        ui.add(Label::new(&self.name).selectable(false));
    }
}

/// Holds image/text data to be used as an icon in the UI.
/// This represents source icon data before rendering.
#[derive(Clone, Debug)]
pub enum Icon {
    /// RGBA image used for rendering the icon.
    Image(ImageIcon),
    /// Text-based icon, e.g. from a font like Nerd Fonts.
    Text(String),
}

impl Icon {
    /// Attempts to create an [`Icon`] from a string path or text glyph/glyphs.
    ///
    /// - Environment variables in the path are resolved using [`PathExt::replace_env`].
    /// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved path.
    /// - If the path is invalid but the string is non-empty, it is interpreted as a text-based icon and
    ///   returned as [`Icon::Text`].
    /// - Returns `None` if the input is empty, `None`, or image loading fails.
    #[inline]
    pub fn try_from_path(icon: Option<&str>) -> Option<Self> {
        let icon = icon.map(str::trim)?;
        if icon.is_empty() {
            return None;
        }

        let path = icon.replace_env();
        if !path.is_file() {
            return Some(Icon::Text(icon.to_owned()));
        }

        let image_icon = ImageIcon::try_load(path.as_ref(), || match image::open(&path) {
            Ok(img) => Some(img),
            Err(err) => {
                tracing::error!("Failed to load icon from {:?}, error: {}", path, err);
                None
            }
        })?;

        Some(Icon::Image(image_icon))
    }

    /// Attempts to create an [`Icon`] by extracting an image from the executable path of a [`UserCommand`].
    ///
    /// - Uses [`ImageIcon::try_load`] to load and cache the icon image based on the resolved executable path.
    /// - Returns [`Icon::Image`] if an icon is successfully extracted.
    /// - Returns `None` if the executable path is unavailable or icon extraction fails.
    #[inline]
    pub fn try_from_command(command: &UserCommand) -> Option<Self> {
        let path = command.get_executable()?;
        let image_icon = ImageIcon::try_load(path.as_ref(), || {
            let path_str = path.to_str()?;
            windows_icons::get_icon_by_path(path_str)
                .or_else(|| windows_icons_fallback::get_icon_by_path(path_str))
        })?;
        Some(Icon::Image(image_icon))
    }

    /// Renders the icon in the given [`Ui`] using the provided [`IconConfig`].
    #[inline]
    pub fn draw(&self, ctx: &Context, ui: &mut Ui, icon_config: &IconConfig) {
        match self {
            Icon::Image(image_icon) => {
                Frame::NONE
                    .inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
                    .show(ui, |ui| {
                        ui.add(
                            Image::from_texture(&image_icon.texture(ctx))
                                .maintain_aspect_ratio(true)
                                .fit_to_exact_size(Vec2::splat(icon_config.size)),
                        );
                    });
            }
            Icon::Text(icon) => {
                let rich_text = RichText::new(icon)
                    .font(icon_config.font_id.clone())
                    .size(icon_config.size)
                    .color(icon_config.color);
                ui.add(Label::new(rich_text).selectable(false));
            }
        }
    }

    /// Draws a fallback icon when the specified icon cannot be loaded.
    /// Displays a simple crossed-out rectangle as a placeholder.
    #[inline]
    pub fn draw_fallback(ui: &mut Ui, icon_size: Vec2) {
        let (response, painter) = ui.allocate_painter(icon_size, Sense::hover());
        let stroke = Stroke::new(1.0_f32, ui.style().visuals.text_color());
        let mut rect = response.rect;
        let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
        rect = rect.shrink(stroke.width);
        let c = rect.center();
        let r = rect.width() / 2.0;
        painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
        painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
    }
}

/// Configuration structure for icon rendering
#[derive(Clone, Debug)]
pub struct IconConfig {
    /// Font used for text-based icons
    pub font_id: FontId,
    /// Size of the icon
    pub size: f32,
    /// Color of the icon used for text-based icons
    pub color: Color32,
}

/// A structure to manage command execution with cooldown prevention.
#[derive(Clone, Debug)]
pub struct UserCommand {
    /// The command string to execute
    pub command: Arc<str>,
    /// Last time this command was executed (used for cooldown control)
    pub last_launch: Instant,
}

impl AsRef<str> for UserCommand {
    #[inline]
    fn as_ref(&self) -> &str {
        &self.command
    }
}

impl UserCommand {
    /// Creates a new [`UserCommand`] with environment variables in the command path
    /// resolved using [`PathExt::replace_env`].
    #[inline]
    pub fn new(command: &str) -> Self {
        // Allow immediate launch by initializing last_launch in the past
        let last_launch = Instant::now() - 2 * MIN_LAUNCH_INTERVAL;

        Self {
            command: Arc::from(command.replace_env().to_str().unwrap_or_default()),
            last_launch,
        }
    }

    /// Attempts to resolve the executable path from the command string.
    ///
    /// Resolution logic:
    /// - Splits the command by ".exe" and checks if the first part is an existing file.
    /// - If not, attempts to locate the binary using [`which`] on this name.
    /// - If still unresolved, takes the first word (separated by whitespace) and attempts
    ///   to find it in the system `PATH` using [`which`].
    ///
    /// Returns `None` if no executable path can be determined.
    #[inline]
    pub fn get_executable(&self) -> Option<Cow<'_, Path>> {
        if let Some(binary) = self.command.split(".exe").next().map(Path::new) {
            if binary.is_file() {
                return Some(Cow::Borrowed(binary));
            } else if let Ok(binary) = which(binary) {
                return Some(Cow::Owned(binary));
            }
        }

        which(self.command.split(' ').next()?).ok().map(Cow::Owned)
    }

    /// Attempts to launch the specified command in a separate thread if enough time has passed
    /// since the last launch. This prevents repeated launches from rapid consecutive clicks.
    ///
    /// Errors during launch are logged using the `tracing` crate.
    pub fn launch_if_ready(&mut self) {
        let now = Instant::now();
        // Check if enough time has passed since the last launch
        if now.duration_since(self.last_launch) < MIN_LAUNCH_INTERVAL {
            return;
        }

        self.last_launch = now;
        let command_string = self.command.clone();
        // Launch the application in a separate thread to avoid blocking the UI
        std::thread::spawn(move || {
            if let Err(e) = Command::new("cmd").args(["/C", &command_string]).spawn() {
                tracing::error!("Failed to launch command '{}': {}", command_string, e);
            }
        });
    }
}
