//! Command-line argument parsing for the huskies binary.

use std::path::PathBuf;

/// Parsed CLI arguments.
#[derive(Debug, PartialEq)]
pub(crate) struct CliArgs {
    /// Value from `--port <VALUE>` flag, if supplied.
    pub(crate) port: Option<u16>,
    /// Positional project path argument, if supplied.
    pub(crate) path: Option<String>,
    /// Whether the `init` subcommand was given.
    pub(crate) init: bool,
    /// Whether the `agent` subcommand was given.
    pub(crate) agent: bool,
    /// Rendezvous WebSocket URL for agent mode (e.g. `ws://host:3001/crdt-sync`).
    pub(crate) rendezvous: Option<String>,
    /// Whether `--gateway` mode was requested (proxy MCP calls to per-project containers).
    pub(crate) gateway: bool,
    /// One-time join token for registering this build agent with a gateway (`--join-token`).
    pub(crate) join_token: Option<String>,
    /// HTTP URL of the gateway to register with when a join token is provided (`--gateway-url`).
    pub(crate) gateway_url: Option<String>,
    /// WebSocket URL of the upstream gateway to forward permission requests to (`--upstream-gateway`).
    ///
    /// When set, the sled spawns a background uplink task that registers as a
    /// permission responder and forwards all `prompt_permission` tool calls to
    /// the gateway over a WebSocket.
    /// Also readable from the `HUSKIES_UPSTREAM_GATEWAY` env var.
    pub(crate) upstream_gateway: Option<String>,
    /// Path to a trampoline job file (`--trampoline <path>`).
    ///
    /// When set, the binary runs as a detached trampoline helper: it kills the
    /// old gateway, starts the new one, polls its health, and rolls back on failure.
    pub(crate) trampoline: Option<String>,
}

/// Parse CLI arguments into `CliArgs`, or exit early for `--help` / `--version`.
#[allow(clippy::string_slice)] // all slices use ASCII prefix lengths (e.g. "--port=".len()), always valid
pub(crate) fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
    let mut port: Option<u16> = None;
    let mut path: Option<String> = None;
    let mut init = false;
    let mut agent = false;
    let mut gateway = false;
    let mut rendezvous: Option<String> = None;
    let mut join_token: Option<String> = None;
    let mut gateway_url: Option<String> = None;
    let mut upstream_gateway: Option<String> = None;
    let mut trampoline: Option<String> = None;
    let mut i = 0;

    while i < args.len() {
        match args[i].as_str() {
            "--help" | "-h" => {
                print_help();
                std::process::exit(0);
            }
            "--version" | "-V" => {
                println!("huskies {}", env!("CARGO_PKG_VERSION"));
                std::process::exit(0);
            }
            "--port" => {
                i += 1;
                if i >= args.len() {
                    return Err("--port requires a value".to_string());
                }
                match args[i].parse::<u16>() {
                    Ok(p) => port = Some(p),
                    Err(_) => return Err(format!("invalid port value: '{}'", args[i])),
                }
            }
            a if a.starts_with("--port=") => {
                let val = &a["--port=".len()..];
                match val.parse::<u16>() {
                    Ok(p) => port = Some(p),
                    Err(_) => return Err(format!("invalid port value: '{val}'")),
                }
            }
            "--rendezvous" => {
                i += 1;
                if i >= args.len() {
                    return Err("--rendezvous requires a value".to_string());
                }
                rendezvous = Some(args[i].clone());
            }
            a if a.starts_with("--rendezvous=") => {
                let val = &a["--rendezvous=".len()..];
                rendezvous = Some(val.to_string());
            }
            "--join-token" => {
                i += 1;
                if i >= args.len() {
                    return Err("--join-token requires a value".to_string());
                }
                join_token = Some(args[i].clone());
            }
            a if a.starts_with("--join-token=") => {
                join_token = Some(a["--join-token=".len()..].to_string());
            }
            "--gateway-url" => {
                i += 1;
                if i >= args.len() {
                    return Err("--gateway-url requires a value".to_string());
                }
                gateway_url = Some(args[i].clone());
            }
            a if a.starts_with("--gateway-url=") => {
                gateway_url = Some(a["--gateway-url=".len()..].to_string());
            }
            "--upstream-gateway" => {
                i += 1;
                if i >= args.len() {
                    return Err("--upstream-gateway requires a value".to_string());
                }
                upstream_gateway = Some(args[i].clone());
            }
            a if a.starts_with("--upstream-gateway=") => {
                upstream_gateway = Some(a["--upstream-gateway=".len()..].to_string());
            }
            "--gateway" => {
                gateway = true;
            }
            "init" => {
                init = true;
            }
            "agent" => {
                agent = true;
            }
            "--trampoline" => {
                i += 1;
                if i >= args.len() {
                    return Err("--trampoline requires a path".to_string());
                }
                trampoline = Some(args[i].clone());
            }
            a if a.starts_with("--trampoline=") => {
                trampoline = Some(a["--trampoline=".len()..].to_string());
            }
            a if a.starts_with('-') => {
                return Err(format!("unknown option: {a}"));
            }
            a => {
                if path.is_some() {
                    return Err(format!("unexpected argument: {a}"));
                }
                path = Some(a.to_string());
            }
        }
        i += 1;
    }

    if agent && rendezvous.is_none() {
        return Err("agent mode requires --rendezvous <URL>".to_string());
    }

    Ok(CliArgs {
        port,
        path,
        init,
        agent,
        rendezvous,
        gateway,
        join_token,
        gateway_url,
        upstream_gateway,
        trampoline,
    })
}

pub(crate) fn print_help() {
    println!("huskies [OPTIONS] [PATH]");
    println!("huskies init [OPTIONS] [PATH]");
    println!("huskies agent --rendezvous <URL> [OPTIONS] [PATH]");
    println!("huskies --gateway [OPTIONS] [PATH]");
    println!();
    println!("Serve a huskies project.");
    println!();
    println!("COMMANDS:");
    println!("  init     Scaffold a new .huskies/ project and start the interactive setup wizard.");
    println!("  agent    Run as a headless build agent — syncs CRDT state, claims and runs work.");
    println!();
    println!("ARGS:");
    println!(
        "  PATH  Path to an existing project directory. \
         If omitted, huskies searches parent directories for a .huskies/ root."
    );
    println!();
    println!("OPTIONS:");
    println!("  -h, --help               Print this help and exit");
    println!("  -V, --version            Print the version and exit");
    println!(
        "  --port <PORT>            Port to listen on (default: 3001). Persisted to project.toml."
    );
    println!("  --rendezvous <URL>       WebSocket URL of the rendezvous peer (agent mode only).");
    println!("                           Example: ws://server:3001/crdt-sync");
    println!("  --gateway                Start in gateway mode. Reads projects.toml from PATH");
    println!(
        "                           (or cwd) and proxies MCP calls to per-project containers."
    );
    println!("  --join-token <TOKEN>     One-time token for registering this build agent with a");
    println!("                           gateway. Also readable from HUSKIES_JOIN_TOKEN env var.");
    println!("  --gateway-url <URL>      HTTP URL of the gateway to register with when");
    println!("                           --join-token is provided (agent mode only).");
    println!("                           Also readable from HUSKIES_GATEWAY_URL env var.");
    println!("  --upstream-gateway <URL> WebSocket URL of an upstream gateway to forward");
    println!("                           permission requests to (sled mode). When set, the");
    println!("                           sled connects to WS URL and forwards all");
    println!("                           prompt_permission calls via the uplink protocol.");
    println!("                           Also readable from HUSKIES_UPSTREAM_GATEWAY env var.");
}

/// Resolve the optional positional path argument into an absolute `PathBuf`.
pub(crate) fn resolve_path_arg(path_str: Option<&str>, cwd: &std::path::Path) -> Option<PathBuf> {
    path_str.map(|s| crate::io::fs::resolve_cli_path(cwd, s))
}

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

    #[test]
    fn parse_no_args() {
        let result = parse_cli_args(&[]).unwrap();
        assert_eq!(result.port, None);
        assert_eq!(result.path, None);
        assert!(!result.init);
        assert!(!result.agent);
        assert!(!result.gateway);
        assert_eq!(result.rendezvous, None);
    }

    #[test]
    fn parse_gateway_flag() {
        let args = vec!["--gateway".to_string()];
        let result = parse_cli_args(&args).unwrap();
        assert!(result.gateway);
        assert!(!result.init);
        assert!(!result.agent);
    }

    #[test]
    fn parse_gateway_with_port_and_path() {
        let args = vec![
            "--gateway".to_string(),
            "--port".to_string(),
            "5000".to_string(),
            "/my/config".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert!(result.gateway);
        assert_eq!(result.port, Some(5000));
        assert_eq!(result.path, Some("/my/config".to_string()));
    }

    #[test]
    fn parse_unknown_flag_is_error() {
        let args = vec!["--serve".to_string()];
        assert!(parse_cli_args(&args).is_err());
    }

    #[test]
    fn parse_path_only() {
        let args = vec!["/some/path".to_string()];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.path, Some("/some/path".to_string()));
        assert_eq!(result.port, None);
        assert!(!result.init);
    }

    #[test]
    fn parse_port_flag() {
        let args = vec!["--port".to_string(), "4000".to_string()];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.port, Some(4000));
        assert_eq!(result.path, None);
    }

    #[test]
    fn parse_port_equals_syntax() {
        let args = vec!["--port=5000".to_string()];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.port, Some(5000));
    }

    #[test]
    fn parse_port_with_path() {
        let args = vec![
            "--port".to_string(),
            "4200".to_string(),
            "/some/path".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.port, Some(4200));
        assert_eq!(result.path, Some("/some/path".to_string()));
    }

    #[test]
    fn parse_port_missing_value_is_error() {
        let args = vec!["--port".to_string()];
        assert!(parse_cli_args(&args).is_err());
    }

    #[test]
    fn parse_port_invalid_value_is_error() {
        let args = vec!["--port".to_string(), "abc".to_string()];
        assert!(parse_cli_args(&args).is_err());
    }

    #[test]
    fn parse_init_subcommand() {
        let args = vec!["init".to_string()];
        let result = parse_cli_args(&args).unwrap();
        assert!(result.init);
        assert_eq!(result.path, None);
    }

    #[test]
    fn parse_init_with_path_and_port() {
        let args = vec![
            "init".to_string(),
            "--port".to_string(),
            "3000".to_string(),
            "/my/project".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert!(result.init);
        assert_eq!(result.port, Some(3000));
        assert_eq!(result.path, Some("/my/project".to_string()));
    }

    #[test]
    fn parse_join_token_flag() {
        let args = vec![
            "agent".to_string(),
            "--rendezvous".to_string(),
            "ws://host:3001/crdt-sync".to_string(),
            "--join-token".to_string(),
            "my-secret-token".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.join_token, Some("my-secret-token".to_string()));
    }

    #[test]
    fn parse_join_token_equals_syntax() {
        let args = vec![
            "agent".to_string(),
            "--rendezvous".to_string(),
            "ws://host:3001/crdt-sync".to_string(),
            "--join-token=abc123".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.join_token, Some("abc123".to_string()));
    }

    #[test]
    fn parse_gateway_url_flag() {
        let args = vec![
            "agent".to_string(),
            "--rendezvous".to_string(),
            "ws://host:3001/crdt-sync".to_string(),
            "--gateway-url".to_string(),
            "http://gateway:3000".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(result.gateway_url, Some("http://gateway:3000".to_string()));
    }

    #[test]
    fn parse_join_token_missing_value_is_error() {
        let args = vec!["--join-token".to_string()];
        assert!(parse_cli_args(&args).is_err());
    }

    #[test]
    fn parse_gateway_url_missing_value_is_error() {
        let args = vec!["--gateway-url".to_string()];
        assert!(parse_cli_args(&args).is_err());
    }

    #[test]
    fn parse_no_args_join_token_and_gateway_url_are_none() {
        let result = parse_cli_args(&[]).unwrap();
        assert_eq!(result.join_token, None);
        assert_eq!(result.gateway_url, None);
        assert_eq!(result.upstream_gateway, None);
    }

    #[test]
    fn parse_upstream_gateway_flag() {
        let args = vec![
            "--upstream-gateway".to_string(),
            "ws://gateway:3001/api/sled-uplink?token=abc".to_string(),
        ];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(
            result.upstream_gateway,
            Some("ws://gateway:3001/api/sled-uplink?token=abc".to_string())
        );
    }

    #[test]
    fn parse_upstream_gateway_equals_syntax() {
        let args = vec!["--upstream-gateway=ws://gw:3001/api/sled-uplink?token=x".to_string()];
        let result = parse_cli_args(&args).unwrap();
        assert_eq!(
            result.upstream_gateway,
            Some("ws://gw:3001/api/sled-uplink?token=x".to_string())
        );
    }

    #[test]
    fn parse_upstream_gateway_missing_value_is_error() {
        let args = vec!["--upstream-gateway".to_string()];
        assert!(parse_cli_args(&args).is_err());
    }

    // ── resolve_path_arg ────────────────────────────────────────────

    #[test]
    fn resolve_path_arg_none_when_no_path() {
        let cwd = PathBuf::from("/home/user/project");
        let result = resolve_path_arg(None, &cwd);
        assert!(result.is_none());
    }

    #[test]
    fn resolve_path_arg_returns_path_for_absolute_arg() {
        let cwd = PathBuf::from("/home/user/project");
        let result = resolve_path_arg(Some("/some/absolute/path"), &cwd).unwrap();
        assert!(result.ends_with("absolute/path") || result == Path::new("/some/absolute/path"));
    }

    #[test]
    fn resolve_path_arg_resolves_dot_to_cwd() {
        let tmp = tempfile::tempdir().unwrap();
        let cwd = tmp.path().to_path_buf();
        let result = resolve_path_arg(Some("."), &cwd).unwrap();
        assert_eq!(result, cwd.canonicalize().unwrap_or(cwd));
    }
}
