use homeboy::core::runners::{
    self as runner, ReverseRunnerWorkerOptions, ReverseRunnerWorkerOutput, RunnerExecOutput,
};
use homeboy::core::server::RunnerSettings;
use serde_json::Value;

use super::super::output_runtime::{CommandPresentation, JsonCommandRun};
use super::super::raw_output::RawCommandRun;
use super::super::CmdResult;
use super::broker::run_broker;
use super::cli::{RunnerArgs, RunnerCommand};
use super::exec::exec;
use super::jobs::RunnerJobCommandOutput;
use super::registry::{add, connect, enable, list, remove, set, show, RunnerAddInput};
use super::types::{RunnerCommandOutput, RunnerEnvOutput, RunnerOutput};
use super::{
    doctor, env as env_mod, jobs, lifecycle, policy, refresh_plan, registry, status as status_mod,
    workspace,
};

pub fn run(
    args: RunnerArgs,
    _global: &crate::commands::GlobalArgs,
) -> CmdResult<RunnerCommandOutput> {
    match args.command {
        RunnerCommand::Add {
            json,
            skip_existing,
            id,
            kind,
            server,
            workspace_root,
            homeboy_path,
            daemon,
            concurrency_limit,
            artifact_policy,
        } => map_registry(add(RunnerAddInput {
            json,
            skip_existing,
            id,
            kind,
            server,
            workspace_root,
            settings: RunnerSettings {
                homeboy_path,
                daemon,
                concurrency_limit,
                artifact_policy,
                ..RunnerSettings::default()
            },
        })),
        RunnerCommand::Enable {
            server_id,
            workspace_root,
            homeboy_path,
            daemon,
            concurrency_limit,
            artifact_policy,
        } => map_registry(enable(
            &server_id,
            workspace_root,
            RunnerSettings {
                homeboy_path,
                daemon,
                concurrency_limit,
                artifact_policy,
                ..RunnerSettings::default()
            },
        )),
        RunnerCommand::List => map_registry(list()),
        RunnerCommand::Show { id } => map_registry(show(&id)),
        RunnerCommand::Set { args } => map_registry(set(args)),
        RunnerCommand::Trust {
            runner_id,
            projects,
            commands,
            allow_raw_exec,
            workspace_roots,
            artifact_policy,
            peers,
            fingerprints,
        } => map_registry(policy::update(
            &runner_id,
            policy::RunnerPolicyPatch::trust(
                peers,
                fingerprints,
                projects,
                commands,
                allow_raw_exec,
                workspace_roots,
                artifact_policy,
            ),
            "runner.trust",
        )),
        RunnerCommand::Pair {
            runner_id,
            peers,
            fingerprints,
            projects,
            workspace_roots,
            allow_raw_exec,
        } => map_registry(policy::update(
            &runner_id,
            policy::RunnerPolicyPatch::pair(
                peers,
                fingerprints,
                projects,
                allow_raw_exec,
                workspace_roots,
            ),
            "runner.pair",
        )),
        RunnerCommand::Remove { id } => map_registry(remove(&id)),
        RunnerCommand::Doctor {
            runner_id,
            path,
            required_extensions,
            required_tools,
            scope,
            repair,
        } => map_doctor(doctor::run_with_options(
            &runner_id,
            doctor::RunnerDoctorOptions {
                path,
                extensions: required_extensions,
                required_tools,
                scope: scope.into(),
                repair,
            },
        )),
        RunnerCommand::Connect {
            id,
            reverse,
            reverse_runner,
            broker_url,
        } => map_registry(connect(&id, reverse, reverse_runner, broker_url)),
        RunnerCommand::Status { id } => map_registry(status_mod::status(id.as_deref())),
        RunnerCommand::Disconnect { id } => map_registry(registry::disconnect(&id)),
        RunnerCommand::RefreshHomeboy {
            runner_id,
            select,
            source,
            git_ref,
            target_dir,
            reconnect,
            dry_run,
        } => map_refresh_homeboy(runner::refresh_homeboy_binary(
            runner::HomeboyBinaryRefreshOptions {
                runner_id,
                mode: match select {
                    Some(binary_path) => runner::HomeboyBinaryRefreshMode::Select { binary_path },
                    None => runner::HomeboyBinaryRefreshMode::Materialize,
                },
                source,
                git_ref,
                target_dir,
                reconnect,
                dry_run,
            },
        )),
        RunnerCommand::DevSync {
            runner_id,
            homeboy_source,
            homeboy_binary,
            extensions,
            reconnect,
            dry_run,
        } => map_dev_sync(runner::runner_dev_sync(runner::RunnerDevSyncOptions {
            runner_id,
            homeboy_source,
            homeboy_binary,
            extensions,
            reconnect,
            dry_run,
        })),
        RunnerCommand::Exec {
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            json: _,
            raw: _,
            command,
        } => map_execution(exec(
            &id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            command,
        )),
        RunnerCommand::Env { id } => map_env(env_mod::env(&id)),
        RunnerCommand::Lifecycle {
            runner_id,
            workspace,
            job_id,
            run_id,
            status,
            exit_code,
        } => lifecycle::lifecycle(runner_id, workspace, job_id, run_id, status, exit_code)
            .map(|(output, exit_code)| (RunnerCommandOutput::Lifecycle(output), exit_code)),
        RunnerCommand::Job { command } => map_job(jobs::job(command)),
        RunnerCommand::Work {
            runner_id,
            broker_url,
            broker_token,
            project,
            lease_ms,
            r#loop,
            idle_backoff_ms,
            max_idle_backoff_ms,
            broker_failure_backoff_ms,
            broker_retry_limit,
        } => {
            let concurrency_limit = runner::load(&runner_id)
                .ok()
                .and_then(|runner| runner.settings.concurrency_limit);
            let broker_token = broker_token.or_else(runner::broker_token_from_env);
            map_worker(runner::run_reverse_worker(ReverseRunnerWorkerOptions {
                runner_id,
                broker_url,
                broker_token,
                project_id: project,
                lease_ms,
                concurrency_limit,
                loop_mode: r#loop,
                idle_backoff_ms,
                max_idle_backoff_ms,
                broker_failure_backoff_ms,
                broker_retry_limit,
            }))
        }
        RunnerCommand::Workspace { command } => workspace::run(command)
            .map(|(output, exit_code)| (RunnerCommandOutput::Workspace(output), exit_code)),
        RunnerCommand::RefreshPlan(args) => refresh_plan::refresh_plan(args)
            .map(|output| (RunnerCommandOutput::RefreshPlan(output), 0)),
        RunnerCommand::Broker { command } => {
            run_broker(command).map(|output| (RunnerCommandOutput::Broker(output), 0))
        }
    }
}

pub fn run_command_output(args: RunnerArgs, _global: &super::super::GlobalArgs) -> JsonCommandRun {
    crate::commands::utils::tty::status("homeboy is working...");

    match args.command {
        RunnerCommand::Exec {
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            json: true,
            raw: false,
            command,
        } => run_json_exec(
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            command,
        ),
        RunnerCommand::Exec {
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            raw: true,
            json: false,
            command,
        } => run_raw_exec(
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            command,
        ),
        RunnerCommand::Exec {
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            raw: false,
            json: false,
            command,
        } => run_compact_exec_json(
            id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            command,
        ),
        command => {
            let (stdout_result, exit_code) =
                crate::commands::utils::response::map_cmd_result_to_json(run(
                    RunnerArgs { command },
                    _global,
                ));
            JsonCommandRun::from_stdout_result(stdout_result, exit_code)
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn run_json_exec(
    id: String,
    cwd: Option<String>,
    sync_workspace: Option<String>,
    project: Option<String>,
    ssh: bool,
    capture_patch: bool,
    require_paths: Vec<String>,
    script_file: Option<String>,
    env: Vec<String>,
    secret_env: Vec<String>,
    secret_env_plan: Option<String>,
    secret_env_plan_file: Option<String>,
    dry_run: bool,
    run_id: Option<String>,
    artifact_outputs: Vec<String>,
    artifact_dir_outputs: Vec<String>,
    summary_outputs: Vec<String>,
    command: Vec<String>,
) -> JsonCommandRun {
    let (stdout_result, exit_code) = crate::commands::utils::response::map_cmd_result_to_json(
        exec(
            &id,
            cwd,
            sync_workspace,
            project,
            ssh,
            capture_patch,
            require_paths,
            script_file,
            env,
            secret_env,
            secret_env_plan,
            secret_env_plan_file,
            dry_run,
            run_id,
            artifact_outputs,
            artifact_dir_outputs,
            summary_outputs,
            command,
        )
        .map(|(output, exit_code)| {
            (
                RunnerCommandOutput::Execution(runner_exec_command_output(output)),
                exit_code,
            )
        }),
    );
    JsonCommandRun::from_stdout_result(stdout_result, exit_code)
}

#[allow(clippy::too_many_arguments)]
fn run_raw_exec(
    id: String,
    cwd: Option<String>,
    sync_workspace: Option<String>,
    project: Option<String>,
    ssh: bool,
    capture_patch: bool,
    require_paths: Vec<String>,
    script_file: Option<String>,
    env: Vec<String>,
    secret_env: Vec<String>,
    secret_env_plan: Option<String>,
    secret_env_plan_file: Option<String>,
    dry_run: bool,
    run_id: Option<String>,
    artifact_outputs: Vec<String>,
    artifact_dir_outputs: Vec<String>,
    summary_outputs: Vec<String>,
    command: Vec<String>,
) -> JsonCommandRun {
    match exec(
        &id,
        cwd,
        sync_workspace,
        project,
        ssh,
        capture_patch,
        require_paths,
        script_file,
        env,
        secret_env,
        secret_env_plan,
        secret_env_plan_file,
        dry_run,
        run_id,
        artifact_outputs,
        artifact_dir_outputs,
        summary_outputs,
        command,
    ) {
        Ok((output, exit_code)) => raw_exec_command_run(output, exit_code),
        Err(err) => {
            let (stdout_result, exit_code) =
                crate::commands::utils::response::map_cmd_result_to_json::<RunnerCommandOutput>(
                    Err(err),
                );
            JsonCommandRun::from_stdout_result(stdout_result, exit_code)
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn run_compact_exec_json(
    id: String,
    cwd: Option<String>,
    sync_workspace: Option<String>,
    project: Option<String>,
    ssh: bool,
    capture_patch: bool,
    require_paths: Vec<String>,
    script_file: Option<String>,
    env: Vec<String>,
    secret_env: Vec<String>,
    secret_env_plan: Option<String>,
    secret_env_plan_file: Option<String>,
    dry_run: bool,
    run_id: Option<String>,
    artifact_outputs: Vec<String>,
    artifact_dir_outputs: Vec<String>,
    summary_outputs: Vec<String>,
    command: Vec<String>,
) -> JsonCommandRun {
    let raw_run = run_compact_exec(
        id,
        cwd,
        sync_workspace,
        project,
        ssh,
        capture_patch,
        require_paths,
        script_file,
        env,
        secret_env,
        secret_env_plan,
        secret_env_plan_file,
        dry_run,
        run_id,
        artifact_outputs,
        artifact_dir_outputs,
        summary_outputs,
        command,
    );
    let (run, raw_stdout) = JsonCommandRun::from_raw(raw_run);
    run.with_presentation(CommandPresentation {
        stdout: raw_stdout.ok(),
        stderr: None,
    })
    .with_command("runner")
}

#[allow(clippy::too_many_arguments)]
pub(super) fn run_compact_exec(
    id: String,
    cwd: Option<String>,
    sync_workspace: Option<String>,
    project: Option<String>,
    ssh: bool,
    capture_patch: bool,
    require_paths: Vec<String>,
    script_file: Option<String>,
    env: Vec<String>,
    secret_env: Vec<String>,
    secret_env_plan: Option<String>,
    secret_env_plan_file: Option<String>,
    dry_run: bool,
    run_id: Option<String>,
    artifact_outputs: Vec<String>,
    artifact_dir_outputs: Vec<String>,
    summary_outputs: Vec<String>,
    command: Vec<String>,
) -> RawCommandRun {
    match exec(
        &id,
        cwd,
        sync_workspace,
        project,
        ssh,
        capture_patch,
        require_paths,
        script_file,
        env,
        secret_env,
        secret_env_plan,
        secret_env_plan_file,
        dry_run,
        run_id,
        artifact_outputs,
        artifact_dir_outputs,
        summary_outputs,
        command,
    ) {
        Ok((output, exit_code)) => compact_exec_command_run(output, exit_code),
        Err(err) => RawCommandRun {
            stdout_result: Err(err),
            exit_code: 1,
            output_file_result: None,
        },
    }
}

pub(super) fn compact_exec_command_run(output: RunnerExecOutput, exit_code: i32) -> RawCommandRun {
    let compact_stdout = render_compact_exec_output(&output);
    let (output_file_result, _) = crate::commands::utils::response::map_cmd_result_to_json(Ok((
        RunnerCommandOutput::Execution(runner_exec_command_output(output)),
        exit_code,
    )));

    RawCommandRun {
        stdout_result: Ok(compact_stdout),
        exit_code,
        output_file_result: Some(output_file_result),
    }
}

pub(super) fn render_compact_exec_output(output: &RunnerExecOutput) -> String {
    let mut rendered = String::new();
    rendered.push_str(&format!("Runner: {}\n", output.runner_id));
    rendered.push_str(&format!("Remote cwd: {}\n", output.remote_cwd));
    rendered.push_str(&format!("Exit status: {}\n", output.exit_code));
    if let Some(job_id) = output.job_id.as_deref() {
        rendered.push_str(&format!("Job: {job_id}\n"));
    }
    if let Some(run_id) = output.mirror_run_id.as_deref() {
        rendered.push_str(&format!("Run: {run_id}\n"));
    }
    for artifact in &output.artifacts {
        let label = artifact.name.as_deref().unwrap_or(&artifact.id);
        let uri = artifact
            .url
            .as_deref()
            .or(artifact.path.as_deref())
            .unwrap_or("unresolved");
        rendered.push_str(&format!("Artifact: {label} ({uri})\n"));
    }
    for artifact in &output.promoted_outputs {
        rendered.push_str(&format!(
            "Artifact: {} ({})\n",
            artifact.artifact_id, artifact.artifact_path
        ));
    }
    rendered.push('\n');
    if !output.stdout.is_empty() {
        rendered.push_str("Stdout:\n");
        rendered.push_str(&render_stream(&output.stdout));
        if !rendered.ends_with('\n') {
            rendered.push('\n');
        }
    }
    if !output.stderr.is_empty() {
        rendered.push_str("Stderr:\n");
        rendered.push_str(&output.stderr);
        if !rendered.ends_with('\n') {
            rendered.push('\n');
        }
    }
    rendered
}

fn render_stream(stream: &str) -> String {
    serde_json::from_str::<Value>(stream)
        .ok()
        .and_then(|value| serde_json::to_string_pretty(&value).ok())
        .unwrap_or_else(|| stream.to_string())
}

pub(super) fn raw_exec_command_run(output: RunnerExecOutput, exit_code: i32) -> JsonCommandRun {
    let presentation_stdout = output.stdout.clone();
    let presentation_stderr = output.stderr.clone();
    let (stdout_result, _) = crate::commands::utils::response::map_cmd_result_to_json(Ok((
        RunnerCommandOutput::Execution(runner_exec_command_output(output)),
        exit_code,
    )));

    JsonCommandRun::from_stdout_result(stdout_result, exit_code).with_presentation(
        CommandPresentation {
            stdout: Some(presentation_stdout),
            stderr: Some(presentation_stderr),
        },
    )
}

pub(super) fn map_registry(result: CmdResult<RunnerOutput>) -> CmdResult<RunnerCommandOutput> {
    result.map(|(mut output, exit_code)| {
        registry::redact_runner_output_env(&mut output);
        output.extra.variant = runner_variant_from_command(&output.command);
        (RunnerCommandOutput::Registry(output), exit_code)
    })
}

fn runner_variant_from_command(command: &str) -> &'static str {
    match command {
        "runner.add" => "add",
        "runner.enable" => "enable",
        "runner.list" => "list",
        "runner.show" => "show",
        "runner.set" => "set",
        "runner.trust" => "trust",
        "runner.pair" => "pair",
        "runner.remove" => "remove",
        "runner.connect" => "connect",
        "runner.status" => "status",
        "runner.disconnect" => "disconnect",
        _ => "registry",
    }
}

fn map_doctor(result: CmdResult<doctor::RunnerDoctorOutput>) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| (RunnerCommandOutput::Doctor(output), exit_code))
}

fn map_execution(result: CmdResult<RunnerExecOutput>) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| {
        (
            RunnerCommandOutput::Execution(runner_exec_command_output(output)),
            exit_code,
        )
    })
}

fn runner_exec_command_output(
    output: RunnerExecOutput,
) -> super::types::RunnerExecutionCommandOutput {
    let actionable = output
        .mirror_run_id
        .as_ref()
        .map(|run_id| {
            crate::commands::utils::response::actionable_metadata_for_run_ref(
                run_id.clone(),
                "runner_exec",
                "homeboy-runner-exec",
            )
        })
        .or_else(|| {
            output.job_id.as_ref().map(|job_id| {
                let mut metadata =
                    crate::commands::utils::response::CommandActionableMetadata::default();
                metadata
                    .refs
                    .jobs
                    .push(crate::commands::utils::response::CommandJobRef {
                        id: job_id.clone(),
                        kind: "runner_job".to_string(),
                        source: "homeboy-runner-exec".to_string(),
                        status_command: format!("homeboy activity show {job_id}"),
                        watch_command: Some(format!("homeboy activity watch {job_id}")),
                    });
                metadata.next_actions.push(
                    crate::commands::utils::response::CommandNextAction::new(
                        "show activity",
                        format!("homeboy activity show {job_id}"),
                    )
                    .with_kind(crate::commands::utils::response::CommandNextActionKind::Show),
                );
                metadata
            })
        });
    super::types::RunnerExecutionCommandOutput { output, actionable }
}

fn map_refresh_homeboy(
    result: CmdResult<runner::HomeboyBinaryRefreshOutput>,
) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| (RunnerCommandOutput::RefreshHomeboy(output), exit_code))
}

fn map_dev_sync(
    result: homeboy::core::Result<(runner::RunnerDevSyncOutput, i32)>,
) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| (RunnerCommandOutput::DevSync(output), exit_code))
}

fn map_env(result: CmdResult<RunnerEnvOutput>) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| (RunnerCommandOutput::Env(output), exit_code))
}

fn map_job(result: CmdResult<RunnerJobCommandOutput>) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| match output {
        RunnerJobCommandOutput::Daemon(output) => (RunnerCommandOutput::Job(output), exit_code),
        RunnerJobCommandOutput::Broker(output) => {
            (RunnerCommandOutput::BrokerJob(output), exit_code)
        }
    })
}

fn map_worker(result: CmdResult<ReverseRunnerWorkerOutput>) -> CmdResult<RunnerCommandOutput> {
    result.map(|(output, exit_code)| (RunnerCommandOutput::Worker(output), exit_code))
}
