use super::errors::OrchestratorError;
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;

use reqwest::blocking::Client;
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
use reqwest::Method;
use serde_json::Value as JsonValue;

use crate::cli::{OrchestratorDeployArgs, OrchestratorDeployProvider, OrchestratorLocalArgs};
use crate::package::{Manifest, TriggerKind};

use super::common;

const CONTAINER_MANIFEST_PATH: &str = "/app/harn.toml";
const RENDER_API_BASE: &str = "https://api.render.com/v1";
const FLY_MACHINES_API_BASE: &str = "https://api.machines.dev/v1";
const RAILWAY_GRAPHQL_ENDPOINT: &str = "https://backboard.railway.com/graphql/v2";

#[derive(Debug)]
struct ValidatedManifest {
    manifest: Manifest,
    manifest_path: PathBuf,
    manifest_dir: PathBuf,
    trigger_count: usize,
    http_trigger_count: usize,
}

#[derive(Debug)]
struct DeployBundle {
    provider_dir: PathBuf,
    context_dir: PathBuf,
    dockerfile_path: PathBuf,
    spec_path: PathBuf,
    spec_contents: String,
}

#[derive(Debug)]
struct DeployEnv {
    public: BTreeMap<String, String>,
    secrets: BTreeMap<String, String>,
    missing_secret_env: Vec<String>,
}

pub(crate) async fn run(args: OrchestratorDeployArgs) -> Result<(), OrchestratorError> {
    let validated = validate_manifest(&args).await?;
    let env = collect_deploy_env(&args, &validated.manifest)?;
    let bundle = write_bundle(&args, &validated, &env.public)?;

    println!(
        "validated {} trigger(s) from {} ({} HTTP-managed)",
        validated.trigger_count,
        validated.manifest_path.display(),
        validated.http_trigger_count
    );
    println!("wrote deploy bundle: {}", bundle.provider_dir.display());

    if !env.missing_secret_env.is_empty() {
        eprintln!(
            "warning: {} manifest secret env var(s) were not set locally and were not synced: {}",
            env.missing_secret_env.len(),
            env.missing_secret_env.join(", ")
        );
    }

    if args.print {
        println!("{}", bundle.spec_contents);
    }

    let secret_sync_plan = if args.no_secret_sync || env.secrets.is_empty() {
        None
    } else {
        Some(secret_sync_plan(&args, &env.secrets)?)
    };

    let mut pre_deploy_plan = Vec::new();
    if args.build {
        pre_deploy_plan.push(build_image_command(&args, &bundle));
    }
    pre_deploy_plan.extend(public_env_sync_commands(&args, &env.public));
    let deploy_command = provider_deploy_command(&args, &bundle);

    if args.dry_run {
        println!("dry run; commands not executed:");
        for command in &pre_deploy_plan {
            println!("  {}", command.display());
        }
        if let Some(secret_sync_plan) = &secret_sync_plan {
            println!("  {}", secret_sync_plan.display());
        }
        println!("  {}", deploy_command.display());
        return Ok(());
    }

    for command in pre_deploy_plan {
        run_checked(command)?;
    }
    if let Some(secret_sync_plan) = secret_sync_plan {
        run_secret_sync(secret_sync_plan)?;
    }
    run_checked(deploy_command)?;

    if let Some(url) = args
        .health_url
        .clone()
        .or_else(|| default_health_url(&args.provider, &args.name))
    {
        probe_health(&url)?;
    }

    Ok(())
}

async fn validate_manifest(
    args: &OrchestratorDeployArgs,
) -> Result<ValidatedManifest, OrchestratorError> {
    let manifest_path = absolutize_from_cwd(&args.manifest)?;
    let manifest_dir = manifest_path
        .parent()
        .map(Path::to_path_buf)
        .ok_or_else(|| {
            format!(
                "manifest has no parent directory: {}",
                manifest_path.display()
            )
        })?;
    let manifest = read_manifest(&manifest_path)?;
    let validation_state = tempfile::Builder::new()
        .prefix("harn-deploy-validate-")
        .tempdir()
        .map_err(|error| {
            format!("failed to create temporary deploy validation state dir: {error}")
        })?;
    let local = OrchestratorLocalArgs {
        config: manifest_path.clone(),
        state_dir: validation_state.path().to_path_buf(),
    };
    let ctx = common::load_local_runtime(&local).await?;
    let trigger_count = ctx.collected_triggers.len();
    let http_trigger_count = ctx
        .collected_triggers
        .iter()
        .filter(|trigger| {
            matches!(
                trigger.config.kind,
                TriggerKind::Webhook | TriggerKind::A2aPush
            )
        })
        .count();
    Ok(ValidatedManifest {
        manifest,
        manifest_path,
        manifest_dir,
        trigger_count,
        http_trigger_count,
    })
}

fn write_bundle(
    args: &OrchestratorDeployArgs,
    validated: &ValidatedManifest,
    public_env: &BTreeMap<String, String>,
) -> Result<DeployBundle, OrchestratorError> {
    let provider_dir = validated
        .manifest_dir
        .join(&args.deploy_dir)
        .join(args.provider.as_str());
    fs::create_dir_all(&provider_dir)
        .map_err(|error| format!("failed to create {}: {error}", provider_dir.display()))?;

    let dockerfile_path = provider_dir.join("Dockerfile");
    let dockerfile = render_dockerfile();
    write_if_changed(&dockerfile_path, &dockerfile)?;

    let spec_contents = match args.provider {
        OrchestratorDeployProvider::Render => render_render_yaml(args, public_env),
        OrchestratorDeployProvider::Fly => render_fly_toml(args, public_env),
        OrchestratorDeployProvider::Railway => render_railway_json(args, public_env)?,
    };
    let spec_path = provider_dir.join(provider_spec_file(args.provider));
    write_if_changed(&spec_path, &spec_contents)?;

    Ok(DeployBundle {
        provider_dir,
        context_dir: validated.manifest_dir.clone(),
        dockerfile_path,
        spec_path,
        spec_contents,
    })
}

fn collect_deploy_env(
    args: &OrchestratorDeployArgs,
    manifest: &Manifest,
) -> Result<DeployEnv, OrchestratorError> {
    let state_dir = format!("{}/state", args.data_dir.trim_end_matches('/'));
    let sqlite_path = format!("{}/events.sqlite", args.data_dir.trim_end_matches('/'));
    let mut public = BTreeMap::from([
        (
            "HARN_ORCHESTRATOR_MANIFEST".to_string(),
            CONTAINER_MANIFEST_PATH.to_string(),
        ),
        (
            "HARN_ORCHESTRATOR_LISTEN".to_string(),
            format!("0.0.0.0:{}", args.port),
        ),
        ("HARN_ORCHESTRATOR_STATE_DIR".to_string(), state_dir),
        ("HARN_EVENT_LOG_BACKEND".to_string(), "sqlite".to_string()),
        ("HARN_EVENT_LOG_SQLITE_PATH".to_string(), sqlite_path),
        ("HARN_SECRET_PROVIDERS".to_string(), "env".to_string()),
        ("RUST_LOG".to_string(), "info".to_string()),
    ]);

    let mut secrets = BTreeMap::new();
    let mut missing_secret_env = Vec::new();

    for pair in &args.env {
        let (key, value) = parse_key_value(pair)?;
        public.insert(key, value);
    }
    for pair in &args.secret {
        let (key, value) = parse_key_value(pair)?;
        secrets.insert(key, value);
    }

    for key in [
        "HARN_ORCHESTRATOR_API_KEYS",
        "HARN_ORCHESTRATOR_HMAC_SECRET",
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "OPENROUTER_API_KEY",
        "HF_TOKEN",
        "HUGGINGFACE_API_KEY",
        "TOGETHER_AI_API_KEY",
    ] {
        if secrets.contains_key(key) {
            continue;
        }
        if let Ok(value) = std::env::var(key) {
            if !value.is_empty() {
                secrets.insert(key.to_string(), value);
            }
        }
    }

    for trigger in &manifest.triggers {
        for secret_ref in trigger.secrets.values() {
            let Some(env_name) = secret_ref_env_name(secret_ref) else {
                continue;
            };
            if secrets.contains_key(&env_name) {
                continue;
            }
            match std::env::var(&env_name) {
                Ok(value) if !value.is_empty() => {
                    secrets.insert(env_name, value);
                }
                _ => missing_secret_env.push(env_name),
            }
        }
    }
    missing_secret_env.sort();
    missing_secret_env.dedup();

    Ok(DeployEnv {
        public,
        secrets,
        missing_secret_env,
    })
}

fn render_dockerfile() -> String {
    r#"FROM ghcr.io/burin-labs/harn:latest

WORKDIR /app
COPY . /app

ENV HARN_ORCHESTRATOR_MANIFEST=/app/harn.toml

ENTRYPOINT ["/usr/local/bin/harn", "orchestrator", "serve"]
"#
    .to_string()
}

fn render_render_yaml(
    args: &OrchestratorDeployArgs,
    public_env: &BTreeMap<String, String>,
) -> String {
    let env_vars = public_env
        .iter()
        .map(|(key, value)| {
            format!(
                "      - key: {}\n        value: {}\n",
                yaml_plain(key),
                serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
            )
        })
        .collect::<String>();
    format!(
        r"services:
  - type: web
    name: {name}
    runtime: image
    image:
      url: {image}
    disk:
      name: harn-data
      mountPath: {data_dir}
      sizeGB: {disk_size_gb}
    envVars:
{env_vars}      - fromGroup: harn-secrets
    healthCheckPath: /healthz
",
        name = yaml_plain(&args.name),
        image = yaml_plain(&args.image),
        data_dir = yaml_plain(&args.data_dir),
        disk_size_gb = args.disk_size_gb,
        env_vars = env_vars,
    )
}

fn render_fly_toml(args: &OrchestratorDeployArgs, public_env: &BTreeMap<String, String>) -> String {
    let region = args
        .region
        .as_ref()
        .map(|region| format!("primary_region = {}\n", toml_string(region)))
        .unwrap_or_default();
    let env_vars = public_env
        .iter()
        .map(|(key, value)| format!("  {key} = {}\n", toml_string(value)))
        .collect::<String>();
    format!(
        r#"app = {name}
{region}kill_signal = "SIGTERM"
kill_timeout = "{shutdown_timeout}s"

[build]
  image = {image}

[env]
{env_vars}

[mounts]
  source = "harn_data"
  destination = {data_dir}

[http_service]
  internal_port = {port}
  force_https = true
  auto_stop_machines = false
  auto_start_machines = true
  min_machines_running = 1

  [[http_service.checks]]
    grace_period = "10s"
    interval = "30s"
    method = "GET"
    path = "/healthz"
    timeout = "5s"

[metrics]
  port = {port}
  path = "/metrics"
"#,
        name = toml_string(&args.name),
        region = region,
        shutdown_timeout = args.shutdown_timeout,
        image = toml_string(&args.image),
        port = args.port,
        env_vars = env_vars,
        data_dir = toml_string(&args.data_dir),
    )
}

fn render_railway_json(
    args: &OrchestratorDeployArgs,
    public_env: &BTreeMap<String, String>,
) -> Result<String, OrchestratorError> {
    let value = serde_json::json!({
        "$schema": "https://railway.app/railway.schema.json",
        "build": {
            "builder": "DOCKERFILE",
            "dockerfilePath": "deploy/railway/Dockerfile"
        },
        "deploy": {
            "startCommand": format!("harn orchestrator serve --shutdown-timeout {}", args.shutdown_timeout),
            "healthcheckPath": "/healthz",
            "healthcheckTimeout": 30,
            "restartPolicyType": "ON_FAILURE",
            "restartPolicyMaxRetries": 10
        },
        "environments": {
            "production": {
                "variables": public_env
            }
        }
    });
    serde_json::to_string_pretty(&value)
        .map(|json| format!("{json}\n"))
        .map_err(|error| {
            OrchestratorError::Deploy(format!("failed to render railway.json: {error}"))
        })
}

fn build_image_command(args: &OrchestratorDeployArgs, bundle: &DeployBundle) -> PlannedCommand {
    let mut command = PlannedCommand::new("docker");
    if args.no_push {
        command.args(["build", "-f"]);
        command.arg(bundle.dockerfile_path.as_os_str());
        command.args(["-t", args.image.as_str(), "."]);
    } else {
        command.args([
            "buildx",
            "build",
            "--platform",
            "linux/amd64,linux/arm64",
            "-f",
        ]);
        command.arg(bundle.dockerfile_path.as_os_str());
        command.args(["-t", args.image.as_str(), "--push", "."]);
    }
    command.cwd = Some(bundle.context_dir.clone());
    command
}

#[derive(Debug, Clone)]
struct SecretSyncPlan {
    provider: OrchestratorDeployProvider,
    target: String,
    secrets: BTreeMap<String, String>,
    auth_token: String,
    railway_project: Option<String>,
    railway_environment: Option<String>,
}

impl SecretSyncPlan {
    fn display(&self) -> String {
        let keys = self.secrets.keys().cloned().collect::<Vec<_>>().join(", ");
        match self.provider {
            OrchestratorDeployProvider::Render => {
                format!(
                    "sync {} secret(s) to Render service {} via API: {}",
                    self.secrets.len(),
                    shell_quote(&self.target),
                    keys
                )
            }
            OrchestratorDeployProvider::Fly => {
                format!(
                    "sync {} secret(s) to Fly app {} via Machines API: {}",
                    self.secrets.len(),
                    shell_quote(&self.target),
                    keys
                )
            }
            OrchestratorDeployProvider::Railway => {
                format!(
                    "sync {} secret(s) to Railway service {} via GraphQL API: {}",
                    self.secrets.len(),
                    shell_quote(&self.target),
                    keys
                )
            }
        }
    }
}

fn secret_sync_plan(
    args: &OrchestratorDeployArgs,
    secrets: &BTreeMap<String, String>,
) -> Result<SecretSyncPlan, OrchestratorError> {
    match args.provider {
        OrchestratorDeployProvider::Render => {
            let service = args.render_service.as_ref().ok_or_else(|| {
                "Render secret sync requires --render-service so Harn can target the Render API"
                    .to_string()
            })?;
            let token = optional_api_token(
                args.render_api_key.as_deref(),
                args.dry_run,
                "Render secret sync requires --render-api-key or RENDER_API_KEY",
            )?;
            Ok(SecretSyncPlan {
                provider: args.provider,
                target: service.clone(),
                secrets: secrets.clone(),
                auth_token: token,
                railway_project: None,
                railway_environment: None,
            })
        }
        OrchestratorDeployProvider::Fly => {
            let token = optional_api_token(
                args.fly_api_token.as_deref(),
                args.dry_run,
                "Fly secret sync requires --fly-api-token or FLY_API_TOKEN",
            )?;
            Ok(SecretSyncPlan {
                provider: args.provider,
                target: args.name.clone(),
                secrets: secrets.clone(),
                auth_token: token,
                railway_project: None,
                railway_environment: None,
            })
        }
        OrchestratorDeployProvider::Railway => {
            let token = optional_api_token(
                args.railway_token.as_deref(),
                args.dry_run,
                "Railway secret sync requires --railway-token or RAILWAY_TOKEN",
            )?;
            let project = args.railway_project.as_ref().ok_or_else(|| {
                "Railway secret sync requires --railway-project or RAILWAY_PROJECT_ID".to_string()
            })?;
            let service = args.railway_service.as_ref().ok_or_else(|| {
                "Railway secret sync requires --railway-service with a Railway service id"
                    .to_string()
            })?;
            let environment = args.railway_environment.as_ref().ok_or_else(|| {
                "Railway secret sync requires --railway-environment with a Railway environment id"
                    .to_string()
            })?;
            Ok(SecretSyncPlan {
                provider: args.provider,
                target: service.clone(),
                secrets: secrets.clone(),
                auth_token: token,
                railway_project: Some(project.clone()),
                railway_environment: Some(environment.clone()),
            })
        }
    }
}

fn optional_api_token(
    token: Option<&str>,
    dry_run: bool,
    missing_message: &str,
) -> Result<String, OrchestratorError> {
    match token {
        Some(token) if !token.is_empty() => Ok(token.to_string()),
        _ if dry_run => Ok(String::new()),
        _ => Err(OrchestratorError::Deploy(missing_message.to_string())),
    }
}

fn run_secret_sync(plan: SecretSyncPlan) -> Result<(), OrchestratorError> {
    println!("running: {}", plan.display());
    let client = Client::builder()
        .timeout(Duration::from_secs(30))
        .build()
        .map_err(|error| format!("failed to create provider API client: {error}"))?;
    match plan.provider {
        OrchestratorDeployProvider::Render => sync_render_secrets(&client, &plan),
        OrchestratorDeployProvider::Fly => sync_fly_secrets(&client, &plan),
        OrchestratorDeployProvider::Railway => sync_railway_secrets(&client, &plan),
    }
}

fn sync_render_secrets(client: &Client, plan: &SecretSyncPlan) -> Result<(), OrchestratorError> {
    for (key, value) in &plan.secrets {
        let url = format!(
            "{}/services/{}/env-vars/{}",
            RENDER_API_BASE,
            path_segment(&plan.target),
            path_segment(key)
        );
        let body = serde_json::json!({ "value": value });
        let response = client
            .request(Method::PUT, &url)
            .header(AUTHORIZATION, bearer_auth(&plan.auth_token))
            .header(ACCEPT, "application/json")
            .header(CONTENT_TYPE, "application/json")
            .json(&body)
            .send()
            .map_err(|error| format!("failed to sync Render secret {key}: {error}"))?;
        ensure_success(response, &format!("sync Render secret {key}"))?;
    }
    Ok(())
}

fn sync_fly_secrets(client: &Client, plan: &SecretSyncPlan) -> Result<(), OrchestratorError> {
    let url = format!(
        "{}/apps/{}/secrets",
        FLY_MACHINES_API_BASE,
        path_segment(&plan.target)
    );
    let values = plan
        .secrets
        .iter()
        .map(|(key, value)| (key.clone(), JsonValue::String(value.clone())))
        .collect::<serde_json::Map<_, _>>();
    let body = serde_json::json!({ "values": values });
    let response = client
        .request(Method::POST, &url)
        .header(AUTHORIZATION, fly_auth(&plan.auth_token))
        .header(ACCEPT, "application/json")
        .header(CONTENT_TYPE, "application/json")
        .json(&body)
        .send()
        .map_err(|error| format!("failed to sync Fly secrets: {error}"))?;
    ensure_success(response, "sync Fly secrets")
}

fn sync_railway_secrets(client: &Client, plan: &SecretSyncPlan) -> Result<(), OrchestratorError> {
    let project_id = plan
        .railway_project
        .as_deref()
        .ok_or_else(|| "Railway secret sync missing project id".to_string())?;
    let environment_id = plan
        .railway_environment
        .as_deref()
        .ok_or_else(|| "Railway secret sync missing environment id".to_string())?;
    let variables = plan
        .secrets
        .iter()
        .map(|(key, value)| (key.clone(), JsonValue::String(value.clone())))
        .collect::<serde_json::Map<_, _>>();
    let body = serde_json::json!({
        "query": r"mutation variableCollectionUpsert($input: VariableCollectionUpsertInput!) {
  variableCollectionUpsert(input: $input)
}",
        "variables": {
            "input": {
                "projectId": project_id,
                "environmentId": environment_id,
                "serviceId": plan.target,
                "variables": variables,
                "skipDeploys": true
            }
        }
    });
    let response = client
        .request(Method::POST, RAILWAY_GRAPHQL_ENDPOINT)
        .header(AUTHORIZATION, bearer_auth(&plan.auth_token))
        .header(ACCEPT, "application/json")
        .header(CONTENT_TYPE, "application/json")
        .json(&body)
        .send()
        .map_err(|error| format!("failed to sync Railway secrets: {error}"))?;
    let payload = ensure_success_json(response, "sync Railway secrets")?;
    if let Some(errors) = payload.get("errors") {
        return Err(format!("Railway GraphQL secret sync failed: {errors}").into());
    }
    Ok(())
}

fn public_env_sync_commands(
    args: &OrchestratorDeployArgs,
    public_env: &BTreeMap<String, String>,
) -> Vec<PlannedCommand> {
    if args.provider != OrchestratorDeployProvider::Railway {
        return Vec::new();
    }

    let mut vars = public_env.clone();
    vars.insert(
        "RAILWAY_DOCKERFILE_PATH".to_string(),
        "deploy/railway/Dockerfile".to_string(),
    );

    vars.iter()
        .map(|(key, value)| {
            let mut command = PlannedCommand::new("railway");
            command.args(["variable", "set"]);
            command.arg(format!("{key}={value}"));
            command.arg("--skip-deploys");
            if let Some(service) = &args.railway_service {
                command.args(["--service", service.as_str()]);
            }
            if let Some(environment) = &args.railway_environment {
                command.args(["--environment", environment.as_str()]);
            }
            command
        })
        .collect()
}

fn provider_deploy_command(args: &OrchestratorDeployArgs, bundle: &DeployBundle) -> PlannedCommand {
    match args.provider {
        OrchestratorDeployProvider::Render => {
            if let Some(service) = &args.render_service {
                let mut command = PlannedCommand::new("render");
                command.args(["deploys", "create", service.as_str()]);
                command.args(["--image", args.image.as_str(), "--wait", "--confirm"]);
                command
            } else {
                let mut command = PlannedCommand::new("render");
                command.args(["blueprints", "validate"]);
                command.arg(bundle.spec_path.as_os_str());
                command
            }
        }
        OrchestratorDeployProvider::Fly => {
            let mut command = PlannedCommand::new("fly");
            command.args(["deploy", "--config"]);
            command.arg(bundle.spec_path.as_os_str());
            command.args(["--app", args.name.as_str()]);
            command
        }
        OrchestratorDeployProvider::Railway => {
            let mut command = PlannedCommand::new("railway");
            command.args(["up", "--yes"]);
            if let Some(service) = &args.railway_service {
                command.args(["--service", service.as_str()]);
            }
            if let Some(environment) = &args.railway_environment {
                command.args(["--environment", environment.as_str()]);
            }
            command.cwd = Some(bundle.context_dir.clone());
            command
        }
    }
}

#[derive(Debug, Clone)]
struct PlannedCommand {
    program: String,
    args: Vec<String>,
    sensitive_args: BTreeSet<usize>,
    cwd: Option<PathBuf>,
}

impl PlannedCommand {
    fn new(program: impl Into<String>) -> Self {
        Self {
            program: program.into(),
            args: Vec::new(),
            sensitive_args: BTreeSet::new(),
            cwd: None,
        }
    }

    fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
        self.args.push(arg.as_ref().to_string_lossy().into_owned());
        self
    }

    fn args<I, S>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        for arg in args {
            self.arg(arg);
        }
        self
    }

    fn display(&self) -> String {
        let mut rendered = shell_quote(&self.program);
        for (index, arg) in self.args.iter().enumerate() {
            rendered.push(' ');
            let display_arg = if self.sensitive_args.contains(&index) {
                redact_arg(arg)
            } else {
                arg.clone()
            };
            rendered.push_str(&shell_quote(&display_arg));
        }
        if let Some(cwd) = &self.cwd {
            format!(
                "(cd {} && {rendered})",
                shell_quote(&cwd.display().to_string())
            )
        } else {
            rendered
        }
    }
}

fn run_checked(command: PlannedCommand) -> Result<(), OrchestratorError> {
    println!("running: {}", command.display());
    let mut process = Command::new(&command.program);
    process.args(&command.args);
    if let Some(cwd) = &command.cwd {
        process.current_dir(cwd);
    }
    let status = process
        .status()
        .map_err(|error| format!("failed to run {}: {error}", command.program))?;
    if status.success() {
        Ok(())
    } else {
        Err(format!("command failed with {status}: {}", command.display()).into())
    }
}

fn ensure_success(
    response: reqwest::blocking::Response,
    action: &str,
) -> Result<(), OrchestratorError> {
    let status = response.status();
    if status.is_success() {
        return Ok(());
    }
    let body = response.text().unwrap_or_default();
    Err(format!("{action} failed with HTTP {status}: {body}").into())
}

fn ensure_success_json(
    response: reqwest::blocking::Response,
    action: &str,
) -> Result<JsonValue, OrchestratorError> {
    let status = response.status();
    let body = response
        .text()
        .map_err(|error| format!("{action} failed to read response body: {error}"))?;
    if !status.is_success() {
        return Err(format!("{action} failed with HTTP {status}: {body}").into());
    }
    serde_json::from_str(&body).map_err(|error| {
        OrchestratorError::Deploy(format!("{action} returned invalid JSON: {error}: {body}"))
    })
}

fn probe_health(url: &str) -> Result<(), OrchestratorError> {
    let client = reqwest::blocking::Client::builder()
        .timeout(Duration::from_secs(20))
        .build()
        .map_err(|error| format!("failed to create health probe client: {error}"))?;
    let response = client
        .get(url)
        .send()
        .map_err(|error| format!("health probe failed for {url}: {error}"))?;
    if response.status().is_success() {
        println!("health check passed: {url}");
        Ok(())
    } else {
        Err(format!("health probe failed for {url}: HTTP {}", response.status()).into())
    }
}

fn default_health_url(provider: &OrchestratorDeployProvider, name: &str) -> Option<String> {
    match provider {
        OrchestratorDeployProvider::Fly => Some(format!("https://{name}.fly.dev/healthz")),
        OrchestratorDeployProvider::Render | OrchestratorDeployProvider::Railway => None,
    }
}

fn provider_spec_file(provider: OrchestratorDeployProvider) -> &'static str {
    match provider {
        OrchestratorDeployProvider::Render => "render.yaml",
        OrchestratorDeployProvider::Fly => "fly.toml",
        OrchestratorDeployProvider::Railway => "railway.json",
    }
}

fn read_manifest(path: &Path) -> Result<Manifest, OrchestratorError> {
    if !path.is_file() {
        return Err(format!("manifest not found: {}", path.display()).into());
    }
    let content = fs::read_to_string(path)
        .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
    toml::from_str(&content).map_err(|error| {
        OrchestratorError::Deploy(format!("failed to parse {}: {error}", path.display()))
    })
}

fn absolutize_from_cwd(path: &Path) -> Result<PathBuf, OrchestratorError> {
    if path.is_absolute() {
        return Ok(path.to_path_buf());
    }
    std::env::current_dir()
        .map(|cwd| cwd.join(path))
        .map_err(|error| {
            OrchestratorError::Deploy(format!("failed to read current directory: {error}"))
        })
}

fn write_if_changed(path: &Path, content: &str) -> Result<(), OrchestratorError> {
    if fs::read_to_string(path).is_ok_and(|existing| existing == content) {
        return Ok(());
    }
    fs::write(path, content).map_err(|error| {
        OrchestratorError::Deploy(format!("failed to write {}: {error}", path.display()))
    })
}

fn parse_key_value(raw: &str) -> Result<(String, String), OrchestratorError> {
    let Some((key, value)) = raw.split_once('=') else {
        return Err(format!("expected KEY=VALUE, got '{raw}'").into());
    };
    let key = key.trim();
    if key.is_empty() {
        return Err(format!("expected non-empty KEY in '{raw}'").into());
    }
    Ok((key.to_string(), value.to_string()))
}

fn secret_ref_env_name(raw: &str) -> Option<String> {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return None;
    }
    let (base, version) = match trimmed.rsplit_once('@') {
        Some((base, version_text)) => Some((base, version_text.parse::<u64>().ok()?)),
        None => None,
    }
    .unwrap_or((trimmed, 0));
    let (namespace, name) = base.split_once('/')?;
    if namespace.is_empty() || name.is_empty() {
        return None;
    }
    let prefix = format!(
        "HARN_SECRET_{}_{}",
        normalize_env_component(namespace),
        normalize_env_component(name)
    );
    if version == 0 {
        Some(prefix)
    } else {
        Some(format!("{prefix}_V{version}"))
    }
}

fn normalize_env_component(value: &str) -> String {
    let mut normalized = String::new();
    let mut last_was_underscore = false;
    for ch in value.chars() {
        let mapped = if ch.is_ascii_alphanumeric() {
            ch.to_ascii_uppercase()
        } else {
            '_'
        };
        if mapped == '_' {
            if !last_was_underscore {
                normalized.push(mapped);
            }
            last_was_underscore = true;
        } else {
            normalized.push(mapped);
            last_was_underscore = false;
        }
    }
    normalized.trim_matches('_').to_string()
}

fn toml_string(value: &str) -> String {
    serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}

fn yaml_plain(value: &str) -> String {
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':' | '/'))
    {
        value.to_string()
    } else {
        serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
    }
}

fn bearer_auth(token: &str) -> String {
    if token.starts_with("Bearer ") || token.starts_with("FlyV1 ") {
        token.to_string()
    } else {
        format!("Bearer {token}")
    }
}

fn fly_auth(token: &str) -> String {
    if token.starts_with("Bearer ") || token.starts_with("FlyV1 ") {
        return token.to_string();
    }
    let trimmed = token.trim_start();
    if trimmed.starts_with("fm1r_") || trimmed.starts_with("fm1a_") || trimmed.starts_with("fm2_") {
        format!("FlyV1 {trimmed}")
    } else {
        format!("Bearer {trimmed}")
    }
}

fn path_segment(value: &str) -> String {
    url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
}

fn shell_quote(value: &str) -> String {
    if value
        .chars()
        .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '/' | '.' | '-' | '_' | ':' | '='))
    {
        return value.to_string();
    }
    format!("'{}'", value.replace('\'', "'\\''"))
}

fn redact_arg(value: &str) -> String {
    match value.split_once('=') {
        Some((key, _)) => format!("{key}=***"),
        None => "***".to_string(),
    }
}

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

    fn args(provider: OrchestratorDeployProvider) -> OrchestratorDeployArgs {
        OrchestratorDeployArgs {
            provider,
            manifest: PathBuf::from("harn.toml"),
            name: "harn-prod".to_string(),
            image: "ghcr.io/acme/harn-prod:latest".to_string(),
            deploy_dir: PathBuf::from("deploy"),
            port: 8080,
            data_dir: "/data".to_string(),
            disk_size_gb: 10,
            shutdown_timeout: 45,
            region: Some("sjc".to_string()),
            render_service: Some("srv-123".to_string()),
            railway_service: Some("harn-prod".to_string()),
            railway_environment: Some("production".to_string()),
            railway_project: Some("project-123".to_string()),
            render_api_key: Some("render-token".to_string()),
            fly_api_token: Some("fly-token".to_string()),
            railway_token: Some("railway-token".to_string()),
            build: false,
            no_push: false,
            env: vec![],
            secret: vec![],
            no_secret_sync: false,
            dry_run: true,
            print: false,
            health_url: None,
        }
    }

    fn env() -> BTreeMap<String, String> {
        BTreeMap::from([
            (
                "HARN_ORCHESTRATOR_MANIFEST".to_string(),
                CONTAINER_MANIFEST_PATH.to_string(),
            ),
            (
                "HARN_ORCHESTRATOR_LISTEN".to_string(),
                "0.0.0.0:8080".to_string(),
            ),
            (
                "HARN_ORCHESTRATOR_STATE_DIR".to_string(),
                "/data/state".to_string(),
            ),
            ("HARN_EVENT_LOG_BACKEND".to_string(), "sqlite".to_string()),
            (
                "HARN_EVENT_LOG_SQLITE_PATH".to_string(),
                "/data/events.sqlite".to_string(),
            ),
            ("HARN_SECRET_PROVIDERS".to_string(), "env".to_string()),
            ("RUST_LOG".to_string(), "info".to_string()),
        ])
    }

    #[test]
    fn render_template_uses_current_orchestrator_env_names() {
        let rendered = render_render_yaml(&args(OrchestratorDeployProvider::Render), &env());
        assert!(rendered.contains("HARN_ORCHESTRATOR_LISTEN"));
        assert!(rendered.contains("HARN_EVENT_LOG_BACKEND"));
        assert!(rendered.contains("healthCheckPath: /healthz"));
    }

    #[test]
    fn fly_template_keeps_one_instance_for_cron_and_metrics() {
        let rendered = render_fly_toml(&args(OrchestratorDeployProvider::Fly), &env());
        assert!(rendered.contains("min_machines_running = 1"));
        assert!(rendered.contains("[metrics]"));
        assert!(rendered.contains("kill_timeout = \"45s\""));
    }

    #[test]
    fn railway_template_is_valid_json() {
        let rendered =
            render_railway_json(&args(OrchestratorDeployProvider::Railway), &env()).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();
        assert_eq!(parsed["deploy"]["healthcheckPath"], "/healthz");
        assert_eq!(
            parsed["environments"]["production"]["variables"]["HARN_SECRET_PROVIDERS"],
            "env"
        );
    }

    #[test]
    fn manifest_secret_refs_map_to_env_vars() {
        assert_eq!(
            secret_ref_env_name("github/installation-123/private-key"),
            Some("HARN_SECRET_GITHUB_INSTALLATION_123_PRIVATE_KEY".to_string())
        );
        assert_eq!(
            secret_ref_env_name("slack/signing-secret@7"),
            Some("HARN_SECRET_SLACK_SIGNING_SECRET_V7".to_string())
        );
    }

    #[test]
    fn provider_commands_are_rendered_without_secrets_in_specs() {
        let mut secrets = BTreeMap::new();
        secrets.insert("OPENAI_API_KEY".to_string(), "sk-test".to_string());
        let plan = secret_sync_plan(&args(OrchestratorDeployProvider::Fly), &secrets).unwrap();
        assert!(plan.display().contains("Fly app"));
        assert!(plan.display().contains("OPENAI_API_KEY"));
        assert!(!plan.display().contains("sk-test"));
    }

    #[test]
    fn dry_run_secret_plan_does_not_require_provider_token() {
        let mut args = args(OrchestratorDeployProvider::Fly);
        args.fly_api_token = None;
        let secrets = BTreeMap::from([("OPENAI_API_KEY".to_string(), "sk-test".to_string())]);
        let plan = secret_sync_plan(&args, &secrets).unwrap();
        assert_eq!(plan.auth_token, "");
        assert!(plan.display().contains("OPENAI_API_KEY"));
    }

    #[test]
    fn fly_auth_uses_flyv1_for_macaroon_tokens() {
        assert_eq!(fly_auth("fm2_example"), "FlyV1 fm2_example");
        assert_eq!(fly_auth("plain-token"), "Bearer plain-token");
    }

    #[test]
    fn railway_syncs_public_env_and_custom_dockerfile_path() {
        let commands = public_env_sync_commands(&args(OrchestratorDeployProvider::Railway), &env());
        assert!(commands
            .iter()
            .any(|command| command.display().contains("RAILWAY_DOCKERFILE_PATH")));
        assert!(commands
            .iter()
            .any(|command| command.display().contains("HARN_ORCHESTRATOR_LISTEN")));
    }

    #[test]
    fn build_command_uses_manifest_context_even_with_nested_deploy_dir() {
        let mut args = args(OrchestratorDeployProvider::Fly);
        args.deploy_dir = PathBuf::from("ops/deploy");
        args.build = true;
        let bundle = DeployBundle {
            provider_dir: PathBuf::from("/repo/ops/deploy/fly"),
            context_dir: PathBuf::from("/repo"),
            dockerfile_path: PathBuf::from("/repo/ops/deploy/fly/Dockerfile"),
            spec_path: PathBuf::from("/repo/ops/deploy/fly/fly.toml"),
            spec_contents: String::new(),
        };
        let command = build_image_command(&args, &bundle);
        assert_eq!(command.cwd.as_deref(), Some(Path::new("/repo")));
        assert!(command
            .display()
            .contains("/repo/ops/deploy/fly/Dockerfile"));
    }
}
