use std::process::Command;
use std::time::Instant;

const POLL_INTERVAL_SECS: u64 = 30;

#[derive(Debug, Clone)]
pub struct BatteryState {
    pub percent: u8,
    pub charging: bool,
    pub present: bool,
}

#[derive(Debug, Clone)]
pub struct WifiState {
    pub connected: bool,
}

#[derive(Debug, Clone)]
pub struct VolumeState {
    pub percent: u8,
    pub muted: bool,
}

#[derive(Debug, Clone)]
pub struct CellularState {
    pub present: bool,
    pub signal_quality: u8,
    pub operator: String,
    pub registration: CellularRegistration,
    pub sim_locked: bool,
    pub data_enabled: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CellularRegistration {
    Unknown,
    Idle,
    Home,
    Searching,
    Denied,
    Roaming,
}

#[derive(Debug, Clone)]
pub struct VpnState {
    pub active: bool,
}

/// Hardware Kill Switch state (Librem 5).
#[derive(Debug, Clone, Default)]
pub struct HksState {
    /// WiFi/Bluetooth kill switch is active (hardware-blocked).
    pub wlan_blocked: bool,
    /// Cellular modem kill switch is active.
    pub wwan_blocked: bool,
    /// Camera/microphone kill switch is active.
    pub camera_blocked: bool,
}

#[derive(Debug, Clone)]
pub struct SystemState {
    pub battery: BatteryState,
    pub wifi: WifiState,
    pub bluetooth_on: bool,
    pub volume: VolumeState,
    pub cellular: CellularState,
    pub vpn: VpnState,
    pub caffeine_enabled: bool,
    pub night_light_on: bool,
    pub dark_mode: bool,
    pub location_enabled: bool,
    pub airplane_mode: bool,
    pub hostname: String,
    /// True when laptop lid is closed (None if no lid detected).
    pub lid_closed: Option<bool>,
    /// Hardware Kill Switch state (Librem 5).
    pub hks: HksState,
    /// Caffeine auto-disable deadline (if duration-limited).
    pub caffeine_deadline: Option<Instant>,
    last_poll: Instant,
}

impl Default for SystemState
{
    fn default() -> Self
    {
        Self {
            battery: BatteryState { percent: 0, charging: false, present: false },
            wifi: WifiState { connected: false },
            bluetooth_on: false,
            volume: VolumeState { percent: 50, muted: false },
            cellular: CellularState {
                present: false,
                signal_quality: 0,
                operator: String::new(),
                registration: CellularRegistration::Unknown,
                sim_locked: false,
                data_enabled: false,
            },
            vpn: VpnState { active: false },
            caffeine_enabled: false,
            night_light_on: false,
            dark_mode: false,
            location_enabled: false,
            airplane_mode: false,
            hostname: String::new(),
            lid_closed: None,
            hks: HksState::default(),
            caffeine_deadline: None,
            last_poll: Instant::now() - std::time::Duration::from_secs( POLL_INTERVAL_SECS + 1 ),
        }
    }
}

impl SystemState {
    /// Build the initial state forge starts with. Reads `dark_mode` synchronously
    /// from gsettings so the first render already picks the right theme variant
    /// instead of rendering in the default (light) mode for 30 s until the
    /// background sysmon thread completes its first poll. All other fields are
    /// left at `Default` — they're cheap to be wrong for one tick because their
    /// consumers tolerate the values we get from the upcoming background poll.
    pub fn bootstrap() -> Self {
        let mut s = Self::default();
        s.dark_mode = Self::read_dark_mode();
        s
    }

    /// Returns true if enough time has elapsed to warrant a new poll.
    pub fn should_poll( &self ) -> bool
    {
        self.last_poll.elapsed().as_secs() >= POLL_INTERVAL_SECS
    }

    /// Run all blocking system queries and return the result.
    /// Must be called from a background thread — never from the event loop.
    ///
    /// `dark_mode` is deliberately NOT polled here: crustace is the
    /// authoritative source via the `set_theme` IPC action (CLI `--dark` /
    /// `--light`, `config.shell.theme`, Auto sunrise/sunset tick, and the
    /// Quick Settings Dark toggle all funnel through it). Polling gsettings
    /// on a 30 s interval would clobber whatever crustace pushed us every
    /// minute, flipping the topbar palette back under the user — which is
    /// exactly the regression reported on the Librem after the initial IPC
    /// patch. `bootstrap` still reads gsettings once so the very first
    /// frame has a sensible default before crustace's set_theme arrives.
    pub fn poll_snapshot() -> Self
    {
        let battery    = Self::read_battery();
        let wifi       = Self::read_wifi();
        let bluetooth_on = Self::read_bluetooth();
        let volume     = Self::read_volume();
        let cellular   = Self::read_cellular();
        let vpn        = Self::read_vpn();
        let lid_closed = Self::read_lid_state();
        let hks        = Self::read_hks();

        Self {
            battery,
            wifi,
            bluetooth_on,
            volume,
            cellular,
            vpn,
            lid_closed,
            hks,
            last_poll: Instant::now(),
            // Fields managed by the main thread are left at defaults here;
            // apply() merges them back from the live state.
            ..Self::default()
        }
    }

    /// Read the desktop color scheme from gsettings. Returns true for
    /// `prefer-dark`, false otherwise. Matches how crustace writes it via
    /// `gsettings set org.gnome.desktop.interface color-scheme ...`.
    fn read_dark_mode() -> bool {
        let output = Command::new("gsettings")
            .args(["get", "org.gnome.desktop.interface", "color-scheme"])
            .output();
        match output {
            Ok(o) if o.status.success() => {
                let s = String::from_utf8_lossy(&o.stdout);
                s.contains("prefer-dark")
            }
            _ => false,
        }
    }

    /// Merge a completed background poll into the live state.
    /// Preserves main-thread-managed fields (night_light_on, caffeine_*,
    /// location_enabled, airplane_mode, hostname).
    pub fn apply( &mut self, snapshot: Self )
    {
        self.battery      = snapshot.battery;
        self.wifi         = snapshot.wifi;
        self.bluetooth_on = snapshot.bluetooth_on;
        self.volume       = snapshot.volume;
        self.cellular     = snapshot.cellular;
        self.vpn          = snapshot.vpn;
        self.lid_closed   = snapshot.lid_closed;
        self.hks          = snapshot.hks;
        // `dark_mode` intentionally not copied — crustace owns it via IPC.
        self.last_poll    = snapshot.last_poll;

        // Auto-disable caffeine if its deadline has passed.
        if let Some( deadline ) = self.caffeine_deadline
        {
            if Instant::now() >= deadline
            {
                self.caffeine_enabled  = false;
                self.caffeine_deadline = None;
            }
        }
    }

    fn read_battery() -> BatteryState {
        // Aggregate every device of type "Battery" so multi-battery systems
        // report a combined charge weighted by each pack's real capacity.
        let entries = match std::fs::read_dir("/sys/class/power_supply") {
            Ok(e) => e,
            Err(_) => return BatteryState { percent: 0, charging: false, present: false },
        };

        let mut total_now: u64 = 0;
        let mut total_full: u64 = 0;
        let mut cap_sum: u64 = 0;
        let mut cap_count: u64 = 0;
        let mut charging = false;
        let mut present = false;

        for entry in entries.flatten() {
            let path = entry.path();
            let is_battery = std::fs::read_to_string(path.join("type"))
                .map(|t| t.trim() == "Battery")
                .unwrap_or(false);
            if !is_battery {
                continue;
            }

            // Skip peripheral packs (mice, controllers, headsets); they
            // report scope=Device and would pollute the system total.
            let scope = std::fs::read_to_string(path.join("scope")).unwrap_or_default();
            if scope.trim() == "Device" {
                continue;
            }

            let cap = std::fs::read_to_string(path.join("capacity"))
                .ok()
                .and_then(|s| s.trim().parse::<u64>().ok());
            if cap.is_none() {
                continue;
            }

            present = true;

            let status = std::fs::read_to_string(path.join("status")).unwrap_or_default();
            let st = status.trim();
            if st == "Charging" || st == "Full" {
                charging = true;
            }

            let read = |name: &str| -> Option<u64> {
                std::fs::read_to_string(path.join(name))
                    .ok()
                    .and_then(|s| s.trim().parse::<u64>().ok())
            };
            let now = read("energy_now").or_else(|| read("charge_now"));
            let full = read("energy_full").or_else(|| read("charge_full"));
            if let (Some(now), Some(full)) = (now, full) {
                if full > 0 {
                    total_now += now;
                    total_full += full;
                    continue;
                }
            }

            cap_sum += cap.unwrap();
            cap_count += 1;
        }

        if !present {
            return BatteryState { percent: 0, charging: false, present: false };
        }

        let percent = if total_full > 0 {
            (total_now * 100 / total_full).min(100) as u8
        } else if cap_count > 0 {
            (cap_sum / cap_count).min(100) as u8
        } else {
            0
        };

        BatteryState { percent, charging, present }
    }

    fn read_wifi() -> WifiState {
        let output = Command::new("nmcli")
            .args(["-t", "-f", "active,ssid", "dev", "wifi"])
            .output();

        match output {
            Ok(out) => {
                let stdout = String::from_utf8_lossy(&out.stdout);
                for line in stdout.lines() {
                    if line.starts_with("yes:") {
                        return WifiState { connected: true };
                    }
                }
                WifiState { connected: false }
            },
            Err(_) => WifiState { connected: false },
        }
    }

    fn read_bluetooth() -> bool {
        Command::new("bluetoothctl")
            .args(["show"])
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).contains("Powered: yes"))
            .unwrap_or(false)
    }

    fn read_volume() -> VolumeState {
        let vol_output = Command::new("pactl")
            .args(["get-sink-volume", "@DEFAULT_SINK@"])
            .output();

        let percent = vol_output
            .ok()
            .and_then(|o| {
                let s = String::from_utf8_lossy(&o.stdout);
                s.split('%')
                    .next()
                    .and_then(|p| p.split_whitespace().last())
                    .and_then(|v| v.parse::<u8>().ok())
            })
            .unwrap_or(50);

        let muted = Command::new("pactl")
            .args(["get-sink-mute", "@DEFAULT_SINK@"])
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).contains("yes"))
            .unwrap_or(false);

        VolumeState { percent, muted }
    }

    fn read_cellular() -> CellularState {
        // Try mmcli to get modem info
        let output = Command::new("mmcli").args(["-m", "0", "-J"]).output();

        match output {
            Ok(out) if out.status.success() => {
                let stdout = String::from_utf8_lossy(&out.stdout);
                Self::parse_mmcli_json(&stdout)
            }
            _ => CellularState {
                present: false,
                signal_quality: 0,
                operator: String::new(),
                registration: CellularRegistration::Unknown,
                sim_locked: false,
                data_enabled: false,
            },
        }
    }

    fn parse_mmcli_json(json_str: &str) -> CellularState {
        // Parse mmcli JSON output for modem status
        let mut state = CellularState {
            present: true,
            signal_quality: 0,
            operator: String::new(),
            registration: CellularRegistration::Unknown,
            sim_locked: false,
            data_enabled: false,
        };

        // Simple JSON field extraction without a full JSON parser dep
        for line in json_str.lines() {
            let line = line.trim();
            if line.contains("\"signal-quality\"") {
                if let Some(val) = extract_json_number(line) {
                    state.signal_quality = val.min(100) as u8;
                }
            } else if line.contains("\"operator-name\"") {
                if let Some(val) = extract_json_string(line) {
                    state.operator = val;
                }
            } else if line.contains("\"m3gpp-registration-state\"") {
                if let Some(val) = extract_json_string(line) {
                    state.registration = match val.as_str() {
                        "home" => CellularRegistration::Home,
                        "roaming" => CellularRegistration::Roaming,
                        "searching" => CellularRegistration::Searching,
                        "denied" => CellularRegistration::Denied,
                        "idle" => CellularRegistration::Idle,
                        _ => CellularRegistration::Unknown,
                    };
                }
            } else if line.contains("\"sim-missing\"") || line.contains("\"lock\"") {
                if let Some(val) = extract_json_string(line) {
                    if val.contains("sim-pin") || val.contains("sim-puk") {
                        state.sim_locked = true;
                    }
                }
            }
        }

        // Check data connection via nmcli
        state.data_enabled = Command::new("nmcli")
            .args(["-t", "-f", "TYPE,STATE", "connection", "show", "--active"])
            .output()
            .map(|o| String::from_utf8_lossy(&o.stdout).contains("gsm:activated"))
            .unwrap_or(false);

        state
    }

    fn read_vpn() -> VpnState {
        let output = Command::new("nmcli")
            .args(["-t", "-f", "TYPE,NAME,STATE", "connection", "show", "--active"])
            .output();

        match output {
            Ok(out) => {
                let stdout = String::from_utf8_lossy(&out.stdout);
                for line in stdout.lines() {
                    // Format: vpn:MyVPN:activated or wireguard:WG0:activated
                    let parts: Vec<&str> = line.split(':').collect();
                    if parts.len() >= 2
                        && (parts[0] == "vpn" || parts[0] == "wireguard" || parts[0] == "tun")
                    {
                        return VpnState { active: true };
                    }
                }
                VpnState { active: false }
            }
            Err(_) => VpnState { active: false },
        }
    }

    /// Lightweight lid check for use when display is off (no subprocess spawning).
    pub fn read_lid_state_only(&self) -> Option<bool> {
        Self::read_lid_state()
    }

    fn read_lid_state() -> Option<bool> {
        // Try ACPI lid state
        let paths = [
            "/proc/acpi/button/lid/LID0/state",
            "/proc/acpi/button/lid/LID/state",
        ];
        for path in paths {
            if let Ok(content) = std::fs::read_to_string(path) {
                return Some(content.contains("closed"));
            }
        }
        None
    }

    /// Read Hardware Kill Switch state from sysfs (Librem 5).
    fn read_hks() -> HksState {
        let mut state = HksState::default();

        // Iterate rfkill entries to check for hard blocks
        if let Ok(entries) = std::fs::read_dir("/sys/class/rfkill") {
            for entry in entries.flatten() {
                let path = entry.path();
                let rfkill_type = std::fs::read_to_string(path.join("type")).unwrap_or_default();
                let hard_blocked = std::fs::read_to_string(path.join("hard"))
                    .unwrap_or_default()
                    .trim()
                    == "1";

                if !hard_blocked {
                    continue;
                }

                let rfkill_type = rfkill_type.trim();
                match rfkill_type {
                    "wlan" | "bluetooth" => state.wlan_blocked = true,
                    "wwan" => state.wwan_blocked = true,
                    _ => {}
                }
            }
        }

        // Camera/mic: check if /dev/video0 is absent
        state.camera_blocked = !std::path::Path::new("/dev/video0").exists();

        state
    }

}

/// Extract a number value from a JSON line like `"signal-quality" : 85,`
fn extract_json_number(line: &str) -> Option<u64> {
    let colon_idx = line.find(':')?;
    let val_part = line[colon_idx + 1..].trim().trim_end_matches(',').trim();
    // Could be inside a tuple like "(85, true)" — take first number
    let val_part = val_part.trim_start_matches('(').trim_start_matches('[');
    val_part.split(|c: char| !c.is_ascii_digit()).next()?.parse().ok()
}

/// Extract a string value from a JSON line like `"operator-name" : "T-Mobile",`
fn extract_json_string(line: &str) -> Option<String> {
    let colon_idx = line.find(':')?;
    let val_part = line[colon_idx + 1..].trim();
    let val_part = val_part.trim_start_matches('"');
    let end = val_part.find('"')?;
    Some(val_part[..end].to_string())
}
