use super::{CameraCodec, CameraConfig, CameraOutput, HdmiConnector, HdmiMode, LAST_CONFIG};
use gstreamer as gst;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{info, warn};

#[cfg(not(coverage))]
const DEFAULT_UVC_CONFIGFS_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0";

#[cfg(not(coverage))]
const UVC_CONFIGFS_BASE_ENV: &str = "LESAVKA_UVC_CONFIGFS_BASE";

#[cfg(not(coverage))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct LiveUvcProfile {
    width: u32,
    height: u32,
    fps: u32,
    interval_100ns: u32,
}

#[cfg(coverage)]
/// Keeps `select_camera_config` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn select_camera_config() -> CameraConfig {
    let output_override = std::env::var("LESAVKA_CAM_OUTPUT")
        .ok()
        .as_deref()
        .and_then(parse_camera_output);

    match output_override.unwrap_or(CameraOutput::Uvc) {
        CameraOutput::Hdmi => select_hdmi_config(detect_hdmi_connector(false)),
        CameraOutput::Uvc => select_uvc_config(),
    }
}

#[cfg(not(coverage))]
/// Keeps `select_camera_config` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(super) fn select_camera_config() -> CameraConfig {
    let output_env = std::env::var("LESAVKA_CAM_OUTPUT").ok();
    let output_override = output_env.as_deref().and_then(parse_camera_output);

    let require_connected = output_override != Some(CameraOutput::Hdmi);
    let hdmi = detect_hdmi_connector(require_connected);
    if output_override == Some(CameraOutput::Hdmi) && hdmi.is_none() {
        warn!("📷 HDMI output forced but no connector detected");
    }
    let output = match output_override {
        Some(v) => v,
        None => {
            if hdmi.is_some() {
                CameraOutput::Hdmi
            } else {
                CameraOutput::Uvc
            }
        }
    };

    let cfg = match output {
        CameraOutput::Hdmi => select_hdmi_config(hdmi),
        CameraOutput::Uvc => select_uvc_config(),
    };

    let (display_width, display_height) = cfg.hdmi_display_size();
    info!(
        output = cfg.output.as_str(),
        codec = cfg.codec.as_str(),
        width = cfg.width,
        height = cfg.height,
        fps = cfg.fps,
        display_width,
        display_height,
        hdmi = cfg.hdmi.as_ref().map(|h| h.name.as_str()).unwrap_or("none"),
        "📷 camera output selected"
    );

    cfg
}

/// Keeps `parse_camera_output` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_camera_output(raw: &str) -> Option<CameraOutput> {
    match raw.trim().to_ascii_lowercase().as_str() {
        "uvc" => Some(CameraOutput::Uvc),
        "hdmi" => Some(CameraOutput::Hdmi),
        "auto" | "" => None,
        _ => None,
    }
}

/// Keeps `parse_camera_codec` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_camera_codec(raw: &str) -> Option<CameraCodec> {
    match raw.trim().to_ascii_lowercase().as_str() {
        "h264" => Some(CameraCodec::H264),
        "hevc" | "h265" | "h.265" => Some(CameraCodec::Hevc),
        "mjpeg" | "mjpg" | "jpeg" => Some(CameraCodec::Mjpeg),
        _ => None,
    }
}

fn parse_uvc_uplink_camera_codec(raw: &str) -> Option<CameraCodec> {
    match raw.trim().to_ascii_lowercase().as_str() {
        "h264" => {
            warn!("📷 UVC output no longer accepts H264 as the normal upstream codec; using MJPEG");
            Some(CameraCodec::Mjpeg)
        }
        _ => parse_camera_codec(raw),
    }
}

fn select_hdmi_codec(hw_decode: bool) -> CameraCodec {
    std::env::var("LESAVKA_CAM_CODEC")
        .ok()
        .as_deref()
        .and_then(parse_camera_codec)
        .unwrap_or(if hw_decode {
            CameraCodec::H264
        } else {
            CameraCodec::Mjpeg
        })
}

fn select_uvc_codec(uvc_env: Option<&HashMap<String, String>>) -> CameraCodec {
    std::env::var("LESAVKA_UPLINK_CAMERA_CODEC")
        .ok()
        .or_else(|| std::env::var("LESAVKA_CAM_CODEC").ok())
        .or_else(|| uvc_env.and_then(|env| env.get("LESAVKA_UPLINK_CAMERA_CODEC").cloned()))
        .or_else(|| uvc_env.and_then(|env| env.get("LESAVKA_CAM_CODEC").cloned()))
        .as_deref()
        .and_then(parse_uvc_uplink_camera_codec)
        .unwrap_or(CameraCodec::Hevc)
}

/// Keeps `select_hdmi_config` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn select_hdmi_config(hdmi: Option<HdmiConnector>) -> CameraConfig {
    let hw_decode = has_hw_h264_decode();
    let (default_width, default_height) = if hw_decode { (1920, 1080) } else { (1280, 720) };
    let width = read_u32_from_env("LESAVKA_CAM_WIDTH").unwrap_or(default_width);
    let height = read_u32_from_env("LESAVKA_CAM_HEIGHT").unwrap_or(default_height);
    let fps = read_u32_from_env("LESAVKA_CAM_FPS").unwrap_or(30).max(1);
    let codec = select_hdmi_codec(hw_decode);
    #[cfg(not(coverage))]
    if !hw_decode {
        if matches!(codec, CameraCodec::Mjpeg) {
            warn!(
                width,
                height,
                fps,
                "📷 HDMI output: hardware H264 decoder not detected; preferring MJPEG uplink"
            );
        } else if width == default_width && height == default_height {
            warn!(
                "📷 HDMI output: hardware H264 decoder not detected; forcing H264 uplink at requested size"
            );
        } else {
            warn!(
                width,
                height,
                fps,
                "📷 HDMI output: hardware H264 decoder not detected; using configured camera uplink size"
            );
        }
    }
    CameraConfig {
        output: CameraOutput::Hdmi,
        codec,
        width,
        height,
        fps,
        hdmi,
    }
}

pub(super) fn select_hdmi_mirror_config(primary: &CameraConfig) -> CameraConfig {
    CameraConfig {
        output: CameraOutput::Hdmi,
        codec: primary.codec,
        width: primary.width,
        height: primary.height,
        fps: primary.fps,
        hdmi: detect_hdmi_connector(false),
    }
}

#[cfg(coverage)]
/// Keeps `select_uvc_config` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn select_uvc_config() -> CameraConfig {
    let width = read_u32_from_env("LESAVKA_UVC_WIDTH").unwrap_or(1280);
    let height = read_u32_from_env("LESAVKA_UVC_HEIGHT").unwrap_or(720);
    let fps = read_u32_from_env("LESAVKA_UVC_FPS")
        .or_else(|| {
            read_u32_from_env("LESAVKA_UVC_INTERVAL").and_then(|interval| {
                if interval == 0 {
                    None
                } else {
                    Some(10_000_000 / interval)
                }
            })
        })
        .unwrap_or(30);
    let codec = select_uvc_codec(None);

    CameraConfig {
        output: CameraOutput::Uvc,
        codec,
        width,
        height,
        fps,
        hdmi: None,
    }
}

#[cfg(not(coverage))]
/// Keeps `select_uvc_config` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn select_uvc_config() -> CameraConfig {
    let mut uvc_env = HashMap::new();
    if let Ok(text) = fs::read_to_string("/etc/lesavka/uvc.env") {
        uvc_env = parse_env_file(&text);
    }

    let mut width = read_u32_from_env("LESAVKA_UVC_WIDTH")
        .or_else(|| read_u32_from_map(&uvc_env, "LESAVKA_UVC_WIDTH"))
        .unwrap_or(1280);
    let mut height = read_u32_from_env("LESAVKA_UVC_HEIGHT")
        .or_else(|| read_u32_from_map(&uvc_env, "LESAVKA_UVC_HEIGHT"))
        .unwrap_or(720);
    let mut fps = read_u32_from_env("LESAVKA_UVC_FPS")
        .or_else(|| read_u32_from_map(&uvc_env, "LESAVKA_UVC_FPS"))
        .or_else(|| {
            read_u32_from_env("LESAVKA_UVC_INTERVAL")
                .or_else(|| read_u32_from_map(&uvc_env, "LESAVKA_UVC_INTERVAL"))
                .and_then(|interval| {
                    if interval == 0 {
                        None
                    } else {
                        Some(10_000_000 / interval)
                    }
                })
        })
        .unwrap_or(30);
    let codec = select_uvc_codec(Some(&uvc_env));

    let live_profiles = read_live_uvc_configfs_profiles();
    if !live_profiles.is_empty() {
        if live_profiles
            .iter()
            .any(|live| (width, height, fps) == (live.width, live.height, live.fps))
        {
            info!(
                configured_width = width,
                configured_height = height,
                configured_fps = fps,
                "📷 configured UVC profile is advertised by the live gadget descriptor"
            );
        } else {
            let live = live_profiles[0];
            warn!(
                configured_width = width,
                configured_height = height,
                configured_fps = fps,
                live_width = live.width,
                live_height = live.height,
                live_fps = live.fps,
                live_interval_100ns = live.interval_100ns,
                "📷 live UVC descriptor differs from configured profile; honoring attached gadget profile until rebuild"
            );
            width = live.width;
            height = live.height;
            fps = live.fps.max(1);
        }
    }

    CameraConfig {
        output: CameraOutput::Uvc,
        codec,
        width,
        height,
        fps,
        hdmi: None,
    }
}

#[cfg(not(coverage))]
/// Keeps `read_live_uvc_configfs_profile` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn read_live_uvc_configfs_profiles() -> Vec<LiveUvcProfile> {
    let base = std::env::var(UVC_CONFIGFS_BASE_ENV)
        .map(PathBuf::from)
        .unwrap_or_else(|_| PathBuf::from(DEFAULT_UVC_CONFIGFS_BASE));
    let mjpeg_dir = base.join("streaming/mjpeg/m");
    let Ok(entries) = fs::read_dir(mjpeg_dir) else {
        return Vec::new();
    };
    let mut profiles = Vec::new();
    for entry in entries.flatten() {
        let path = entry.path();
        let Some(width) = read_u32_file(path.join("wWidth")) else {
            continue;
        };
        let Some(height) = read_u32_file(path.join("wHeight")) else {
            continue;
        };
        if width == 0 || height == 0 {
            continue;
        }
        let intervals = read_u32_lines(path.join("dwFrameInterval"))
            .or_else(|| read_u32_file(path.join("dwDefaultFrameInterval")).map(|v| vec![v]))
            .unwrap_or_default();
        for interval_100ns in intervals.into_iter().filter(|interval| *interval > 0) {
            profiles.push(LiveUvcProfile {
                width,
                height,
                fps: (10_000_000 / interval_100ns).max(1),
                interval_100ns,
            });
        }
    }
    profiles.sort_by_key(|p| {
        (
            std::cmp::Reverse(p.width * p.height),
            std::cmp::Reverse(p.fps),
        )
    });
    profiles
}

#[cfg(not(coverage))]
/// Keeps `read_u32_file` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn read_u32_file(path: impl AsRef<Path>) -> Option<u32> {
    fs::read_to_string(path).ok()?.trim().parse::<u32>().ok()
}

#[cfg(not(coverage))]
fn read_u32_lines(path: impl AsRef<Path>) -> Option<Vec<u32>> {
    let values = fs::read_to_string(path)
        .ok()?
        .lines()
        .filter_map(|line| line.trim().parse::<u32>().ok())
        .collect::<Vec<_>>();
    (!values.is_empty()).then_some(values)
}

#[cfg(coverage)]
fn has_hw_h264_decode() -> bool {
    std::env::var("LESAVKA_HW_H264").is_ok()
}

#[cfg(not(coverage))]
/// Keeps `has_hw_h264_decode` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn has_hw_h264_decode() -> bool {
    if gst::init().is_err() {
        return false;
    }
    for name in ["v4l2h264dec", "v4l2slh264dec", "omxh264dec"] {
        if gst::ElementFactory::find(name).is_some() {
            return true;
        }
    }
    false
}

#[cfg(coverage)]
/// Keeps `detect_hdmi_connector` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn detect_hdmi_connector(require_connected: bool) -> Option<HdmiConnector> {
    let _ = require_connected;
    std::env::var("LESAVKA_HDMI_CONNECTOR")
        .ok()
        .map(|name| HdmiConnector {
            name,
            id: None,
            modes: std::env::var("LESAVKA_HDMI_MODES")
                .ok()
                .map(|raw| parse_hdmi_modes(&raw))
                .unwrap_or_default(),
        })
}

#[cfg(not(coverage))]
/// Keeps `detect_hdmi_connector` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
fn detect_hdmi_connector(require_connected: bool) -> Option<HdmiConnector> {
    let preferred = std::env::var("LESAVKA_HDMI_CONNECTOR").ok();
    let entries = fs::read_dir("/sys/class/drm").ok()?;
    let mut connectors = Vec::new();

    for entry in entries.flatten() {
        let name = entry.file_name().to_string_lossy().into_owned();
        if !name.contains("HDMI-A-") {
            continue;
        }
        let status_path = entry.path().join("status");
        let status = fs::read_to_string(&status_path)
            .ok()
            .map(|v| v.trim().to_string())
            .unwrap_or_default();
        let id = fs::read_to_string(entry.path().join("connector_id"))
            .ok()
            .and_then(|v| v.trim().parse::<u32>().ok());
        let modes = fs::read_to_string(entry.path().join("modes"))
            .ok()
            .map(|raw| parse_hdmi_modes(&raw))
            .unwrap_or_default();
        connectors.push((name, status, id, modes));
    }
    connectors.sort_by(|a, b| a.0.cmp(&b.0));

    let matches_preferred =
        |name: &str, preferred: &str| name == preferred || name.ends_with(preferred);

    if let Some(pref) = preferred.as_deref() {
        for (name, status, id, modes) in &connectors {
            if matches_preferred(name, pref) && (!require_connected || status == "connected") {
                return Some(HdmiConnector {
                    name: name.clone(),
                    id: *id,
                    modes: modes.clone(),
                });
            }
        }
    }

    if preferred.is_none() {
        let previous = LAST_CONFIG
            .get()
            .and_then(|lock| lock.read().ok())
            .and_then(|cfg| cfg.hdmi.as_ref().map(|h| h.name.clone()));
        if let Some(prev) = previous {
            for (name, status, id, modes) in &connectors {
                if *name == prev && (!require_connected || status == "connected") {
                    return Some(HdmiConnector {
                        name: name.clone(),
                        id: *id,
                        modes: modes.clone(),
                    });
                }
            }
        }
    }

    for (name, status, id, modes) in connectors {
        if !require_connected || status == "connected" {
            return Some(HdmiConnector { name, id, modes });
        }
    }

    None
}

pub(crate) fn parse_hdmi_modes(raw: &str) -> Vec<HdmiMode> {
    raw.lines()
        .flat_map(|line| line.split(','))
        .filter_map(parse_hdmi_mode)
        .collect()
}

/// Keeps `parse_hdmi_mode` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(crate) fn parse_hdmi_mode(raw: &str) -> Option<HdmiMode> {
    let raw = raw.trim();
    let (width, rest) = raw.split_once('x')?;
    let width = width.trim().parse::<u32>().ok()?;
    let height_digits: String = rest
        .trim()
        .chars()
        .take_while(|ch| ch.is_ascii_digit())
        .collect();
    let height = height_digits.parse::<u32>().ok()?;
    (width > 0 && height > 0).then_some(HdmiMode { width, height })
}

/// Keeps `preferred_hdmi_mode` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
/// Inputs are the typed parameters; output is the return value or side effect.
pub(crate) fn preferred_hdmi_mode(modes: &[HdmiMode]) -> Option<HdmiMode> {
    for preferred in [
        HdmiMode {
            width: 1920,
            height: 1080,
        },
        HdmiMode {
            width: 1280,
            height: 720,
        },
    ] {
        if modes.contains(&preferred) {
            return Some(preferred);
        }
    }

    modes
        .iter()
        .copied()
        .filter(|mode| mode.width.saturating_mul(9) == mode.height.saturating_mul(16))
        .filter(|mode| mode.width.saturating_mul(mode.height) <= 1920 * 1080)
        .max_by_key(|mode| mode.width.saturating_mul(mode.height))
        .or_else(|| modes.first().copied())
}

include!("selection/config_env.rs");
