#![cfg_attr(coverage, allow(dead_code, unused_imports, unused_variables))]
#![forbid(unsafe_code)]

use anyhow::Context as _;
use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
use tracing::{info, warn};

use crate::gadget::UsbGadget;

fn uvc_by_path_root() -> String {
    std::env::var("LESAVKA_UVC_BY_PATH_ROOT").unwrap_or_else(|_| "/dev/v4l/by-path".to_string())
}

fn any_platform_uvc_by_path() -> Option<String> {
    let root = uvc_by_path_root();
    let root = Path::new(&root);
    let entries = std::fs::read_dir(root).ok()?;
    for entry in entries.flatten() {
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if name.starts_with("platform-") && name.ends_with("-video-index0") {
            return Some(entry.path().to_string_lossy().into_owned());
        }
    }
    None
}

/// Pick the UVC gadget video node.
///
/// Inputs: none; the function inspects environment overrides and udev state.
/// Outputs: the best-matching V4L2 output node for the active USB gadget.
/// Why: the relay must target the gadget output itself, not an unrelated
/// capture card that happens to exist on the same machine.
#[cfg(coverage)]
pub fn pick_uvc_device() -> anyhow::Result<String> {
    if let Ok(path) = std::env::var("LESAVKA_UVC_DEV") {
        return Ok(path);
    }

    if let Ok(ctrl) = UsbGadget::find_controller() {
        return Ok(format!(
            "{}/platform-{ctrl}-video-index0",
            uvc_by_path_root()
        ));
    }

    if let Some(by_path) = any_platform_uvc_by_path() {
        return Ok(by_path);
    }

    Err(anyhow::anyhow!(
        "no Lesavka video_output v4l2 node found; wait for /dev/v4l/by-path/platform-<udc>-video-index0 or set LESAVKA_UVC_DEV"
    ))
}

#[cfg(not(coverage))]
pub fn pick_uvc_device() -> anyhow::Result<String> {
    if let Ok(path) = std::env::var("LESAVKA_UVC_DEV") {
        return Ok(path);
    }

    let ctrl = UsbGadget::find_controller().ok();
    if let Some(ctrl) = ctrl.as_deref() {
        let by_path = format!("{}/platform-{ctrl}-video-index0", uvc_by_path_root());
        if Path::new(&by_path).exists() {
            return Ok(by_path);
        }
    }

    if let Some(by_path) = any_platform_uvc_by_path() {
        return Ok(by_path);
    }

    if std::env::var("LESAVKA_UVC_SKIP_UDEV").is_err()
        && let Ok(mut enumerator) = udev::Enumerator::new()
    {
        let _ = enumerator.match_subsystem("video4linux");
        if let Ok(devices) = enumerator.scan_devices() {
            for device in devices {
                let caps = device
                    .property_value("ID_V4L_CAPABILITIES")
                    .and_then(|value| value.to_str())
                    .unwrap_or_default();
                if !caps.contains(":video_output:") {
                    continue;
                }
                let Some(node) = device.devnode() else {
                    continue;
                };
                let node = node.to_string_lossy().into_owned();
                let product = device
                    .property_value("ID_V4L_PRODUCT")
                    .and_then(|value| value.to_str())
                    .unwrap_or_default();
                let path = device
                    .property_value("ID_PATH")
                    .and_then(|value| value.to_str())
                    .unwrap_or_default();
                if let Some(ctrl) = ctrl.as_deref()
                    && (product == ctrl || path.contains(ctrl))
                {
                    return Ok(node);
                }
            }
        }
    }

    Err(anyhow::anyhow!(
        "no Lesavka video_output v4l2 node found; wait for /dev/v4l/by-path/platform-<udc>-video-index0 or set LESAVKA_UVC_DEV"
    ))
}

/// Resolve the UVC control helper binary path.
///
/// Inputs: none.
/// Outputs: the configured executable path.
/// Why: production installs ship the helper as a separate binary, but CI and
/// local development sometimes need to override that location.
#[must_use]
pub fn uvc_ctrl_bin() -> String {
    std::env::var("LESAVKA_UVC_CTRL_BIN")
        .unwrap_or_else(|_| "/usr/local/bin/lesavka-uvc".to_string())
}

/// Spawn the external UVC control helper.
///
/// Inputs: the helper binary path plus the selected UVC device node.
/// Outputs: a running child process handle.
/// Why: the helper owns low-level configfs and V4L2 control handling that we
/// keep out of the main gRPC server process.
pub fn spawn_uvc_control(bin: &str, uvc_dev: &str) -> anyhow::Result<tokio::process::Child> {
    Command::new(bin)
        .arg("--device")
        .arg(uvc_dev)
        .spawn()
        .context("spawning lesavka-uvc")
}

fn uvc_control_restart_delay() -> Duration {
    let millis = std::env::var("LESAVKA_UVC_RESTART_DELAY_MS")
        .ok()
        .and_then(|value| value.parse::<u64>().ok())
        .unwrap_or(2_000)
        .min(10_000);
    Duration::from_millis(millis)
}

/// Supervise the external UVC control helper forever.
///
/// Inputs: the helper binary path.
/// Outputs: none; the task loops until the process exits.
/// Why: UVC device nodes can appear after boot, so the supervisor waits for a
/// usable device and restarts the helper whenever it exits.
#[cfg(coverage)]
pub async fn supervise_uvc_control(bin: String) {
    while let Ok(uvc_dev) = pick_uvc_device() {
        if let Ok(mut child) = spawn_uvc_control(&bin, &uvc_dev) {
            let _ = child.wait().await;
        }
        tokio::task::yield_now().await;
    }
}

#[cfg(not(coverage))]
pub async fn supervise_uvc_control(bin: String) {
    let mut waiting_logged = false;
    loop {
        let uvc_dev = match pick_uvc_device() {
            Ok(device) => {
                if waiting_logged {
                    info!(%device, "📷 UVC device discovered");
                    waiting_logged = false;
                }
                device
            }
            Err(error) => {
                if !waiting_logged {
                    warn!("⚠️  UVC device not ready: {error:#}");
                    waiting_logged = true;
                }
                tokio::time::sleep(Duration::from_secs(2)).await;
                continue;
            }
        };

        match spawn_uvc_control(&bin, &uvc_dev) {
            Ok(mut child) => {
                info!(%uvc_dev, "📷 UVC control helper started");
                match child.wait().await {
                    Ok(status) => {
                        warn!(%uvc_dev, "⚠️  lesavka-uvc exited: {status}");
                    }
                    Err(error) => {
                        warn!(%uvc_dev, "⚠️  lesavka-uvc wait failed: {error:#}");
                    }
                }
            }
            Err(error) => {
                warn!(%uvc_dev, "⚠️  failed to start lesavka-uvc: {error:#}");
            }
        }

        tokio::time::sleep(uvc_control_restart_delay()).await;
    }
}

#[cfg(test)]
mod tests {
    use super::{pick_uvc_device, spawn_uvc_control, uvc_ctrl_bin};
    use serial_test::serial;
    use std::fs;
    use std::os::unix::fs::PermissionsExt;
    use temp_env::with_var;
    use tempfile::tempdir;

    #[test]
    #[serial]
    fn uvc_ctrl_bin_prefers_env_override() {
        with_var("LESAVKA_UVC_CTRL_BIN", None::<&str>, || {
            assert_eq!(uvc_ctrl_bin(), "/usr/local/bin/lesavka-uvc");
        });
        with_var("LESAVKA_UVC_CTRL_BIN", Some("/tmp/uvc-helper"), || {
            assert_eq!(uvc_ctrl_bin(), "/tmp/uvc-helper");
        });
    }

    #[test]
    #[serial]
    fn pick_uvc_device_prefers_env_override() {
        with_var("LESAVKA_UVC_DEV", Some("/dev/video-test"), || {
            assert_eq!(pick_uvc_device().unwrap(), "/dev/video-test");
        });
    }

    #[tokio::test]
    #[serial]
    async fn spawn_uvc_control_runs_the_helper_script() {
        let dir = tempdir().expect("tempdir");
        let output = dir.path().join("args.txt");
        let script = dir.path().join("helper.sh");
        fs::write(
            &script,
            format!(
                "#!/usr/bin/env bash\nset -euo pipefail\nprintf '%s %s' \"$1\" \"$2\" > '{}'\n",
                output.display()
            ),
        )
        .expect("write helper");
        let mut perms = fs::metadata(&script).expect("metadata").permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&script, perms).expect("chmod helper");

        let mut child = spawn_uvc_control(script.to_str().unwrap(), "/dev/video42")
            .expect("helper should spawn");
        let status = child.wait().await.expect("wait helper");
        assert!(status.success());
        assert_eq!(
            fs::read_to_string(output).expect("read output"),
            "--device /dev/video42"
        );
    }
}
