use crate::cli::{CreateServiceArgs, ServiceCommand, ServiceTargetArgs};
use crate::util::{Result, make_vars, repo_path, repo_root, run_make, usage_error};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::ffi::OsStr;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

pub(crate) 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)
    }
}

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

pub(crate) 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(())
}

pub(crate) 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)
    })
}

pub(crate) 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
}

pub(crate) 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("__"))
}

pub(crate) 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}")),
    }
}
