use clap::{Args, Parser, Subcommand};
use regex::Regex;
use serde::Deserialize;
use std::collections::{BTreeSet, HashMap, HashSet};
use std::env;
use std::error::Error;
use std::ffi::OsStr;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::thread;
use std::time::{Duration, UNIX_EPOCH};

type AnyError = Box<dyn Error>;
type Result<T> = std::result::Result<T, AnyError>;

fn main() {
    if let Err(err) = run_cli(Cli::parse()) {
        eprintln!("{err}");
        std::process::exit(1);
    }
}

#[derive(Parser)]
#[command(name = "exe", about = "ExecuteSoft repository tooling")]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    Service {
        #[command(subcommand)]
        command: ServiceCommand,
    },
    Gateway {
        #[command(subcommand)]
        command: GatewayCommand,
    },
    Db {
        #[command(subcommand)]
        command: DbCommand,
    },
    Dev {
        #[command(subcommand)]
        command: DevCommand,
    },
    Sync,
    Release(KeyValueArgs),
    Deploy {
        #[command(subcommand)]
        command: DeployCommand,
    },
}

#[derive(Subcommand)]
enum ServiceCommand {
    Check { path: String },
    CheckAll,
    Create(CreateServiceArgs),
    Test(ServiceTargetArgs),
    Build(ServiceTargetArgs),
    Generate(ServiceTargetArgs),
    Run(ServiceTargetArgs),
    Clippy(ServiceTargetArgs),
    Compliance(ServiceTargetArgs),
    GatewayRoutesCheck(ServiceTargetArgs),
}

#[derive(Args)]
struct CreateServiceArgs {
    #[arg(long, alias = "service")]
    name: String,
    #[arg(long, default_value = "core")]
    domain: String,
    #[arg(long = "template", alias = "tpl", default_value = "go")]
    template_lang: String,
    #[arg(long = "type", default_value = "grpc")]
    service_type: String,
    #[arg(long, default_value = "native")]
    framework: String,
    #[arg(long)]
    owner_team: Option<String>,
    #[arg(long)]
    output: Option<String>,
    #[arg(long)]
    template_name: Option<String>,
    #[arg(long)]
    proto_package: Option<String>,
    #[arg(long)]
    service_class: Option<String>,
}

#[derive(Args)]
struct ServiceTargetArgs {
    target: String,
    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    vars: Vec<String>,
}

#[derive(Subcommand)]
enum GatewayCommand {
    Generate,
    Route {
        #[command(subcommand)]
        command: GatewayRouteCommand,
    },
    ValidateRoutes {
        files: Vec<String>,
    },
}

#[derive(Subcommand)]
enum GatewayRouteCommand {
    Create(GatewayRouteArgs),
    FromProto(GatewayRouteArgs),
    Entry(GatewayRouteEntryArgs),
}

#[derive(Args)]
struct GatewayRouteArgs {
    #[arg(long, alias = "name")]
    service: String,
    #[arg(long)]
    force: bool,
    #[arg(long)]
    append: bool,
}

#[derive(Args)]
struct GatewayRouteEntryArgs {
    #[arg(long, alias = "name")]
    service: String,
    #[arg(long)]
    rpc: String,
    #[arg(long)]
    force: bool,
    #[arg(long)]
    append: bool,
}

#[derive(Subcommand)]
enum DbCommand {
    Migrate(KeyValueArgs),
    Up(KeyValueArgs),
    Status(KeyValueArgs),
    Revert(KeyValueArgs),
    Down(KeyValueArgs),
    Reset(KeyValueArgs),
    New(KeyValueArgs),
}

#[derive(Subcommand)]
enum DevCommand {
    Up(KeyValueArgs),
    All(KeyValueArgs),
    Build(KeyValueArgs),
    Down(KeyValueArgs),
    Logs(KeyValueArgs),
    Reset(KeyValueArgs),
    Watch(WatchArgs),
}

#[derive(Args)]
struct WatchArgs {
    #[arg(long, default_value = "configs/app.env.example")]
    env_file: String,
    #[arg(long = "watch", default_value = ".")]
    watch_root: String,
    #[arg(long)]
    ignore: Vec<String>,
    #[arg(long, default_value_t = 1.0)]
    poll: f64,
    #[arg(last = true, required = true)]
    command: Vec<String>,
}

#[derive(Subcommand)]
enum DeployCommand {
    Kubernetes(KeyValueArgs),
    Docker(KeyValueArgs),
}

#[derive(Args, Clone)]
struct KeyValueArgs {
    #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
    vars: Vec<String>,
}

fn run_cli(cli: Cli) -> Result<()> {
    match cli.command {
        Commands::Service { command } => run_service_command(command),
        Commands::Gateway { command } => run_gateway_command(command),
        Commands::Db { command } => run_db_command(command),
        Commands::Dev { command } => run_dev_command(command),
        Commands::Sync => run_sync(),
        Commands::Release(args) => run_release(&args.vars),
        Commands::Deploy { command } => run_deploy_command(command),
    }
}

fn usage_error<T>(message: String) -> Result<T> {
    Err(message.into())
}

fn repo_root() -> PathBuf {
    if let Ok(root) = env::var("EXECUTESOFT_ROOT") {
        return PathBuf::from(root);
    }
    let mut dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    loop {
        if dir.join("contracts/protobuf").exists() && dir.join("services").exists() {
            return dir;
        }
        if !dir.pop() {
            return PathBuf::from(".");
        }
    }
}

fn repo_path(path: &str) -> PathBuf {
    let raw = PathBuf::from(path);
    if raw.is_absolute() || raw.exists() {
        return raw;
    }
    let from_root = repo_root().join(path);
    if from_root.exists() {
        return from_root;
    }
    raw
}

fn run_cmd(dir: &Path, program: &str, args: &[String]) -> Result<()> {
    let status = Command::new(program)
        .args(args)
        .current_dir(dir)
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()?;
    if status.success() {
        Ok(())
    } else {
        Err(format!("{program} exited with {status}").into())
    }
}

fn run_make(dir: &Path, target: &str, vars: &[String]) -> Result<()> {
    let mut args = vec![target.to_string()];
    args.extend(vars.iter().cloned());
    run_cmd(dir, "make", &args)
}

fn run_service_command(command: ServiceCommand) -> Result<()> {
    match command {
        ServiceCommand::Check { path } => check_service(&repo_path(&path)),
        ServiceCommand::CheckAll => check_all_services(),
        ServiceCommand::Create(args) => create_service(args),
        ServiceCommand::Test(args) => run_service_make("test", args),
        ServiceCommand::Build(args) => run_service_make("build", args),
        ServiceCommand::Generate(args) => run_service_make("generate", args),
        ServiceCommand::Run(args) => run_service_make("run", args),
        ServiceCommand::Clippy(args) => run_service_make("clippy", args),
        ServiceCommand::Compliance(args) => run_service_make("compliance", args),
        ServiceCommand::GatewayRoutesCheck(args) => run_service_make("gateway-routes-check", args),
    }
}

fn run_service_make(target: &str, args: ServiceTargetArgs) -> Result<()> {
    let dir = resolve_service_dir(&args.target)?;
    run_make(&dir, target, &make_vars(&args.vars))
}

fn create_service(args: CreateServiceArgs) -> Result<()> {
    let name = args.name;
    let domain = args.domain;
    let template_lang = args.template_lang;
    let service_type = args.service_type;
    let framework = args.framework;
    let owner_team = args.owner_team.unwrap_or_else(|| format!("team-{name}"));
    let output = args
        .output
        .unwrap_or_else(|| format!("services/{domain}/{name}"));
    let template = format!("tools/templates/template-{template_lang}-{service_type}");
    let root = repo_root();
    let template_dir = root.join(&template);
    if !template_dir.is_dir() {
        return usage_error(format!("Template not found: {template}"));
    }
    let output_dir = repo_path_for_create(&root, &output);
    let template_name = args.template_name.unwrap_or_else(|| {
        template_dir
            .file_name()
            .and_then(OsStr::to_str)
            .unwrap_or("template")
            .to_string()
    });
    let proto_package = args
        .proto_package
        .unwrap_or_else(|| package_from_name(&domain, &name));
    let service_class = args.service_class.unwrap_or_else(|| class_from_name(&name));
    create_service_from_template(CreateServiceSpec {
        root: &root,
        template_dir: &template_dir,
        output_dir: &output_dir,
        service_name: &name,
        domain: &domain,
        owner_team: &owner_team,
        framework: &framework,
        template_name: &template_name,
        proto_package: &proto_package,
        service_class: &service_class,
    })?;
    check_service(&output_dir)
}

fn repo_path_for_create(root: &Path, value: &str) -> PathBuf {
    let path = PathBuf::from(value);
    if path.is_absolute() {
        path
    } else {
        root.join(path)
    }
}

struct CreateServiceSpec<'a> {
    root: &'a Path,
    template_dir: &'a Path,
    output_dir: &'a Path,
    service_name: &'a str,
    domain: &'a str,
    owner_team: &'a str,
    framework: &'a str,
    template_name: &'a str,
    proto_package: &'a str,
    service_class: &'a str,
}

fn create_service_from_template(spec: CreateServiceSpec<'_>) -> Result<()> {
    let framework_skeleton = spec
        .template_dir
        .join("skeleton-frameworks")
        .join(spec.framework);
    let skeleton = if spec.framework != "native" && framework_skeleton.is_dir() {
        framework_skeleton
    } else {
        spec.template_dir.join("skeleton")
    };
    if !skeleton.is_dir() {
        return usage_error(format!(
            "template skeleton does not exist: {}",
            skeleton.display()
        ));
    }
    if spec.output_dir.exists() {
        return usage_error(format!(
            "output already exists: {}",
            spec.output_dir.display()
        ));
    }

    copy_template_dir(&skeleton, spec.output_dir)?;
    replace_template_tokens(spec.output_dir, &spec)?;
    let local_contract = spec.output_dir.join("api/service.proto");
    let shared_contract = spec.root.join(format!(
        "contracts/protobuf/{}-{}.proto",
        spec.domain, spec.service_name
    ));
    if local_contract.is_file() {
        if !shared_contract.exists() {
            if let Some(parent) = shared_contract.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(&local_contract, &shared_contract)?;
        }
        println!("created shared contract {}", shared_contract.display());
    }
    println!("created {}", spec.output_dir.display());
    Ok(())
}

fn copy_template_dir(source: &Path, destination: &Path) -> Result<()> {
    fs::create_dir_all(destination)?;
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let path = entry.path();
        let name = entry.file_name();
        let name_str = name.to_string_lossy();
        if ["target", "__pycache__", ".pytest_cache"].contains(&name_str.as_ref()) {
            continue;
        }
        let target = destination.join(name);
        if path.is_dir() {
            copy_template_dir(&path, &target)?;
        } else if path.is_file() {
            fs::copy(&path, &target)?;
        }
    }
    Ok(())
}

fn replace_template_tokens(root: &Path, spec: &CreateServiceSpec<'_>) -> Result<()> {
    let service_title = title_from_name(spec.service_name);
    let service_title_lc = service_title.to_lowercase();
    let crate_name = crate_from_name(spec.service_name);
    let replacements = [
        ("__SERVICE_NAME__", spec.service_name.to_string()),
        ("__SERVICE_TITLE__", service_title),
        ("__SERVICE_TITLE_LC__", service_title_lc),
        ("__DOMAIN__", spec.domain.to_string()),
        ("__OWNER_TEAM__", spec.owner_team.to_string()),
        ("__FRAMEWORK__", spec.framework.to_string()),
        ("__TEMPLATE_NAME__", spec.template_name.to_string()),
        ("__PROTO_PACKAGE__", spec.proto_package.to_string()),
        ("__SERVICE_CLASS__", spec.service_class.to_string()),
        ("template_service", crate_name),
        ("template-service", spec.service_name.to_string()),
    ];
    replace_template_tokens_walk(root, &replacements)
}

fn replace_template_tokens_walk(root: &Path, replacements: &[(&str, String)]) -> Result<()> {
    for entry in fs::read_dir(root)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            replace_template_tokens_walk(&path, replacements)?;
            continue;
        }
        if !path.is_file() || !is_text_file(&path)? {
            continue;
        }
        let mut text = fs::read_to_string(&path)?;
        for (needle, replacement) in replacements {
            text = text.replace(needle, replacement);
        }
        fs::write(&path, text)?;
    }
    Ok(())
}

fn is_text_file(path: &Path) -> Result<bool> {
    let mut file = fs::File::open(path)?;
    let mut buf = Vec::new();
    file.read_to_end(&mut buf)?;
    Ok(!buf.contains(&0) && std::str::from_utf8(&buf).is_ok())
}

fn title_from_name(value: &str) -> String {
    value
        .split(['-', '_'])
        .filter(|part| !part.is_empty())
        .map(capitalize)
        .collect::<Vec<_>>()
        .join(" ")
}

fn class_from_name(value: &str) -> String {
    value
        .split(['-', '_', '.'])
        .filter(|part| !part.is_empty())
        .map(capitalize)
        .collect::<String>()
}

fn package_from_name(domain: &str, service_name: &str) -> String {
    format!(
        "executesoft.{}.{}.v1",
        domain.replace(['-', '_'], "."),
        service_name.replace(['-', '_'], ".")
    )
}

fn crate_from_name(service_name: &str) -> String {
    service_name.replace(['-', '.'], "_")
}

fn capitalize(value: &str) -> String {
    let mut chars = value.chars();
    match chars.next() {
        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
        None => String::new(),
    }
}

fn check_all_services() -> Result<()> {
    let root = repo_root();
    let mut failed = false;
    let mut service_yamls = Vec::new();
    collect_template_service_yamls(&root.join("tools/templates"), &mut service_yamls)?;
    collect_service_root_yamls(&root.join("services"), &mut service_yamls)?;
    service_yamls.sort();
    service_yamls.dedup();
    for service_yaml in service_yamls {
        if let Some(dir) = service_yaml.parent()
            && let Err(err) = check_service(dir)
        {
            failed = true;
            eprintln!("{err}");
        }
    }
    if failed {
        Err("service compliance failed".into())
    } else {
        Ok(())
    }
}

fn collect_template_service_yamls(templates_dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
    if !templates_dir.exists() {
        return Ok(());
    }
    for entry in fs::read_dir(templates_dir)? {
        let path = entry?.path();
        if path.is_dir() {
            let candidate = path.join("skeleton/service.yaml");
            if candidate.exists() {
                out.push(candidate);
            }
        }
    }
    Ok(())
}

fn collect_service_root_yamls(services_dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
    if !services_dir.exists() {
        return Ok(());
    }
    for domain in fs::read_dir(services_dir)? {
        let domain = domain?.path();
        if !domain.is_dir() {
            continue;
        }
        let direct = domain.join("service.yaml");
        if direct.exists() {
            out.push(direct);
        }
        for service in fs::read_dir(&domain)? {
            let service = service?.path();
            if !service.is_dir() {
                continue;
            }
            let candidate = service.join("service.yaml");
            if candidate.exists() {
                out.push(candidate);
            }
        }
    }
    Ok(())
}

fn check_service(service_dir: &Path) -> Result<()> {
    let service_dir = service_dir
        .canonicalize()
        .unwrap_or_else(|_| service_dir.to_path_buf());
    let failures = validate_service(&service_dir);
    if failures.is_empty() {
        println!("OK {}", service_dir.display());
        Ok(())
    } else {
        for failure in &failures {
            eprintln!("FAIL {}: {failure}", service_dir.display());
        }
        Err("service validation failed".into())
    }
}

fn validate_service(service_dir: &Path) -> Vec<String> {
    let mut failures = Vec::new();
    if !service_dir.is_dir() {
        return vec!["service directory does not exist".into()];
    }
    let metadata_path = service_dir.join("service.yaml");
    if !metadata_path.exists() {
        return vec!["service.yaml missing".into()];
    }
    let meta = parse_simple_yaml(&metadata_path);
    for key in [
        "name",
        "domain",
        "owner_team",
        "runtime",
        "framework",
        "template",
        "certified_template",
        "api_type",
    ] {
        if meta
            .get(key)
            .map(String::as_str)
            .unwrap_or("")
            .trim()
            .is_empty()
        {
            failures.push(format!("service.yaml missing metadata key: {key}"));
        }
    }
    if !failures.is_empty() {
        return failures;
    }

    let name = meta.get("name").map(String::as_str).unwrap_or("");
    let domain = meta.get("domain").map(String::as_str).unwrap_or("");
    let runtime = meta.get("runtime").map(String::as_str).unwrap_or("");
    let api_type = meta.get("api_type").map(String::as_str).unwrap_or("");
    let is_gateway = domain == "core" && name == "gateway";
    let is_cdn = domain == "core" && name == "cdn";

    let allowed: HashSet<&str> = [
        "api",
        "cmd",
        "configs",
        "deploy",
        "development",
        "docs",
        "gateway",
        "migrations",
        "observability",
        "scripts",
        "internal",
        "seeds",
        "src",
        "tasks",
        "tests",
        "tools",
        ".agents",
        ".dockerignore",
        "AGENTS.md",
        "README.md",
        "go.mod",
        "go.sum",
        "package.json",
        "package-lock.json",
        "tsconfig.json",
        "pyproject.toml",
        "Cargo.toml",
        "Cargo.lock",
        "build.rs",
        "Dockerfile",
        "Makefile",
        "service.yaml",
        "taskfile.yaml",
    ]
    .into_iter()
    .collect();
    let ignored: HashSet<&str> = [
        ".git",
        ".pytest_cache",
        "__pycache__",
        "node_modules",
        "target",
        ".DS_Store",
    ]
    .into_iter()
    .collect();
    if let Ok(entries) = fs::read_dir(service_dir) {
        for entry in entries.flatten() {
            let name = entry.file_name();
            let name = name.to_string_lossy();
            if !ignored.contains(name.as_ref()) && !allowed.contains(name.as_ref()) {
                failures.push(format!("top-level path is not allowed: {name}"));
            }
        }
    }

    if is_cdn {
        require_files(
            service_dir,
            &mut failures,
            &["Cargo.toml", "src/main.rs", "configs/cdn.toml"],
        );
        require_text(service_dir, &mut failures, "Dockerfile", &["EXPOSE 8090"]);
        require_text(
            service_dir,
            &mut failures,
            "README.md",
            &["CDN Service", "DigitalOcean Spaces"],
        );
        return failures;
    }

    require_dirs(
        service_dir,
        &mut failures,
        &[
            "api",
            "configs",
            "deploy",
            "development",
            "docs",
            "gateway",
            "gateway/routes",
            "migrations",
            "observability",
            "tasks",
            "tests",
            "docs/Overview",
            "docs/Architecture",
            "docs/Setup",
            "docs/API",
            "docs/Tasks",
            "docs/Bugs",
            "docs/Decisions",
            "docs/Changelog",
            "tests/contract",
        ],
    );
    require_files(
        service_dir,
        &mut failures,
        &[
            "AGENTS.md",
            "Dockerfile",
            "service.yaml",
            ".agents/skills/service-structure/SKILL.md",
            "docs/Overview/README.md",
            "docs/API/api.md",
            "docs/API/api-development.md",
            "docs/API/api-errors.md",
            "docs/Setup/developer-onboarding.md",
            "docs/Setup/runbook.md",
            "tasks/README.md",
            "configs/app.env.example",
            "development/README.md",
            "development/compose.dev.yml",
            "gateway/README.md",
            "deploy/deployment.yaml",
            "deploy/service.yaml",
            "deploy/hpa.yaml",
            "deploy/pdb.yaml",
            "deploy/network-policy.yaml",
            "deploy/service-account.yaml",
            "deploy/configmap.yaml",
            "deploy/external-secret.yaml",
            "observability/alerts.yaml",
            "observability/dashboard.md",
        ],
    );
    if !has_matching_file(&service_dir.join("gateway/routes"), "-routes.json") {
        failures.push("gateway route request missing: gateway/routes/*-routes.json".into());
    }
    if !service_dir.join("Makefile").exists() && !service_dir.join("taskfile.yaml").exists() {
        failures
            .push("one required file missing from alternatives: Makefile, taskfile.yaml".into());
    }
    if !service_dir.join("src").exists() && !service_dir.join("cmd").exists() {
        failures.push("one application directory is required: src, cmd".into());
    }

    if is_gateway {
        match runtime {
            "go" => {
                require_dirs(
                    service_dir,
                    &mut failures,
                    &[
                        "internal/bootstrap",
                        "internal/infrastructure/config",
                        "internal/infrastructure/logger",
                        "internal/interfaces/http",
                    ],
                );
                require_files(
                    service_dir,
                    &mut failures,
                    &[
                        "internal/bootstrap/container.go",
                        "internal/bootstrap/infrastructure.go",
                        "internal/bootstrap/handlers.go",
                        "internal/bootstrap/server.go",
                    ],
                );
            }
            "rust" => {
                require_dirs(
                    service_dir,
                    &mut failures,
                    &[
                        "src/bootstrap",
                        "src/infrastructure/config",
                        "src/infrastructure/logger",
                        "src/interfaces/http",
                    ],
                );
                require_files(
                    service_dir,
                    &mut failures,
                    &[
                        "Cargo.toml",
                        "src/bootstrap/container.rs",
                        "src/bootstrap/infrastructure.rs",
                        "src/bootstrap/handlers.rs",
                        "src/bootstrap/server.rs",
                        "src/bootstrap/mod.rs",
                        "src/main.rs",
                    ],
                );
            }
            _ => {}
        }
        require_files(
            service_dir,
            &mut failures,
            &[
                "api/routes.yaml",
                "api/public-routes.json",
                "api/openapi.yaml",
                "docs/Architecture/public-api.md",
            ],
        );
        require_text(
            service_dir,
            &mut failures,
            "api/openapi.yaml",
            &[
                "openapi: 3.1.0",
                "bearerAuth:",
                "x-target-service:",
                "x-target-rpc:",
            ],
        );
    } else {
        match runtime {
            "go" => {
                require_dirs(
                    service_dir,
                    &mut failures,
                    &[
                        "internal/domain",
                        "internal/application",
                        "internal/infrastructure",
                        "internal/infrastructure/cache",
                        "internal/infrastructure/persistence",
                        "internal/infrastructure/persistence/database",
                        "internal/interfaces",
                        "internal/bootstrap",
                    ],
                );
                require_files(
                    service_dir,
                    &mut failures,
                    &[
                        "go.mod",
                        "internal/bootstrap/container.go",
                        "internal/bootstrap/infrastructure.go",
                        "internal/bootstrap/repositories.go",
                        "internal/bootstrap/use_cases.go",
                        "internal/bootstrap/handlers.go",
                        "internal/bootstrap/server.go",
                        "cmd/server/main.go",
                    ],
                );
            }
            "typescript" => {
                require_dirs(
                    service_dir,
                    &mut failures,
                    &[
                        "src/domain",
                        "src/application",
                        "src/infrastructure",
                        "src/infrastructure/cache",
                        "src/infrastructure/persistence",
                        "src/infrastructure/persistence/database",
                        "src/interfaces",
                        "src/bootstrap",
                    ],
                );
                require_files(
                    service_dir,
                    &mut failures,
                    &[
                        "package.json",
                        "src/bootstrap/container.ts",
                        "src/bootstrap/infrastructure.ts",
                        "src/bootstrap/repositories.ts",
                        "src/bootstrap/use-cases.ts",
                        "src/bootstrap/handlers.ts",
                        "src/bootstrap/server.ts",
                        "src/server.ts",
                    ],
                );
            }
            "python" => {
                require_dirs(
                    service_dir,
                    &mut failures,
                    &[
                        "src/domain",
                        "src/application",
                        "src/infrastructure",
                        "src/infrastructure/cache",
                        "src/infrastructure/persistence",
                        "src/infrastructure/persistence/database",
                        "src/interfaces",
                        "src/bootstrap",
                    ],
                );
                require_files(
                    service_dir,
                    &mut failures,
                    &[
                        "pyproject.toml",
                        "src/bootstrap/container.py",
                        "src/bootstrap/infrastructure.py",
                        "src/bootstrap/repositories.py",
                        "src/bootstrap/use_cases.py",
                        "src/bootstrap/handlers.py",
                        "src/bootstrap/server.py",
                        "src/server.py",
                    ],
                );
            }
            "rust" => {
                require_dirs(
                    service_dir,
                    &mut failures,
                    &[
                        "src/domain",
                        "src/application",
                        "src/infrastructure",
                        "src/infrastructure/cache",
                        "src/infrastructure/persistence",
                        "src/infrastructure/persistence/database",
                        "src/interfaces",
                        "src/bootstrap",
                    ],
                );
                require_files(
                    service_dir,
                    &mut failures,
                    &[
                        "Cargo.toml",
                        "src/domain/model.rs",
                        "src/bootstrap/container.rs",
                        "src/bootstrap/infrastructure.rs",
                        "src/bootstrap/repositories.rs",
                        "src/bootstrap/use_cases.rs",
                        "src/bootstrap/handlers.rs",
                        "src/bootstrap/server.rs",
                        "src/bootstrap/mod.rs",
                        "src/main.rs",
                    ],
                );
            }
            _ => {}
        }
    }

    for (key, value) in parse_exposes(&metadata_path) {
        if value.is_empty() {
            failures.push(format!("service.yaml exposes.{key} must be a path string"));
        } else if !service_dir.join(&value).exists() {
            failures.push(format!("service.yaml exposes path missing: {key}: {value}"));
        }
    }

    match api_type {
        "grpc" => {
            if !has_matching_file(&service_dir.join("api"), ".proto") {
                failures.push("required grpc contract glob missing: one of api/*.proto".into());
            }
            if !parse_exposes(&metadata_path).contains_key("protobuf") {
                failures.push("service.yaml exposes must include key: protobuf".into());
            }
            let shared = repo_root().join(format!("contracts/protobuf/{domain}-{name}.proto"));
            if !metadata_has_placeholder(&metadata_path) && !shared.exists() {
                failures.push(format!(
                    "shared contract missing: contracts/protobuf/{domain}-{name}.proto"
                ));
            }
            require_text(service_dir, &mut failures, "Dockerfile", &["EXPOSE"]);
            require_text(
                service_dir,
                &mut failures,
                "deploy/deployment.yaml",
                &["containerPort:", "name: grpc"],
            );
            require_text(
                service_dir,
                &mut failures,
                "deploy/service.yaml",
                &["port:", "name: grpc"],
            );
            require_text(
                service_dir,
                &mut failures,
                "docs/API/api.md",
                &[
                    "Local contract:",
                    "Shared contract:",
                    "gRPC services:",
                    "RPC methods:",
                ],
            );
        }
        "rest" => {
            if !has_matching_file(&service_dir.join("api"), ".yaml")
                && !has_matching_file(&service_dir.join("api"), ".yml")
            {
                failures.push(
                    "required rest contract glob missing: one of api/*.yaml, api/*.yml".into(),
                );
            }
            if !parse_exposes(&metadata_path).contains_key("openapi") {
                failures.push("service.yaml exposes must include key: openapi".into());
            }
        }
        _ => {}
    }

    if metadata_has_placeholder(&metadata_path) {
        require_text(
            service_dir,
            &mut failures,
            "configs/app.env.example",
            &[
                "LOG_LEVEL=",
                "LOG_FORMAT=json",
                "SERVICE_DOMAIN=",
                "SERVICE_ENVIRONMENT=",
                "DB_DRIVER=",
                "DATABASE_URL=",
                "CACHE_DRIVER=",
                "CACHE_URL=",
                "CACHE_PREFIX=",
                "OTEL_SERVICE_NAME=",
                "OTEL_EXPORTER_OTLP_ENDPOINT=",
                "OTEL_TRACES_EXPORTER=otlp",
                "OTEL_METRICS_EXPORTER=otlp",
                "OTEL_LOGS_EXPORTER=otlp",
            ],
        );
    }
    require_text(
        service_dir,
        &mut failures,
        "deploy/configmap.yaml",
        &[
            "DB_DRIVER:",
            "CACHE_DRIVER:",
            "CACHE_PREFIX:",
            "SERVICE_DOMAIN:",
            "SERVICE_ENVIRONMENT:",
            "LOG_FORMAT:",
        ],
    );
    require_text(
        service_dir,
        &mut failures,
        "deploy/deployment.yaml",
        &[
            "envFrom:",
            "configMapRef:",
            "secretRef:",
            "-runtime-secrets",
        ],
    );
    require_text(
        service_dir,
        &mut failures,
        "docs/API/api-errors.md",
        &[
            "contracts/protobuf/common/error.proto",
            "ErrorResponse",
            "INVALID_ARGUMENT",
            "UNAUTHENTICATED",
            "PERMISSION_DENIED",
            "NOT_FOUND",
            "INTERNAL",
        ],
    );
    require_text(
        service_dir,
        &mut failures,
        "docs/API/api.md",
        &[
            "Owner team:",
            "Protocol:",
            "Development endpoint:",
            "Auth",
            "Error Model",
            "Consumer Workflow",
            "Do not use another team's source code",
        ],
    );
    require_text(
        service_dir,
        &mut failures,
        "docs/API/api-development.md",
        &[
            "New API Development Process",
            "gateway-routes.json",
            "make generate",
            "Do not manually edit",
            "Golden Rules",
        ],
    );
    if !is_gateway && meta.get("certified_template").map(String::as_str) != Some("true") {
        failures.push("service.yaml certified_template must be true".into());
    }
    failures
}

fn require_files(root: &Path, failures: &mut Vec<String>, files: &[&str]) {
    for file in files {
        if !root.join(file).is_file() {
            failures.push(format!("required file missing: {file}"));
        }
    }
}

fn require_dirs(root: &Path, failures: &mut Vec<String>, dirs: &[&str]) {
    for dir in dirs {
        if !root.join(dir).is_dir() {
            failures.push(format!("required directory missing: {dir}"));
        }
    }
}

fn require_text(root: &Path, failures: &mut Vec<String>, file: &str, snippets: &[&str]) {
    let path = root.join(file);
    let Ok(text) = fs::read_to_string(&path) else {
        failures.push(format!("required text file missing: {file}"));
        return;
    };
    for snippet in snippets {
        if !text.contains(snippet) {
            failures.push(format!("{file} missing required text: {snippet}"));
        }
    }
}

fn has_matching_file(dir: &Path, suffix: &str) -> bool {
    let Ok(entries) = fs::read_dir(dir) else {
        return false;
    };
    entries.flatten().any(|entry| {
        entry.path().is_file() && entry.file_name().to_string_lossy().ends_with(suffix)
    })
}

fn parse_simple_yaml(path: &Path) -> HashMap<String, String> {
    let text = fs::read_to_string(path).unwrap_or_default();
    let mut values = HashMap::new();
    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty()
            || trimmed.starts_with('#')
            || line.starts_with(' ')
            || line.starts_with('\t')
        {
            continue;
        }
        if let Some((key, value)) = line.split_once(':') {
            values.insert(
                key.trim().to_string(),
                value.trim().trim_matches(['"', '\'']).to_string(),
            );
        }
    }
    values
}

fn parse_exposes(path: &Path) -> HashMap<String, String> {
    let text = fs::read_to_string(path).unwrap_or_default();
    let mut out = HashMap::new();
    let mut in_exposes = false;
    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed == "exposes:" {
            in_exposes = true;
            continue;
        }
        if in_exposes && !trimmed.is_empty() && !line.starts_with(' ') && !line.starts_with('\t') {
            break;
        }
        if in_exposes
            && trimmed.starts_with("- ")
            && let Some((key, value)) = trimmed.trim_start_matches("- ").split_once(':')
        {
            out.insert(
                key.trim().to_string(),
                value.trim().trim_matches(['"', '\'']).to_string(),
            );
        }
    }
    out
}

fn metadata_has_placeholder(path: &Path) -> bool {
    parse_simple_yaml(path)
        .values()
        .any(|value| value.starts_with("__"))
}

fn resolve_service_dir(value: &str) -> Result<PathBuf> {
    if value.contains('/') || value.starts_with('.') {
        let path = repo_path(value);
        if !path.join("Makefile").exists() {
            return usage_error(format!("service Makefile not found: {value}"));
        }
        return Ok(path);
    }
    let mut matches = BTreeSet::new();
    let mut service_yamls = Vec::new();
    collect_service_root_yamls(&repo_root().join("services"), &mut service_yamls)?;
    for service_yaml in service_yamls {
        if let Some(dir) = service_yaml.parent()
            && dir.file_name().and_then(OsStr::to_str) == Some(value)
        {
            matches.insert(dir.to_path_buf());
        }
    }
    match matches.len() {
        0 => usage_error(format!("service not found: {value}")),
        1 => Ok(matches.into_iter().next().unwrap()),
        _ => usage_error(format!("service name is ambiguous: {value}")),
    }
}

fn run_gateway_command(command: GatewayCommand) -> Result<()> {
    match command {
        GatewayCommand::Generate => {
            run_make(&repo_root().join("services/core/gateway"), "generate", &[])
        }
        GatewayCommand::Route { command } => run_gateway_route_command(command),
        GatewayCommand::ValidateRoutes { files } => validate_gateway_routes(&files),
    }
}

fn run_gateway_route_command(command: GatewayRouteCommand) -> Result<()> {
    match command {
        GatewayRouteCommand::Create(args) => {
            run_gateway_route_make("route-file", args.service, None, args.force, args.append)
        }
        GatewayRouteCommand::FromProto(args) => run_gateway_route_make(
            "route-file-from-proto",
            args.service,
            None,
            args.force,
            args.append,
        ),
        GatewayRouteCommand::Entry(args) => run_gateway_route_make(
            "route-entry",
            args.service,
            Some(args.rpc),
            args.force,
            args.append,
        ),
    }
}

fn run_gateway_route_make(
    target: &str,
    service: String,
    rpc: Option<String>,
    force: bool,
    append: bool,
) -> Result<()> {
    let mut vars = vec![format!("SERVICE={service}")];
    if let Some(rpc) = rpc {
        vars.push(format!("RPC={rpc}"));
    }
    if force {
        vars.push("FORCE=true".into());
    }
    if append {
        vars.push("APPEND=true".into());
    }
    run_make(&repo_root().join("services/core/gateway"), target, &vars)
}

#[derive(Deserialize)]
struct RouteModule {
    version: i64,
    routes: Vec<RouteEntry>,
}

#[derive(Deserialize)]
struct RouteEntry {
    id: String,
    public: PublicRoute,
    target: TargetRoute,
}

#[derive(Deserialize)]
struct PublicRoute {
    method: String,
    path: String,
    auth: String,
    #[serde(default)]
    permission: String,
}

#[derive(Deserialize)]
struct TargetRoute {
    protocol: String,
    proto: String,
    package: String,
    service: String,
    rpc: String,
    endpoint_env: String,
}

fn validate_gateway_routes(args: &[String]) -> Result<()> {
    let files: Vec<PathBuf> = if args.is_empty() {
        let routes_dir = env::current_dir()?.join("gateway/routes");
        fs::read_dir(routes_dir)?
            .flatten()
            .map(|entry| entry.path())
            .filter(|path| {
                path.file_name()
                    .and_then(OsStr::to_str)
                    .is_some_and(|name| name.ends_with("-routes.json"))
            })
            .collect()
    } else {
        args.iter().map(|arg| repo_path(arg)).collect()
    };
    if files.is_empty() {
        println!("No gateway route request files found.");
        return Ok(());
    }
    for file in &files {
        validate_gateway_route_file(file)?;
    }
    println!("OK gateway route request files: {}", files.len());
    Ok(())
}

fn validate_gateway_route_file(path: &Path) -> Result<()> {
    let text = fs::read_to_string(path)?;
    let module: RouteModule = serde_json::from_str(&text)?;
    if module.version != 1 {
        return usage_error(format!("{}: version must be 1", path.display()));
    }
    let mut seen = HashSet::new();
    for route in module.routes {
        if route.id.is_empty() {
            return usage_error(format!("{}: route id is required", path.display()));
        }
        if !seen.insert(route.id.clone()) {
            return usage_error(format!(
                "{}: duplicate route id: {}",
                path.display(),
                route.id
            ));
        }
        if !["GET", "POST", "PUT", "PATCH", "DELETE"].contains(&route.public.method.as_str()) {
            return usage_error(format!(
                "{}: {}.public.method is invalid",
                path.display(),
                route.id
            ));
        }
        if !route.public.path.starts_with('/') {
            return usage_error(format!(
                "{}: {}.public.path must start with /",
                path.display(),
                route.id
            ));
        }
        if route.public.auth != "public" && route.public.auth != "permission" {
            return usage_error(format!(
                "{}: {}.public.auth must be public or permission",
                path.display(),
                route.id
            ));
        }
        if route.public.auth == "permission" && route.public.permission.is_empty() {
            return usage_error(format!(
                "{}: {}.public.permission is required for permission auth",
                path.display(),
                route.id
            ));
        }
        if route.target.protocol != "grpc" {
            return usage_error(format!(
                "{}: {}.target.protocol must be grpc",
                path.display(),
                route.id
            ));
        }
        for (field, value) in [
            ("proto", route.target.proto),
            ("package", route.target.package),
            ("service", route.target.service),
            ("rpc", route.target.rpc),
            ("endpoint_env", route.target.endpoint_env),
        ] {
            if value.is_empty() {
                return usage_error(format!(
                    "{}: {}.target.{field} is required",
                    path.display(),
                    route.id
                ));
            }
        }
    }
    Ok(())
}

fn run_db_command(command: DbCommand) -> Result<()> {
    let (action, vars) = match command {
        DbCommand::Migrate(args) => ("migrate", args.vars),
        DbCommand::Up(args) => ("migrate", args.vars),
        DbCommand::Status(args) => ("status", args.vars),
        DbCommand::Revert(args) => ("revert", args.vars),
        DbCommand::Down(args) => ("revert", args.vars),
        DbCommand::Reset(args) => ("reset", args.vars),
        DbCommand::New(args) => ("new", args.vars),
    };
    for arg in vars {
        if let Some((key, value)) = arg.split_once('=') {
            unsafe { env::set_var(key, value) };
        }
    }
    let cfg = MigrationConfig::from_env();
    match action {
        "migrate" | "up" => apply_migrations(&cfg),
        "status" => migration_status(&cfg),
        "revert" | "down" => revert_migration(&cfg),
        "reset" => reset_database(&cfg),
        "new" => create_migration(&cfg.dir),
        other => usage_error(format!("unknown db command: {other}")),
    }
}

struct MigrationConfig {
    dir: PathBuf,
    container: String,
    user: String,
    database: String,
    schema: String,
    table: String,
}

impl MigrationConfig {
    fn from_env() -> Self {
        Self {
            dir: PathBuf::from(env::var("MIGRATIONS_DIR").unwrap_or_else(|_| "migrations".into())),
            container: env::var("MIGRATION_DB_CONTAINER")
                .unwrap_or_else(|_| "executesoft-dev-postgres-1".into()),
            user: env::var("MIGRATION_DB_USER").unwrap_or_else(|_| "executesoft".into()),
            database: env::var("MIGRATION_DB_NAME").unwrap_or_default(),
            schema: env::var("MIGRATION_SCHEMA").unwrap_or_else(|_| "public".into()),
            table: env::var("MIGRATION_TABLE").unwrap_or_else(|_| "schema_migrations".into()),
        }
    }
}

fn require_database(cfg: &MigrationConfig) -> Result<()> {
    if cfg.database.is_empty() {
        usage_error("MIGRATION_DB_NAME is required".into())
    } else {
        Ok(())
    }
}

fn docker_psql(
    cfg: &MigrationConfig,
    database: &str,
    extra: &[&str],
    stdin: Option<&str>,
) -> Result<String> {
    let mut cmd = Command::new("docker");
    cmd.args([
        "exec",
        "-i",
        &cfg.container,
        "psql",
        "-v",
        "ON_ERROR_STOP=1",
        "-U",
        &cfg.user,
        "-d",
        database,
    ]);
    cmd.args(extra);
    cmd.stderr(Stdio::inherit());
    if stdin.is_some() {
        cmd.stdin(Stdio::piped());
    }
    cmd.stdout(Stdio::piped());
    let mut child = cmd.spawn()?;
    if let Some(input) = stdin {
        child.stdin.as_mut().unwrap().write_all(input.as_bytes())?;
    }
    let output = child.wait_with_output()?;
    if !output.status.success() {
        return Err(format!("docker psql exited with {}", output.status).into());
    }
    Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

fn ensure_database(cfg: &MigrationConfig) -> Result<()> {
    require_database(cfg)?;
    let sql = "SELECT format('CREATE DATABASE %I', :'db_name') WHERE NOT EXISTS (SELECT 1 FROM pg_database WHERE datname = :'db_name')\\gexec\n".to_string();
    docker_psql(
        cfg,
        "postgres",
        &["-v", &format!("db_name={}", cfg.database)],
        Some(&sql),
    )?;
    Ok(())
}

fn psql(cfg: &MigrationConfig, extra: &[&str], stdin: Option<&str>) -> Result<String> {
    ensure_database(cfg)?;
    docker_psql(cfg, &cfg.database, extra, stdin)
}

fn ensure_tracking_table(cfg: &MigrationConfig) -> Result<()> {
    let sql = format!(
        "CREATE TABLE IF NOT EXISTS {}.{} (version TEXT PRIMARY KEY, filename TEXT NOT NULL, applied_at TIMESTAMPTZ NOT NULL DEFAULT now());",
        cfg.schema, cfg.table
    );
    psql(cfg, &[], Some(&sql)).map(|_| ())
}

fn migration_files(dir: &Path) -> Result<Vec<PathBuf>> {
    if !dir.exists() {
        println!("No migrations directory found: {}", dir.display());
        return Ok(Vec::new());
    }
    let mut files = Vec::new();
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        let path = entry.path();
        let name = entry.file_name().to_string_lossy().to_string();
        if path.is_file()
            && name.ends_with(".sql")
            && !name.ends_with(".down.sql")
            && !name.ends_with("_down.sql")
        {
            files.push(path);
        }
    }
    files.sort();
    Ok(files)
}

fn migration_version(path: &Path) -> String {
    path.file_stem()
        .unwrap_or_default()
        .to_string_lossy()
        .to_string()
}

fn apply_migrations(cfg: &MigrationConfig) -> Result<()> {
    let files = migration_files(&cfg.dir)?;
    if files.is_empty() {
        println!("No migration files found in {}.", cfg.dir.display());
        return Ok(());
    }
    ensure_tracking_table(cfg)?;
    let mut applied = 0;
    for file in files {
        let version = migration_version(&file);
        let exists = psql(
            cfg,
            &[
                "-At",
                "-c",
                &format!(
                    "SELECT EXISTS (SELECT 1 FROM {}.{} WHERE version = '{}');",
                    cfg.schema,
                    cfg.table,
                    sql_literal(&version)
                ),
            ],
            None,
        )?;
        if exists == "t" {
            println!("Skipping already applied migration: {version}");
            continue;
        }
        println!("Applying migration: {version}");
        let sql = fs::read_to_string(&file)?;
        psql(cfg, &[], Some(&sql))?;
        psql(
            cfg,
            &[
                "-c",
                &format!(
                    "INSERT INTO {}.{} (version, filename) VALUES ('{}', '{}');",
                    cfg.schema,
                    cfg.table,
                    sql_literal(&version),
                    sql_literal(file.file_name().unwrap().to_string_lossy().as_ref())
                ),
            ],
            None,
        )?;
        applied += 1;
    }
    if applied == 0 {
        println!("No pending migrations.");
    }
    Ok(())
}

fn migration_status(cfg: &MigrationConfig) -> Result<()> {
    let files = migration_files(&cfg.dir)?;
    if files.is_empty() {
        println!("No migration files found in {}.", cfg.dir.display());
        return Ok(());
    }
    ensure_tracking_table(cfg)?;
    for file in files {
        let version = migration_version(&file);
        let exists = psql(
            cfg,
            &[
                "-At",
                "-c",
                &format!(
                    "SELECT EXISTS (SELECT 1 FROM {}.{} WHERE version = '{}');",
                    cfg.schema,
                    cfg.table,
                    sql_literal(&version)
                ),
            ],
            None,
        )?;
        println!(
            "{}  {version}",
            if exists == "t" { "applied" } else { "pending" }
        );
    }
    Ok(())
}

fn down_file_for(dir: &Path, version: &str) -> Option<PathBuf> {
    [
        dir.join("down").join(format!("{version}.sql")),
        dir.join(format!("{version}.down.sql")),
        dir.join(format!("{version}_down.sql")),
    ]
    .into_iter()
    .find(|path| path.exists())
}

fn revert_migration(cfg: &MigrationConfig) -> Result<()> {
    ensure_tracking_table(cfg)?;
    let latest = psql(
        cfg,
        &[
            "-At",
            "-c",
            &format!(
                "SELECT version FROM {}.{} ORDER BY applied_at DESC, version DESC LIMIT 1;",
                cfg.schema, cfg.table
            ),
        ],
        None,
    )?;
    if latest.is_empty() {
        println!("No applied migrations to revert.");
        return Ok(());
    }
    let down = down_file_for(&cfg.dir, &latest)
        .ok_or_else(|| format!("No down migration found for {latest}"))?;
    println!("Reverting migration: {latest}");
    psql(cfg, &[], Some(&fs::read_to_string(down)?))?;
    psql(
        cfg,
        &[
            "-c",
            &format!(
                "DELETE FROM {}.{} WHERE version = '{}';",
                cfg.schema,
                cfg.table,
                sql_literal(&latest)
            ),
        ],
        None,
    )?;
    Ok(())
}

fn reset_database(cfg: &MigrationConfig) -> Result<()> {
    if env::var("CONFIRM").ok().as_deref() != Some("yes") {
        return usage_error("Refusing to reset database without CONFIRM=yes".into());
    }
    println!(
        "Resetting schema {} in database {}.",
        cfg.schema, cfg.database
    );
    psql(
        cfg,
        &[],
        Some(&format!(
            "DROP SCHEMA IF EXISTS {} CASCADE; CREATE SCHEMA {};",
            cfg.schema, cfg.schema
        )),
    )?;
    apply_migrations(cfg)
}

fn create_migration(dir: &Path) -> Result<()> {
    let name =
        env::var("NAME").map_err(|_| "NAME is required. Example: exe db new NAME=create_orders")?;
    fs::create_dir_all(dir.join("down"))?;
    let ts = Command::new("date").arg("+%Y%m%d%H%M%S").output()?;
    let timestamp = String::from_utf8_lossy(&ts.stdout).trim().to_string();
    let safe = Regex::new("[^a-z0-9_]+")?
        .replace_all(&name.to_lowercase(), "_")
        .trim_matches('_')
        .to_string();
    let version = format!("{timestamp}_{safe}");
    fs::write(
        dir.join(format!("{version}.sql")),
        "-- Add migration SQL here.\n",
    )?;
    fs::write(
        dir.join("down").join(format!("{version}.sql")),
        "-- Add rollback SQL here.\n",
    )?;
    println!("Created {}/{}.sql", dir.display(), version);
    println!("Created {}/down/{}.sql", dir.display(), version);
    Ok(())
}

fn sql_literal(value: &str) -> String {
    value.replace('\'', "''")
}

fn run_dev_command(command: DevCommand) -> Result<()> {
    match command {
        DevCommand::Up(_) => docker_compose(&["up", "-d", "postgres", "redis", "nats"]),
        DevCommand::All(_) => docker_compose(&["up", "--build"]),
        DevCommand::Build(_) => docker_compose(&["build"]),
        DevCommand::Down(_) => docker_compose(&["down"]),
        DevCommand::Logs(_) => docker_compose(&["logs", "-f", "postgres", "redis", "nats"]),
        DevCommand::Reset(_) => docker_compose(&["down", "-v"]),
        DevCommand::Watch(args) => run_dev_watch(args),
    }
}

fn docker_compose(args: &[&str]) -> Result<()> {
    let file = env::var("DEV_COMPOSE_FILE")
        .unwrap_or_else(|_| "development/docker/compose.dev.yml".into());
    let mut cmd_args = vec!["compose".to_string(), "-f".to_string(), file];
    cmd_args.extend(args.iter().map(|arg| arg.to_string()));
    run_cmd(&repo_root(), "docker", &cmd_args)
}

fn run_dev_watch(args: WatchArgs) -> Result<()> {
    let mut ignored: HashSet<String> = [
        "response-mappers.json",
        "routes.yaml",
        "public-routes.json",
        "route-permissions.json",
        "openapi.yaml",
    ]
    .into_iter()
    .map(String::from)
    .collect();
    for item in args.ignore {
        ignored.insert(item);
    }
    if args.command.is_empty() {
        return usage_error("dev command is required".into());
    }
    load_env_file(Path::new(&args.env_file));
    let mut previous = snapshot(Path::new(&args.watch_root), &ignored)?;
    let mut child = start_child(&args.command)?;
    let poll = Duration::from_millis((args.poll * 1000.0) as u64);
    loop {
        thread::sleep(poll);
        let current = snapshot(Path::new(&args.watch_root), &ignored)?;
        if current != previous {
            println!("[dev] change detected, restarting");
            stop_child(&mut child);
            child = start_child(&args.command)?;
            previous = current;
        }
    }
}

fn load_env_file(path: &Path) {
    let Ok(text) = fs::read_to_string(path) else {
        return;
    };
    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if let Some((key, value)) = line.split_once('=') {
            unsafe { env::set_var(key.trim(), value.trim().trim_matches(['"', '\''])) };
        }
    }
}

fn start_child(command: &[String]) -> Result<Child> {
    println!("[dev] starting: {}", command.join(" "));
    Ok(Command::new(&command[0])
        .args(&command[1..])
        .stdin(Stdio::inherit())
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()?)
}

fn stop_child(child: &mut Child) {
    let _ = child.kill();
    let _ = child.wait();
}

fn snapshot(root: &Path, ignored: &HashSet<String>) -> Result<String> {
    let mut rows = Vec::new();
    snapshot_walk(root, ignored, &mut rows)?;
    rows.sort();
    Ok(rows.join("\n"))
}

fn snapshot_walk(root: &Path, ignored: &HashSet<String>, rows: &mut Vec<String>) -> Result<()> {
    if !root.exists() {
        return Ok(());
    }
    let pruned: HashSet<&str> = [
        ".git",
        ".pytest_cache",
        "__pycache__",
        "build",
        "coverage",
        "dist",
        "node_modules",
        "target",
    ]
    .into_iter()
    .collect();
    let allowed: HashSet<&str> = [
        ".go", ".mod", ".sum", ".ts", ".js", ".json", ".py", ".toml", ".rs", ".yaml", ".yml",
        ".proto",
    ]
    .into_iter()
    .collect();
    for entry in fs::read_dir(root)? {
        let entry = entry?;
        let path = entry.path();
        let name = entry.file_name().to_string_lossy().to_string();
        if path.is_dir() {
            if !pruned.contains(name.as_str()) {
                snapshot_walk(&path, ignored, rows)?;
            }
        } else if !ignored.contains(&name)
            && path
                .extension()
                .and_then(OsStr::to_str)
                .is_some_and(|ext| allowed.contains(format!(".{ext}").as_str()))
        {
            let modified = entry
                .metadata()?
                .modified()?
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_nanos();
            rows.push(format!("{modified} {}", path.display()));
        }
    }
    Ok(())
}

fn run_deploy_command(command: DeployCommand) -> Result<()> {
    match command {
        DeployCommand::Kubernetes(args) => {
            run_sync()?;
            run_make(
                &repo_root().join("devops"),
                "deploy-kubernetes",
                &make_vars(&args.vars),
            )
        }
        DeployCommand::Docker(args) => run_make(
            &repo_root().join("devops"),
            "deploy-docker",
            &make_vars(&args.vars),
        ),
    }
}

fn run_sync() -> Result<()> {
    run_cmd(
        &repo_root(),
        "devops/droplet/scripts/sync-service-assets.sh",
        &[],
    )
}

fn run_release(args: &[String]) -> Result<()> {
    run_make(&repo_root().join("devops"), "release", &make_vars(args))
}

fn make_vars(args: &[String]) -> Vec<String> {
    let mut vars = Vec::new();
    for arg in args {
        if let Some(raw) = arg.strip_prefix("--") {
            if let Some((key, value)) = raw.split_once('=') {
                vars.push(format!(
                    "{}={}",
                    key.replace('-', "_").to_uppercase(),
                    value
                ));
            }
        } else if arg.contains('=') {
            vars.push(arg.clone());
        }
    }
    vars
}

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

    #[test]
    fn clap_parses_service_create() {
        let cli = Cli::try_parse_from([
            "exe",
            "service",
            "create",
            "--name",
            "orders",
            "--domain",
            "commerce",
            "--template",
            "rust",
        ])
        .unwrap();
        match cli.command {
            Commands::Service {
                command: ServiceCommand::Create(args),
            } => {
                assert_eq!(args.name, "orders");
                assert_eq!(args.domain, "commerce");
                assert_eq!(args.template_lang, "rust");
            }
            _ => panic!("unexpected command"),
        }
    }

    #[test]
    fn maps_make_vars() {
        let vars = make_vars(&[
            "tag=latest".into(),
            "--image-tag=v1".into(),
            "--ignored".into(),
        ]);
        assert_eq!(vars, vec!["tag=latest", "IMAGE_TAG=v1"]);
    }

    #[test]
    fn parses_exposes() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("service.yaml");
        fs::write(&path, "name: auth\nexposes:\n  - protobuf: api/auth.proto\n  - openapi: api/openapi.yaml\nruntime: rust\n").unwrap();
        let exposes = parse_exposes(&path);
        assert_eq!(exposes.get("protobuf").unwrap(), "api/auth.proto");
        assert_eq!(exposes.get("openapi").unwrap(), "api/openapi.yaml");
    }

    #[test]
    fn creates_service_from_template() {
        let dir = tempfile::tempdir().unwrap();
        let root = dir.path();
        let template = root.join("tools/templates/template-go-grpc");
        let skeleton = template.join("skeleton");
        fs::create_dir_all(skeleton.join("api")).unwrap();
        fs::write(
            skeleton.join("service.yaml"),
            "name: __SERVICE_NAME__\ndomain: __DOMAIN__\nowner_team: __OWNER_TEAM__\n",
        )
        .unwrap();
        fs::write(
            skeleton.join("api/service.proto"),
            "package __PROTO_PACKAGE__;\nservice __SERVICE_CLASS__Service {}\n",
        )
        .unwrap();

        let output = root.join("services/commerce/order-service");
        create_service_from_template(CreateServiceSpec {
            root,
            template_dir: &template,
            output_dir: &output,
            service_name: "order-service",
            domain: "commerce",
            owner_team: "team-orders",
            framework: "native",
            template_name: "template-go-grpc",
            proto_package: "executesoft.commerce.order.service.v1",
            service_class: "OrderService",
        })
        .unwrap();

        let metadata = fs::read_to_string(output.join("service.yaml")).unwrap();
        assert!(metadata.contains("name: order-service"));
        assert!(metadata.contains("domain: commerce"));
        assert!(metadata.contains("owner_team: team-orders"));

        let shared =
            fs::read_to_string(root.join("contracts/protobuf/commerce-order-service.proto"))
                .unwrap();
        assert!(shared.contains("package executesoft.commerce.order.service.v1;"));
        assert!(shared.contains("service OrderServiceService {}"));
    }
}
