use clap::{Args, Subcommand};
use std::path::PathBuf;

use super::{BANNER, HELP_STYLES};

/// Jack a role into an isolated container
///
/// TARGET can be a path (~/Projects/my-app), a path with container
/// destination (~/Projects/my-app:/app), or a saved workspace name.
/// When omitted, the current directory is used.
//
// Five launch-time toggles (rebuild / no_intro / debug / force) plus the
// positional `selector` / `target` / `mounts` map directly to CLI flags;
// bundling them into nested structs would obscure rather than clarify.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Args, PartialEq, Eq)]
#[command(
    before_help = BANNER,
    styles = HELP_STYLES,
    after_long_help = "\
Examples:
  jackin load                                          # use workspace + last role for cwd
  jackin load --rebuild                                # same, with fresh agent install
  jackin load agent-smith
  jackin load agent-smith ~/Projects/my-app
  jackin load agent-smith ~/Projects/my-app:/app
  jackin load agent-smith big-monorepo
  jackin load agent-smith big-monorepo --mount ~/extra-data
  jackin load agent-smith ~/app --mount ~/cache:/cache:ro
  jackin load the-architect --role-branch feat/my-pr   # build + test a PR branch locally"
)]
pub struct LoadArgs {
    /// Role class selector (e.g. `agent-smith`, `chainargos/agent-brown`).
    /// When omitted, uses the last-used or default role for the workspace.
    pub selector: Option<String>,
    /// Path, `path:container-dest`, or saved workspace name
    #[arg(value_name = "TARGET")]
    pub target: Option<String>,
    /// Additional bind-mount spec as `path[:ro]` or `src:dst[:ro]` (repeatable)
    #[arg(long = "mount")]
    pub mounts: Vec<String>,
    /// Force rebuild the Docker image and refresh agent CLI install layers
    #[arg(long, default_value_t = false)]
    pub rebuild: bool,
    /// Skip the animated intro sequence
    #[arg(long, default_value_t = false)]
    pub no_intro: bool,
    /// Acknowledge a dirty host working tree for isolated mounts.
    #[arg(long)]
    pub force: bool,
    /// Agent to launch under (claude, codex, amp, kimi, or opencode). Overrides the
    /// workspace's `default_agent` field for this launch only. When
    /// neither is set, defaults to claude.
    #[arg(long, value_parser = parse_agent)]
    pub agent: Option<crate::agent::Agent>,
    /// Check out a specific branch of the role repository for local testing.
    /// The published image is ignored and the image is built from the branch's
    /// Dockerfile using Docker's layer cache. Useful for verifying a PR before
    /// it merges to the default branch.
    #[arg(long)]
    pub role_branch: Option<String>,
}

fn parse_agent(s: &str) -> Result<crate::agent::Agent, String> {
    s.parse()
        .map_err(|e: crate::agent::ParseAgentError| e.to_string())
}

/// Reattach to a running role's session
///
/// When omitted, finds the saved workspace for the current directory and
/// reconnects to a running role container belonging to it.
#[derive(Debug, Args, PartialEq, Eq)]
#[command(
    before_help = BANNER,
    styles = HELP_STYLES,
    after_long_help = "\
Examples:
  jackin hardline                              # auto-detect workspace + running role for cwd
  jackin hardline --inspect                    # inspect detected instance state without attaching
  jackin hardline --new                        # start another agent session in the detected instance
  jackin hardline --new --agent codex          # start a specific runtime in the selected instance
  jackin hardline --shell                      # open a one-shot zsh shell in the detected instance
  jackin hardline agent-smith
  jackin hardline --inspect k7p9m2xq
  jackin hardline chainargos/the-architect
  jackin hardline k7p9m2xq
  jackin hardline jk-k7p9m2xq-agentsmith"
)]
pub struct HardlineArgs {
    /// Role class selector, instance ID, or container name to reconnect to.
    /// When omitted, uses the running role in the workspace for the current directory.
    pub selector: Option<String>,
    /// Print manifest, Docker, `DinD`, and mount state without attaching or restarting.
    #[arg(long)]
    pub inspect: bool,
    /// Start a new foreground agent process inside the selected running instance.
    #[arg(long, conflicts_with = "inspect")]
    pub new: bool,
    /// Agent runtime for `--new` (claude, codex, amp, kimi, or opencode). Defaults to the instance manifest.
    #[arg(long, value_parser = parse_agent, requires = "new")]
    pub agent: Option<crate::agent::Agent>,
    /// Open a one-shot zsh shell in the selected running instance without a named tmux session.
    #[arg(long, conflicts_with_all = ["inspect", "new"])]
    pub shell: bool,
}

/// Open the operator console to manage workspaces, launch roles, and more
///
/// Running `jackin` with no subcommand on an interactive terminal opens the
/// same console.
#[derive(Debug, Args, PartialEq, Eq, Default, Clone)]
#[command(before_help = BANNER, styles = HELP_STYLES)]
pub struct ConsoleArgs {}

/// Validate, migrate, and scaffold role repositories
#[derive(Debug, Subcommand, PartialEq, Eq)]
pub enum RoleCommand {
    /// Validate a role repository's manifest, Dockerfile, hooks, and env declarations
    #[command(before_help = BANNER, styles = HELP_STYLES)]
    Validate(RoleRepoPathArgs),
    /// Migrate a role manifest to the current schema version, then validate it
    #[command(before_help = BANNER, styles = HELP_STYLES)]
    Migrate(RoleRepoPathArgs),
    /// Create a new role repository scaffold
    #[command(before_help = BANNER, styles = HELP_STYLES)]
    Create(RoleCreateArgs),
    /// Print the construct image version tag pinned in the role Dockerfile
    #[command(before_help = BANNER, styles = HELP_STYLES)]
    ConstructVersion(RoleRepoPathArgs),
    /// Print the published Docker image declared in the role manifest
    #[command(before_help = BANNER, styles = HELP_STYLES)]
    PublishedImage(RoleRepoPathArgs),
}

/// Role repository path argument shared by `validate` and `migrate`.
#[derive(Debug, Args, PartialEq, Eq)]
pub struct RoleRepoPathArgs {
    /// Role repository path. Defaults to the current directory.
    #[arg(value_name = "ROLE_REPO_PATH")]
    pub path: Option<PathBuf>,
}

/// Arguments for `jackin role create`.
#[derive(Debug, Args, PartialEq, Eq)]
pub struct RoleCreateArgs {
    /// Role name or namespace/name selector, e.g. `docs-writer` or `chainargos/backend-engineer`
    pub role: String,
    /// Projects directory where the role repo should be created. Defaults to `JACKIN_PROJECTS_DIR` or ~/Projects.
    #[arg(value_name = "PROJECTS_DIR")]
    pub projects_dir: Option<PathBuf>,
}

#[cfg(test)]
mod tests {
    use crate::cli::{Cli, Command};
    use clap::Parser;

    /// Strip ANSI escape sequences for clean test assertions.
    fn strip_ansi(s: &str) -> String {
        let mut result = String::with_capacity(s.len());
        let mut chars = s.chars();
        while let Some(ch) = chars.next() {
            if ch == '\x1b' {
                // Skip until 'm' (SGR) or other terminator
                for inner in chars.by_ref() {
                    if inner.is_ascii_alphabetic() {
                        break;
                    }
                }
            } else {
                result.push(ch);
            }
        }
        result
    }

    fn help_text(args: &[&str]) -> String {
        let err = Cli::try_parse_from(args).unwrap_err();
        strip_ansi(&err.to_string())
    }

    #[test]
    fn load_args_parses_agent_flag() {
        let cli =
            Cli::try_parse_from(["jackin", "load", "agent-smith", "--agent", "codex"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                agent: Some(crate::agent::Agent::Codex),
                ..
            }))
        ));
    }

    #[test]
    fn load_args_parses_amp_agent_flag() {
        let cli = Cli::try_parse_from(["jackin", "load", "agent-smith", "--agent", "amp"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                agent: Some(crate::agent::Agent::Amp),
                ..
            }))
        ));
    }

    #[test]
    fn load_args_rejects_unknown_agent() {
        let res = Cli::try_parse_from(["jackin", "load", "agent-smith", "--agent", "foo"]);
        assert!(res.is_err());
    }

    #[test]
    fn load_args_agent_optional() {
        let cli = Cli::try_parse_from(["jackin", "load", "agent-smith"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs { agent: None, .. }))
        ));
    }

    #[test]
    fn load_args_parses_branch_flag() {
        let cli = Cli::try_parse_from([
            "jackin",
            "load",
            "the-architect",
            "--role-branch",
            "feat/my-pr",
        ])
        .unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                role_branch: Some(ref b),
                ..
            })) if b == "feat/my-pr"
        ));
    }

    #[test]
    fn load_args_branch_optional() {
        let cli = Cli::try_parse_from(["jackin", "load", "the-architect"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                role_branch: None,
                ..
            }))
        ));
    }

    #[test]
    fn parses_load_command() {
        let cli = Cli::try_parse_from(["jackin", "load", "agent-smith"]).unwrap();
        // `debug` is omitted from the pattern: it is env-backed
        // (`JACKIN_DEBUG`), so its default depends on the runner's env.
        // `tests/cli_debug_env.rs` covers the env-driven behavior.
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                selector: Some(ref s),
                target: None,
                no_intro: false,
                ..
            })) if s == "agent-smith"
        ));
    }

    #[test]
    fn parses_load_without_selector() {
        let cli = Cli::try_parse_from(["jackin", "load"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                selector: None,
                target: None,
                ..
            }))
        ));
    }

    #[test]
    fn parses_load_rebuild_without_selector() {
        let cli = Cli::try_parse_from(["jackin", "load", "--rebuild"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                selector: None,
                rebuild: true,
                ..
            }))
        ));
    }

    #[test]
    fn parses_load_with_target_path() {
        let cli =
            Cli::try_parse_from(["jackin", "load", "agent-smith", "~/Projects/my-app"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                target: Some(ref t),
                ..
            })) if t == "~/Projects/my-app"
        ));
    }

    #[test]
    fn parses_load_with_target_and_mount() {
        let cli = Cli::try_parse_from([
            "jackin",
            "load",
            "agent-smith",
            "big-monorepo",
            "--mount",
            "/tmp/cache:/workspace/cache:ro",
        ])
        .unwrap();

        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                target: Some(ref t),
                ref mounts,
                ..
            })) if t == "big-monorepo" && mounts.len() == 1
        ));
    }

    #[test]
    fn parses_load_with_mount_only() {
        let cli = Cli::try_parse_from([
            "jackin",
            "load",
            "agent-smith",
            "--mount",
            "/tmp/project:/workspace/project",
            "--mount",
            "/tmp/cache:/workspace/cache:ro",
        ])
        .unwrap();

        assert!(matches!(
            cli.command,
            Some(Command::Load(super::LoadArgs {
                target: None,
                ref mounts,
                ..
            })) if mounts.len() == 2
        ));
    }

    #[test]
    fn parses_console_command() {
        let cli = Cli::try_parse_from(["jackin", "console"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Console(super::ConsoleArgs { .. }))
        ));
    }

    #[test]
    fn parses_console_with_debug() {
        let cli = Cli::try_parse_from(["jackin", "console", "--debug"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Console(super::ConsoleArgs { .. }))
        ));
        // --debug is global on Cli, not on ConsoleArgs.
        assert!(cli.debug);
    }

    #[test]
    fn parses_bare_jackin_as_no_subcommand() {
        let cli = Cli::try_parse_from(["jackin"]).unwrap();
        assert!(cli.command.is_none());
    }

    #[test]
    fn parses_bare_jackin_with_top_level_debug() {
        let cli = Cli::try_parse_from(["jackin", "--debug"]).unwrap();
        assert!(cli.command.is_none());
        // CLI flag wins over env, so this assertion holds even when
        // `JACKIN_DEBUG=0` is set in the runner's env.
        assert!(cli.debug);
    }

    // ── Load help ───────────────────────────────────────────────────────

    #[test]
    fn load_help_shows_description_and_examples() {
        let help = help_text(&["jackin", "load", "--help"]);
        assert!(help.contains("Jack a role into an isolated container"));
        assert!(help.contains("Examples:"));
        assert!(help.contains("jackin load agent-smith"));
        assert!(help.contains("jackin load agent-smith big-monorepo"));
    }

    #[test]
    fn load_help_shows_mount_format() {
        let help = help_text(&["jackin", "load", "--help"]);
        assert!(
            help.contains("path[:ro]") && help.contains("src:dst[:ro]"),
            "mount format missing"
        );
    }

    // ── Hardline help ───────────────────────────────────────────────────

    #[test]
    fn hardline_help_shows_examples() {
        let help = help_text(&["jackin", "hardline", "--help"]);
        assert!(help.contains("Reattach to a running role"));
        assert!(help.contains("jackin hardline agent-smith"));
        assert!(
            help.contains("jackin hardline ") && help.contains("auto-detect workspace"),
            "missing no-arg usage in hardline help: {help}"
        );
    }

    #[test]
    fn parses_hardline_without_selector() {
        let cli = Cli::try_parse_from(["jackin", "hardline"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Hardline(super::HardlineArgs {
                selector: None,
                inspect: false,
                new: false,
                agent: None,
                shell: false,
            }))
        ));
    }

    #[test]
    fn parses_hardline_with_selector() {
        let cli = Cli::try_parse_from(["jackin", "hardline", "agent-smith"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Hardline(super::HardlineArgs {
                selector: Some(ref s),
                inspect: false,
                new: false,
                agent: None,
                shell: false,
            })) if s == "agent-smith"
        ));
    }

    #[test]
    fn parses_hardline_inspect_flag() {
        let cli = Cli::try_parse_from(["jackin", "hardline", "--inspect", "k7p9m2xq"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Hardline(super::HardlineArgs {
                selector: Some(ref s),
                inspect: true,
                new: false,
                agent: None,
                shell: false,
            })) if s == "k7p9m2xq"
        ));
    }

    #[test]
    fn parses_hardline_new_agent_flags() {
        let cli = Cli::try_parse_from(["jackin", "hardline", "--new", "--agent", "codex"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Hardline(super::HardlineArgs {
                selector: None,
                inspect: false,
                new: true,
                agent: Some(crate::agent::Agent::Codex),
                shell: false,
            }))
        ));
    }

    #[test]
    fn parses_hardline_shell_flag() {
        let cli = Cli::try_parse_from(["jackin", "hardline", "--shell"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Hardline(super::HardlineArgs {
                selector: None,
                inspect: false,
                new: false,
                agent: None,
                shell: true,
            }))
        ));
    }

    #[test]
    fn rejects_hardline_shell_with_new() {
        let res = Cli::try_parse_from(["jackin", "hardline", "--shell", "--new"]);
        assert!(res.is_err());
    }

    #[test]
    fn rejects_hardline_shell_with_inspect() {
        let res = Cli::try_parse_from(["jackin", "hardline", "--shell", "--inspect"]);
        assert!(res.is_err());
    }

    #[test]
    fn rejects_hardline_agent_without_new() {
        let res = Cli::try_parse_from(["jackin", "hardline", "--agent", "codex"]);

        assert!(res.is_err());
    }

    #[test]
    fn rejects_hardline_inspect_with_new() {
        let res = Cli::try_parse_from(["jackin", "hardline", "--inspect", "--new"]);

        assert!(res.is_err());
    }

    #[test]
    fn parses_role_validate_with_default_path() {
        let cli = Cli::try_parse_from(["jackin", "role", "validate"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Role(super::RoleCommand::Validate(
                super::RoleRepoPathArgs { path: None }
            )))
        ));
    }

    #[test]
    fn parses_role_migrate_with_path() {
        let cli = Cli::try_parse_from(["jackin", "role", "migrate", "/tmp/my-role"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Role(super::RoleCommand::Migrate(
                super::RoleRepoPathArgs { path: Some(ref path) }
            ))) if path == std::path::Path::new("/tmp/my-role")
        ));
    }

    #[test]
    fn parses_role_construct_version_with_default_path() {
        let cli = Cli::try_parse_from(["jackin", "role", "construct-version"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Role(super::RoleCommand::ConstructVersion(
                super::RoleRepoPathArgs { path: None }
            )))
        ));
    }

    #[test]
    fn parses_role_construct_version_with_path() {
        let cli =
            Cli::try_parse_from(["jackin", "role", "construct-version", "/tmp/my-role"]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Role(super::RoleCommand::ConstructVersion(
                super::RoleRepoPathArgs { path: Some(ref p) }
            ))) if p == std::path::Path::new("/tmp/my-role")
        ));
    }

    #[test]
    fn parses_role_create_with_projects_dir() {
        let cli =
            Cli::try_parse_from(["jackin", "role", "create", "ChainArgos/Backend", "."]).unwrap();
        assert!(matches!(
            cli.command,
            Some(Command::Role(super::RoleCommand::Create(
                super::RoleCreateArgs {
                    ref role,
                    projects_dir: Some(ref path),
                }
            ))) if role == "ChainArgos/Backend" && path == std::path::Path::new(".")
        ));
    }
}
