use crate::{
    components::icons::{DynamicIcon, StaticIcon, icon},
    config::CustomModuleDef,
    theme::use_theme,
    utils::launcher::execute_command,
};
use iced::widget::canvas;
use iced::{
    Element, Length, Subscription, Theme,
    stream::channel,
    widget::{Space, Stack, row, text},
};
use iced::{
    mouse::Cursor,
    widget::{
        canvas::{Cache, Geometry, Path, Program},
        container,
    },
};
use log::{error, info, warn};
use serde::Deserialize;
use std::process::Stdio;
use tokio::{
    io::{AsyncBufReadExt, BufReader},
    process::Command,
};

#[derive(Debug, Clone)]
pub struct Custom {
    pub config: CustomModuleDef,
    data: CustomListenData,
}

#[derive(Debug, Clone, Deserialize, Default)]
pub struct CustomListenData {
    pub alt: String,
    pub text: Option<String>,
}

#[derive(Debug, Clone)]
pub enum Message {
    LaunchCommand,
    LaunchRightClickCommand,
    LaunchMiddleClickCommand,
    LaunchScrollUpCommand,
    LaunchScrollDownCommand,
    Update(CustomListenData),
}

// Define a struct for the canvas program
#[derive(Debug, Clone, Copy, Default)]
struct AlertIndicator;

impl<Message> Program<Message> for AlertIndicator {
    type State = Cache;

    fn draw(
        &self,
        cache: &Self::State,
        renderer: &iced::Renderer,
        theme: &Theme,
        bounds: iced::Rectangle,
        _cursor: Cursor,
    ) -> Vec<Geometry> {
        let geometry = cache.draw(renderer, bounds.size(), |frame| {
            let center = frame.center();
            // Use a smaller radius so the circle doesn't touch the canvas edges
            let radius = 2.0; // Creates a 4px diameter circle
            let circle = Path::circle(center, radius);
            frame.fill(&circle, theme.palette().danger);
        });

        vec![geometry]
    }
}

impl Custom {
    pub fn new(config: CustomModuleDef) -> Self {
        Self {
            config,
            data: CustomListenData::default(),
        }
    }

    pub fn module_type(&self) -> crate::config::CustomModuleType {
        self.config.r#type
    }

    pub fn update(&mut self, msg: Message) {
        match msg {
            Message::LaunchCommand => {
                if let Some(cmd) = &self.config.command {
                    execute_command(cmd);
                }
            }
            Message::LaunchRightClickCommand => {
                if let Some(cmd) = &self.config.on_right_click {
                    execute_command(cmd);
                }
            }
            Message::LaunchMiddleClickCommand => {
                if let Some(cmd) = &self.config.on_middle_click {
                    execute_command(cmd);
                }
            }
            Message::LaunchScrollUpCommand => {
                if let Some(cmd) = &self.config.on_scroll_up {
                    execute_command(cmd);
                }
            }
            Message::LaunchScrollDownCommand => {
                if let Some(cmd) = &self.config.on_scroll_down {
                    execute_command(cmd);
                }
            }
            Message::Update(data) => {
                self.data = data;
            }
        }
    }

    pub fn view(&'_ self) -> Element<'_, Message> {
        let space = use_theme(|theme| theme.space);
        match self.config.r#type {
            crate::config::CustomModuleType::Text => self
                .data
                .text
                .as_ref()
                .and_then(|text_content| {
                    if !text_content.is_empty() {
                        Some(text(text_content.clone()).into())
                    } else {
                        None
                    }
                })
                .unwrap_or_else(|| Space::new().width(Length::Shrink).into()),
            crate::config::CustomModuleType::Button => {
                let mut icon_element = self.config.icon.as_ref().map_or_else(
                    || icon(StaticIcon::None),
                    |text| icon(DynamicIcon(text.clone())),
                );

                if let Some(icons_map) = &self.config.icons {
                    for (re, icon_str) in icons_map {
                        if re.is_match(&self.data.alt) {
                            icon_element = icon(DynamicIcon(icon_str.clone()));
                            break; // Use the first match
                        }
                    }
                }

                // Wrap the icon in a container to apply padding
                let padded_icon_container = container(icon_element).padding([0, 1]);

                let show_alert = if let Some(re) = &self.config.alert
                    && re.is_match(&self.data.alt)
                {
                    true
                } else {
                    false
                };

                let icon_with_alert = if show_alert {
                    let alert_canvas = canvas(AlertIndicator)
                        .width(Length::Fixed(space.xs)) // Size of the dot
                        .height(Length::Fixed(space.xs));

                    // Container to position the dot at the top-right
                    let alert_indicator_container = container(alert_canvas)
                        .width(Length::Fill) // Take full width of the stack item
                        .height(Length::Fill) // Take full height
                        .align_x(iced::alignment::Horizontal::Right)
                        .align_y(iced::alignment::Vertical::Top);

                    Stack::new()
                        .push(padded_icon_container) // Padded icon is the base layer
                        .push(alert_indicator_container) // Dot container on top
                        .into()
                } else {
                    padded_icon_container.into() // No alert, just the padded icon
                };

                let maybe_text_element = self.data.text.as_ref().and_then(|text_content| {
                    if !text_content.is_empty() {
                        Some(text(text_content.clone()))
                    } else {
                        None
                    }
                });

                if let Some(text_element) = maybe_text_element {
                    row![icon_with_alert, text_element].spacing(space.xs).into()
                } else {
                    icon_with_alert
                }
            }
        }
    }

    pub fn subscription(&self) -> Subscription<(String, Message)> {
        let name = self.config.name.clone();
        if let Some(listen_cmd) = self.config.listen_cmd.clone() {
            Subscription::run_with((name, listen_cmd), |data| {
                let (name, listen_cmd) = data.clone();
                channel(10, async move |mut output| {
                    let command = Command::new("bash")
                        .arg("-c")
                        .arg(&listen_cmd)
                        .stdout(Stdio::piped())
                        .spawn();

                    match command {
                        Ok(mut child) => {
                            if let Some(stdout) = child.stdout.take() {
                                let mut reader = BufReader::new(stdout).lines();
                                let mut buf = String::new();

                                // Ensure the child process is spawned in the runtime so it can
                                // make progress on its own while we await for any output.
                                tokio::spawn(async move {
                                    match child.wait().await {
                                        Ok(status) => info!("child status was: {status}"),
                                        Err(e) => warn!("child process encountered an error: {e}"),
                                    }
                                });

                                while let Some(line) = reader.next_line().await.ok().flatten() {
                                    buf.push_str(&line);
                                    buf.push('\n');
                                    match serde_json::from_str::<CustomListenData>(&buf) {
                                        Ok(event) => {
                                            buf.clear();
                                            if let Err(e) = output
                                                .try_send((name.clone(), Message::Update(event)))
                                            {
                                                error!(
                                                    "Failed to send update for custom module '{name}': {e}"
                                                );
                                            }
                                        }
                                        Err(e) if e.is_eof() => {
                                            if buf.len() > 1 << 20 {
                                                warn!(
                                                    "custom module '{name}': dropping {} bytes of unterminated JSON",
                                                    buf.len()
                                                );
                                                buf.clear();
                                            }
                                        }
                                        Err(e) => {
                                            error!(
                                                "Failed to parse JSON for custom module '{name}': {e} (payload: {buf})"
                                            );
                                            buf.clear();
                                        }
                                    }
                                }
                            } else {
                                error!("Failed to capture stdout for command: {listen_cmd}");
                            }
                        }
                        Err(error) => {
                            error!("Failed to execute command: {error}");
                        }
                    }
                })
            })
        } else {
            Subscription::none()
        }
    }
}
