use apiforge::cli::{Cli, Commands};
use apiforge::config::Config;
use clap::Parser;
use std::path::PathBuf;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    tracing_subscriber::fmt()
        .with_env_filter(
            tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| {
                if cli.debug {
                    "apiforge=debug".into()
                } else {
                    "apiforge=info".into()
                }
            }),
        )
        .without_time()
        .init();

    match cli.command {
        Commands::Init(args) => cmd_init(args).await,
        Commands::Doctor => cmd_doctor(&cli.config).await,
        Commands::Release(args) => cmd_release(&cli.config, args).await,
        Commands::Rollback(args) => cmd_rollback(&cli.config, args).await,
        Commands::History(args) => cmd_history(args).await,
        Commands::Status => cmd_status(&cli.config).await,
        Commands::Config(args) => cmd_config(&cli.config, args).await,
    }
}

async fn cmd_init(args: apiforge::cli::InitArgs) -> anyhow::Result<()> {
    let name = args.name.unwrap_or_else(|| {
        std::env::current_dir()
            .ok()
            .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
            .unwrap_or_else(|| "my-project".to_string())
    });

    let config_path = PathBuf::from("apiforge.toml");
    if config_path.exists() && !args.force {
        anyhow::bail!("apiforge.toml already exists. Use --force to overwrite.");
    }

    // Generate a default config
    let default_config = format!(
        r#"[project]
name = "{name}"
language = "rust"

[git]
main_branch = "main"
tag_format = "v{{version}}"
changelog = true
commit_message = "chore: release v{{{{ version }}}}"
remote = "origin"
require_clean = true
require_main_branch = true
fetch_timeout_secs = 60
push_timeout_secs = 120
operation_timeout_secs = 30

[docker]
registry = "aws_ecr"
repository = "{name}"
dockerfile = "Dockerfile"
context = "."
tags = ["{{version}}", "latest"]

[kubernetes]
context = "production"
namespace = "default"
deployment = "{name}"
manifest_path = "k8s/deployment.yaml"
image_field = ".spec.template.spec.containers[0].image"
rollout_timeout = 300
min_ready_percent = 100

[aws]
region = "us-east-1"

# Optional: GitHub release configuration
# [github]
# repository = "owner/repo"
# token = "${{GITHUB_TOKEN}}"
# create_release = true
# prerelease = false
# draft = false

# Optional: Notifications
# [notifications.slack]
# webhook_url = "${{SLACK_WEBHOOK_URL}}"
# message = "{{{{ status_emoji }}}} Release {{{{ version }}}} of {{{{ project }}}}: {{{{ status }}}}"
# notify_on = "both"

# Optional: Health check
# [health_check]
# url = "https://api.example.com/health"
# expected_status = 200
# timeout = 60
# interval = 5
"#,
        name = name
    );

    std::fs::write(&config_path, default_config)?;

    // Keep apiforge's local state (audit store) out of version control.
    let gitignore_path = PathBuf::from(".gitignore");
    let gitignore = std::fs::read_to_string(&gitignore_path).unwrap_or_default();
    if !gitignore.lines().any(|l| l.trim() == ".apiforge/") {
        let mut updated = gitignore;
        if !updated.is_empty() && !updated.ends_with('\n') {
            updated.push('\n');
        }
        updated.push_str(".apiforge/\n");
        std::fs::write(&gitignore_path, updated)?;
        println!("  Added .apiforge/ to .gitignore");
    }

    println!("✓ Initialized apiforge for '{}'", name);
    println!("  Created apiforge.toml — edit it to match your project setup.");
    println!("\nNext steps:");
    println!("  1. Edit apiforge.toml with your project settings");
    println!("  2. Run 'apiforge doctor' to validate your environment");
    println!("  3. Run 'apiforge release patch --dry-run' to preview a release");

    Ok(())
}

async fn cmd_doctor(config_path: &str) -> anyhow::Result<()> {
    use colored::Colorize;

    println!("\n{}", "▸ Environment checks".bold().cyan());

    type ToolCheck = (&'static str, fn() -> bool, &'static str);
    let checks: Vec<ToolCheck> = vec![
        ("git", || which::which("git").is_ok(), "Version control"),
        (
            "docker",
            || which::which("docker").is_ok(),
            "Container builds",
        ),
        (
            "kubectl",
            || which::which("kubectl").is_ok(),
            "Kubernetes deployment",
        ),
        ("aws", || which::which("aws").is_ok(), "AWS CLI (ECR auth)"),
    ];

    let mut all_ok = true;
    for (name, check, purpose) in &checks {
        let (status, color) = if check() {
            ("OK", "green")
        } else {
            all_ok = false;
            ("MISSING", "yellow")
        };
        let status_colored = match color {
            "green" => status.green(),
            "yellow" => status.yellow(),
            _ => status.normal(),
        };
        println!(
            "  {} {} ... {} ({})",
            "•".dimmed(),
            name.bold(),
            status_colored,
            purpose.dimmed()
        );
    }

    println!("\n{}", "▸ Configuration".bold().cyan());

    let path = PathBuf::from(config_path);
    if path.exists() {
        match Config::from_file(&path) {
            Ok(config) => {
                println!("  {} config ... {}", "•".dimmed(), "OK".green());
                println!("    Project: {}", config.project.name);
                println!("    Language: {:?}", config.project.language);
                println!("    Registry: {:?}", config.docker.registry);
            }
            Err(e) => {
                all_ok = false;
                println!("  {} config ... {} ({})", "•".dimmed(), "INVALID".red(), e);
            }
        }
    } else {
        all_ok = false;
        println!(
            "  {} config ... {} (run `apiforge init`)",
            "•".dimmed(),
            "NOT FOUND".yellow()
        );
    }

    println!("\n{}", "▸ Git repository".bold().cyan());

    match apiforge::integrations::git::GitRepo::open() {
        Ok(repo) => {
            println!("  {} repository ... {}", "•".dimmed(), "OK".green());
            if let Ok(branch) = repo.current_branch() {
                println!("    Branch: {}", branch);
            }
            if let Ok(Some(tag)) = repo.get_latest_tag("v*") {
                println!("    Latest tag: {}", tag);
            }
            if let Ok(clean) = repo.is_working_tree_clean() {
                let status = if clean {
                    "clean".green()
                } else {
                    "dirty".yellow()
                };
                println!("    Working tree: {}", status);
            }
        }
        Err(_) => {
            all_ok = false;
            println!("  {} repository ... {}", "•".dimmed(), "NOT FOUND".red());
        }
    }

    println!();
    if all_ok {
        println!("{}", "  ✓ All checks passed!".green().bold());
    } else {
        println!(
            "{}",
            "  ⚠ Some checks failed. Fix the issues above before releasing.".yellow()
        );
    }

    Ok(())
}

async fn cmd_release(config_path: &str, args: apiforge::cli::ReleaseArgs) -> anyhow::Result<()> {
    use colored::Colorize;
    use dialoguer::Confirm;

    let path = PathBuf::from(config_path);
    let mut config = Config::from_file(&path)?;
    // Resolve ${ssm:/path} secrets from AWS Parameter Store (no-op when
    // unused). Skipped in dry-run so previews never need AWS credentials.
    if !args.dry_run {
        config.resolve_ssm_parameters().await?;
    }
    let config = config;

    let bump_type = args.bump.parse::<apiforge::utils::BumpType>()?;

    let repo = apiforge::integrations::git::GitRepo::open()?;
    let version_file = config.project.language.version_file();
    let version_path = repo.root_path().join(version_file);

    let current_version = apiforge::utils::read_version(config.project.language, &version_path)?;

    let new_version = apiforge::utils::bump_version(&current_version, bump_type)?;
    let new_version_str = new_version.to_string();

    let previous_tag = repo.get_latest_tag(&config.git.tag_format.replace("{version}", "*"))?;

    // In JSON output mode stdout must carry only the JSON document; all
    // human-readable chrome (plan, step progress) goes to stderr instead.
    let json_mode = args.output == "json";

    // Show release plan
    use std::fmt::Write as _;
    let mut plan = String::new();
    let _ = writeln!(plan, "\n{}", "▸ Release Plan".bold().cyan());
    let _ = writeln!(plan, "  Project:     {}", config.project.name.bold());
    let _ = writeln!(
        plan,
        "  Version:     {} → {}",
        current_version.dimmed(),
        new_version_str.green().bold()
    );
    let _ = writeln!(plan, "  Bump type:   {}", args.bump);
    if let Some(ref tag) = previous_tag {
        let _ = writeln!(plan, "  Previous:    {}", tag.dimmed());
    }
    let _ = writeln!(plan);

    // Show what will happen
    let _ = writeln!(plan, "{}", "  Steps to execute:".dimmed());
    let _ = writeln!(plan, "  1. Validate git repository state");
    let _ = writeln!(plan, "  2. Bump version in {}", version_file);
    if config.git.changelog && !args.no_changelog {
        let _ = writeln!(plan, "  3. Generate changelog");
    }
    let _ = writeln!(plan, "  4. Commit and tag");
    let _ = writeln!(plan, "  5. Push to remote");

    if !args.skip_docker {
        let _ = writeln!(plan, "  6. Build Docker image");
        let _ = writeln!(plan, "  7. Push to {:?}", config.docker.registry);
    }

    if !args.skip_k8s {
        let _ = writeln!(plan, "  8. Update Kubernetes deployment");
        let _ = writeln!(plan, "  9. Wait for rollout");
    }

    if !args.skip_cloudfront && config.cloudfront.is_some() {
        let _ = writeln!(plan, "  10. Invalidate CloudFront cache");
    }

    let github_release_enabled = config
        .github
        .as_ref()
        .map(|g| g.create_release)
        .unwrap_or(false);
    if !args.skip_github && github_release_enabled {
        let _ = writeln!(plan, "  11. Create GitHub release");
    }

    if config.health_check.is_some() {
        let _ = writeln!(plan, "  12. Verify service health");
    }

    if json_mode {
        eprintln!("{}", plan);
    } else {
        println!("{}", plan);
    }

    // Confirmation prompt (unless --yes or --dry-run)
    if !args.dry_run && !args.yes {
        let confirmed = Confirm::new()
            .with_prompt("Proceed with release?")
            .default(false)
            .interact()?;

        if !confirmed {
            println!("Release cancelled.");
            return Ok(());
        }
    }

    // Build the orchestrator with all steps
    let mut orchestrator =
        apiforge::orchestrator::ReleaseOrchestrator::new(config.clone(), args.dry_run);
    if json_mode {
        orchestrator = orchestrator.with_stderr_output();
    }

    // Git steps
    orchestrator.add_step(Box::new(apiforge::steps::git::GitPreflightStep::new()));
    orchestrator.add_step(Box::new(apiforge::steps::git::VersionBumpStep::new(
        bump_type,
    )));

    let changelog_enabled = config.git.changelog && !args.no_changelog;
    if changelog_enabled {
        orchestrator.add_step(Box::new(apiforge::steps::git::ChangelogStep::new(
            new_version_str.clone(),
            previous_tag.clone(),
        )));
    }

    orchestrator.add_step(Box::new(
        apiforge::steps::git::GitCommitStep::new(new_version_str.clone())
            .with_changelog(changelog_enabled),
    ));
    orchestrator.add_step(Box::new(apiforge::steps::git::GitTagStep::new(
        new_version.clone(),
    )));
    orchestrator.add_step(Box::new(apiforge::steps::git::GitPushStep::new(
        new_version.clone(),
    )));

    // Docker steps
    if !args.skip_docker {
        orchestrator.add_step(Box::new(apiforge::steps::docker::DockerBuildStep::new(
            new_version.clone(),
        )));
        orchestrator.add_step(Box::new(apiforge::steps::docker::DockerPushStep::new(
            new_version.clone(),
        )));
    }

    // Kubernetes steps
    if !args.skip_k8s {
        orchestrator.add_step(Box::new(apiforge::steps::kubernetes::K8sUpdateStep::new(
            new_version.clone(),
        )));
        orchestrator.add_step(Box::new(apiforge::steps::kubernetes::K8sRolloutStep::new()));
    }

    // CloudFront cache invalidation (after the new version is serving)
    if !args.skip_cloudfront && config.cloudfront.is_some() {
        orchestrator.add_step(Box::new(
            apiforge::steps::cloudfront::CloudFrontInvalidateStep::new(),
        ));
    }

    // GitHub release (honors the create_release config flag)
    if !args.skip_github && github_release_enabled {
        orchestrator.add_step(Box::new(
            apiforge::steps::github::GitHubReleaseStep::new(new_version.clone())
                .with_previous_tag(previous_tag.clone()),
        ));
    }

    // Health check
    if config.health_check.is_some() {
        orchestrator.add_step(Box::new(apiforge::steps::health::HealthCheckStep::new(
            new_version.clone(),
        )));
    }

    // Run the pipeline
    let pipeline_start = std::time::Instant::now();
    let report = orchestrator.run().await;
    let success = report.error.is_none();
    let error_message = report
        .error
        .as_ref()
        .map(|e| apiforge::utils::sanitize_message(&e.to_string()));

    // Notifications fire for both success and failure (notify_on filtering
    // happens inside the steps), but never in dry-run mode.
    if !args.dry_run && !args.skip_notify && config.notifications.is_some() {
        send_notifications(&config, &new_version, success, error_message.clone()).await;
    }

    // Record in audit log. Dry-runs are not recorded: --dry-run promises no
    // side effects, and phantom entries would pollute release history.
    if !args.dry_run {
        record_audit(
            &new_version_str,
            &bump_type.to_string(),
            &report,
            pipeline_start.elapsed(),
            error_message.clone(),
        );
    }

    // Output results
    if args.output == "json" {
        let result = serde_json::json!({
            "success": success,
            "version": new_version_str,
            "bump_type": bump_type.to_string(),
            "dry_run": args.dry_run,
            "rolled_back": report.rolled_back,
            "error": error_message,
            "steps": report.steps.iter().map(|(name, o)| serde_json::json!({
                "name": name,
                "status": match o.status {
                    apiforge::steps::StepStatus::Success => "success",
                    apiforge::steps::StepStatus::Skipped => "skipped",
                    apiforge::steps::StepStatus::Failed => "failed",
                },
                "message": o.message,
                "duration_ms": o.duration_ms
            })).collect::<Vec<_>>()
        });
        println!("{}", serde_json::to_string_pretty(&result)?);
    } else if success {
        println!(
            "\n{}",
            format!("✨ Release {} complete!", new_version)
                .green()
                .bold()
        );
        println!("   {} steps executed successfully", report.steps.len());
    }

    match report.error {
        Some(e) => Err(e.into()),
        None => Ok(()),
    }
}

/// Dispatch configured notifications (Slack and/or generic webhook) using the
/// notification steps, which handle notify_on filtering and templating.
async fn send_notifications(
    config: &Config,
    version: &semver::Version,
    success: bool,
    error_message: Option<String>,
) {
    use apiforge::steps::notify::{SlackNotifyStep, WebhookNotifyStep};
    use apiforge::steps::{Step, StepContext};

    let notifications = match config.notifications {
        Some(ref n) => n,
        None => return,
    };

    let ctx = StepContext {
        config: config.clone(),
        dry_run: false,
        state: std::collections::HashMap::new(),
        progress: None,
    };

    if notifications.slack.is_some() {
        let mut step = SlackNotifyStep::new(version.clone(), success);
        if let Some(ref err) = error_message {
            step = step.with_error(err.clone());
        }
        if let Err(e) = step.execute(&ctx).await {
            tracing::warn!("Failed to send Slack notification: {}", e);
        }
    }

    if notifications.webhook.is_some() {
        let mut step = WebhookNotifyStep::new(version.clone(), success);
        if let Some(ref err) = error_message {
            step = step.with_error(err.clone());
        }
        if let Err(e) = step.execute(&ctx).await {
            tracing::warn!("Failed to send webhook notification: {}", e);
        }
    }
}

/// Persist a release audit record reflecting the actual run outcome:
/// per-step results, total duration, and Success/Failed/RolledBack status.
fn record_audit(
    version: &str,
    bump_type: &str,
    report: &apiforge::orchestrator::RunReport,
    duration: std::time::Duration,
    error_message: Option<String>,
) {
    use apiforge::audit::{AuditStore, ReleaseStatus, StepRecord, StepStatus as AuditStepStatus};

    let audit_dir = std::path::Path::new(".apiforge/audit");
    match AuditStore::open(audit_dir) {
        Ok(store) => {
            let mut record = AuditStore::new_record(version, bump_type, false);
            record.status = if report.error.is_none() {
                ReleaseStatus::Success
            } else if report.rolled_back {
                ReleaseStatus::RolledBack
            } else {
                ReleaseStatus::Failed
            };
            record.duration_ms = duration.as_millis() as u64;
            record.steps = report
                .steps
                .iter()
                .map(|(name, o)| StepRecord {
                    name: name.clone(),
                    status: match o.status {
                        apiforge::steps::StepStatus::Success => AuditStepStatus::Success,
                        apiforge::steps::StepStatus::Skipped => AuditStepStatus::Skipped,
                        apiforge::steps::StepStatus::Failed => AuditStepStatus::Failed,
                    },
                    duration_ms: o.duration_ms,
                    message: Some(o.message.clone()),
                })
                .collect();
            if let Some(err) = error_message {
                record.metadata.insert("error".to_string(), err);
            }

            if let Err(e) = store.record(&record) {
                tracing::warn!("Failed to write audit record: {}", e);
            }
            if let Err(e) = store.flush() {
                tracing::warn!("Failed to flush audit database: {}", e);
            }
        }
        Err(_) => tracing::warn!("Failed to open audit database at {:?}", audit_dir),
    }
}

async fn cmd_rollback(config_path: &str, args: apiforge::cli::RollbackArgs) -> anyhow::Result<()> {
    use colored::Colorize;
    use dialoguer::Confirm;

    let path = PathBuf::from(config_path);
    let mut config = Config::from_file(&path)?;
    // Resolve ${ssm:/path} secrets from AWS Parameter Store (no-op when
    // unused). Skipped in dry-run so previews never need AWS credentials.
    if !args.dry_run {
        config.resolve_ssm_parameters().await?;
    }
    let config = config;

    // Resolve the target version: explicit --to wins, otherwise auto-detect
    // from release history / git tags anchored on the deployed version.
    let (target_version, current_version, source) = if let Some(ref to_version) = args.to {
        (to_version.clone(), None, "--to flag")
    } else {
        detect_rollback_target(&config, args.dry_run).await?
    };

    println!("\n{}", "▸ Rollback Plan".bold().cyan());
    if let Some(ref current) = current_version {
        println!("  Currently deployed: {}", current);
    }
    println!(
        "  Target version:     {} {}",
        target_version.bold(),
        format!("(from {})", source).dimmed()
    );
    if config.health_check.is_some() {
        println!("  Post-rollback:      health check verification");
    }

    if args.dry_run {
        println!("\n{}", "[dry-run] Would perform the following:".yellow());
        println!("  1. Update Kubernetes deployment to {}", target_version);
        println!("  2. Wait for rollout");
        if config.health_check.is_some() {
            println!("  3. Verify health check");
        }
        return Ok(());
    }

    if !args.yes {
        let confirmed = Confirm::new()
            .with_prompt(format!("Roll back to {}?", target_version))
            .default(false)
            .interact()?;
        if !confirmed {
            println!("Rollback cancelled.");
            return Ok(());
        }
    }

    let out = apiforge::output::OutputManager::new();
    out.section("Executing rollback");

    // Perform Kubernetes rollback
    let k8s =
        apiforge::integrations::kubernetes::K8sClient::new(&config.kubernetes.context).await?;

    // Build the full image name with target version
    let image_base = match config.docker.registry {
        apiforge::config::DockerRegistry::AwsEcr => {
            let aws = if let Some(ref profile) = config.aws.profile {
                apiforge::integrations::aws::AwsClient::with_profile(&config.aws.region, profile)
                    .await?
            } else {
                apiforge::integrations::aws::AwsClient::new(&config.aws.region).await?
            };
            let (account_id, _) = aws.get_caller_identity().await?;
            let registry_url = aws.get_ecr_registry_url(&account_id);
            format!("{}/{}", registry_url, config.docker.repository)
        }
        apiforge::config::DockerRegistry::Ghcr => {
            format!("ghcr.io/{}", config.docker.repository)
        }
        _ => config.docker.repository.clone(),
    };

    let target_image = format!("{}:{}", image_base, target_version.trim_start_matches('v'));

    out.step_status("k8s-update", &format!("setting image {}...", target_image));
    k8s.update_deployment_image(
        &config.kubernetes.namespace,
        &config.kubernetes.deployment,
        &config.kubernetes.image_field,
        &target_image,
    )
    .await?;
    out.step_ok("k8s-update");

    out.step_status("k8s-rollout", "waiting for rollout...");
    let reporter = out.progress_reporter();
    k8s.wait_for_rollout(
        &config.kubernetes.namespace,
        &config.kubernetes.deployment,
        config.kubernetes.rollout_timeout,
        |status| {
            reporter.set_message(&format!(
                "k8s-rollout: {}/{} replicas ready",
                status.ready_replicas, status.desired_replicas
            ));
        },
    )
    .await?;
    out.step_ok("k8s-rollout");

    // Verify the rolled-back service is actually healthy before declaring success.
    if config.health_check.is_some() {
        let target_semver =
            apiforge::utils::parse_version(&target_version).map_err(anyhow::Error::from)?;

        out.step_status("health-check", "verifying service health...");
        let ctx = apiforge::steps::StepContext {
            config: config.clone(),
            dry_run: false,
            state: std::collections::HashMap::new(),
            progress: Some(out.progress_reporter()),
        };

        use apiforge::steps::Step;
        let step = apiforge::steps::health::HealthCheckStep::new(target_semver);
        match step.execute(&ctx).await {
            Ok(output) => out.step_done("health-check", &output),
            Err(e) => {
                let safe = apiforge::utils::sanitize_message(&e.to_string());
                out.step_fail("health-check", &safe);
                anyhow::bail!(
                    "Rollback to {} applied, but the health check did not pass: {}",
                    target_version,
                    safe
                );
            }
        }
    }

    println!(
        "\n{}",
        format!("✓ Rollback to {} complete!", target_version)
            .green()
            .bold()
    );

    Ok(())
}

/// Auto-detect the rollback target.
///
/// Sources, in order: successful non-dry-run releases from the audit store,
/// then semver git tags. Anchored on the currently deployed version read from
/// the Kubernetes deployment image (skipped in dry-run so previews work
/// without cluster access).
async fn detect_rollback_target(
    config: &Config,
    dry_run: bool,
) -> anyhow::Result<(String, Option<String>, &'static str)> {
    // Currently deployed version (best effort — cluster may be unreachable).
    let current: Option<semver::Version> = if dry_run {
        None
    } else {
        match apiforge::integrations::kubernetes::K8sClient::new(&config.kubernetes.context).await {
            Ok(k8s) => k8s
                .get_deployment(&config.kubernetes.namespace, &config.kubernetes.deployment)
                .await
                .ok()
                .and_then(|d| {
                    d.spec
                        .as_ref()
                        .and_then(|s| s.template.spec.as_ref())
                        .and_then(|s| s.containers.first())
                        .and_then(|c| c.image.as_deref())
                        .and_then(apiforge::utils::version_from_image)
                }),
            Err(_) => None,
        }
    };

    // Successful, non-dry-run releases from the audit store (newest first).
    let mut successful: Vec<semver::Version> = Vec::new();
    let audit_dir = std::path::Path::new(".apiforge/audit");
    if audit_dir.exists() {
        if let Ok(store) = apiforge::audit::AuditStore::open(audit_dir) {
            if let Ok(records) = store.list(usize::MAX) {
                successful = records
                    .iter()
                    .filter(|r| r.status == apiforge::audit::ReleaseStatus::Success && !r.dry_run)
                    .filter_map(|r| semver::Version::parse(r.version.trim_start_matches('v')).ok())
                    .collect();
            }
        }
    }

    // Git tags as fallback (already sorted newest-first by list_tags).
    let tags: Vec<semver::Version> = apiforge::integrations::git::GitRepo::open()
        .ok()
        .and_then(|repo| {
            repo.list_tags(&config.git.tag_format.replace("{version}", "*"))
                .ok()
        })
        .map(|tags| {
            tags.iter()
                .filter_map(|t| semver::Version::parse(t.trim_start_matches('v')).ok())
                .collect()
        })
        .unwrap_or_default();

    let source = if !successful.is_empty() {
        "release history"
    } else {
        "git tags"
    };

    let target = apiforge::utils::select_rollback_target(current.as_ref(), &successful, &tags)
        .ok_or_else(|| {
            anyhow::anyhow!(
                "Could not auto-detect a rollback target: no older successful release or tag found. \
                 Specify the version explicitly with --to <version>."
            )
        })?;

    Ok((target.to_string(), current.map(|v| v.to_string()), source))
}

async fn cmd_history(args: apiforge::cli::HistoryArgs) -> anyhow::Result<()> {
    use colored::Colorize;
    use comfy_table::{ContentArrangement, Table};

    // Don't create the audit store just to read it — a missing directory
    // simply means no releases have been recorded yet.
    let audit_dir = std::path::Path::new(".apiforge/audit");
    if !audit_dir.exists() {
        println!("No release history found.");
        println!("Run 'apiforge release patch' to create your first release.");
        return Ok(());
    }

    let store = apiforge::audit::AuditStore::open(audit_dir)?;
    // Fetch broadly, filter by status, then apply the limit — filtering after
    // limiting would silently return fewer rows than requested.
    let all_records = store.list(usize::MAX)?;
    // store will be dropped at end of scope, triggering flush

    let records: Vec<_> = all_records
        .into_iter()
        .filter(|record| match args.filter.as_deref() {
            Some("success") => record.status == apiforge::audit::ReleaseStatus::Success,
            // Rolled-back releases are failed releases that were recovered.
            Some("failed") => matches!(
                record.status,
                apiforge::audit::ReleaseStatus::Failed | apiforge::audit::ReleaseStatus::RolledBack
            ),
            _ => true,
        })
        .take(args.limit)
        .collect();

    if records.is_empty() {
        println!("No release history found.");
        println!("Run 'apiforge release patch' to create your first release.");
        return Ok(());
    }

    if args.output == "json" {
        let json = serde_json::to_string_pretty(&records)?;
        println!("{}", json);
        return Ok(());
    }

    let mut table = Table::new();
    table.set_content_arrangement(ContentArrangement::Dynamic);
    table.set_header(vec!["Timestamp", "Version", "Type", "Status", "Duration"]);

    for record in records {
        let status_display = match record.status {
            apiforge::audit::ReleaseStatus::Success => "✓ success".green().to_string(),
            apiforge::audit::ReleaseStatus::Failed => "✗ failed".red().to_string(),
            apiforge::audit::ReleaseStatus::RolledBack => "⟲ rolled back".yellow().to_string(),
        };

        let dry_run_marker = if record.dry_run { " (dry-run)" } else { "" };

        table.add_row(vec![
            record.timestamp,
            format!("{}{}", record.version, dry_run_marker),
            record.bump_type,
            status_display,
            format!("{}ms", record.duration_ms),
        ]);
    }

    println!("\n{}", "▸ Release History".bold().cyan());
    println!("{table}");

    Ok(())
}

async fn cmd_status(config_path: &str) -> anyhow::Result<()> {
    use colored::Colorize;

    let path = PathBuf::from(config_path);
    if !path.exists() {
        anyhow::bail!("No apiforge.toml found. Run `apiforge init` first.");
    }

    let config = Config::from_file(&path)?;

    println!("\n{}", "▸ Project Status".bold().cyan());
    println!("  Project:  {}", config.project.name.bold());
    println!("  Language: {:?}", config.project.language);

    if let Ok(repo) = apiforge::integrations::git::GitRepo::open() {
        println!("\n{}", "▸ Git".bold().cyan());
        if let Ok(branch) = repo.current_branch() {
            println!("  Branch:      {}", branch);
        }
        if let Ok(Some(tag)) = repo.get_latest_tag("v*") {
            println!("  Latest tag:  {}", tag.green());
        }
        if let Ok(sha) = repo.current_commit_sha() {
            println!("  HEAD:        {}", &sha[..8].dimmed());
        }
    }

    // Try to get current deployed version from Kubernetes
    println!("\n{}", "▸ Kubernetes".bold().cyan());
    match apiforge::integrations::kubernetes::K8sClient::new(&config.kubernetes.context).await {
        Ok(k8s) => {
            println!("  Context:    {}", config.kubernetes.context);
            println!("  Namespace:  {}", config.kubernetes.namespace);

            match k8s
                .get_deployment(&config.kubernetes.namespace, &config.kubernetes.deployment)
                .await
            {
                Ok(deployment) => {
                    let image = deployment
                        .spec
                        .as_ref()
                        .and_then(|s| s.template.spec.as_ref())
                        .and_then(|s| s.containers.first())
                        .map(|c| c.image.as_deref().unwrap_or("unknown"))
                        .unwrap_or("unknown");

                    println!(
                        "  Deployment: {} ({})",
                        config.kubernetes.deployment,
                        "running".green()
                    );
                    println!("  Image:      {}", image);

                    if let Ok(status) = k8s
                        .get_rollout_status(
                            &config.kubernetes.namespace,
                            &config.kubernetes.deployment,
                        )
                        .await
                    {
                        let ready_status = if status.ready {
                            format!(
                                "{}/{} ready",
                                status.ready_replicas, status.desired_replicas
                            )
                            .green()
                        } else {
                            format!(
                                "{}/{} ready",
                                status.ready_replicas, status.desired_replicas
                            )
                            .yellow()
                        };
                        println!("  Replicas:   {}", ready_status);
                    }
                }
                Err(_) => {
                    println!(
                        "  Deployment: {} ({})",
                        config.kubernetes.deployment,
                        "not found".red()
                    );
                }
            }
        }
        Err(_) => {
            println!("  {} Unable to connect to cluster", "⚠".yellow());
        }
    }

    Ok(())
}

async fn cmd_config(config_path: &str, args: apiforge::cli::ConfigArgs) -> anyhow::Result<()> {
    use apiforge::cli::ConfigCommands;

    match args.command {
        ConfigCommands::Validate(validate_args) => {
            cmd_config_validate(config_path, validate_args).await
        }
    }
}

async fn cmd_config_validate(
    config_path: &str,
    args: apiforge::cli::ConfigValidateArgs,
) -> anyhow::Result<()> {
    use colored::Colorize;

    let path = PathBuf::from(config_path);

    // Structure to hold validation results
    let mut checks: Vec<(String, bool, Option<String>)> = Vec::new();
    let mut all_ok = true;

    // Check 1: File exists
    let file_exists = path.exists();
    checks.push((
        "Configuration file exists".to_string(),
        file_exists,
        if file_exists {
            None
        } else {
            Some(format!("File not found: {}", path.display()))
        },
    ));

    if !file_exists {
        all_ok = false;
    } else {
        // Check 2: File readable
        let content_result = std::fs::read_to_string(&path);
        let readable = content_result.is_ok();
        let content_error_msg = content_result.as_ref().err().map(|e| e.to_string());
        checks.push((
            "Configuration file readable".to_string(),
            readable,
            content_error_msg,
        ));

        if let Ok(content) = content_result {
            // Check 3: Valid TOML
            let toml_result: Result<toml::Value, _> = toml::from_str(&content);
            let valid_toml = toml_result.is_ok();
            let toml_error_msg = toml_result.as_ref().err().map(|e| e.to_string());
            checks.push(("Valid TOML syntax".to_string(), valid_toml, toml_error_msg));

            if toml_result.is_ok() {
                // Check 4: Schema validation (Config struct)
                let config_result = Config::from_file(&path);
                let valid_schema = config_result.is_ok();
                let config_error_msg = config_result.as_ref().err().map(|e| e.to_string());
                checks.push((
                    "Valid configuration schema".to_string(),
                    valid_schema,
                    config_error_msg,
                ));

                if let Ok(config) = config_result {
                    // Check 5: Project configuration
                    checks.push((
                        "Project name specified".to_string(),
                        !config.project.name.is_empty(),
                        if config.project.name.is_empty() {
                            Some("Project name is empty".to_string())
                        } else {
                            None
                        },
                    ));

                    // Check 6: Git configuration
                    let tag_format_valid = config.git.tag_format.contains("{version}");
                    checks.push((
                        "Git tag format contains {version}".to_string(),
                        tag_format_valid,
                        if tag_format_valid {
                            None
                        } else {
                            Some(format!(
                                "tag_format '{}' must contain {{version}} placeholder",
                                config.git.tag_format
                            ))
                        },
                    ));

                    // Check 7: Docker configuration
                    checks.push((
                        "Docker repository specified".to_string(),
                        !config.docker.repository.is_empty(),
                        if config.docker.repository.is_empty() {
                            Some("Docker repository is empty".to_string())
                        } else {
                            None
                        },
                    ));

                    let has_docker_tags = !config.docker.tags.is_empty();
                    checks.push((
                        "Docker tags specified".to_string(),
                        has_docker_tags,
                        if has_docker_tags {
                            None
                        } else {
                            Some("At least one Docker tag is required".to_string())
                        },
                    ));

                    // Check 8: Kubernetes configuration
                    checks.push((
                        "Kubernetes namespace specified".to_string(),
                        !config.kubernetes.namespace.is_empty(),
                        None,
                    ));
                    checks.push((
                        "Kubernetes deployment specified".to_string(),
                        !config.kubernetes.deployment.is_empty(),
                        None,
                    ));
                    checks.push((
                        "Kubernetes context specified".to_string(),
                        !config.kubernetes.context.is_empty(),
                        None,
                    ));

                    // Check 9: AWS configuration (if ECR)
                    let ecr_check = if matches!(
                        config.docker.registry,
                        apiforge::config::DockerRegistry::AwsEcr
                    ) {
                        let has_region = !config.aws.region.is_empty();
                        (
                            "AWS region specified (for ECR)".to_string(),
                            has_region,
                            if has_region {
                                None
                            } else {
                                Some("AWS region is required when using ECR".to_string())
                            },
                        )
                    } else {
                        (
                            "AWS region (not required for non-ECR)".to_string(),
                            true,
                            None,
                        )
                    };
                    checks.push(ecr_check);

                    // Check 10: GitHub configuration (if present)
                    if let Some(ref github) = config.github {
                        let repo_valid =
                            !github.repository.is_empty() && github.repository.contains('/');
                        checks.push((
                            "GitHub repository format valid".to_string(),
                            repo_valid,
                            if repo_valid {
                                None
                            } else {
                                Some(format!(
                                    "GitHub repository '{}' must be in 'owner/repo' format",
                                    github.repository
                                ))
                            },
                        ));
                    }

                    // Check 11: Health check configuration (if present)
                    if let Some(ref hc) = config.health_check {
                        let url_valid = !hc.url.is_empty()
                            && (hc.url.starts_with("http://") || hc.url.starts_with("https://"));
                        checks.push((
                            "Health check URL valid".to_string(),
                            url_valid,
                            if url_valid {
                                None
                            } else {
                                Some(format!(
                                    "Health check URL '{}' must start with http:// or https://",
                                    hc.url
                                ))
                            },
                        ));

                        let interval_valid = hc.interval > 0;
                        checks.push((
                            "Health check interval > 0".to_string(),
                            interval_valid,
                            if interval_valid {
                                None
                            } else {
                                Some("Health check interval must be greater than 0".to_string())
                            },
                        ));

                        let timeout_valid = hc.timeout > 0;
                        checks.push((
                            "Health check timeout > 0".to_string(),
                            timeout_valid,
                            if timeout_valid {
                                None
                            } else {
                                Some("Health check timeout must be greater than 0".to_string())
                            },
                        ));
                    }

                    // Check 12: Notifications configuration (if present)
                    if let Some(ref notifications) = config.notifications {
                        if let Some(ref slack) = notifications.slack {
                            // ${ssm:...} placeholders resolve at release time;
                            // the URL-shape check only applies to literal values.
                            let webhook_valid = !slack.webhook_url.is_empty()
                                && (slack.webhook_url.starts_with("https://hooks.slack.com")
                                    || slack.webhook_url.contains("${ssm:"));
                            checks.push((
                                "Slack webhook URL format valid".to_string(),
                                webhook_valid,
                                if webhook_valid {
                                    None
                                } else {
                                    Some(format!(
                                        "Slack webhook URL should start with https://hooks.slack.com (got: {})",
                                        slack.webhook_url.chars().take(30).collect::<String>()
                                    ))
                                },
                            ));
                        }
                    }

                    // Verbose mode: Additional informational checks
                    if args.verbose {
                        checks.push((
                            format!("Language: {:?}", config.project.language),
                            true,
                            None,
                        ));
                        checks.push((
                            format!("Registry: {:?}", config.docker.registry),
                            true,
                            None,
                        ));
                        checks.push((
                            format!("Main branch: {}", config.git.main_branch),
                            true,
                            None,
                        ));
                        checks.push((
                            format!(
                                "Timeout settings: fetch={}s push={}s op={}s",
                                config.git.fetch_timeout_secs,
                                config.git.push_timeout_secs,
                                config.git.operation_timeout_secs
                            ),
                            true,
                            None,
                        ));
                    }
                } else {
                    all_ok = false;
                }
            } else {
                all_ok = false;
            }
        } else {
            all_ok = false;
        }
    }

    // Count failures
    let failures: Vec<_> = checks.iter().filter(|(_, ok, _)| !ok).collect();
    if !failures.is_empty() {
        all_ok = false;
    }

    // Output results
    if args.output == "json" {
        let result = serde_json::json!({
            "valid": all_ok,
            "file": config_path,
            "checks": checks.iter().map(|(name, ok, error)| {
                serde_json::json!({
                    "name": name,
                    "passed": *ok,
                    "error": error
                })
            }).collect::<Vec<_>>()
        });
        println!("{}", serde_json::to_string_pretty(&result)?);
    } else {
        println!("\n{}", "▸ Configuration Validation".bold().cyan());
        println!("  File: {}\n", path.display().to_string().dimmed());

        for (name, ok, error) in &checks {
            let status = if *ok { "✓".green() } else { "✗".red() };
            println!("  {} {}", status, name);
            if let Some(ref err) = error {
                println!("    {} {}", "→".dimmed(), err.dimmed());
            }
        }

        println!();
        if all_ok {
            println!("{}", "  ✓ Configuration is valid!".green().bold());
        } else {
            println!("{}", "  ✗ Configuration has errors".red().bold());
        }
    }

    if all_ok {
        Ok(())
    } else {
        anyhow::bail!("Configuration validation failed");
    }
}
