use anyhow::{Context, Result, bail};
use lesavka_common::lesavka::{
    CalibrationAction, CalibrationRequest, CalibrationState, CapturePowerCommand, HandshakeSet,
    OutputDelayProbeRequest, SetCapturePowerRequest, relay_client::RelayClient,
};
#[cfg(not(coverage))]
use lesavka_common::lesavka::{Empty, handshake_client::HandshakeClient};
#[cfg(not(coverage))]
use tonic::Request;
use tonic::transport::Channel;

use lesavka_client::relay_transport;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CommandKind {
    Status,
    Version,
    CalibrationStatus,
    CalibrationAdjust,
    CalibrationRestoreDefault,
    CalibrationRestoreFactory,
    CalibrationSaveDefault,
    Auto,
    On,
    Off,
    RecoverUsb,
    RecoverUac,
    RecoverUvc,
    ResetUsb,
    UpstreamSync,
    OutputDelayProbe,
}

impl CommandKind {
    /// Parse the intentionally small CLI vocabulary into safe relay actions.
    fn parse(value: &str) -> Option<Self> {
        match value {
            "status" | "get" => Some(Self::Status),
            "version" | "versions" => Some(Self::Version),
            "calibration" | "calibration-status" => Some(Self::CalibrationStatus),
            "calibrate" | "calibration-adjust" => Some(Self::CalibrationAdjust),
            "calibration-restore-default" | "restore-calibration" => {
                Some(Self::CalibrationRestoreDefault)
            }
            "calibration-restore-factory" | "factory-calibration" => {
                Some(Self::CalibrationRestoreFactory)
            }
            "calibration-save-default" | "save-calibration" => Some(Self::CalibrationSaveDefault),
            "auto" => Some(Self::Auto),
            "on" | "force-on" => Some(Self::On),
            "off" | "force-off" => Some(Self::Off),
            "recover-usb" => Some(Self::RecoverUsb),
            "recover-uac" | "heal-av" | "heal-upstream" | "recover-upstream" => {
                Some(Self::RecoverUac)
            }
            "recover-uvc" => Some(Self::RecoverUvc),
            "reset-usb" | "hard-reset-usb" => Some(Self::ResetUsb),
            "upstream-sync" | "sync" => Some(Self::UpstreamSync),
            "output-delay-probe" | "probe-output-delay" => Some(Self::OutputDelayProbe),
            _ => None,
        }
    }
}

#[derive(Debug, Eq, PartialEq)]
struct Config {
    server: String,
    command: CommandKind,
    audio_delta_us: i64,
    video_delta_us: i64,
    note: String,
    probe_duration_seconds: u32,
    probe_warmup_seconds: u32,
    probe_pulse_period_ms: u32,
    probe_pulse_width_ms: u32,
    probe_event_width_codes: String,
    probe_audio_delay_us: i64,
    probe_video_delay_us: i64,
}

#[derive(Debug, Default, Eq, PartialEq)]
struct ParsedCommandArgs {
    audio_delta_us: i64,
    video_delta_us: i64,
    note: String,
    probe_duration_seconds: u32,
    probe_warmup_seconds: u32,
    probe_pulse_period_ms: u32,
    probe_pulse_width_ms: u32,
    probe_event_width_codes: String,
    probe_audio_delay_us: i64,
    probe_video_delay_us: i64,
}

#[derive(Debug, Eq, PartialEq)]
enum ParseOutcome {
    Run(Config),
    Help,
}

fn usage() -> &'static str {
    "Usage: lesavka-relayctl [--server http://HOST:50051] <status|version|upstream-sync|output-delay-probe [duration_s warmup_s period_ms width_ms codes audio_delay_us video_delay_us]|calibration|calibrate <audio_delta_us> <video_delta_us> [note]|calibration-save-default|calibration-restore-default|calibration-restore-factory|auto|on|off|recover-usb|recover-uac|heal-av|recover-uvc|reset-usb>"
}

/// Keeps `parse_args_outcome_from` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_args_outcome_from<I, S>(args: I) -> Result<ParseOutcome>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    let mut args = args.into_iter().map(Into::into);
    let mut server = "http://127.0.0.1:50051".to_string();
    let mut command = None;
    let mut command_args = Vec::new();

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "--server" => {
                server = args
                    .next()
                    .context("missing value after --server")?
                    .trim()
                    .to_string();
            }
            "--help" | "-h" => {
                return Ok(ParseOutcome::Help);
            }
            _ if command.is_none() => {
                command = CommandKind::parse(arg.as_str());
                if command.is_none() {
                    bail!("unknown command `{arg}`\n{}", usage());
                }
            }
            _ => command_args.push(arg),
        }
    }

    let command = command.unwrap_or(CommandKind::Status);
    let parsed = parse_command_args(command, command_args)?;
    Ok(ParseOutcome::Run(Config {
        server,
        command,
        audio_delta_us: parsed.audio_delta_us,
        video_delta_us: parsed.video_delta_us,
        note: parsed.note,
        probe_duration_seconds: parsed.probe_duration_seconds,
        probe_warmup_seconds: parsed.probe_warmup_seconds,
        probe_pulse_period_ms: parsed.probe_pulse_period_ms,
        probe_pulse_width_ms: parsed.probe_pulse_width_ms,
        probe_event_width_codes: parsed.probe_event_width_codes,
        probe_audio_delay_us: parsed.probe_audio_delay_us,
        probe_video_delay_us: parsed.probe_video_delay_us,
    }))
}

/// Keeps `parse_command_args` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_command_args(command: CommandKind, args: Vec<String>) -> Result<ParsedCommandArgs> {
    if command == CommandKind::OutputDelayProbe {
        return parse_output_delay_probe_args(args);
    }

    if command != CommandKind::CalibrationAdjust {
        if let Some(arg) = args.first() {
            bail!("unexpected argument `{arg}`\n{}", usage());
        }
        return Ok(ParsedCommandArgs::default());
    }

    if args.len() < 2 {
        bail!(
            "calibrate requires audio_delta_us and video_delta_us\n{}",
            usage()
        );
    }

    let audio_delta_us = args[0]
        .parse::<i64>()
        .with_context(|| format!("parsing audio_delta_us `{}`", args[0]))?;
    let video_delta_us = args[1]
        .parse::<i64>()
        .with_context(|| format!("parsing video_delta_us `{}`", args[1]))?;
    let note = args[2..].join(" ");
    Ok(ParsedCommandArgs {
        audio_delta_us,
        video_delta_us,
        note,
        ..ParsedCommandArgs::default()
    })
}

/// Keeps `parse_output_delay_probe_args` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_output_delay_probe_args(args: Vec<String>) -> Result<ParsedCommandArgs> {
    if args.len() > 7 {
        bail!(
            "output-delay-probe accepts at most seven arguments\n{}",
            usage()
        );
    }
    let parse_u32 = |index: usize, name: &str| -> Result<u32> {
        args.get(index)
            .map(|value| {
                value
                    .parse::<u32>()
                    .with_context(|| format!("parsing {name} `{value}`"))
            })
            .transpose()
            .map(|value| value.unwrap_or(0))
    };
    let parse_i64 = |index: usize, name: &str| -> Result<i64> {
        args.get(index)
            .map(|value| {
                value
                    .parse::<i64>()
                    .with_context(|| format!("parsing {name} `{value}`"))
            })
            .transpose()
            .map(|value| value.unwrap_or(0))
    };
    Ok(ParsedCommandArgs {
        probe_duration_seconds: parse_u32(0, "duration_s")?,
        probe_warmup_seconds: parse_u32(1, "warmup_s")?,
        probe_pulse_period_ms: parse_u32(2, "period_ms")?,
        probe_pulse_width_ms: parse_u32(3, "width_ms")?,
        probe_event_width_codes: args.get(4).cloned().unwrap_or_default(),
        probe_audio_delay_us: parse_i64(5, "audio_delay_us")?,
        probe_video_delay_us: parse_i64(6, "video_delay_us")?,
        ..ParsedCommandArgs::default()
    })
}

#[cfg(test)]
/// Keeps `parse_args_from` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn parse_args_from<I, S>(args: I) -> Result<Config>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    match parse_args_outcome_from(args)? {
        ParseOutcome::Run(config) => Ok(config),
        ParseOutcome::Help => bail!("{}", usage()),
    }
}

fn parse_args() -> Result<ParseOutcome> {
    parse_args_outcome_from(std::env::args().skip(1))
}

/// Keeps `capture_power_request` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn capture_power_request(command: CommandKind) -> Option<SetCapturePowerRequest> {
    let (enabled, command) = match command {
        CommandKind::Status
        | CommandKind::Version
        | CommandKind::CalibrationStatus
        | CommandKind::CalibrationAdjust
        | CommandKind::CalibrationRestoreDefault
        | CommandKind::CalibrationRestoreFactory
        | CommandKind::CalibrationSaveDefault
        | CommandKind::RecoverUsb
        | CommandKind::RecoverUac
        | CommandKind::RecoverUvc
        | CommandKind::ResetUsb
        | CommandKind::UpstreamSync
        | CommandKind::OutputDelayProbe => return None,
        CommandKind::Auto => (false, CapturePowerCommand::Auto),
        CommandKind::On => (true, CapturePowerCommand::ForceOn),
        CommandKind::Off => (false, CapturePowerCommand::ForceOff),
    };

    Some(SetCapturePowerRequest {
        enabled,
        command: command as i32,
    })
}

#[cfg(not(coverage))]
async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
    let channel = relay_transport::endpoint(server_addr)?
        .tcp_nodelay(true)
        .connect()
        .await
        .with_context(|| format!("connecting to relay at {server_addr}"))?;
    Ok(RelayClient::new(channel))
}

#[cfg(not(coverage))]
/// Keeps `get_server_capabilities` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
async fn get_server_capabilities(server_addr: &str) -> Result<HandshakeSet> {
    let channel = relay_transport::endpoint(server_addr)?
        .tcp_nodelay(true)
        .connect()
        .await
        .with_context(|| format!("connecting to handshake at {server_addr}"))?;
    let mut client = HandshakeClient::new(channel);
    Ok(client
        .get_capabilities(Request::new(Empty {}))
        .await
        .context("querying server capabilities")?
        .into_inner())
}

#[cfg(coverage)]
async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
    let channel = relay_transport::endpoint(server_addr)?
        .tcp_nodelay(true)
        .connect_lazy();
    Ok(RelayClient::new(channel))
}

fn print_state(state: lesavka_common::lesavka::CapturePowerState) {
    println!("available={}", state.available);
    println!("enabled={}", state.enabled);
    println!("mode={}", state.mode);
    println!("detected_devices={}", state.detected_devices);
    println!("active_leases={}", state.active_leases);
    println!("unit={}", state.unit);
    println!("detail={}", state.detail);
}

/// Keeps `print_versions` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn print_versions(server_addr: &str, caps: &HandshakeSet) {
    let server_version = if caps.server_version.is_empty() {
        "unknown"
    } else {
        caps.server_version.as_str()
    };
    let server_revision = if caps.server_revision.is_empty() {
        "unknown"
    } else {
        caps.server_revision.as_str()
    };
    println!("client_version={}", lesavka_client::VERSION);
    println!("client_revision={}", lesavka_client::REVISION);
    println!("server_addr={server_addr}");
    println!("server_version={server_version}");
    println!("server_revision={server_revision}");
    println!("server_camera_output={}", caps.camera_output);
    println!("server_camera_codec={}", caps.camera_codec);
    println!("server_camera_width={}", caps.camera_width);
    println!("server_camera_height={}", caps.camera_height);
    println!("server_camera_fps={}", caps.camera_fps);
    println!("server_bundled_webcam_media={}", caps.bundled_webcam_media);
}

include!("lesavka_relayctl/upstream_sync_formatting.rs");
/// Keeps `print_calibration_state` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn print_calibration_state(state: CalibrationState) {
    println!("calibration_profile={}", state.profile);
    println!(
        "calibration_factory_audio_offset_us={}",
        state.factory_audio_offset_us
    );
    println!(
        "calibration_factory_video_offset_us={}",
        state.factory_video_offset_us
    );
    println!(
        "calibration_default_audio_offset_us={}",
        state.default_audio_offset_us
    );
    println!(
        "calibration_default_video_offset_us={}",
        state.default_video_offset_us
    );
    println!(
        "calibration_active_audio_offset_us={}",
        state.active_audio_offset_us
    );
    println!(
        "calibration_active_video_offset_us={}",
        state.active_video_offset_us
    );
    println!("calibration_source={}", state.source);
    println!("calibration_confidence={}", state.confidence);
    println!("calibration_updated_at={}", state.updated_at);
    println!("calibration_detail={}", state.detail);
}

/// Keeps `calibration_request_for` explicit because it sits on CLI orchestration, where operators need deterministic exits and artifact paths.
/// Inputs are the typed parameters; output is the return value or side effect.
fn calibration_request_for(config: &Config) -> Option<CalibrationRequest> {
    let action = match config.command {
        CommandKind::CalibrationAdjust => CalibrationAction::AdjustActive,
        CommandKind::CalibrationRestoreDefault => CalibrationAction::RestoreDefault,
        CommandKind::CalibrationRestoreFactory => CalibrationAction::RestoreFactory,
        CommandKind::CalibrationSaveDefault => CalibrationAction::SaveActiveAsDefault,
        CommandKind::Status
        | CommandKind::Version
        | CommandKind::CalibrationStatus
        | CommandKind::Auto
        | CommandKind::On
        | CommandKind::Off
        | CommandKind::RecoverUsb
        | CommandKind::RecoverUac
        | CommandKind::RecoverUvc
        | CommandKind::ResetUsb
        | CommandKind::UpstreamSync
        | CommandKind::OutputDelayProbe => return None,
    };
    Some(CalibrationRequest {
        action: action as i32,
        audio_delta_us: config.audio_delta_us,
        video_delta_us: config.video_delta_us,
        observed_delivery_skew_ms: 0.0,
        observed_enqueue_skew_ms: 0.0,
        note: config.note.clone(),
    })
}

include!("lesavka_relayctl/command_dispatch.rs");
#[cfg(coverage)]
fn main() {
    let _ = parse_args as fn() -> Result<ParseOutcome>;
}

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