use std::{
    cell::RefCell,
    fs, io,
    os::unix::fs::FileTypeExt,
    process::{Child, ChildStdout, Command, Stdio},
    rc::Rc,
    time::{Duration, Instant},
};

use chrono::Local;
use chrono_tz::Tz;
use interprocess::os::unix::fifo_file::create_fifo;
use wgui::{
    drawing,
    event::{self, EventCallback},
    i18n::Translation,
    layout::Layout,
    parser::{CustomAttribsInfoOwned, parse_color_hex},
    widget::{EventResult, label::WidgetLabel},
};

use crate::state::AppState;

use super::helper::{expand_env_vars, read_label_from_pipe};

#[allow(clippy::too_many_lines)]
pub(super) fn setup_custom_label<S: 'static>(
    layout: &mut Layout,
    attribs: &CustomAttribsInfoOwned,
    app: &AppState,
) {
    let Some(source) = attribs.get_value("_source") else {
        log::warn!("custom label with no source!");
        return;
    };

    let callback: EventCallback<AppState, S> = match source {
        "shell" => {
            let Some(exec) = attribs.get_value("_exec") else {
                log::warn!("label with shell source but no exec attribute!");
                return;
            };
            let state = ShellLabelState {
                exec: exec.to_string(),
                mut_state: RefCell::new(ShellLabelMutableState {
                    child: None,
                    reader: None,
                    next_try: Instant::now(),
                }),
                carry_over: RefCell::new(None),
            };
            Box::new(move |common, data, _, _| {
                shell_on_tick(&state, common, data);
                Ok(EventResult::Pass)
            })
        }
        "fifo" => {
            let Some(path) = attribs.get_value("_path") else {
                log::warn!("label with fifo source but no path attribute!");
                return;
            };
            let state = FifoLabelState {
                path: expand_env_vars(path),
                carry_over: RefCell::new(None),
                mut_state: RefCell::new(FifoLabelMutableState {
                    reader: None,
                    next_try: Instant::now(),
                }),
            };
            Box::new(move |common, data, _, _| {
                pipe_on_tick(&state, common, data);
                Ok(EventResult::Pass)
            })
        }
        "battery" => {
            let Some(device) = attribs
                .get_value("_device")
                .and_then(|s| s.parse::<usize>().ok())
            else {
                log::warn!("label with battery source but no device attribute!");
                return;
            };

            let state = BatteryLabelState {
                low_color: attribs
                    .get_value("_low_color")
                    .and_then(parse_color_hex)
                    .unwrap_or(BAT_LOW),
                normal_color: attribs
                    .get_value("_normal_color")
                    .and_then(parse_color_hex)
                    .unwrap_or(BAT_NORMAL),
                charging_color: attribs
                    .get_value("_charging_color")
                    .and_then(parse_color_hex)
                    .unwrap_or(BAT_CHARGING),
                low_threshold: attribs
                    .get_value("_low_threshold")
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(BAT_LOW_THRESHOLD),
                device,
            };
            Box::new(move |common, data, app, _| {
                battery_on_tick(&state, common, data, app);
                Ok(EventResult::Pass)
            })
        }
        "clock" => {
            let Some(display) = attribs.get_value("_display") else {
                log::warn!("label with clock source but no display attribute!");
                return;
            };

            let format = match display {
                "name" => {
                    let maybe_pretty_tz = attribs
                        .get_value("_timezone")
                        .and_then(|tz| tz.parse::<usize>().ok())
                        .and_then(|tz_idx| app.session.config.timezones.get(tz_idx))
                        .and_then(|tz_name| {
                            tz_name.split('/').next_back().map(|x| x.replace('_', " "))
                        });

                    let pretty_tz = maybe_pretty_tz.as_ref().map_or("Local", |x| x.as_str());

                    let mut globals = layout.state.globals.get();
                    layout
                        .state
                        .widgets
                        .get_as::<WidgetLabel>(attribs.widget_id)
                        .unwrap()
                        .set_text_simple(&mut globals, Translation::from_raw_text(pretty_tz));

                    // does not need to be dynamic
                    return;
                }
                "date" => "%x",
                "dow" => "%A",
                "time" => {
                    if app.session.config.clock_12h {
                        "%I:%M %p"
                    } else {
                        "%H:%M"
                    }
                }
                unk => {
                    log::warn!("Unknown display value for clock label source: {unk}");
                    return;
                }
            };

            let tz_str = attribs
                .get_value("_timezone")
                .and_then(|tz| tz.parse::<usize>().ok())
                .and_then(|tz_idx| app.session.config.timezones.get(tz_idx));

            let state = ClockLabelState {
                timezone: tz_str.and_then(|tz| {
                    tz.parse()
                        .inspect_err(|e| log::warn!("Invalid timezone: {e:?}"))
                        .ok()
                }),
                format: format.into(),
            };

            Box::new(move |common, data, _, _| {
                clock_on_tick(&state, common, data);
                Ok(EventResult::Pass)
            })
        }
        "ipd" => Box::new(|common, data, app, _| {
            ipd_on_tick(common, data, app);
            Ok(EventResult::Pass)
        }),
        unk => {
            log::warn!("Unknown source value for label: {unk}");
            return;
        }
    };

    layout.add_event_listener(
        attribs.widget_id,
        wgui::event::EventListenerKind::InternalStateChange,
        callback,
    );
}

struct ShellLabelMutableState {
    child: Option<Child>,
    reader: Option<io::BufReader<ChildStdout>>,
    next_try: Instant,
}

struct ShellLabelState {
    exec: String,
    mut_state: RefCell<ShellLabelMutableState>,
    carry_over: RefCell<Option<String>>,
}

#[allow(clippy::redundant_else)]
fn shell_on_tick(
    state: &ShellLabelState,
    common: &mut event::CallbackDataCommon,
    data: &mut event::CallbackData,
) {
    let mut mut_state = state.mut_state.borrow_mut();

    if let Some(mut child) = mut_state.child.take() {
        match child.try_wait() {
            // not exited yet
            Ok(None) => {
                if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
                    read_label_from_pipe("child process", r, &mut state.carry_over.borrow_mut())
                }) {
                    let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
                    label.set_text(common, Translation::from_raw_text(&text));
                }
                mut_state.child = Some(child);
                return;
            }
            // exited successfully
            Ok(Some(code)) if code.success() => {
                if let Some(text) = mut_state.reader.as_mut().and_then(|r| {
                    read_label_from_pipe("child process", r, &mut state.carry_over.borrow_mut())
                }) {
                    let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
                    label.set_text(common, Translation::from_raw_text(&text));
                }
                mut_state.child = None;
                return;
            }
            // exited with failure
            Ok(Some(code)) => {
                mut_state.child = None;
                mut_state.next_try = Instant::now() + Duration::from_secs(15);
                log::warn!("Label process exited with code {code}");
                return;
            }
            // lost
            Err(_) => {
                mut_state.child = None;
                mut_state.next_try = Instant::now() + Duration::from_secs(15);
                log::warn!("Label child process lost.");
                return;
            }
        }
    } else if mut_state.next_try > Instant::now() {
        return;
    }

    match Command::new("sh")
        .arg("-c")
        .arg(&state.exec)
        .stdout(Stdio::piped())
        .spawn()
    {
        Ok(mut child) => {
            let stdout = child.stdout.take().unwrap();
            mut_state.child = Some(child);
            mut_state.reader = Some(io::BufReader::new(stdout));
        }
        Err(e) => {
            log::warn!("Failed to run shell script '{}': {e:?}", &state.exec);
        }
    }
}

struct FifoLabelMutableState {
    reader: Option<io::BufReader<fs::File>>,
    next_try: Instant,
}

struct FifoLabelState {
    path: String,
    mut_state: RefCell<FifoLabelMutableState>,
    carry_over: RefCell<Option<String>>,
}

impl FifoLabelState {
    fn try_remove_fifo(&self) -> anyhow::Result<()> {
        let meta = match fs::metadata(&self.path) {
            Ok(meta) => meta,
            Err(e) => {
                if fs::exists(&self.path).unwrap_or(true) {
                    anyhow::bail!("Could not stat existing file at {}: {e:?}", &self.path);
                }
                return Ok(());
            }
        };

        if !meta.file_type().is_fifo() {
            anyhow::bail!("Existing file at {} is not a FIFO", &self.path);
        }

        if let Err(e) = fs::remove_file(&self.path) {
            anyhow::bail!("Unable to remove existing FIFO at {}: {e:?}", &self.path);
        }

        Ok(())
    }
}

impl Drop for FifoLabelState {
    fn drop(&mut self) {
        if let Err(e) = self.try_remove_fifo() {
            log::debug!("{e:?}");
        }
    }
}

fn pipe_on_tick(
    state: &FifoLabelState,
    common: &mut event::CallbackDataCommon,
    data: &mut event::CallbackData,
) {
    let mut mut_state = state.mut_state.borrow_mut();

    let reader = if let Some(f) = mut_state.reader.as_mut() {
        f
    } else {
        if mut_state.next_try > Instant::now() {
            return;
        }

        if let Err(e) = state.try_remove_fifo() {
            mut_state.next_try = Instant::now() + Duration::from_secs(15);
            log::warn!("Requested FIFO path is taken: {e:?}");
            return;
        }

        if let Err(e) = create_fifo(&state.path, 0o777) {
            mut_state.next_try = Instant::now() + Duration::from_secs(15);
            log::warn!("Failed to create FIFO: {e:?}");
            return;
        }

        mut_state.reader = fs::File::open(&state.path)
            .inspect_err(|e| {
                log::warn!("Failed to open FIFO: {e:?}");
                mut_state.next_try = Instant::now() + Duration::from_secs(15);
            })
            .map(io::BufReader::new)
            .ok();

        mut_state.reader.as_mut().unwrap()
    };

    if let Some(text) =
        read_label_from_pipe(&state.path, reader, &mut state.carry_over.borrow_mut())
    {
        let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
        label.set_text(common, Translation::from_raw_text(&text));
    }
}

const BAT_LOW: drawing::Color = drawing::Color::new(0.69, 0.38, 0.38, 1.);
const BAT_NORMAL: drawing::Color = drawing::Color::new(0.55, 0.84, 0.79, 1.);
const BAT_CHARGING: drawing::Color = drawing::Color::new(0.38, 0.50, 0.62, 1.);
const BAT_LOW_THRESHOLD: u32 = 30;

struct BatteryLabelState {
    device: usize,
    low_color: drawing::Color,
    normal_color: drawing::Color,
    charging_color: drawing::Color,
    low_threshold: u32,
}

fn battery_on_tick(
    state: &BatteryLabelState,
    common: &mut event::CallbackDataCommon,
    data: &mut event::CallbackData,
    app: &AppState,
) {
    let device = app.input_state.devices.get(state.device);
    let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();

    if let Some(device) = device
        && let Some(soc) = device.soc
    {
        let soc = (soc * 100.).min(99.) as u32;
        let text = soc.to_string();
        let color = if device.charging {
            state.charging_color
        } else if soc < state.low_threshold {
            state.low_color
        } else {
            state.normal_color
        };
        label.set_color(common, color, false);
        label.set_text(common, Translation::from_raw_text(&text));
        return;
    }
    label.set_text(common, Translation::default());
}

struct ClockLabelState {
    timezone: Option<Tz>,
    format: Rc<str>,
}

fn clock_on_tick(
    state: &ClockLabelState,
    common: &mut event::CallbackDataCommon,
    data: &mut event::CallbackData,
) {
    let date_time = state.timezone.as_ref().map_or_else(
        || format!("{}", Local::now().format(&state.format)),
        |tz| format!("{}", Local::now().with_timezone(tz).format(&state.format)),
    );

    let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
    label.set_text(common, Translation::from_raw_text(&date_time));
}

fn ipd_on_tick(
    common: &mut event::CallbackDataCommon,
    data: &mut event::CallbackData,
    app: &AppState,
) {
    let text = app.input_state.ipd.to_string();
    let label = data.obj.get_as_mut::<WidgetLabel>().unwrap();
    label.set_text(common, Translation::from_raw_text(&text));
}
