use crate::{
    components::MenuSize,
    components::divider,
    components::icons::{StaticIcon, icon},
    config::{
        CpuFormat, DiskFormat, MemoryFormat, SystemInfoIndicator, SystemInfoModuleConfig,
        SystemInfoTemperature, TemperatureSensor, TemperatureSensorType,
    },
    i18n::{UnitSystem, unit_system},
    t,
    theme::use_theme,
    utils,
};
use iced::{
    Alignment, Element, Length, Subscription, Theme,
    time::every,
    widget::{Column, Row, column, container, row, text},
};
use itertools::Itertools;
use log::{info, warn};
use std::time::{Duration, Instant};
use sysinfo::{Components, Disks, Networks, System};

const MAX_IP_LEN: usize = 45;

#[derive(Clone, Copy)]
struct FixedIp([u8; MAX_IP_LEN], usize);

impl FixedIp {
    fn from_str(s: &str) -> Option<Self> {
        if s.len() < MAX_IP_LEN {
            let mut arr = [0u8; MAX_IP_LEN];
            arr[..s.len()].copy_from_slice(s.as_bytes());
            Some(Self(arr, s.len()))
        } else {
            None
        }
    }
}

impl std::fmt::Display for FixedIp {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}",
            std::str::from_utf8(&self.0[..self.1]).unwrap_or("")
        )
    }
}

struct NetworkData {
    ip: FixedIp,
    download_speed: u32,
    upload_speed: u32,
    last_check: Instant,
}

struct MemoryUsage {
    percentage: u32,
    fraction: String,
}

struct CpuUsage {
    percentage: u32,
    frequency: f32,
}

struct Temperature {
    celsius: Option<i32>,
}

struct DiskView {
    percentage: u32,
    fraction: String,
}

struct SystemInfoData {
    cpu_usage: CpuUsage,
    memory_usage: MemoryUsage,
    memory_swap_usage: MemoryUsage,
    temperature: Temperature,
    disks: Vec<(String, DiskView)>,
    network: Option<NetworkData>,
}

fn find_first_sensor_label(labels: &[&str], components: &Components) -> Option<String> {
    labels
        .iter()
        .find(|&&label| components.iter().any(|c| c.label() == label))
        .map(|&label| label.to_string())
}

enum SensorMatch<'a> {
    Exact(&'a [&'a str]),
    StartsWith(&'a [&'a str]),
    Contains(&'a [&'a str]),
}

fn find_sensor(components: &Components, matches: &[SensorMatch]) -> Option<String> {
    for m in matches {
        let result = match m {
            SensorMatch::Exact(labels) => find_first_sensor_label(labels, components),
            SensorMatch::StartsWith(prefixes) => {
                let mut result = None;
                for &prefix in *prefixes {
                    if let Some(c) = components.iter().find(|c| c.label().starts_with(prefix)) {
                        result = Some(c.label().to_string());
                        break;
                    }
                }
                result
            }
            SensorMatch::Contains(substrs) => {
                let mut result = None;
                for &substr in *substrs {
                    if let Some(c) = components
                        .iter()
                        .find(|c| c.label().to_lowercase().contains(substr))
                    {
                        result = Some(c.label().to_string());
                        break;
                    }
                }
                result
            }
        };
        if result.is_some() {
            return result;
        }
    }
    None
}

static CPU_MATCHES: [SensorMatch<'static>; 2] = [
    SensorMatch::Exact(&[
        "coretemp Package id 0",
        "coretemp Core 0",
        "coretemp Physical id 0",
    ]),
    SensorMatch::StartsWith(&["k10temp", "applesmc"]),
];

static GPU_MATCHES: [SensorMatch<'static>; 3] = [
    SensorMatch::StartsWith(&["xe"]),
    SensorMatch::Exact(&["amdgpu edge", "nouveau temp1"]),
    SensorMatch::Contains(&["nvidia"]),
];

static ACPI_MATCHES: [SensorMatch<'static>; 1] =
    [SensorMatch::Exact(&["acpitz temp1", "acpitz temp0"])];

static NVME_MATCHES: [SensorMatch<'static>; 1] = [SensorMatch::StartsWith(&["nvme"])];

fn get_temperature_sensor_label(
    sensor: &TemperatureSensor,
    components: &Components,
) -> Option<String> {
    let sensor_type = match sensor {
        TemperatureSensor::Label(label) => {
            if components.iter().any(|c| c.label() == label) {
                info!("Using manually configured temperature sensor: {label}");
                return Some(label.clone());
            }
            warn!(
                "Configured temperature sensor '{label}' not found, falling back to Cpu auto-detection"
            );
            &TemperatureSensorType::Cpu
        }
        TemperatureSensor::Type(sensor_type) => sensor_type,
    };

    let matches: &[SensorMatch] = match sensor_type {
        TemperatureSensorType::Cpu => &CPU_MATCHES,
        TemperatureSensorType::Gpu => &GPU_MATCHES,
        TemperatureSensorType::Acpi => &ACPI_MATCHES,
        TemperatureSensorType::Nvme => &NVME_MATCHES,
    };

    let sensor_label = find_sensor(components, matches);

    if let Some(ref label) = sensor_label {
        info!("Auto-detected temperature sensor for {sensor_type:?}: {label}");
    } else {
        warn!("No temperature sensor found for {sensor_type:?} type");
    }

    sensor_label
}

#[allow(clippy::too_many_arguments)]
fn get_system_info(
    system: &mut System,
    components: &mut Components,
    disks: &mut Disks,
    (networks, last_check): (&mut Networks, Option<Instant>),
    temperature_config: &SystemInfoTemperature,
    cached_sensor_label: &mut Option<String>,
    mounts: Option<&[String]>,
) -> SystemInfoData {
    system.refresh_memory();
    system.refresh_cpu_all();

    components.refresh(true);
    disks.refresh(true);
    networks.refresh(true);

    let cpus = system.cpus();
    let avg_freq = cpus.iter().map(|cpu| cpu.frequency() as f32).sum::<f32>() / cpus.len() as f32;

    let cpu_usage = CpuUsage {
        percentage: system.global_cpu_usage() as u32,
        frequency: utils::floor_dp(avg_freq / 1000.0, 2),
    };

    let total_mem = system.total_memory();
    let used_mem = total_mem - system.available_memory();

    let memory_usage = MemoryUsage {
        percentage: if total_mem > 0 {
            (used_mem as f32 / total_mem as f32 * 100.) as u32
        } else {
            0
        },
        fraction: format!(
            "{:.2}/{:.2}",
            utils::bytes_to_gib(used_mem),
            utils::bytes_to_gib(total_mem)
        ),
    };

    let total_swap = system.total_swap();
    let used_swap = total_swap - system.free_swap();

    let memory_swap_usage = MemoryUsage {
        percentage: if total_swap > 0 {
            (used_swap as f32 / total_swap as f32 * 100.) as u32
        } else {
            0
        },
        fraction: format!(
            "{:.2}/{:.2}",
            utils::bytes_to_gib(used_swap),
            utils::bytes_to_gib(total_swap)
        ),
    };

    let temperature_cel = {
        if cached_sensor_label.is_none() {
            *cached_sensor_label =
                get_temperature_sensor_label(&temperature_config.sensor, components);
        }

        let mut reading = cached_sensor_label.as_ref().and_then(|label| {
            components
                .iter()
                .find(|c| c.label() == label)
                .and_then(|c| c.temperature().map(|t| t as i32))
        });

        if reading.is_none() {
            if let Some(label) = (*cached_sensor_label).take() {
                warn!("Cached sensor '{}' invalid; re-detecting", label);
            }
            *cached_sensor_label =
                get_temperature_sensor_label(&temperature_config.sensor, components);
            reading = cached_sensor_label.as_ref().and_then(|label| {
                components
                    .iter()
                    .find(|c| c.label() == label)
                    .and_then(|c| c.temperature().map(|t| t as i32))
            });
        }

        reading
    };

    let temperature = Temperature {
        celsius: temperature_cel,
    };

    let disks: Vec<(String, DiskView)> = disks
        .iter()
        .filter(|d| !d.is_removable() && d.total_space() != 0)
        .filter(|d| {
            if let Some(mounts) = mounts {
                let mount_str = d.mount_point().display().to_string();
                mounts.contains(&mount_str)
            } else {
                true
            }
        })
        .map(|d| {
            let total_space = d.total_space();
            let avail_space = d.available_space();

            let space_per = (total_space - avail_space) as f32 / total_space as f32 * 100.;

            (
                d.mount_point().display().to_string(),
                DiskView {
                    percentage: space_per as u32,
                    fraction: format!(
                        "{:.2}/{:.2}",
                        utils::bytes_to_gb(total_space - avail_space),
                        utils::bytes_to_gb(total_space)
                    ),
                },
            )
        })
        .sorted_by(|a, b| {
            if let Some(mounts_list) = mounts {
                let pos_a = mounts_list
                    .iter()
                    .position(|m| m == &a.0)
                    .unwrap_or(usize::MAX);
                let pos_b = mounts_list
                    .iter()
                    .position(|m| m == &b.0)
                    .unwrap_or(usize::MAX);
                pos_a.cmp(&pos_b)
            } else {
                a.0.cmp(&b.0)
            }
        })
        .collect();

    let elapsed = last_check.map(|v| v.elapsed().as_secs());

    let network = networks
        .iter()
        .filter(|(name, _)| {
            name.contains("en")
                || name.contains("eth")
                || name.contains("wl")
                || name.contains("wlan")
                || name.contains("br")
        })
        .sorted_by_key(|(name, _)| {
            if name.contains("en") {
                return 0;
            }

            if name.contains("eth") {
                return 1;
            }

            if name.contains("wl") {
                return 2;
            }

            if name.contains("wlan") {
                return 3;
            }
            if name.contains("br") {
                return 4;
            }

            99
        })
        .fold(
            (None, 0, 0),
            |(first_ip, total_received, total_transmitted), (_, data)| {
                let ip = first_ip.or_else(|| {
                    data.ip_networks()
                        .iter()
                        .sorted_by(|a, b| a.addr.cmp(&b.addr))
                        .next()
                        .map(|ip| ip.addr)
                });

                let received = data.received();
                let transmitted = data.transmitted();

                (
                    first_ip.or(ip),
                    total_received + received,
                    total_transmitted + transmitted,
                )
            },
        );

    let network_speed = |value: u64| {
        match elapsed {
            None | Some(0) => 0, // avoid division by zero
            Some(elapsed) => (value / 1000) as u32 / elapsed as u32,
        }
    };

    SystemInfoData {
        cpu_usage,
        memory_usage,
        memory_swap_usage,
        temperature,
        disks,
        network: network.0.and_then(|ip| {
            let ip_str = ip.to_string();
            FixedIp::from_str(&ip_str).map(|ip| NetworkData {
                ip,
                download_speed: network_speed(network.1),
                upload_speed: network_speed(network.2),
                last_check: Instant::now(),
            })
        }),
    }
}

#[derive(Debug, Clone)]
pub enum Message {
    Update,
}

pub struct SystemInfo {
    config: SystemInfoModuleConfig,
    system: System,
    components: Components,
    disks: Disks,
    networks: Networks,
    data: SystemInfoData,
    cached_sensor_label: Option<String>,
}

impl SystemInfo {
    pub fn new(config: SystemInfoModuleConfig) -> Self {
        let mut system = System::new();
        let mut components = Components::new_with_refreshed_list();
        let mut disks = Disks::new_with_refreshed_list();
        let mut networks = Networks::new_with_refreshed_list();

        let mut cached_sensor_label = None;

        let data = get_system_info(
            &mut system,
            &mut components,
            &mut disks,
            (&mut networks, None),
            &config.temperature,
            &mut cached_sensor_label,
            config.disk.mounts.as_deref(),
        );

        if let Some(label) = cached_sensor_label.as_ref() {
            info!("Using temperature sensor: {label}");
        }

        Self {
            config,
            system,
            components,
            disks,
            data,
            networks,
            cached_sensor_label,
        }
    }

    pub fn update(&mut self, message: Message) {
        match message {
            Message::Update => {
                self.data = get_system_info(
                    &mut self.system,
                    &mut self.components,
                    &mut self.disks,
                    (
                        &mut self.networks,
                        self.data.network.as_ref().map(|n| n.last_check),
                    ),
                    &self.config.temperature,
                    &mut self.cached_sensor_label,
                    self.config.disk.mounts.as_deref(),
                );
            }
        }
    }

    fn info_element<'a>(
        info_icon: StaticIcon,
        label: String,
        value: String,
    ) -> Element<'a, Message> {
        let (font_size, space) = use_theme(|t| (t.font_size, t.space));
        row!(
            container(icon(info_icon).size(font_size.xl)).center_x(Length::Fixed(space.xl)),
            text(label).width(Length::Fill),
            text(value)
        )
        .align_y(Alignment::Center)
        .spacing(space.xs)
        .into()
    }

    fn indicator_info_element<'a, V: PartialOrd + 'a>(
        info_icon: StaticIcon,
        (display, unit): (impl std::fmt::Display + 'a, &str),
        threshold: Option<(V, V, V)>,
        prefix: Option<String>,
    ) -> Element<'a, Message> {
        let space = use_theme(|t| t.space);
        let element = container(
            row!(
                icon(info_icon),
                if let Some(prefix) = prefix {
                    text(format!("{prefix} {display}{unit}"))
                } else {
                    text(format!("{display}{unit}"))
                }
            )
            .spacing(space.xxs),
        );

        if let Some((value, warn_threshold, alert_threshold)) = threshold {
            element
                .style(move |theme: &Theme| container::Style {
                    text_color: if value > warn_threshold && value < alert_threshold {
                        Some(theme.palette().warning)
                    } else if value >= alert_threshold {
                        Some(theme.palette().danger)
                    } else {
                        None
                    },
                    ..Default::default()
                })
                .into()
        } else {
            element.into()
        }
    }

    pub fn menu_view(&'_ self) -> Element<'_, Message> {
        let (font_size, space) = use_theme(|t| (t.font_size, t.space));
        container(
            column!(
                text(t!("system-info-heading")).size(font_size.lg),
                divider(),
                Column::with_capacity(6)
                    .push(Self::info_element(
                        StaticIcon::Cpu,
                        t!("system-info-cpu-usage"),
                        match self.config.cpu.format {
                            CpuFormat::Percentage => format!("{}%", self.data.cpu_usage.percentage),
                            CpuFormat::Frequency =>
                                format!("{} GHz", self.data.cpu_usage.frequency),
                        }
                    ))
                    .push(Self::info_element(
                        StaticIcon::Mem,
                        t!("system-info-memory-usage"),
                        match self.config.memory.format {
                            MemoryFormat::Percentage =>
                                format!("{}%", self.data.memory_usage.percentage),
                            MemoryFormat::Fraction =>
                                format!("{} GiB", self.data.memory_usage.fraction),
                        }
                    ))
                    .push(Self::info_element(
                        StaticIcon::Mem,
                        t!("system-info-swap-memory-usage"),
                        match self.config.memory.format {
                            MemoryFormat::Percentage =>
                                format!("{}%", self.data.memory_swap_usage.percentage),
                            MemoryFormat::Fraction =>
                                format!("{} GiB", self.data.memory_swap_usage.fraction),
                        }
                    ))
                    .push(self.data.temperature.celsius.map(|cel| {
                        Self::info_element(StaticIcon::Temp, t!("system-info-temperature"), {
                            let units = unit_system();
                            let value = match units {
                                UnitSystem::Metric => cel,
                                UnitSystem::Imperial => utils::celsius_to_fahrenheit(cel),
                            };
                            format!("{value}{}", units.temperature_symbol())
                        })
                    }))
                    .push(
                        Column::with_children(
                            self.data
                                .disks
                                .iter()
                                .map(|(mount_point, usage)| {
                                    Self::info_element(
                                        StaticIcon::Drive,
                                        t!("system-info-disk-usage", mount = mount_point.as_str()),
                                        match self.config.disk.format {
                                            DiskFormat::Percentage => {
                                                format!("{}%", usage.percentage)
                                            }
                                            DiskFormat::Fraction => {
                                                format!("{} GB", usage.fraction)
                                            }
                                        },
                                    )
                                })
                                .collect::<Vec<Element<_>>>(),
                        )
                        .spacing(space.xxs),
                    )
                    .push(self.data.network.as_ref().map(|network| {
                        Column::with_children(vec![
                            Self::info_element(
                                StaticIcon::IpAddress,
                                t!("system-info-ip-address"),
                                network.ip.to_string(),
                            ),
                            Self::info_element(
                                StaticIcon::DownloadSpeed,
                                t!("system-info-download-speed"),
                                if network.download_speed > 1000 {
                                    format!("{} MB/s", network.download_speed / 1000)
                                } else {
                                    format!("{} KB/s", network.download_speed)
                                },
                            ),
                            Self::info_element(
                                StaticIcon::UploadSpeed,
                                t!("system-info-upload-speed"),
                                if network.upload_speed > 1000 {
                                    format!("{} MB/s", network.upload_speed / 1000)
                                } else {
                                    format!("{} KB/s", network.upload_speed)
                                },
                            ),
                        ])
                    }))
                    .spacing(space.xxs)
                    .padding([0.0, space.xs])
            )
            .spacing(space.xs),
        )
        .width(MenuSize::Medium)
        .into()
    }

    pub fn view(&'_ self) -> Element<'_, Message> {
        let space = use_theme(|t| t.space);
        let indicators = self.config.indicators.iter().filter_map(|i| match i {
            SystemInfoIndicator::Cpu => Some(Self::indicator_info_element(
                StaticIcon::Cpu,
                match self.config.cpu.format {
                    CpuFormat::Percentage => (self.data.cpu_usage.percentage.to_string(), "%"),
                    CpuFormat::Frequency => (self.data.cpu_usage.frequency.to_string(), " GHz"),
                },
                Some((
                    self.data.cpu_usage.percentage,
                    self.config.cpu.warn_threshold,
                    self.config.cpu.alert_threshold,
                )),
                None,
            )),

            SystemInfoIndicator::Memory => Some(Self::indicator_info_element(
                StaticIcon::Mem,
                match self.config.memory.format {
                    MemoryFormat::Percentage => {
                        (self.data.memory_usage.percentage.to_string(), "%")
                    }
                    MemoryFormat::Fraction => (self.data.memory_usage.fraction.clone(), " GiB"),
                },
                Some((
                    self.data.memory_usage.percentage,
                    self.config.memory.warn_threshold,
                    self.config.memory.alert_threshold,
                )),
                None,
            )),

            SystemInfoIndicator::MemorySwap => Some(Self::indicator_info_element(
                StaticIcon::Mem,
                match self.config.memory.format {
                    MemoryFormat::Percentage => {
                        (self.data.memory_swap_usage.percentage.to_string(), "%")
                    }
                    MemoryFormat::Fraction => {
                        (self.data.memory_swap_usage.fraction.clone(), " GiB")
                    }
                },
                Some((
                    self.data.memory_swap_usage.percentage,
                    self.config.memory.warn_threshold,
                    self.config.memory.alert_threshold,
                )),
                Some(t!("system-info-swap-indicator-prefix")),
            )),

            SystemInfoIndicator::Temperature => self.data.temperature.celsius.map(|cel| {
                let units = unit_system();
                let temp_value = match units {
                    UnitSystem::Metric => cel,
                    UnitSystem::Imperial => utils::celsius_to_fahrenheit(cel),
                };
                Self::indicator_info_element(
                    StaticIcon::Temp,
                    (temp_value, units.temperature_symbol()),
                    Some((
                        temp_value,
                        self.config.temperature.warn_threshold(),
                        self.config.temperature.alert_threshold(),
                    )),
                    None,
                )
            }),
            SystemInfoIndicator::Disk(config) => {
                self.data.disks.iter().find_map(|(disk_mount, disk)| {
                    if disk_mount == &config.path {
                        Some(Self::indicator_info_element(
                            StaticIcon::Drive,
                            match self.config.disk.format {
                                DiskFormat::Percentage => (disk.percentage.to_string(), "%"),
                                DiskFormat::Fraction => (disk.fraction.clone(), " GB"),
                            },
                            Some((
                                disk.percentage,
                                self.config.disk.warn_threshold,
                                self.config.disk.alert_threshold,
                            )),
                            Some(config.name.as_deref().unwrap_or(disk_mount).to_string()),
                        ))
                    } else {
                        None
                    }
                })
            }
            SystemInfoIndicator::IpAddress => self.data.network.as_ref().map(|network| {
                Self::indicator_info_element(
                    StaticIcon::IpAddress,
                    (network.ip.to_string(), ""),
                    None::<(u32, u32, u32)>,
                    None,
                )
            }),
            SystemInfoIndicator::DownloadSpeed => self.data.network.as_ref().map(|network| {
                Self::indicator_info_element(
                    StaticIcon::DownloadSpeed,
                    (
                        if network.download_speed > 1000 {
                            network.download_speed / 1000
                        } else {
                            network.download_speed
                        },
                        if network.download_speed > 1000 {
                            "MB/s"
                        } else {
                            "KB/s"
                        },
                    ),
                    None::<(u32, u32, u32)>,
                    None,
                )
            }),
            SystemInfoIndicator::UploadSpeed => self.data.network.as_ref().map(|network| {
                Self::indicator_info_element(
                    StaticIcon::UploadSpeed,
                    (
                        if network.upload_speed > 1000 {
                            network.upload_speed / 1000
                        } else {
                            network.upload_speed
                        },
                        if network.upload_speed > 1000 {
                            "MB/s"
                        } else {
                            "KB/s"
                        },
                    ),
                    None::<(u32, u32, u32)>,
                    None,
                )
            }),
        });

        Row::with_children(indicators)
            .align_y(Alignment::Center)
            .spacing(space.xxs)
            .into()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        every(Duration::from_secs(self.config.interval)).map(|_| Message::Update)
    }
}
