use clap::{builder::TypedValueParser, ArgGroup, Parser, Subcommand, ValueEnum};
use litra::{Device, DeviceError, DeviceHandle, DeviceResult, DeviceType, Litra};
use serde::Serialize;
use std::fmt;
use std::process::ExitCode;
use std::str::FromStr;

#[cfg(feature = "cli")]
use tabled::{Table, Tabled};

// Custom parser for DeviceType
#[derive(Debug, Clone)]
struct DeviceTypeValueParser;

impl TypedValueParser for DeviceTypeValueParser {
    type Value = DeviceType;

    fn parse_ref(
        &self,
        _cmd: &clap::Command,
        arg: Option<&clap::Arg>,
        value: &std::ffi::OsStr,
    ) -> Result<Self::Value, clap::Error> {
        let value_str = value.to_string_lossy();
        DeviceType::from_str(&value_str).map_err(|_| {
            let mut err = clap::Error::new(clap::error::ErrorKind::InvalidValue);
            if let Some(arg) = arg {
                err.insert(
                    clap::error::ContextKind::InvalidArg,
                    clap::error::ContextValue::String(arg.to_string()),
                );
            }
            err.insert(
                clap::error::ContextKind::Custom,
                clap::error::ContextValue::String(format!("Invalid device type: {}", value_str)),
            );
            err
        })
    }
}

#[cfg(feature = "mcp")]
mod mcp;

/// Control your USB-connected Logitech Litra lights from the command line
#[cfg(feature = "cli")]
#[derive(Debug, Parser)]
#[clap(name = "litra", version)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

const SERIAL_NUMBER_ARGUMENT_HELP: &str = "Specify the device to target by its serial number";
const DEVICE_PATH_ARGUMENT_HELP: &str =
    "Specify the device to target by its path (useful for devices that don't show a serial number)";
const DEVICE_TYPE_ARGUMENT_HELP: &str =
    "Specify the device to target by its type (`glow`, `beam` or `beam_lx`)";

/// Named colors for the back-color command
#[cfg(feature = "cli")]
#[derive(Debug, Clone, Copy, ValueEnum)]
enum NamedColor {
    Red,
    Green,
    Blue,
    Yellow,
    Orange,
    Purple,
    Pink,
    Cyan,
    White,
    Magenta,
}

#[cfg(feature = "cli")]
impl NamedColor {
    fn to_hex(self) -> &'static str {
        match self {
            NamedColor::Red => "FF0000",
            NamedColor::Green => "00FF00",
            NamedColor::Blue => "0000FF",
            NamedColor::Yellow => "FFFF00",
            NamedColor::Orange => "FFA500",
            NamedColor::Purple => "800080",
            NamedColor::Pink => "FFC0CB",
            NamedColor::Cyan => "00FFFF",
            NamedColor::White => "FFFFFF",
            NamedColor::Magenta => "FF00FF",
        }
    }
}

#[cfg(feature = "cli")]
#[derive(Debug, Subcommand)]
enum Commands {
    /// Turn your Logitech Litra device on. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    On {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
    },
    /// Turn your Logitech Litra device off. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    Off {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
    },
    /// Toggles your Logitech Litra device on or off. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    Toggle {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
    },
    /// Sets the brightness of your Logitech Litra device. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    #[clap(group = ArgGroup::new("brightness").required(true).multiple(false))]
    Brightness {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
        #[clap(
            long,
            short,
            help = "The brightness to set, measured in lumens. This can be set to any value between the minimum and maximum for the device returned by the `devices` command.",
            group = "brightness"
        )]
        value: Option<u16>,
        #[clap(
            long,
            short('b'),
            help = "The brightness to set, as a percentage of the maximum brightness",
            group = "brightness",
            value_parser = clap::value_parser!(u8).range(1..=100)
        )]
        percentage: Option<u8>,
    },
    /// Increases the brightness of your Logitech Litra device. The command will error if trying to increase the brightness beyond the device's maximum. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    #[clap(group = ArgGroup::new("brightness-up").required(true).multiple(false))]
    BrightnessUp {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
        #[clap(
            long,
            short,
            help = "The amount to increase the brightness by, measured in lumens.",
            group = "brightness-up"
        )]
        value: Option<u16>,
        #[clap(
            long,
            short,
            help = "The number of percentage points to increase the brightness by",
            group = "brightness-up",
            value_parser = clap::value_parser!(u8).range(1..=100)
        )]
        percentage: Option<u8>,
    },
    /// Decreases the brightness of your Logitech Litra device. The command will error if trying to decrease the brightness below the device's minimum. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    #[clap(group = ArgGroup::new("brightness-down").required(true).multiple(false))]
    BrightnessDown {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
        #[clap(
            long,
            short,
            help = "The amount to decrease the brightness by, measured in lumens.",
            group = "brightness-down"
        )]
        value: Option<u16>,
        #[clap(
            long,
            short,
            help = "The number of percentage points to reduce the brightness by",
            group = "brightness-down",
            value_parser = clap::value_parser!(u8).range(1..=100)
        )]
        percentage: Option<u8>,
    },
    /// Sets the temperature of your Logitech Litra device. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    Temperature {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
        #[clap(
            long,
            short,
            help = "The temperature to set, measured in Kelvin. This can be set to any multiple of 100 between the minimum and maximum for the device returned by the `devices` command."
        )]
        value: u16,
    },
    /// Increases the temperature of your Logitech Litra device. The command will error if trying to increase the temperature beyond the device's maximum. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    TemperatureUp {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
        #[clap(
            long,
            short,
            help = "The amount to increase the temperature by, measured in Kelvin. This must be a multiple of 100."
        )]
        value: u16,
    },
    /// Decreases the temperature of your Logitech Litra device. The command will error if trying to decrease the temperature below the device's minimum. By default, all devices are targeted, unless one or more devices are specified with --device-type, --serial-number or --device-path.
    TemperatureDown {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with_all = ["device_path", "device_type"]
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with_all = ["serial_number", "device_type"]
        )]
        device_path: Option<String>,
        #[clap(long, short('t'), help = DEVICE_TYPE_ARGUMENT_HELP, value_parser = DeviceTypeValueParser, conflicts_with_all = ["serial_number", "device_path"])]
        device_type: Option<DeviceType>,
        #[clap(
            long,
            short,
            help = "The amount to decrease the temperature by, measured in Kelvin. This must be a multiple of 100."
        )]
        value: u16,
    },
    /// Set the color of one or more zones on the back of your Logitech Litra Beam LX device. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    #[clap(group = ArgGroup::new("color-input").required(true).multiple(false))]
    BackColor {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
        #[clap(
            long,
            short,
            help = "The hexadecimal color code to use (e.g. FF0000 for red). Either --value or --color must be specified.",
            group = "color-input"
        )]
        value: Option<String>,
        #[clap(
            long,
            short,
            help = "A named color to use. Either --value or --color must be specified.",
            group = "color-input"
        )]
        color: Option<NamedColor>,
        #[clap(
            long,
            short('z'),
            help = "The zone of the light to control, numbered 1 to 7 from left to right. If not specified, all zones will be targeted."
        )]
        zone: Option<u8>,
    },
    /// Set the brightness of the colorful backlight on your Logitech Litra Beam LX device. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    BackBrightness {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
        #[clap(
            long,
            short('b'),
            help = "The brightness to set, as a percentage of the maximum brightness",
            value_parser = clap::value_parser!(u8).range(1..=100)
        )]
        percentage: u8,
    },
    /// Turn off the colorful backlight on your Logitech Litra Beam LX device. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    BackOff {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
    },
    /// Turn on the colorful backlight on your Logitech Litra Beam LX device. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    BackOn {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
    },
    /// Toggles the colorful backlight on your Logitech Litra Beam LX device on or off. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    BackToggle {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
    },
    /// Increases the brightness of the colorful backlight on your Logitech Litra Beam LX device. The command will error if trying to increase the brightness beyond 100%. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    BackBrightnessUp {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
        #[clap(
            long,
            short('b'),
            help = "The number of percentage points to increase the brightness by",
            value_parser = clap::value_parser!(u8).range(1..=100)
        )]
        percentage: u8,
    },
    /// Decreases the brightness of the colorful backlight on your Logitech Litra Beam LX device. The command will error if trying to decrease the brightness below 1%. By default, all Litra Beam LX devices are targeted, unless a specific device is specified with --serial-number or --device-path.
    BackBrightnessDown {
        #[clap(
            long,
            short,
            help = SERIAL_NUMBER_ARGUMENT_HELP,
            conflicts_with = "device_path"
        )]
        serial_number: Option<String>,
        #[clap(
            long,
            short('p'),
            help = DEVICE_PATH_ARGUMENT_HELP,
            conflicts_with = "serial_number"
        )]
        device_path: Option<String>,
        #[clap(
            long,
            short('b'),
            help = "The number of percentage points to decrease the brightness by",
            value_parser = clap::value_parser!(u8).range(1..=100)
        )]
        percentage: u8,
    },
    /// List Logitech Litra devices connected to your computer
    Devices {
        #[clap(long, short, action, help = "Return the results in JSON format")]
        json: bool,
    },
    /// Start a MCP (Model Context Protocol) server for controlling Litra devices
    #[cfg(feature = "mcp")]
    Mcp,
}

fn percentage_within_range(percentage: u32, start_range: u32, end_range: u32) -> u32 {
    // Handle edge cases: 0% should return exactly start_range, 100% should return exactly end_range
    if percentage == 0 {
        return start_range;
    }
    if percentage == 100 {
        return end_range;
    }

    // For values between 0 and 100, use ceiling to ensure 1% is always > 0%
    // This fixes the bug where small percentages would round back to the minimum
    let range = end_range as f64 - start_range as f64;
    let result = (percentage as f64 / 100.0) * range + start_range as f64;
    result.ceil() as u32
}

fn get_is_on_text(is_on: bool) -> &'static str {
    if is_on {
        "On"
    } else {
        "Off"
    }
}

fn get_is_on_emoji(is_on: bool) -> &'static str {
    if is_on {
        "💡"
    } else {
        "🌑"
    }
}

fn get_is_back_on_emoji(is_on: bool) -> &'static str {
    if is_on {
        "🌈"
    } else {
        "🌑"
    }
}

fn check_device_filters<'a>(
    _context: &'a Litra,
    _serial_number: Option<&'a str>,
    device_path: Option<&'a str>,
    device_type: Option<&'a DeviceType>,
) -> impl Fn(&Device) -> bool + 'a {
    move |device| {
        // Check device path if specified
        if let Some(path) = device_path {
            return device.device_path() == path;
        }

        // Check device type if specified
        if let Some(expected_type) = device_type {
            if device.device_type() != *expected_type {
                return false;
            }
        }

        // If a serial number is specified, we'll filter by it after opening the device
        // since serial numbers are only accessible after opening
        true
    }
}

#[derive(Debug)]
enum CliError {
    DeviceError(DeviceError),
    SerializationFailed(serde_json::Error),
    DeviceNotFound,
    MCPError(String),
}

impl fmt::Display for CliError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CliError::DeviceError(error) => error.fmt(f),
            CliError::SerializationFailed(error) => error.fmt(f),
            CliError::DeviceNotFound => write!(f, "Device not found."),
            CliError::MCPError(message) => write!(f, "MCP server error: {}", message),
        }
    }
}

impl From<DeviceError> for CliError {
    fn from(error: DeviceError) -> Self {
        CliError::DeviceError(error)
    }
}

type CliResult = Result<(), CliError>;

/// Get all devices matching the given filters
fn get_all_supported_devices(
    context: &Litra,
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
) -> Result<Vec<DeviceHandle>, CliError> {
    // Filter by various criteria
    let potential_devices: Vec<Device> = context
        .get_connected_devices()
        .filter(check_device_filters(
            context,
            serial_number,
            device_path,
            device_type,
        ))
        .collect();

    // If we need to filter by serial, open devices and check
    if let Some(serial) = serial_number {
        let mut handles = Vec::new();
        for device in potential_devices {
            if let Ok(handle) = device.open(context) {
                if let Ok(Some(actual_serial)) = handle.serial_number() {
                    if actual_serial == serial {
                        handles.push(handle);
                    }
                }
            }
        }
        Ok(handles)
    } else {
        // No serial filter, include all devices that matched the other filters
        Ok(potential_devices
            .into_iter()
            .filter_map(|dev| dev.open(context).ok())
            .collect())
    }
}

/// Apply a command to device(s)
fn with_device<F>(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    callback: F,
) -> CliResult
where
    F: Fn(&DeviceHandle) -> DeviceResult<()>,
{
    let context = Litra::new()?;

    let devices = get_all_supported_devices(&context, serial_number, device_path, device_type)?;
    if devices.is_empty() {
        return Err(CliError::DeviceNotFound);
    }

    for device_handle in devices {
        // Ignore device-specific errors (e.g. unsupported device type) but propagate
        // validation errors (e.g. invalid brightness) since those indicate user input errors
        if let Err(e) = callback(&device_handle) {
            if is_user_input_error(&e) {
                return Err(e.into());
            }
        }
    }
    Ok(())
}

/// Returns true if the error is caused by invalid user input and should be propagated
fn is_user_input_error(error: &DeviceError) -> bool {
    matches!(
        error,
        DeviceError::InvalidBrightness(_)
            | DeviceError::InvalidTemperature(_)
            | DeviceError::InvalidZone(_)
            | DeviceError::InvalidColor(_)
            | DeviceError::InvalidPercentage(_)
    )
}

#[cfg_attr(feature = "cli", derive(Tabled))]
#[cfg_attr(feature = "mcp", derive(schemars::JsonSchema))]
#[derive(Serialize, Debug)]
pub struct DeviceInfo {
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub device_type: DeviceType,
    #[cfg_attr(feature = "cli", tabled(rename = "Type"))]
    pub device_type_display: String,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub has_back_side: bool,
    #[cfg_attr(feature = "cli", tabled(rename = "Serial Number"))]
    pub serial_number: String,
    #[cfg_attr(feature = "cli", tabled(rename = "Device Path"))]
    pub device_path: String,
    #[cfg_attr(feature = "cli", tabled(rename = "Status"))]
    pub status_display: String,
    #[cfg_attr(feature = "cli", tabled(rename = "Brightness (lm)"))]
    pub brightness_display: String,
    #[cfg_attr(feature = "cli", tabled(rename = "Temperature (K)"))]
    pub temperature_display: String,
    #[cfg_attr(feature = "cli", tabled(rename = "Back Status"))]
    pub back_status_display: String,
    #[cfg_attr(feature = "cli", tabled(rename = "Back Brightness (%)"))]
    pub back_brightness_display: String,
    // Keep original fields for JSON output
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub is_on: bool,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub brightness_in_lumen: u16,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub temperature_in_kelvin: u16,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub minimum_brightness_in_lumen: u16,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub maximum_brightness_in_lumen: u16,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub minimum_temperature_in_kelvin: u16,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub maximum_temperature_in_kelvin: u16,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub is_back_on: Option<bool>,
    #[cfg_attr(feature = "cli", tabled(skip))]
    pub back_brightness_percentage: Option<u8>,
}

fn get_connected_devices() -> Result<Vec<DeviceInfo>, CliError> {
    let context = Litra::new()?;

    let litra_devices: Vec<DeviceInfo> = context
        .get_connected_devices()
        .filter_map(|device| {
            let device_handle = match device.open(&context) {
                Ok(handle) => handle,
                Err(_e) => {
                    return None;
                }
            };

            // Get the device path
            let device_path = device.device_path();

            // Get serial number if available
            let serial = match device_handle.serial_number() {
                Ok(Some(s)) => s,
                Ok(None) => "UNKNOWN".to_string(),
                Err(_e) => "UNKNOWN".to_string(),
            };

            // Try to get attributes, log errors
            let is_on = match device_handle.is_on() {
                Ok(on) => on,
                Err(_e) => {
                    return None;
                }
            };

            let brightness = match device_handle.brightness_in_lumen() {
                Ok(b) => b,
                Err(_e) => {
                    return None;
                }
            };

            let temperature = match device_handle.temperature_in_kelvin() {
                Ok(t) => t,
                Err(_e) => {
                    return None;
                }
            };

            // Get back light status for Litra Beam LX devices
            let (
                is_back_on,
                back_brightness_percentage,
                back_status_display,
                back_brightness_display,
            ) = if device.device_type() == DeviceType::LitraBeamLX {
                let back_on = device_handle.is_back_on().ok();
                let back_brightness = device_handle.back_brightness_percentage().ok();
                let status_display = match back_on {
                    Some(on) => format!("{} {}", get_is_on_text(on), get_is_back_on_emoji(on)),
                    None => "Unknown".to_string(),
                };
                let brightness_display = match back_brightness {
                    Some(b) => format!("{}%", b),
                    None => "Unknown".to_string(),
                };
                (back_on, back_brightness, status_display, brightness_display)
            } else {
                (None, None, "N/A".to_string(), "N/A".to_string())
            };

            Some(DeviceInfo {
                device_type: device.device_type(),
                device_type_display: device.device_type().to_string(),
                has_back_side: device.device_type().has_back_side(),
                serial_number: serial,
                device_path,
                status_display: format!("{} {}", get_is_on_text(is_on), get_is_on_emoji(is_on)),
                brightness_display: format!(
                    "{}/{}",
                    brightness,
                    device_handle.maximum_brightness_in_lumen()
                ),
                temperature_display: format!(
                    "{}/{}",
                    temperature,
                    device_handle.maximum_temperature_in_kelvin()
                ),
                back_status_display,
                back_brightness_display,
                // Keep original fields for JSON output
                is_on,
                brightness_in_lumen: brightness,
                temperature_in_kelvin: temperature,
                minimum_brightness_in_lumen: device_handle.minimum_brightness_in_lumen(),
                maximum_brightness_in_lumen: device_handle.maximum_brightness_in_lumen(),
                minimum_temperature_in_kelvin: device_handle.minimum_temperature_in_kelvin(),
                maximum_temperature_in_kelvin: device_handle.maximum_temperature_in_kelvin(),
                is_back_on,
                back_brightness_percentage,
            })
        })
        .collect();
    Ok(litra_devices)
}

#[cfg(feature = "cli")]
fn handle_devices_command(json: bool) -> CliResult {
    let litra_devices = get_connected_devices()?;

    if json {
        println!(
            "{}",
            serde_json::to_string(&litra_devices).map_err(CliError::SerializationFailed)?
        );
        Ok(())
    } else {
        if litra_devices.is_empty() {
            println!("No Logitech Litra devices found");
        } else {
            let table = Table::new(&litra_devices);
            println!("{}", table);
        }

        Ok(())
    }
}

fn handle_on_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
) -> CliResult {
    with_device(serial_number, device_path, device_type, |device_handle| {
        device_handle.set_on(true)
    })
}

fn handle_off_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
) -> CliResult {
    with_device(serial_number, device_path, device_type, |device_handle| {
        device_handle.set_on(false)
    })
}

fn handle_toggle_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
) -> CliResult {
    // Get context to work with devices
    let context = Litra::new()?;

    // Get all matched devices
    let devices = get_all_supported_devices(&context, serial_number, device_path, device_type)?;
    if devices.is_empty() {
        return Err(CliError::DeviceNotFound);
    }

    // Toggle each device individually
    for device_handle in devices {
        // Toggle each device individually, ignoring errors
        if let Ok(is_on) = device_handle.is_on() {
            let _ = device_handle.set_on(!is_on);
        }
    }
    Ok(())
}

/// Create a general purpose function to handle brightness setting
fn with_brightness_setting<F>(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    brightness_fn: F,
) -> CliResult
where
    F: Fn(&DeviceHandle) -> Result<u16, DeviceError>,
{
    let context = Litra::new()?;

    // Get all matched devices
    let devices = get_all_supported_devices(&context, serial_number, device_path, device_type)?;
    if devices.is_empty() {
        return Err(CliError::DeviceNotFound);
    }

    for device_handle in devices {
        if let Ok(brightness) = brightness_fn(&device_handle) {
            let _ = device_handle.set_brightness_in_lumen(brightness);
        }
    }
    Ok(())
}

fn handle_brightness_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    value: Option<u16>,
    percentage: Option<u8>,
) -> CliResult {
    match (value, percentage) {
        (Some(brightness), None) => {
            with_device(serial_number, device_path, device_type, |device_handle| {
                device_handle.set_brightness_in_lumen(brightness)
            })
        }
        (None, Some(pct)) => {
            with_brightness_setting(serial_number, device_path, device_type, |device_handle| {
                let brightness_in_lumen = percentage_within_range(
                    pct.into(),
                    device_handle.minimum_brightness_in_lumen().into(),
                    device_handle.maximum_brightness_in_lumen().into(),
                );

                // Convert to u16, handling any potential conversion errors
                // DeviceError doesn't have a constructor for this error type,
                // so we'll use InvalidBrightness as the closest match
                brightness_in_lumen
                    .try_into()
                    .map_err(|_| DeviceError::InvalidBrightness(0))
            })
        }
        _ => unreachable!(),
    }
}

fn handle_brightness_up_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    value: Option<u16>,
    percentage: Option<u8>,
) -> CliResult {
    match (value, percentage) {
        (Some(brightness_to_add), None) => {
            with_brightness_setting(serial_number, device_path, device_type, |device_handle| {
                let current_brightness = device_handle.brightness_in_lumen()?;
                let new_brightness = current_brightness + brightness_to_add;
                Ok(new_brightness)
            })
        }
        (None, Some(pct)) => {
            with_brightness_setting(serial_number, device_path, device_type, |device_handle| {
                let current_brightness = device_handle.brightness_in_lumen()?;
                let brightness_to_add = percentage_within_range(
                    pct.into(),
                    device_handle.minimum_brightness_in_lumen().into(),
                    device_handle.maximum_brightness_in_lumen().into(),
                ) as u16
                    - device_handle.minimum_brightness_in_lumen();

                let new_brightness = current_brightness + brightness_to_add;
                Ok(new_brightness)
            })
        }
        _ => unreachable!(),
    }
}

fn handle_brightness_down_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    value: Option<u16>,
    percentage: Option<u8>,
) -> CliResult {
    match (value, percentage) {
        (Some(brightness_to_subtract), None) => {
            with_brightness_setting(serial_number, device_path, device_type, |device_handle| {
                let current_brightness = device_handle.brightness_in_lumen()?;

                if current_brightness <= brightness_to_subtract {
                    // Skip this device by returning an error which will be ignored
                    return Err(DeviceError::InvalidBrightness(0));
                }

                let new_brightness = current_brightness - brightness_to_subtract;
                Ok(new_brightness)
            })
        }
        (None, Some(pct)) => {
            with_brightness_setting(serial_number, device_path, device_type, |device_handle| {
                let current_brightness = device_handle.brightness_in_lumen()?;

                let brightness_to_subtract = percentage_within_range(
                    pct.into(),
                    device_handle.minimum_brightness_in_lumen().into(),
                    device_handle.maximum_brightness_in_lumen().into(),
                ) as u16
                    - device_handle.minimum_brightness_in_lumen();

                let new_brightness = current_brightness as i16 - brightness_to_subtract as i16;

                if new_brightness <= 0 {
                    // Skip this device by returning an error which will be ignored
                    return Err(DeviceError::InvalidBrightness(0));
                }

                Ok(new_brightness as u16)
            })
        }
        _ => unreachable!(),
    }
}

fn handle_temperature_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    value: u16,
) -> CliResult {
    with_device(serial_number, device_path, device_type, |device_handle| {
        device_handle.set_temperature_in_kelvin(value)
    })
}

fn handle_temperature_up_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    value: u16,
) -> CliResult {
    with_device(serial_number, device_path, device_type, |device_handle| {
        let current_temperature = device_handle.temperature_in_kelvin()?;
        let new_temperature = current_temperature + value;

        // Check if new temperature would exceed maximum
        if new_temperature > device_handle.maximum_temperature_in_kelvin() {
            return Err(DeviceError::InvalidTemperature(new_temperature));
        }

        device_handle.set_temperature_in_kelvin(new_temperature)
    })
}

fn handle_temperature_down_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    device_type: Option<&DeviceType>,
    value: u16,
) -> CliResult {
    with_device(serial_number, device_path, device_type, |device_handle| {
        let current_temperature = device_handle.temperature_in_kelvin()?;

        // Check if new temperature would be below minimum
        if current_temperature <= value {
            // Skip this device by returning an error which will be ignored
            return Err(DeviceError::InvalidTemperature(0));
        }

        let new_temperature = current_temperature - value;
        device_handle.set_temperature_in_kelvin(new_temperature)
    })
}

fn hex_to_rgb(hex: &str) -> Result<(u8, u8, u8), String> {
    let hex = hex.trim_start_matches('#');

    if hex.len() != 6 {
        return Err("Hex color must be exactly 6 characters long".into());
    }

    let r = u8::from_str_radix(&hex[0..2], 16)
        .map_err(|_| "Failed to parse red component from hex color")?;
    let g = u8::from_str_radix(&hex[2..4], 16)
        .map_err(|_| "Failed to parse green component from hex color")?;
    let b = u8::from_str_radix(&hex[4..6], 16)
        .map_err(|_| "Failed to parse blue component from hex color")?;

    Ok((r, g, b))
}

fn handle_back_color_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    hex: &str,
    zone_id: Option<u8>,
) -> CliResult {
    with_device(
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
        |device_handle| match hex_to_rgb(hex) {
            Ok((r, g, b)) => match zone_id {
                None => {
                    for i in 1..=7 {
                        device_handle.set_back_color(i, r, g, b)?;
                        std::thread::sleep(std::time::Duration::from_millis(10));
                    }
                    Ok(())
                }
                Some(id) => {
                    device_handle.set_back_color(id, r, g, b)?;
                    Ok(())
                }
            },
            Err(error) => Err(DeviceError::InvalidColor(error)),
        },
    )
}

fn handle_back_brightness_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    brightness: u8,
) -> CliResult {
    with_device(
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
        |device_handle| device_handle.set_back_brightness_percentage(brightness),
    )
}

fn handle_back_off_command(serial_number: Option<&str>, device_path: Option<&str>) -> CliResult {
    with_device(
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
        |device_handle| device_handle.set_back_on(false),
    )
}

fn handle_back_on_command(serial_number: Option<&str>, device_path: Option<&str>) -> CliResult {
    with_device(
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
        |device_handle| device_handle.set_back_on(true),
    )
}

fn handle_back_toggle_command(serial_number: Option<&str>, device_path: Option<&str>) -> CliResult {
    // Get context to work with devices
    let context = Litra::new()?;

    // Get all matched devices (only Litra Beam LX supports back light)
    let devices = get_all_supported_devices(
        &context,
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
    )?;
    if devices.is_empty() {
        return Err(CliError::DeviceNotFound);
    }

    // Toggle each device individually
    for device_handle in devices {
        // Toggle each device individually, ignoring errors
        if let Ok(is_on) = device_handle.is_back_on() {
            let _ = device_handle.set_back_on(!is_on);
        }
    }
    Ok(())
}

fn handle_back_brightness_up_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    percentage: u8,
) -> CliResult {
    // Get context to work with devices
    let context = Litra::new()?;

    // Get all matched devices (only Litra Beam LX supports back light)
    let devices = get_all_supported_devices(
        &context,
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
    )?;
    if devices.is_empty() {
        return Err(CliError::DeviceNotFound);
    }

    for device_handle in devices {
        if let Ok(current_brightness) = device_handle.back_brightness_percentage() {
            let new_brightness = current_brightness.saturating_add(percentage).min(100);
            let _ = device_handle.set_back_brightness_percentage(new_brightness);
        }
    }
    Ok(())
}

fn handle_back_brightness_down_command(
    serial_number: Option<&str>,
    device_path: Option<&str>,
    percentage: u8,
) -> CliResult {
    // Get context to work with devices
    let context = Litra::new()?;

    // Get all matched devices (only Litra Beam LX supports back light)
    let devices = get_all_supported_devices(
        &context,
        serial_number,
        device_path,
        Some(&DeviceType::LitraBeamLX),
    )?;
    if devices.is_empty() {
        return Err(CliError::DeviceNotFound);
    }

    for device_handle in devices {
        if let Ok(current_brightness) = device_handle.back_brightness_percentage() {
            let new_brightness = current_brightness.saturating_sub(percentage).max(1);
            let _ = device_handle.set_back_brightness_percentage(new_brightness);
        }
    }
    Ok(())
}

#[cfg(feature = "mcp")]
fn handle_mcp_command() -> CliResult {
    mcp::handle_mcp_command()
}

#[cfg(feature = "cli")]
fn main() -> ExitCode {
    let args = Cli::parse();

    let result = match &args.command {
        Commands::Devices { json } => handle_devices_command(*json),
        Commands::On {
            serial_number,
            device_path,
            device_type,
        } => handle_on_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
        ),
        Commands::Off {
            serial_number,
            device_path,
            device_type,
        } => handle_off_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
        ),
        Commands::Toggle {
            serial_number,
            device_path,
            device_type,
        } => handle_toggle_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
        ),
        Commands::Brightness {
            serial_number,
            device_path,
            device_type,
            value,
            percentage,
        } => handle_brightness_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
            *value,
            *percentage,
        ),
        Commands::BrightnessUp {
            serial_number,
            device_path,
            device_type,
            value,
            percentage,
        } => handle_brightness_up_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
            *value,
            *percentage,
        ),
        Commands::BrightnessDown {
            serial_number,
            device_path,
            device_type,
            value,
            percentage,
        } => handle_brightness_down_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
            *value,
            *percentage,
        ),
        Commands::Temperature {
            serial_number,
            device_path,
            device_type,
            value,
        } => handle_temperature_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
            *value,
        ),
        Commands::TemperatureUp {
            serial_number,
            device_path,
            device_type,
            value,
        } => handle_temperature_up_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
            *value,
        ),
        Commands::TemperatureDown {
            serial_number,
            device_path,
            device_type,
            value,
        } => handle_temperature_down_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            device_type.as_ref(),
            *value,
        ),
        Commands::BackColor {
            serial_number,
            device_path,
            value,
            color,
            zone: zone_id,
        } => {
            let hex = match (value, color) {
                (Some(v), None) => v.clone(),
                (None, Some(c)) => c.to_hex().to_string(),
                _ => unreachable!("clap ensures exactly one of value or color is provided"),
            };
            handle_back_color_command(
                serial_number.as_deref(),
                device_path.as_deref(),
                &hex,
                *zone_id,
            )
        }
        Commands::BackBrightness {
            serial_number,
            device_path,
            percentage,
        } => handle_back_brightness_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            *percentage,
        ),
        Commands::BackOff {
            serial_number,
            device_path,
        } => handle_back_off_command(serial_number.as_deref(), device_path.as_deref()),
        Commands::BackOn {
            serial_number,
            device_path,
        } => handle_back_on_command(serial_number.as_deref(), device_path.as_deref()),
        Commands::BackToggle {
            serial_number,
            device_path,
        } => handle_back_toggle_command(serial_number.as_deref(), device_path.as_deref()),
        Commands::BackBrightnessUp {
            serial_number,
            device_path,
            percentage,
        } => handle_back_brightness_up_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            *percentage,
        ),
        Commands::BackBrightnessDown {
            serial_number,
            device_path,
            percentage,
        } => handle_back_brightness_down_command(
            serial_number.as_deref(),
            device_path.as_deref(),
            *percentage,
        ),
        #[cfg(feature = "mcp")]
        Commands::Mcp => handle_mcp_command(),
    };

    if let Err(error) = result {
        eprintln!("{}", error);
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_percentage_within_range_zero_percent() {
        // 0% should return exactly the minimum value
        assert_eq!(percentage_within_range(0, 20, 250), 20);
        assert_eq!(percentage_within_range(0, 30, 400), 30);
        assert_eq!(percentage_within_range(0, 100, 200), 100);
    }

    #[test]
    fn test_percentage_within_range_hundred_percent() {
        // 100% should return exactly the maximum value
        assert_eq!(percentage_within_range(100, 20, 250), 250);
        assert_eq!(percentage_within_range(100, 30, 400), 400);
        assert_eq!(percentage_within_range(100, 100, 200), 200);
    }

    #[test]
    fn test_percentage_within_range_one_percent_greater_than_zero() {
        // 1% should return a value greater than 0%
        // This is the key bug fix: 1% should never equal the minimum
        let zero_pct = percentage_within_range(0, 20, 250);
        let one_pct = percentage_within_range(1, 20, 250);
        assert!(
            one_pct > zero_pct,
            "1% ({}) should be greater than 0% ({})",
            one_pct,
            zero_pct
        );

        // Test with Litra Beam range
        let zero_pct_beam = percentage_within_range(0, 30, 400);
        let one_pct_beam = percentage_within_range(1, 30, 400);
        assert!(
            one_pct_beam > zero_pct_beam,
            "1% ({}) should be greater than 0% ({})",
            one_pct_beam,
            zero_pct_beam
        );

        // Test with a small range where the bug is most apparent
        let zero_pct_small = percentage_within_range(0, 20, 30);
        let one_pct_small = percentage_within_range(1, 20, 30);
        assert!(
            one_pct_small > zero_pct_small,
            "1% ({}) should be greater than 0% ({}) even with small range",
            one_pct_small,
            zero_pct_small
        );
    }

    #[test]
    fn test_percentage_within_range_midpoint() {
        // 50% should return approximately the midpoint
        // For range 20-250: midpoint is 135
        let result = percentage_within_range(50, 20, 250);
        assert_eq!(result, 135);

        // For range 30-400: midpoint is 215
        let result = percentage_within_range(50, 30, 400);
        assert_eq!(result, 215);
    }

    #[test]
    fn test_percentage_within_range_litra_glow() {
        // Test with Litra Glow's actual brightness range (20-250lm)
        assert_eq!(percentage_within_range(0, 20, 250), 20);
        assert!(percentage_within_range(1, 20, 250) > 20);
        assert_eq!(percentage_within_range(100, 20, 250), 250);

        // Verify that small percentages are distinguishable
        let one = percentage_within_range(1, 20, 250);
        let two = percentage_within_range(2, 20, 250);
        let three = percentage_within_range(3, 20, 250);

        // Each should be at least the minimum
        assert!(one >= 20);
        assert!(two >= 20);
        assert!(three >= 20);

        // And 1% should be greater than 0%
        assert!(one > 20);
    }

    #[test]
    fn test_percentage_within_range_litra_beam() {
        // Test with Litra Beam's actual brightness range (30-400lm)
        assert_eq!(percentage_within_range(0, 30, 400), 30);
        assert!(percentage_within_range(1, 30, 400) > 30);
        assert_eq!(percentage_within_range(100, 30, 400), 400);

        // Verify that small percentages are distinguishable
        let one = percentage_within_range(1, 30, 400);
        let two = percentage_within_range(2, 30, 400);

        // Each should be greater than minimum
        assert!(one > 30);
        assert!(two > 30);
    }

    #[test]
    fn test_percentage_within_range_small_range() {
        // Test with a small range where rounding issues are most apparent
        // This is the case that exposes the original bug
        let range_start = 20;
        let range_end = 30;

        assert_eq!(
            percentage_within_range(0, range_start, range_end),
            range_start
        );
        assert!(
            percentage_within_range(1, range_start, range_end) > range_start,
            "1% should be greater than start even with small range"
        );
        assert!(
            percentage_within_range(5, range_start, range_end) > range_start,
            "5% should be greater than start even with small range"
        );
        assert_eq!(
            percentage_within_range(100, range_start, range_end),
            range_end
        );
    }

    #[test]
    fn test_percentage_within_range_monotonic() {
        // Verify that the function is monotonically increasing
        // (higher percentages should never give lower values)
        let range_start = 20;
        let range_end = 250;

        let mut prev_value = percentage_within_range(0, range_start, range_end);
        for pct in 1..=100 {
            let current_value = percentage_within_range(pct, range_start, range_end);
            assert!(
                current_value >= prev_value,
                "{}% ({}) should be >= {}% ({})",
                pct,
                current_value,
                pct - 1,
                prev_value
            );
            prev_value = current_value;
        }
    }
}
