use std::collections::{BTreeMap, BTreeSet};

use evdev::{AbsoluteAxisCode, Device, EventType, KeyCode, RelativeAxisCode};
use serde::{Deserialize, Serialize};
use serde_json::Value;

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeviceCatalog {
    pub cameras: Vec<String>,
    pub camera_modes: BTreeMap<String, Vec<CameraMode>>,
    pub microphones: Vec<String>,
    pub speakers: Vec<String>,
    pub keyboards: Vec<String>,
    pub mice: Vec<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CameraMode {
    pub width: u32,
    pub height: u32,
    pub fps: u32,
}

impl CameraMode {
    pub const fn new(width: u32, height: u32, fps: u32) -> Self {
        Self { width, height, fps }
    }

    pub fn id(self) -> String {
        format!("{}x{}@{}", self.width, self.height, self.fps)
    }

    pub fn from_id(raw: &str) -> Option<Self> {
        let (size, fps) = raw.split_once('@')?;
        let (width, height) = size.split_once('x')?;
        Some(Self {
            width: width.parse().ok()?,
            height: height.parse().ok()?,
            fps: fps.parse().ok()?,
        })
        .filter(|mode| mode.width > 0 && mode.height > 0 && mode.fps > 0)
    }

    pub fn short_label(self) -> String {
        format!("{}p@{}", self.height, self.fps)
    }

    pub fn h264_bitrate_kbit(self) -> u32 {
        if self.width >= 1920 || self.height >= 1080 {
            12_000
        } else if self.width >= 1280 || self.height >= 720 {
            6_000
        } else {
            3_000
        }
    }
}

impl DeviceCatalog {
    pub fn discover() -> Self {
        Self::discover_with_camera_override(std::env::var("LESAVKA_LAUNCHER_CAMERA_DIR").ok())
    }

    pub fn is_empty(&self) -> bool {
        self.cameras.is_empty()
            && self.microphones.is_empty()
            && self.speakers.is_empty()
            && self.keyboards.is_empty()
            && self.mice.is_empty()
    }

    fn discover_with_camera_override(override_dir: Option<String>) -> Self {
        let cameras = discover_camera_devices(override_dir);
        let camera_modes = discover_camera_modes(&cameras);
        let microphones = discover_microphone_devices();
        let speakers = discover_speaker_devices();
        let keyboards = discover_input_devices(InputDeviceKind::Keyboard);
        let mice = discover_input_devices(InputDeviceKind::Mouse);
        Self {
            cameras,
            camera_modes,
            microphones,
            speakers,
            keyboards,
            mice,
        }
    }
}

fn discover_microphone_devices() -> Vec<String> {
    let mut set = BTreeSet::new();
    for source in discover_pactl_devices("sources") {
        if !source.ends_with(".monitor") {
            set.insert(source);
        }
    }
    for source in discover_pipewire_audio_nodes("Audio/Source") {
        if !source.ends_with(".monitor") {
            set.insert(source);
        }
    }
    set.into_iter().collect()
}

fn dedupe_camera_devices(names: impl IntoIterator<Item = String>) -> Vec<String> {
    let mut by_physical_device: BTreeMap<String, String> = BTreeMap::new();
    for name in names {
        let key = camera_physical_key(&name);
        match by_physical_device.get(&key) {
            Some(existing) if camera_device_rank(existing) <= camera_device_rank(&name) => {}
            _ => {
                by_physical_device.insert(key, name);
            }
        }
    }
    by_physical_device.into_values().collect()
}

fn camera_physical_key(name: &str) -> String {
    let trimmed = name.trim();
    trimmed
        .strip_prefix("usb-")
        .unwrap_or(trimmed)
        .split("-video-index")
        .next()
        .unwrap_or(trimmed)
        .to_ascii_lowercase()
}

fn camera_device_rank(name: &str) -> u8 {
    if name.contains("-video-index0") {
        0
    } else if name.contains("-video-index") {
        1
    } else {
        2
    }
}

fn discover_speaker_devices() -> Vec<String> {
    let mut set = BTreeSet::new();
    for sink in discover_pactl_devices("sinks") {
        set.insert(sink);
    }
    for sink in discover_pipewire_audio_nodes("Audio/Sink") {
        set.insert(sink);
    }
    set.into_iter().collect()
}

fn discover_camera_devices(override_dir: Option<String>) -> Vec<String> {
    let dir = override_dir.unwrap_or_else(|| "/dev/v4l/by-id".to_string());
    let Ok(iter) = std::fs::read_dir(dir) else {
        return Vec::new();
    };

    let mut set = BTreeSet::new();
    for entry in iter.flatten() {
        let path = entry.path();
        if let Some(name) = path.file_name() {
            set.insert(name.to_string_lossy().to_string());
        }
    }
    dedupe_camera_devices(set)
}

fn discover_camera_modes(cameras: &[String]) -> BTreeMap<String, Vec<CameraMode>> {
    cameras
        .iter()
        .filter_map(|camera| {
            let modes = discover_camera_modes_for(camera);
            (!modes.is_empty()).then(|| (camera.clone(), modes))
        })
        .collect()
}

fn discover_camera_modes_for(camera: &str) -> Vec<CameraMode> {
    let device = if camera.starts_with("/dev/") {
        camera.to_string()
    } else {
        format!("/dev/v4l/by-id/{camera}")
    };
    let output = std::process::Command::new("v4l2-ctl")
        .args(["--list-formats-ext", "-d", &device])
        .output();

    let Ok(output) = output else {
        return Vec::new();
    };
    if !output.status.success() {
        return Vec::new();
    }
    parse_supported_camera_modes(&String::from_utf8_lossy(&output.stdout))
}

pub fn lesavka_supported_camera_modes() -> Vec<CameraMode> {
    vec![
        CameraMode::new(1920, 1080, 30),
        CameraMode::new(1920, 1080, 20),
        CameraMode::new(1280, 720, 30),
        CameraMode::new(1280, 720, 20),
    ]
}

pub fn parse_supported_camera_modes(stdout: &str) -> Vec<CameraMode> {
    let mut discovered = BTreeSet::new();
    let mut current_size = None::<(u32, u32)>;

    for line in stdout.lines() {
        let trimmed = line.trim();
        if let Some(size) = parse_v4l2_size_line(trimmed) {
            current_size = Some(size);
            continue;
        }
        if trimmed.starts_with("Interval:")
            && let Some((width, height)) = current_size
            && let Some(fps) = parse_v4l2_interval_fps(trimmed)
        {
            discovered.insert(CameraMode::new(width, height, fps));
        }
    }

    lesavka_supported_camera_modes()
        .into_iter()
        .filter(|supported| {
            discovered.iter().any(|actual| {
                actual.width == supported.width
                    && actual.height == supported.height
                    && actual.fps == supported.fps
            })
        })
        .collect()
}

fn parse_v4l2_size_line(line: &str) -> Option<(u32, u32)> {
    let (_, rest) = line.split_once("Size:")?;
    let token = rest.split_whitespace().find(|part| part.contains('x'))?;
    let (width, height) = token.split_once('x')?;
    Some((width.parse().ok()?, height.parse().ok()?))
        .filter(|(width, height)| *width > 0 && *height > 0)
}

fn parse_v4l2_interval_fps(line: &str) -> Option<u32> {
    let (_, rest) = line.split_once('(')?;
    let (fps, _) = rest.split_once(" fps")?;
    let rounded = fps.parse::<f32>().ok()?.round() as u32;
    (rounded > 0).then_some(rounded)
}

fn discover_pactl_devices(kind: &str) -> Vec<String> {
    let output = std::process::Command::new("pactl")
        .args(["list", "short", kind])
        .output();

    let Ok(output) = output else {
        return Vec::new();
    };

    if !output.status.success() {
        return Vec::new();
    }

    parse_pactl_short(&String::from_utf8_lossy(&output.stdout))
}

pub fn parse_pactl_short(stdout: &str) -> Vec<String> {
    let mut set = BTreeSet::new();
    for line in stdout.lines() {
        let mut cols = line.split_whitespace();
        let _id = cols.next();
        if let Some(name) = cols.next() {
            set.insert(name.to_string());
        }
    }
    set.into_iter().collect()
}

fn discover_pipewire_audio_nodes(media_class: &str) -> Vec<String> {
    let output = std::process::Command::new("pw-dump").output();
    let Ok(output) = output else {
        return Vec::new();
    };
    if !output.status.success() {
        return Vec::new();
    }
    parse_pipewire_audio_nodes(&output.stdout, media_class)
}

pub fn parse_pipewire_audio_nodes(stdout: &[u8], media_class: &str) -> Vec<String> {
    let Ok(Value::Array(objects)) = serde_json::from_slice::<Value>(stdout) else {
        return Vec::new();
    };

    let mut set = BTreeSet::new();
    for object in objects {
        let Some(props) = object
            .get("info")
            .and_then(|info| info.get("props"))
            .and_then(Value::as_object)
        else {
            continue;
        };
        if props.get("media.class").and_then(Value::as_str) != Some(media_class) {
            continue;
        }
        let Some(name) = props
            .get("node.name")
            .or_else(|| props.get("node.nick"))
            .or_else(|| props.get("device.name"))
            .and_then(Value::as_str)
        else {
            continue;
        };
        if !name.trim().is_empty() {
            set.insert(name.to_string());
        }
    }
    set.into_iter().collect()
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InputDeviceKind {
    Keyboard,
    Mouse,
}

fn discover_input_devices(kind: InputDeviceKind) -> Vec<String> {
    let Ok(iter) = std::fs::read_dir("/dev/input") else {
        return Vec::new();
    };

    let mut set = BTreeSet::new();
    for entry in iter.flatten() {
        let path = entry.path();
        if !path
            .file_name()
            .is_some_and(|name| name.to_string_lossy().starts_with("event"))
        {
            continue;
        }
        let Ok(device) = Device::open(&path) else {
            continue;
        };
        if classify_input_device(&device) == Some(kind) {
            set.insert(path.to_string_lossy().to_string());
        }
    }

    set.into_iter().collect()
}

fn classify_input_device(device: &Device) -> Option<InputDeviceKind> {
    let events = device.supported_events();

    if events.contains(EventType::KEY)
        && device
            .supported_keys()
            .is_some_and(|keys| keys.contains(KeyCode::KEY_A) || keys.contains(KeyCode::KEY_ENTER))
    {
        return Some(InputDeviceKind::Keyboard);
    }

    if events.contains(EventType::RELATIVE)
        && let (Some(rel), Some(keys)) = (device.supported_relative_axes(), device.supported_keys())
        && rel.contains(RelativeAxisCode::REL_X)
        && rel.contains(RelativeAxisCode::REL_Y)
        && (keys.contains(KeyCode::BTN_LEFT) || keys.contains(KeyCode::BTN_RIGHT))
    {
        return Some(InputDeviceKind::Mouse);
    }

    if events.contains(EventType::ABSOLUTE)
        && let (Some(abs), Some(keys)) = (device.supported_absolute_axes(), device.supported_keys())
        && ((abs.contains(AbsoluteAxisCode::ABS_X) && abs.contains(AbsoluteAxisCode::ABS_Y))
            || (abs.contains(AbsoluteAxisCode::ABS_MT_POSITION_X)
                && abs.contains(AbsoluteAxisCode::ABS_MT_POSITION_Y)))
        && (keys.contains(KeyCode::BTN_TOUCH) || keys.contains(KeyCode::BTN_LEFT))
    {
        return Some(InputDeviceKind::Mouse);
    }

    None
}

#[cfg(test)]
#[path = "tests/devices.rs"]
mod tests;
