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

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CommandKind {
    Status,
    Auto,
    On,
    Off,
    ResetUsb,
}

impl CommandKind {
    fn parse(value: &str) -> Option<Self> {
        match value {
            "status" | "get" => Some(Self::Status),
            "auto" => Some(Self::Auto),
            "on" | "force-on" => Some(Self::On),
            "off" | "force-off" => Some(Self::Off),
            "reset-usb" | "recover-usb" => Some(Self::ResetUsb),
            _ => None,
        }
    }
}

#[derive(Debug, Eq, PartialEq)]
struct Config {
    server: String,
    command: CommandKind,
}

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

fn usage() -> &'static str {
    "Usage: lesavka-relayctl [--server http://HOST:50051] <status|auto|on|off|reset-usb>"
}

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;

    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());
                }
            }
            _ => bail!("unexpected argument `{arg}`\n{}", usage()),
        }
    }

    Ok(ParseOutcome::Run(Config {
        server,
        command: command.unwrap_or(CommandKind::Status),
    }))
}

#[cfg(test)]
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))
}

fn capture_power_request(command: CommandKind) -> Option<SetCapturePowerRequest> {
    let (enabled, command) = match command {
        CommandKind::Status | CommandKind::ResetUsb => 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 = Channel::from_shared(server_addr.to_string())
        .context("invalid relay server address")?
        .tcp_nodelay(true)
        .connect()
        .await
        .with_context(|| format!("connecting to relay at {server_addr}"))?;
    Ok(RelayClient::new(channel))
}

#[cfg(coverage)]
async fn connect(server_addr: &str) -> Result<RelayClient<Channel>> {
    let channel = Channel::from_shared(server_addr.to_string())
        .context("invalid relay server address")?
        .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);
}

#[cfg(not(coverage))]
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
    let config = match parse_args()? {
        ParseOutcome::Run(config) => config,
        ParseOutcome::Help => {
            println!("{}", usage());
            return Ok(());
        }
    };
    let mut client = connect(config.server.as_str()).await?;

    if let Some(request) = capture_power_request(config.command) {
        let context = match config.command {
            CommandKind::Auto => "setting capture power to auto",
            CommandKind::On => "forcing capture power on",
            CommandKind::Off => "forcing capture power off",
            CommandKind::Status | CommandKind::ResetUsb => unreachable!(),
        };
        let reply = client
            .set_capture_power(Request::new(request))
            .await
            .context(context)?
            .into_inner();
        print_state(reply);
        return Ok(());
    }

    let reply = match config.command {
        CommandKind::Status => client
            .get_capture_power(Request::new(Empty {}))
            .await
            .context("querying capture power state")?
            .into_inner(),
        CommandKind::ResetUsb => {
            let reply = client
                .reset_usb(Request::new(Empty {}))
                .await
                .context("forcing USB gadget recovery")?
                .into_inner();
            println!("ok={}", reply.ok);
            return Ok(());
        }
        CommandKind::Auto | CommandKind::On | CommandKind::Off => unreachable!(),
    };

    print_state(reply);
    Ok(())
}

#[cfg(coverage)]
fn main() {
    let _ = parse_args as fn() -> Result<ParseOutcome>;
}

#[cfg(test)]
mod tests {
    use super::{
        CapturePowerCommand, CommandKind, ParseOutcome, capture_power_request, parse_args_from,
        parse_args_outcome_from,
    };
    use lesavka_common::lesavka::CapturePowerState;

    #[test]
    fn command_aliases_parse_to_stable_actions() {
        assert_eq!(CommandKind::parse("status"), Some(CommandKind::Status));
        assert_eq!(CommandKind::parse("get"), Some(CommandKind::Status));
        assert_eq!(CommandKind::parse("force-on"), Some(CommandKind::On));
        assert_eq!(CommandKind::parse("force-off"), Some(CommandKind::Off));
        assert_eq!(
            CommandKind::parse("recover-usb"),
            Some(CommandKind::ResetUsb)
        );
        assert_eq!(CommandKind::parse("wat"), None);
    }

    #[test]
    fn parse_args_defaults_to_local_status() {
        let config = parse_args_from(std::iter::empty::<&str>()).expect("default config");
        assert_eq!(config.server, "http://127.0.0.1:50051");
        assert_eq!(config.command, CommandKind::Status);
    }

    #[test]
    fn parse_args_accepts_server_and_command() {
        let config =
            parse_args_from(["--server", " http://lab:50051 ", "reset-usb"]).expect("config");
        assert_eq!(config.server, "http://lab:50051");
        assert_eq!(config.command, CommandKind::ResetUsb);
    }

    #[test]
    fn parse_args_rejects_bad_inputs() {
        assert!(parse_args_from(["--server"]).is_err());
        assert!(parse_args_from(["nope"]).is_err());
        assert!(parse_args_from(["status", "extra"]).is_err());
    }

    #[test]
    fn parse_args_reports_help_without_exiting_test_process() {
        assert_eq!(
            parse_args_outcome_from(["--help"]).unwrap(),
            ParseOutcome::Help
        );
        assert_eq!(parse_args_outcome_from(["-h"]).unwrap(), ParseOutcome::Help);
        assert!(parse_args_from(["--help"]).is_err());
    }

    #[test]
    fn parse_args_runtime_wrapper_is_non_panicking_under_tests() {
        let _ = super::parse_args();
    }

    #[cfg(coverage)]
    #[test]
    fn coverage_main_references_runtime_parser() {
        super::main();
    }

    #[cfg(coverage)]
    #[tokio::test(flavor = "current_thread")]
    async fn coverage_connect_uses_lazy_channel_after_endpoint_validation() {
        let client = super::connect("http://127.0.0.1:1")
            .await
            .expect("coverage lazy channel");
        drop(client);
    }

    #[test]
    fn mutating_commands_map_to_capture_power_requests() {
        let auto = capture_power_request(CommandKind::Auto).expect("auto request");
        assert!(!auto.enabled);
        assert_eq!(auto.command, CapturePowerCommand::Auto as i32);

        let on = capture_power_request(CommandKind::On).expect("on request");
        assert!(on.enabled);
        assert_eq!(on.command, CapturePowerCommand::ForceOn as i32);

        let off = capture_power_request(CommandKind::Off).expect("off request");
        assert!(!off.enabled);
        assert_eq!(off.command, CapturePowerCommand::ForceOff as i32);

        assert!(capture_power_request(CommandKind::Status).is_none());
        assert!(capture_power_request(CommandKind::ResetUsb).is_none());
    }

    #[test]
    fn print_state_accepts_full_capture_power_payload() {
        super::print_state(CapturePowerState {
            available: true,
            enabled: false,
            mode: "auto".to_string(),
            detected_devices: 2,
            active_leases: 1,
            unit: "lesavka-capture-power.service".to_string(),
            detail: "ready".to_string(),
        });
    }
}
