//! Migrate command - run database migrations via the configured backend.

use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::cli::{MigrateArgs, MigrateBackend};
use crate::commands::messaging::DEV_FIX_ENV_COMMAND;
use crate::commands::messaging::GREENFIELD_NEW_APP_PRESET_API;
use crate::database::{resolve_database_url, validate_database_url};
use crate::env::{ensure_env, ensure_project_dir, read_env_map};
use crate::{
    CommandRuntime, ExecutionPlan, PlannedCommand, error_contract, print_info, print_success,
    print_warning,
};

pub fn run(args: MigrateArgs) -> Result<()> {
    run_with_runtime(args, CommandRuntime::from_process_state())
}

pub fn run_with_runtime(args: MigrateArgs, runtime: CommandRuntime) -> Result<()> {
    runtime.install();

    if runtime.plan_mode() {
        build_migrate_plan(&args).emit(runtime);
        return Ok(());
    }
    let project_dir = PathBuf::from(&args.path);
    ensure_project_dir(&project_dir)?;

    if !args.no_env {
        ensure_env(&project_dir, args.fix_env)?;
    }

    if args.action == "init" {
        let backend = resolve_backend(&project_dir, args.backend)?;
        return match backend {
            MigrateBackend::SeaOrm => init_sea_orm_migration(&project_dir),
            MigrateBackend::Auto => Err(anyhow::anyhow!(error_contract(
                "Unable to detect migration backend.",
                "Pass `--backend sea-orm`.",
                "Add SeaORM dependencies and Tideway `database` feature, then rerun."
            ))),
        };
    }

    let backend = resolve_backend(&project_dir, args.backend)?;
    match backend {
        MigrateBackend::SeaOrm => run_sea_orm_cli(&project_dir, &args),
        MigrateBackend::Auto => Err(anyhow::anyhow!(error_contract(
            "Unable to detect migration backend.",
            "Pass `--backend sea-orm`.",
            "Add SeaORM dependencies and Tideway `database` feature, then rerun."
        ))),
    }
}

fn build_migrate_plan(args: &MigrateArgs) -> ExecutionPlan {
    let mut command = PlannedCommand::new("sea-orm-cli")
        .arg("migrate")
        .arg(args.action.clone())
        .cwd(args.path.clone());

    if !args.args.is_empty() {
        command = command.args(args.args.clone());
    }

    let mut plan =
        ExecutionPlan::new(format!("would run migrations ({})", args.action)).command(command);

    if args.backend == MigrateBackend::Auto {
        plan = plan.info("Migration backend will be auto-detected at execution time.");
    }

    if args.no_env {
        plan = plan.info("Skipping `.env` loading (--no-env).");
    } else if args.fix_env {
        plan = plan.info("Would bootstrap `.env` from `.env.example` when missing.");
    }

    if matches!(args.action.as_str(), "status" | "up" | "down" | "reset") {
        plan.info("DATABASE_URL will be validated at execution time.")
    } else {
        plan
    }
}

fn resolve_backend(project_dir: &Path, backend: MigrateBackend) -> Result<MigrateBackend> {
    match backend {
        MigrateBackend::Auto => detect_backend(project_dir),
        MigrateBackend::SeaOrm => Ok(MigrateBackend::SeaOrm),
    }
}

fn detect_backend(project_dir: &Path) -> Result<MigrateBackend> {
    let cargo_path = project_dir.join("Cargo.toml");
    let contents = fs::read_to_string(&cargo_path)
        .with_context(|| format!("Failed to read {}", cargo_path.display()))?;
    let doc = contents
        .parse::<toml_edit::DocumentMut>()
        .context("Failed to parse Cargo.toml")?;

    let has_sea_orm = has_dependency(&doc, "sea-orm");
    let has_tideway_db = has_tideway_feature(&doc, "database");

    if has_sea_orm || has_tideway_db {
        Ok(MigrateBackend::SeaOrm)
    } else {
        Err(anyhow::anyhow!(error_contract(
            "Could not detect migration backend.",
            "Add SeaORM dependencies or use `--backend sea-orm`.",
            GREENFIELD_NEW_APP_PRESET_API
        )))
    }
}

fn has_tideway_feature(doc: &toml_edit::DocumentMut, feature: &str) -> bool {
    dependency_sections(doc)
        .into_iter()
        .filter_map(|deps| deps.get("tideway"))
        .filter_map(|tideway| tideway.get("features"))
        .filter_map(|features| features.as_array())
        .any(|arr| arr.iter().any(|value| value.as_str() == Some(feature)))
}

fn has_dependency(doc: &toml_edit::DocumentMut, dependency: &str) -> bool {
    dependency_sections(doc)
        .into_iter()
        .any(|deps| deps.get(dependency).is_some())
}

fn dependency_sections<'a>(doc: &'a toml_edit::DocumentMut) -> Vec<&'a toml_edit::Item> {
    let mut sections = Vec::new();

    if let Some(item) = doc.get("dependencies") {
        sections.push(item);
    }

    if let Some(item) = doc.get("build-dependencies") {
        sections.push(item);
    }

    if let Some(item) = doc.get("dev-dependencies") {
        sections.push(item);
    }

    if let Some(targets) = doc.get("target").and_then(|item| item.as_table()) {
        for (_, target) in targets.iter() {
            if let Some(deps) = target.get("dependencies") {
                sections.push(deps);
            }
        }
    }

    sections
}

fn run_sea_orm_cli(project_dir: &Path, args: &MigrateArgs) -> Result<()> {
    let migrations_dir = project_dir.join("migration");
    if !migrations_dir.exists() {
        print_warning("migration/ directory not found; sea-orm-cli may fail");
    }

    if args.action == "status"
        || args.action == "up"
        || args.action == "down"
        || args.action == "reset"
    {
        ensure_database_url(project_dir)?;
    }

    let mut command = Command::new("sea-orm-cli");
    command
        .arg("migrate")
        .arg(&args.action)
        .current_dir(project_dir);

    if !args.args.is_empty() {
        command.args(&args.args);
    }

    if !args.no_env {
        if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
            command.envs(env_map);
        }
    }

    print_info(&format!("Running sea-orm-cli migrate {}...", args.action));
    let status = command
        .status()
        .context("Failed to run sea-orm-cli (is it installed?)")?;

    if status.success() {
        print_success("Migrations completed");
        Ok(())
    } else {
        Err(anyhow::anyhow!("sea-orm-cli exited with status {}", status))
    }
}

fn ensure_database_url(project_dir: &Path) -> Result<()> {
    let env_map = read_env_map(&project_dir.join(".env"));
    let Some(value) = resolve_database_url(&env_map) else {
        return Err(anyhow::anyhow!(
            "{}",
            error_contract(
                "DATABASE_URL is missing.",
                "Set DATABASE_URL in `.env` and rerun `tideway migrate`.",
                &format!(
                    "Run {} to bootstrap `.env` from `.env.example`.",
                    DEV_FIX_ENV_COMMAND
                )
            )
        ));
    };

    if let Err(err) = validate_database_url(&value) {
        return Err(anyhow::anyhow!(
            "{}",
            error_contract(
                &err.to_string(),
                "Use a URL like `postgres://...` or `sqlite:...`.",
                "Regenerate config with `tideway doctor --fix` and update DATABASE_URL."
            )
        ));
    }

    Ok(())
}

fn init_sea_orm_migration(project_dir: &Path) -> Result<()> {
    let migration_root = project_dir.join("migration");
    if migration_root.exists() {
        print_warning("migration/ already exists; skipping init");
        return Ok(());
    }

    let mut command = Command::new("sea-orm-cli");
    command.arg("migrate").arg("init").current_dir(project_dir);

    if let Some(env_map) = read_env_map(&project_dir.join(".env")) {
        command.envs(env_map);
    }

    print_info("Initializing SeaORM migration crate...");
    let status = command
        .status()
        .context("Failed to run sea-orm-cli (is it installed?)")?;

    if status.success() {
        print_success("Migration crate initialized");
        Ok(())
    } else {
        Err(anyhow::anyhow!("sea-orm-cli exited with status {}", status))
    }
}
