// SPEC: PJ2026-010401 Web工作台 - Workbench 可见性与性能探针
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import * as http from "node:http";
import * as https from "node:https";
import path from "node:path";
import { promisify } from "node:util";

import { parseStructuredConfig, readStructuredFileIfPresent } from "./structured-config.mjs";

const execFileAsync = promisify(execFile);

export const CI_PLAN_VERSION = "v1";
export const ENV_REUSE_RUNTIME_MODE = "env-reuse-git-mirror-checkout";
export const V02_ENV_REUSE_RUNTIME_MODE = ENV_REUSE_RUNTIME_MODE;

export const DEFAULT_RUNTIME_DEP_PATHS = Object.freeze([
  "package.json",
  "package-lock.json"
]);

export const GO_RUNTIME_DEP_PATHS = Object.freeze([
  "go.mod",
  "go.sum"
]);

export const DEFAULT_RUNTIME_PACKAGE_FIELDS = Object.freeze([
  "dependencies",
  "optionalDependencies",
  "peerDependencies",
  "overrides",
  "resolutions",
  "engines",
  "packageManager",
  "type"
]);

export const DEFAULT_SHARED_RUNTIME_PATHS = Object.freeze([
  "internal/protocol/",
  "internal/build-metadata.mjs"
]);

export const DEFAULT_DOCS_ONLY_PATHS = Object.freeze([
  "docs/",
  "README.md",
  "AGENTS.md"
]);

export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([
  "deploy/gitops/",
  "deploy/frp/",
  "scripts/gitops-render.mjs",
  "scripts/src/runtime-lane.ts",
  "tsconfig.gitops.json"
]);

export async function createCiPlan(options = {}) {
  const repoRoot = options.repoRoot ?? process.cwd();
  const deployConfigPath = options.deployConfigPath ?? "deploy/deploy.yaml";
  const targetRef = options.targetRef ?? "HEAD";
  const baseRef = options.baseRef ?? await defaultBaseRef(repoRoot, targetRef);
  const lane = options.lane ?? "v02";
  const registryPrefix = options.registryPrefix ?? process.env.HWLAB_DEV_REGISTRY_PREFIX ?? process.env.HWLAB_DEV_REGISTRY ?? "127.0.0.1:5000/hwlab";
  const baseImage = options.baseImage ?? process.env.HWLAB_DEV_BASE_IMAGE ?? "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim";
  const verifyReuseRegistry = options.verifyReuseRegistry ?? booleanEnv(process.env.HWLAB_CI_PLAN_VERIFY_REUSE_REGISTRY);
  const reuseRegistryProbe = options.reuseRegistryProbe ?? (verifyReuseRegistry ? probeRegistryManifest : null);
  const registryProbeTimeoutMs = Number.parseInt(String(options.registryProbeTimeoutMs ?? process.env.HWLAB_CI_PLAN_REGISTRY_PROBE_TIMEOUT_MS ?? 3000), 10);

  const deployJson = await readStructuredFileIfPresent(repoRoot, deployConfigPath, {});
  const laneConfig = laneDeployConfig(deployJson, lane);
  assertLaneEnvReuseConfig(laneConfig, lane);
  const artifactCatalogPath = options.artifactCatalogPath ?? defaultArtifactCatalogPath(laneConfig, lane);
  const artifactCatalog = await readStructuredFileIfPresent(repoRoot, artifactCatalogPath, null);
  const sourceCommitId = await gitValue(repoRoot, ["rev-parse", targetRef]);
  const shortCommitId = await gitValue(repoRoot, ["rev-parse", "--short=7", targetRef]);
  const changedPaths = await changedPathsBetween(repoRoot, baseRef, targetRef);
  const normalizedChangedPaths = changedPaths.map(normalizeRepoPath).filter(Boolean);
  const semanticRuntimeDepPaths = await runtimeDependencyChangedPaths(repoRoot, baseRef, targetRef, normalizedChangedPaths);
  const serviceIdResolution = resolveServiceIds({ deployJson, options, laneConfig, lane });
  const envReuseRecipe = envReuseRecipeForLane(deployJson, lane);
  const componentModels = componentModelsForServices(serviceIdResolution.serviceIds, laneConfig, lane);
  const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds);
  const globalChange = classifyGlobalChange(normalizedChangedPaths);
  const hasGoService = componentModels.some((model) => model.runtimeKind === "go-service");
  const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [
    ...envReuseRecipe.additionalEnvPaths,
    ...DEFAULT_RUNTIME_DEP_PATHS,
    ...(hasGoService ? GO_RUNTIME_DEP_PATHS : [])
  ], { semanticPackageJson: true });
  const catalogByServiceId = new Map((artifactCatalog?.services ?? []).map((service) => [service.serviceId, service]));

  const services = [];
  for (const model of componentModels) {
    const directMatches = matchingPaths(normalizedChangedPaths, model.componentPaths);
    const sharedMatches = matchingPaths(normalizedChangedPaths, model.sharedPaths);
    const rawRuntimeDepMatches = matchingPaths(normalizedChangedPaths, model.runtimeDeps);
    const runtimeDepMatches = rawRuntimeDepMatches.filter((item) => semanticRuntimeDepPaths.has(item));
    const buildSystemMatches = matchingPaths(normalizedChangedPaths, model.buildSystemPaths);
    const envReuse = envReuseServices.has(model.serviceId);
    const bootSh = envReuse ? bootShForService(deployJson, model.serviceId, lane) : null;
    const serviceEnvReuseRecipe = envReuse ? envReuseRecipeForService(envReuseRecipe.publicRecipe, model) : null;
    const runtimeConfigChanged = envReuse ? await serviceRuntimeConfigChanged(repoRoot, deployConfigPath, baseRef, targetRef, lane, model.serviceId) : null;
    const envInputPaths = envReuse ? uniqueSorted([
      ...model.runtimeDeps,
      ...envReuseRecipe.launcherInputPaths
    ]) : [];
    const codeInputPaths = envReuse ? uniqueSorted([
      ...model.componentPaths,
      ...model.sharedPaths,
      bootSh
    ]) : [];
    const envMatches = envReuse
      ? matchingPaths(normalizedChangedPaths, envInputPaths)
        .filter((item) => !model.runtimeDeps.includes(item) || semanticRuntimeDepPaths.has(item))
      : [];
    const codeMatches = envReuse ? matchingPaths(normalizedChangedPaths, codeInputPaths) : [];
    const relevantEnvMatches = envMatches.filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item));
    const relevantCodeMatches = codeMatches.filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item));
    const imageRelevantChangedPaths = uniqueSorted([
      ...directMatches,
      ...sharedMatches,
      ...runtimeDepMatches,
      ...buildSystemMatches
    ].filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item)));
    const catalogRecord = catalogByServiceId.get(model.serviceId) ?? null;
    const environmentInputHash = envReuse ? await hashEnvReuseInputs(repoRoot, targetRef, envInputPaths, serviceEnvReuseRecipe, {
      skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath),
      semanticPackageJson: true
    }) : null;
    const codeInputHash = envReuse ? await hashGitPaths(repoRoot, targetRef, codeInputPaths, {
      skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath)
    }) : null;
    const environmentImage = envReuse ? environmentImageFromCatalog(model.serviceId, catalogRecord) : null;
    const environmentDigest = envReuse ? environmentDigestFromCatalog(catalogRecord) : null;
    const environmentReady = envReuse && /^sha256:[a-f0-9]{64}$/u.test(environmentDigest ?? "") && Boolean(environmentImage);
    const environmentInputChanged = envReuse ? catalogRecord?.environmentInputHash !== environmentInputHash : null;
    const reuseRegistry = envReuse && reuseRegistryProbe && environmentReady
      ? await reuseRegistryProbe({
        serviceId: model.serviceId,
        image: environmentImage,
        digest: environmentDigest,
        registryPrefix,
        timeoutMs: Number.isFinite(registryProbeTimeoutMs) ? registryProbeTimeoutMs : 3000
      })
      : null;
    const reuseRegistryUnavailable = Boolean(reuseRegistry && reuseRegistry.status !== "present");
    const envChanged = envReuse ? Boolean(relevantEnvMatches.length > 0 || !environmentReady || environmentInputChanged || reuseRegistryUnavailable) : null;
    const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash : null;
    const componentInputPaths = uniqueSorted([
      ...model.componentPaths,
      ...model.sharedPaths,
      ...model.runtimeDeps,
      ...model.buildSystemPaths
    ]);
    const componentCommitId = await lastCommitForPaths(repoRoot, targetRef, componentInputPaths);
    const componentInputHash = await hashGitPaths(repoRoot, targetRef, componentInputPaths, {
      skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath),
      semanticPackageJson: true
    });
    const buildArgsHash = stableHash({
      serviceId: model.serviceId,
      baseImage,
      registryPrefix,
      runtimeKind: model.runtimeKind,
      entrypoint: model.entrypoint
    });
    const catalogSourceCommitId = !envReuse ? text(catalogRecord?.sourceCommitId) : "";
    const catalogComponentInputHash = !envReuse ? text(catalogRecord?.componentInputHash) : "";
    const catalogComponentInputHashAtSource = !envReuse && catalogSourceCommitId
      ? await hashGitPathsIfCommitExists(repoRoot, catalogSourceCommitId, componentInputPaths, {
        skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath),
        semanticPackageJson: true
      })
      : null;
    const catalogComponentProvenance = !envReuse ? catalogComponentProvenanceStatus({
      catalogRecord,
      catalogSourceCommitId,
      catalogComponentInputHash,
      catalogComponentInputHashAtSource,
      componentInputHash,
      dockerfileHash,
      buildArgsHash
    }) : null;
    const catalogRecordForReuse = catalogRecord && catalogComponentProvenance ? {
      ...catalogRecord,
      componentProvenanceStatus: catalogComponentProvenance.status,
      componentProvenanceReason: catalogComponentProvenance.reason
    } : catalogRecord;
    const componentInputChanged = !envReuse && catalogComponentProvenance?.safeToReuse !== true;
    const catalogReuse = envReuse
      ? envReuseCandidate(catalogRecord, envChanged)
      : reuseCandidate(catalogRecordForReuse, imageRelevantChangedPaths.length > 0, componentInputChanged);
    const affected = envReuse
      ? Boolean(envChanged || codeChanged || runtimeConfigChanged)
      : imageRelevantChangedPaths.length > 0 || componentInputChanged || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest";
    const buildRequired = envReuse ? envChanged === true : affected;
    services.push({
      serviceId: model.serviceId,
      runtimeKind: model.runtimeKind,
      entrypoint: model.entrypoint,
      artifactKind: model.artifactKind,
      healthPath: model.healthPath,
      healthPort: model.healthPort,
      healthProbe: model.healthProbe,
      componentPaths: model.componentPaths,
      sharedPaths: model.sharedPaths,
      runtimeDeps: model.runtimeDeps,
      buildSystemPaths: model.buildSystemPaths,
      envReuseRecipe: serviceEnvReuseRecipe,
      componentCommitId,
      componentInputHash,
      catalogSourceCommitId: catalogSourceCommitId || null,
      catalogComponentInputHash: catalogComponentInputHash || null,
      catalogComponentInputHashAtSource,
      catalogComponentProvenance,
      dockerfileHash,
      baseImageReference: baseImage,
      baseImageDigest: digestFromImageReference(baseImage),
      buildArgsHash,
      changedPaths: imageRelevantChangedPaths,
      affected,
      buildRequired,
      componentInputChanged,
      runtimeMode: envReuse ? ENV_REUSE_RUNTIME_MODE : "service-image",
      envReuse,
      envChanged,
      runtimeConfigChanged,
      environmentInputChanged,
      codeChanged,
      reuseRegistry,
      environmentInputHash,
      codeInputHash,
      bootRepo: envReuse ? canonicalBootRepo(deployJson, lane) : null,
      bootCommit: envReuse ? sourceCommitId : null,
      bootSh,
      environmentImage,
      environmentDigest,
      envChangedPaths: relevantEnvMatches,
      codeChangedPaths: relevantCodeMatches,
      reason: affected
        ? reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, runtimeConfigChanged, environmentInputChanged, codeChanged, reuseRegistry })
        : reasonForUnchanged(globalChange),
      reuse: catalogReuse
    });
  }

  const affectedServices = services.filter((service) => service.affected).map((service) => service.serviceId);
  const buildServices = services.filter((service) => service.buildRequired).map((service) => service.serviceId);
  const reusedServices = services.filter((service) => !service.affected).map((service) => service.serviceId);
  return {
    planVersion: CI_PLAN_VERSION,
    sourceCommitId,
    shortCommitId,
    baseRef,
    targetRef,
    compatibility: {
      deployConfig: deployConfigPath,
      artifactCatalog: artifactCatalog ? artifactCatalogPath : null,
      lane,
      ciContractSource: "scripts/gitops-render.mjs",
      mode: "advisory-read-only",
      currentRenderCompatible: true,
      plannerMutatesDeployJson: false,
      serviceIdSource: serviceIdResolution.source,
      serviceDeclarationSource: `deploy.lanes.${lane}.serviceDeclarations`,
      envRecipeSource: `deploy.lanes.${lane}.envRecipe`,
      envReuseServiceIds: [...envReuseServices],
      v02EnvReuseServiceIds: lane === "v02" ? [...envReuseServices] : [],
      defaultComponentLazyBuild: true,
      envReuseCodeOnlyFastLane: true,
      fullBuildFallback: false
    },
    inputs: {
      registryPrefix,
      baseImage,
      serviceCount: services.length,
      verifyReuseRegistry
    },
    changedPaths: normalizedChangedPaths,
    changedPathSummary: globalChange,
    imageBuildRequired: buildServices.length > 0,
    affectedServices,
    rolloutServices: affectedServices,
    buildServices,
    reusedServices,
    buildSkippedCount: services.length - buildServices.length,
    services,
    ciCdPlan: {
      willBuild: buildServices,
      willRollout: affectedServices,
      willReuse: reusedServices,
      willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0,
      noImageBuildReason: buildServices.length === 0
        ? (affectedServices.length > 0 ? "env-reuse-code-only-rollout" : globalChange.summary)
        : null
    }
  };
}

export function componentModelsForServices(serviceIds, laneConfig = null, lane = "v02") {
  return serviceIds.map((serviceId) => {
    const declaration = serviceDeclarationFor(laneConfig, serviceId, lane);
    return {
      serviceId,
      runtimeKind: declaration.runtimeKind,
      entrypoint: declaration.entrypoint,
      artifactKind: declaration.artifactKind,
      healthPath: declaration.healthPath,
      healthPort: declaration.healthPort,
      healthProbe: declaration.healthProbe,
      env: declaration.env,
      observable: declaration.observable,
      componentPaths: uniqueSorted(declaration.componentPaths.map(normalizeRepoPath)),
      sharedPaths: uniqueSorted(DEFAULT_SHARED_RUNTIME_PATHS.map(normalizeRepoPath)),
      runtimeDeps: runtimeDependencyPathsForDeclaration(declaration),
      buildSystemPaths: uniqueSorted(envReuseRecipeForLaneConfig(laneConfig, lane).additionalEnvPaths)
    };
  });
}

function runtimeDependencyPathsForDeclaration(declaration) {
  const paths = [...DEFAULT_RUNTIME_DEP_PATHS];
  if (declaration.runtimeKind === "go-service" || declaration.artifactKind === "go-service") {
    paths.push(...GO_RUNTIME_DEP_PATHS);
  }
  return uniqueSorted(paths.map(normalizeRepoPath));
}

function serviceDeclarationFor(laneConfig, serviceId, lane) {
  const configured = laneConfig?.serviceDeclarations?.[serviceId];
  const runtimeKind = requireDeclarationString(configured?.runtimeKind, lane, `serviceDeclarations.${serviceId}.runtimeKind`);
  const entrypoint = requireDeclarationString(configured?.entrypoint, lane, `serviceDeclarations.${serviceId}.entrypoint`);
  const artifactKind = normalizeDeclarationString(configured?.artifactKind) || runtimeKind;
  if (!Array.isArray(configured?.componentPaths) || configured.componentPaths.length === 0) {
    throw new Error(`deploy.lanes.${lane}.serviceDeclarations.${serviceId}.componentPaths is required`);
  }
  return {
    runtimeKind,
    entrypoint,
    artifactKind,
    healthPath: normalizeDeclarationString(configured?.healthPath) || "/health/live",
    healthPort: Number.isInteger(configured?.healthPort) ? configured.healthPort : null,
    healthProbe: normalizeServiceHealthProbe(configured?.healthProbe),
    componentPaths: configured.componentPaths,
    env: normalizeServiceEnv(configured?.env),
    observable: configured?.observable === true
  };
}

function normalizeServiceEnv(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  return Object.fromEntries(Object.entries(value).map(([key, raw]) => [String(key), raw == null ? null : String(raw)]).sort(([left], [right]) => left.localeCompare(right)));
}

function normalizeServiceHealthProbe(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  const result = {};
  for (const key of ["readiness", "liveness", "startup"]) {
    const timing = normalizeProbeTiming(value[key]);
    if (Object.keys(timing).length > 0) result[key] = timing;
  }
  return result;
}

function normalizeProbeTiming(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
  const result = {};
  for (const field of ["initialDelaySeconds", "periodSeconds", "timeoutSeconds", "failureThreshold", "successThreshold"]) {
    const raw = value[field];
    const minimum = field === "initialDelaySeconds" ? 0 : 1;
    if (Number.isInteger(raw) && raw >= minimum) result[field] = raw;
  }
  return result;
}

function requireDeclarationString(value, lane, label) {
  const text = normalizeDeclarationString(value);
  if (!text) throw new Error(`deploy.lanes.${lane}.${label} is required`);
  return text;
}

function normalizeDeclarationString(value) {
  const text = String(value ?? "").trim();
  return text || null;
}

function laneDeployConfig(deployJson, lane) {
  return deployJson?.lanes?.[lane] ?? null;
}

function defaultArtifactCatalogPath(laneConfig, lane) {
  return normalizeDeclarationString(laneConfig?.artifactCatalog) ?? `deploy/artifact-catalog.${lane}.json`;
}

export function envReuseRecipeForLane(deployJson, lane = "v02") {
  return envReuseRecipeForLaneConfig(laneDeployConfig(deployJson, lane), lane);
}

function envReuseRecipeForLaneConfig(laneConfig, lane = "v02") {
  const configured = laneConfig?.envRecipe;
  if (!configured) throw new Error(`deploy.lanes.${lane}.envRecipe is required`);
  const osPackages = normalizeStringList(configured.osPackages, []);
  const goOsPackages = normalizeStringList(configured.goOsPackages, []);
  const bunVersion = requireDeclarationString(configured.bunVersion, lane, "envRecipe.bunVersion");
  const launcherPath = normalizeRepoPath(requireDeclarationString(configured.launcherPath, lane, "envRecipe.launcherPath"));
  const runtimeNodeModulesPath = normalizeAbsolutePath(configured.runtimeNodeModulesPath);
  const runtimeGoModCachePath = normalizeAbsolutePath(configured.runtimeGoModCachePath);
  const runtimeGoBuildCachePath = normalizeAbsolutePath(configured.runtimeGoBuildCachePath);
  const goToolchain = normalizeDeclarationString(configured.goToolchain);
  if (!runtimeNodeModulesPath) throw new Error(`deploy.lanes.${lane}.envRecipe.runtimeNodeModulesPath must be absolute`);
  const additionalEnvPaths = uniqueSorted(normalizeStringList(configured.additionalEnvPaths, []).map(normalizeRepoPath));
  const launcherInputPaths = uniqueSorted([
    launcherPath,
    ...additionalEnvPaths
  ]);
  const hwpodAliases = normalizeHwpodAliases(configured.hwpodAliases);
  const downloadStack = normalizeDownloadStack(configured.downloadStack, lane);
  if (osPackages.length === 0) throw new Error(`deploy.lanes.${lane}.envRecipe.osPackages is required`);
  if ((runtimeGoModCachePath || runtimeGoBuildCachePath) && goOsPackages.length === 0) {
    throw new Error(`deploy.lanes.${lane}.envRecipe.goOsPackages is required when Go cache paths are configured`);
  }
  if (additionalEnvPaths.length === 0) throw new Error(`deploy.lanes.${lane}.envRecipe.additionalEnvPaths is required`);
  if (hwpodAliases.length === 0) throw new Error(`deploy.lanes.${lane}.envRecipe.hwpodAliases is required`);
  return {
    osPackages,
    goOsPackages,
    bunVersion,
    launcherPath,
    runtimeNodeModulesPath,
    runtimeGoModCachePath,
    runtimeGoBuildCachePath,
    goToolchain,
    additionalEnvPaths,
    launcherInputPaths,
    downloadStack,
    hwpodAliases,
    publicRecipe: {
      osPackages,
      goOsPackages,
      bunVersion,
      launcherPath,
      runtimeNodeModulesPath,
      runtimeGoModCachePath,
      runtimeGoBuildCachePath,
      goToolchain,
      additionalEnvPaths,
      downloadStack,
      hwpodAliases
    }
  };
}

function envReuseRecipeForService(recipe, model) {
  if (model?.runtimeKind === "go-service" || model?.artifactKind === "go-service") return recipe;
  const {
    goOsPackages: _goOsPackages,
    runtimeGoModCachePath: _runtimeGoModCachePath,
    runtimeGoBuildCachePath: _runtimeGoBuildCachePath,
    goToolchain: _goToolchain,
    ...nodeRecipe
  } = recipe;
  return nodeRecipe;
}

function normalizeDownloadStack(value, lane) {
  const configured = effectiveDownloadStackConfig(value, process.env);
  if (!configured) throw new Error(`deploy.lanes.${lane}.envRecipe.downloadStack is required`);
  const httpProxy = normalizeOptionalHttpUrl(configured.httpProxy, `deploy.lanes.${lane}.envRecipe.downloadStack.httpProxy`);
  const httpsProxy = normalizeOptionalHttpUrl(configured.httpsProxy, `deploy.lanes.${lane}.envRecipe.downloadStack.httpsProxy`);
  const npmRegistry = normalizeOptionalHttpUrl(configured.npmRegistry, `deploy.lanes.${lane}.envRecipe.downloadStack.npmRegistry`);
  const goProxy = normalizeDeclarationString(configured.goProxy);
  const npmFetchTimeoutMs = Number(configured.npmFetchTimeoutMs);
  const bunPackages = configured.bunPackages && typeof configured.bunPackages === "object" ? configured.bunPackages : null;
  const x64 = normalizePackageName(bunPackages?.x64);
  const arm64 = normalizePackageName(bunPackages?.arm64);
  if (!npmRegistry) throw new Error(`deploy.lanes.${lane}.envRecipe.downloadStack.npmRegistry is required`);
  if (!Number.isInteger(npmFetchTimeoutMs) || npmFetchTimeoutMs <= 0) {
    throw new Error(`deploy.lanes.${lane}.envRecipe.downloadStack.npmFetchTimeoutMs must be a positive integer`);
  }
  if (!x64 || !arm64) throw new Error(`deploy.lanes.${lane}.envRecipe.downloadStack.bunPackages.x64/arm64 are required`);
  return {
    httpProxy,
    httpsProxy,
    noProxy: normalizeStringList(configured.noProxy, []),
    npmRegistry,
    npmFetchTimeoutMs,
    goProxy,
    bunPackages: { x64, arm64 }
  };
}

function effectiveDownloadStackConfig(value, env = process.env) {
  const configured = value && typeof value === "object" ? value : null;
  if (!configured || !usesK8sNativeDownloadProxy(env)) return configured;
  const httpProxy = normalizeDeclarationString(env.HTTP_PROXY) || normalizeDeclarationString(env.http_proxy) || configured.httpProxy;
  const httpsProxy = normalizeDeclarationString(env.HTTPS_PROXY) || normalizeDeclarationString(env.https_proxy) || httpProxy || configured.httpsProxy;
  const noProxy = noProxyFromEnv(env) || configured.noProxy;
  return {
    ...configured,
    httpProxy,
    httpsProxy,
    noProxy
  };
}

function usesK8sNativeDownloadProxy(env = process.env) {
  return env.HWLAB_DEV_REGISTRY_K8S_NATIVE === "1" || Boolean(normalizeDeclarationString(env.HWLAB_TEKTON_PIPELINERUN));
}

function noProxyFromEnv(env = process.env) {
  const text = normalizeDeclarationString(env.NO_PROXY) || normalizeDeclarationString(env.no_proxy);
  if (!text) return null;
  return text.split(",").map((item) => item.trim()).filter(Boolean);
}

function normalizeOptionalHttpUrl(value, label) {
  const text = normalizeDeclarationString(value);
  if (!text) return null;
  try {
    const url = new URL(text);
    if (url.protocol === "http:" || url.protocol === "https:") return text;
  } catch {}
  throw new Error(`${label} must be an http(s) URL`);
}

function normalizePackageName(value) {
  const text = normalizeDeclarationString(value);
  return text && !text.includes(" ") ? text : null;
}

function normalizeStringList(value, fallback) {
  const list = Array.isArray(value) ? value : fallback;
  return list.map((item) => String(item ?? "").trim()).filter(Boolean);
}

function normalizeAbsolutePath(value) {
  const text = String(value ?? "").trim();
  return text.startsWith("/") && !text.includes("..") ? text : null;
}

function normalizeHwpodAliases(value) {
  const list = Array.isArray(value) ? value : [];
  return list.map((item) => ({
    command: normalizeDeclarationString(item?.command),
    script: normalizeRepoPath(item?.script)
  })).filter((item) => item.command && item.script);
}

function assertLaneEnvReuseConfig(laneConfig, lane) {
  if (!laneConfig?.serviceDeclarations || Object.keys(laneConfig.serviceDeclarations).length === 0) {
    throw new Error(`deploy.lanes.${lane}.serviceDeclarations is required for ${lane} env reuse planning`);
  }
  if (!laneConfig?.envRecipe) throw new Error(`deploy.lanes.${lane}.envRecipe is required for ${lane} env reuse planning`);
}

export async function runtimeDependencyChangedPaths(repoRoot, baseRef, targetRef, changedPaths) {
  const result = new Set();
  for (const filePath of changedPaths) {
    if (filePath === "package-lock.json") {
      result.add(filePath);
      continue;
    }
    if (filePath !== "package.json") continue;
    if (await packageRuntimeFieldsChanged(repoRoot, baseRef, targetRef, filePath)) result.add(filePath);
  }
  return result;
}

async function serviceRuntimeConfigChanged(repoRoot, deployConfigPath, baseRef, targetRef, lane, serviceId) {
  const [before, after] = await Promise.all([
    readDeployConfigFromGit(repoRoot, baseRef, deployConfigPath),
    readDeployConfigFromGit(repoRoot, targetRef, deployConfigPath)
  ]);
  if (!before || !after) return true;
  return stableJson(serviceRuntimeConfigIdentity(before, lane, serviceId)) !== stableJson(serviceRuntimeConfigIdentity(after, lane, serviceId));
}

async function readDeployConfigFromGit(repoRoot, ref, deployConfigPath) {
  try {
    return parseStructuredConfig(await gitValue(repoRoot, ["show", `${ref}:${deployConfigPath}`]), deployConfigPath);
  } catch {
    return null;
  }
}

function serviceRuntimeConfigIdentity(deployJson, lane, serviceId) {
  const laneConfig = laneDeployConfig(deployJson, lane);
  const declaration = laneConfig?.serviceDeclarations?.[serviceId] ?? {};
  return {
    namespace: normalizeDeclarationString(laneConfig?.namespace),
    serviceId,
    bootScript: normalizeRepoPath(laneConfig?.bootScripts?.[serviceId]),
    runtimeKind: normalizeDeclarationString(declaration.runtimeKind),
    artifactKind: normalizeDeclarationString(declaration.artifactKind),
    entrypoint: normalizeRepoPath(declaration.entrypoint),
    healthPath: normalizeDeclarationString(declaration.healthPath),
    healthPort: Number.isInteger(declaration.healthPort) ? declaration.healthPort : null,
    healthProbe: normalizeServiceHealthProbe(declaration.healthProbe),
    env: normalizeServiceEnv(declaration.env),
    observable: declaration.observable === true
  };
}

export async function packageRuntimeFieldsChanged(repoRoot, baseRef, targetRef, filePath = "package.json") {
  const [before, after] = await Promise.all([
    readJsonFromGit(repoRoot, baseRef, filePath, null),
    readJsonFromGit(repoRoot, targetRef, filePath, null)
  ]);
  if (!before || !after) return true;
  return stableJson(pickRuntimePackageJson(before)) !== stableJson(pickRuntimePackageJson(after));
}

function pickRuntimePackageJson(packageJson) {
  if (!packageJson || typeof packageJson !== "object") return null;
  const picked = {};
  for (const field of DEFAULT_RUNTIME_PACKAGE_FIELDS) {
    if (Object.prototype.hasOwnProperty.call(packageJson, field)) picked[field] = packageJson[field];
  }
  return picked;
}

async function readJsonFromGit(repoRoot, ref, filePath, fallback) {
  try {
    const value = await gitValue(repoRoot, ["show", `${ref}:${filePath}`]);
    return JSON.parse(value);
  } catch {
    return fallback;
  }
}

export function enabledEnvReuseServices(deployJson, lane, serviceIds) {
  const configured = deployJson?.lanes?.[lane]?.envReuseServices;
  const values = Array.isArray(configured) ? configured : [];
  const allowed = new Set(serviceIds);
  return new Set(values.filter((serviceId) => allowed.has(serviceId)));
}

export function bootShForService(deployJson, serviceId, lane = "v02") {
  const configured = deployJson?.lanes?.[lane]?.bootScripts?.[serviceId];
  if (!configured) throw new Error(`deploy.lanes.${lane}.bootScripts.${serviceId} is required`);
  const bootSh = normalizeRepoPath(configured);
  if (!bootSh || bootSh.startsWith("/") || bootSh.split("/").includes("..")) {
    throw new Error(`invalid ${lane} boot script for ${serviceId}: ${configured}`);
  }
  return bootSh;
}

function canonicalBootRepo(deployJson, lane) {
  return deployJson?.lanes?.[lane]?.sourceRepo || "git@github.com:pikasTech/HWLAB.git";
}

export function classifyGlobalChange(changedPaths) {
  const relevant = changedPaths.filter(Boolean);
  const testOnly = relevant.length > 0 && relevant.every(isTestOnlyPath);
  const docsOnly = relevant.length > 0 && relevant.every((item) => isDocsOnlyPath(item) || isTestOnlyPath(item));
  const gitopsOnly = relevant.length > 0 && relevant.every((item) => isGitOpsOnlyPath(item) || isDocsOnlyPath(item) || isTestOnlyPath(item));
  return {
    count: relevant.length,
    testOnly,
    docsOnly,
    gitopsOnly,
    summary: relevant.length === 0
      ? "no-source-diff"
      : testOnly
        ? "test-only-change"
        : docsOnly
          ? "docs-only-change"
          : gitopsOnly
            ? "gitops-only-change"
            : "runtime-or-build-change"
  };
}

export function matchingPaths(changedPaths, patterns) {
  return uniqueSorted(changedPaths.filter((filePath) => patterns.some((pattern) => pathMatches(pattern, filePath))));
}

export function normalizeRepoPath(value) {
  return String(value ?? "")
    .replaceAll("\\", "/")
    .replace(/^\.\//u, "")
    .replace(/^\/+/, "")
    .trim();
}

export function pathMatches(pattern, filePath) {
  const normalizedPattern = normalizeRepoPath(pattern);
  const normalizedFilePath = normalizeRepoPath(filePath);
  if (!normalizedPattern || !normalizedFilePath) return false;
  if (normalizedPattern.endsWith("/")) return normalizedFilePath.startsWith(normalizedPattern);
  return normalizedFilePath === normalizedPattern;
}

export function isTestOnlyPath(filePath) {
  const normalized = normalizeRepoPath(filePath);
  return /\.test\.(mjs|ts)$/u.test(normalized) || normalized.includes("/__tests__/") || normalized.includes("/fixtures/");
}

export function isDocsOnlyPath(filePath) {
  const normalized = normalizeRepoPath(filePath);
  if (normalized.startsWith("internal/agent/prompts/")) return false;
  if (normalized.startsWith("skills/")) return false;
  return DEFAULT_DOCS_ONLY_PATHS.some((pattern) => pathMatches(pattern, normalized)) || normalized.endsWith(".md");
}

export function isGitOpsOnlyPath(filePath) {
  const normalized = normalizeRepoPath(filePath);
  return DEFAULT_GITOPS_ONLY_PATHS.some((pattern) => pathMatches(pattern, normalized));
}

export async function changedPathsBetween(repoRoot, baseRef, targetRef) {
  const diff = await gitValue(repoRoot, ["diff", "--name-only", baseRef, targetRef]);
  return diff.split("\n").map((line) => line.trim()).filter(Boolean);
}

export async function hashGitPaths(repoRoot, targetRef, paths, options = {}) {
  const normalizedPaths = uniqueSorted(paths.map(normalizeRepoPath).filter(Boolean));
  if (normalizedPaths.length === 0) return stableHash({ empty: true });
  const output = await gitValue(repoRoot, ["ls-tree", "-r", targetRef, "--", ...normalizedPaths]).catch(() => "");
  const lines = (await Promise.all(output.split("\n")
    .map((line) => line.trim())
    .filter(Boolean)
    .map(async (line) => {
      const filePath = line.slice(line.indexOf("\t") + 1);
      if (options.skipPath?.(filePath)) return null;
      if (options.semanticPackageJson === true && filePath === "package.json") {
        const packageJson = await readJsonFromGit(repoRoot, targetRef, filePath, null);
        return `100644 blob ${stableHash(pickRuntimePackageJson(packageJson))}\t${filePath}`;
      }
      return line;
    })))
    .filter(Boolean)
    .sort();
  return stableHash({ entries: lines });
}

async function hashEnvReuseInputs(repoRoot, targetRef, paths, recipe, options = {}) {
  return stableHash({
    gitPaths: await hashGitPaths(repoRoot, targetRef, paths, options),
    envRecipe: envReuseIdentityRecipe(recipe)
  });
}

function envReuseIdentityRecipe(recipe) {
  const identity = JSON.parse(JSON.stringify(recipe ?? {}));
  if (identity.downloadStack && typeof identity.downloadStack === "object") {
    delete identity.downloadStack.httpProxy;
    delete identity.downloadStack.httpsProxy;
    delete identity.downloadStack.noProxy;
  }
  return identity;
}

export async function lastCommitForPaths(repoRoot, targetRef, paths) {
  const normalizedPaths = uniqueSorted(paths.map(normalizeRepoPath).filter(Boolean));
  if (normalizedPaths.length === 0) return null;
  const value = await gitValue(repoRoot, ["rev-list", "-1", targetRef, "--", ...normalizedPaths]).catch(() => "");
  return value.trim() || null;
}

function resolveServiceIds({ options, laneConfig, lane }) {
  const allowedServiceIds = knownServiceIds(laneConfig, lane);
  if (Array.isArray(options.services) && options.services.length > 0) {
    const serviceIds = uniqueSorted(options.services.map(String));
    const unknownServiceIds = serviceIds.filter((serviceId) => !allowedServiceIds.has(serviceId));
    if (unknownServiceIds.length > 0) {
      throw new Error(`unknown service IDs for node CI plan: ${unknownServiceIds.join(", ")}`);
    }
    return { source: "cli-services", serviceIds };
  }
  const configured = Array.isArray(laneConfig?.envReuseServices) ? laneConfig.envReuseServices.filter(Boolean) : [];
  if (configured.length === 0) throw new Error(`deploy.lanes.${lane}.envReuseServices is required for ${lane} CI planning`);
  return { source: `deploy.lanes.${lane}.envReuseServices`, serviceIds: uniquePreserveOrder(configured) };
}

function knownServiceIds(laneConfig, lane) {
  const values = new Set(Object.keys(laneConfig?.serviceDeclarations ?? {}));
  for (const serviceId of laneConfig?.envReuseServices ?? []) values.add(serviceId);
  return values;
}

async function defaultBaseRef(repoRoot, targetRef) {
  const parent = await gitValue(repoRoot, ["rev-parse", `${targetRef}^`]).catch(() => "");
  return parent || targetRef;
}

async function gitValue(repoRoot, args) {
  const result = await execFileAsync("git", args, {
    cwd: repoRoot,
    maxBuffer: 8 * 1024 * 1024,
    timeout: 15000
  });
  return result.stdout.trim();
}

function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse = null, envReuse = false, envChanged = null, runtimeConfigChanged = null, environmentInputChanged = null, codeChanged = null, reuseRegistry = null }) {
  const reasons = [];
  if (envReuse && environmentInputChanged) reasons.push("environment-input-mismatch");
  if (envReuse && reuseRegistry && reuseRegistry.status !== "present") reasons.push("reuse-registry-missing");
  if (envReuse && envChanged && reasons.length === 0) reasons.push("environment-input-changed");
  if (envReuse && runtimeConfigChanged) reasons.push("runtime-config-changed");
  if (envReuse && codeChanged) reasons.push("code-input-changed");
  if (directMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("component-path-changed");
  if (sharedMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("shared-runtime-changed");
  if (runtimeDepMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("runtime-deps-changed");
  if (buildSystemMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("build-system-changed");
  if (catalogReuse?.status === "component-input-mismatch") reasons.push("component-input-mismatch");
  if (catalogReuse?.status === "component-source-mismatch") reasons.push("component-source-mismatch");
  if (catalogReuse?.status === "component-source-unavailable") reasons.push("component-source-unavailable");
  if (catalogReuse?.status === "component-provenance-missing") reasons.push("component-provenance-missing");
  if (catalogReuse?.status === "component-build-mismatch") reasons.push("component-build-mismatch");
  if (catalogReuse?.status === "candidate-no-catalog") reasons.push("catalog-record-missing");
  if (catalogReuse?.status === "candidate-unverified-digest") reasons.push("catalog-digest-missing");
  return reasons.length ? reasons : ["affected"];
}

function reasonForUnchanged(globalChange) {
  if (globalChange.summary === "docs-only-change") return ["docs-only-no-image-build"];
  if (globalChange.summary === "gitops-only-change") return ["gitops-only-no-image-build"];
  if (globalChange.summary === "test-only-change") return ["test-only-no-image-build"];
  if (globalChange.summary === "no-source-diff") return ["no-source-diff"];
  return ["component-inputs-unchanged"];
}

function reuseCandidate(catalogRecord, affected, componentInputChanged = false) {
  if (affected) return { status: "not-reused", reason: "component-affected" };
  if (!catalogRecord) return { status: "candidate-no-catalog", reason: "no-previous-artifact-record" };
  if (componentInputChanged && catalogRecord?.componentProvenanceStatus) {
    return {
      status: catalogRecord.componentProvenanceStatus,
      reason: catalogRecord.componentProvenanceReason,
      image: catalogRecord.image ?? null,
      digest: catalogRecord.digest ?? null,
      reusedFrom: catalogRecord.componentInputHash ?? catalogRecord.sourceCommitId ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
    };
  }
  if (componentInputChanged && typeof catalogRecord?.componentInputHash === "string") {
    return {
      status: "component-input-mismatch",
      reason: "catalog-component-input-hash-differs-from-current-plan",
      image: catalogRecord.image ?? null,
      digest: catalogRecord.digest ?? null,
      reusedFrom: catalogRecord.componentInputHash ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
    };
  }
  if (componentInputChanged) {
    return {
      status: "component-source-mismatch",
      reason: "catalog-source-commit-does-not-contain-current-component-input",
      image: catalogRecord.image ?? null,
      digest: catalogRecord.digest ?? null,
      reusedFrom: catalogRecord.sourceCommitId ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
    };
  }
  if (!/^sha256:[a-f0-9]{64}$/u.test(catalogRecord.digest ?? "")) {
    return { status: "candidate-unverified-digest", image: catalogRecord.image ?? null, digest: catalogRecord.digest ?? null };
  }
  return {
    status: catalogRecord.componentInputHash ? "ready" : "candidate-without-component-metadata",
    image: catalogRecord.image,
    digest: catalogRecord.digest,
    reusedFrom: catalogRecord.componentInputHash ? catalogRecord.componentInputHash : catalogRecord.commitId ?? catalogRecord.imageTag ?? null
  };
}

function catalogComponentProvenanceStatus({ catalogRecord, catalogSourceCommitId, catalogComponentInputHash, catalogComponentInputHashAtSource, componentInputHash, dockerfileHash, buildArgsHash }) {
  if (!catalogRecord) return { safeToReuse: false, status: "candidate-no-catalog", reason: "no-previous-artifact-record" };
  if (!catalogSourceCommitId) return { safeToReuse: false, status: "component-provenance-missing", reason: "catalog-source-commit-missing" };
  if (!catalogComponentInputHash) return { safeToReuse: false, status: "component-provenance-missing", reason: "catalog-component-input-hash-missing" };
  if (!catalogComponentInputHashAtSource) return { safeToReuse: false, status: "component-source-unavailable", reason: "catalog-source-commit-unavailable" };
  if (catalogComponentInputHashAtSource !== catalogComponentInputHash) {
    return { safeToReuse: false, status: "component-source-mismatch", reason: "catalog-component-input-hash-does-not-match-catalog-source-tree" };
  }
  if (catalogComponentInputHash !== componentInputHash) {
    return { safeToReuse: false, status: "component-input-mismatch", reason: "catalog-component-input-hash-differs-from-current-plan" };
  }
  if (text(catalogRecord.dockerfileHash) && catalogRecord.dockerfileHash !== dockerfileHash) {
    return { safeToReuse: false, status: "component-build-mismatch", reason: "catalog-dockerfile-hash-differs-from-current-plan" };
  }
  if (text(catalogRecord.buildArgsHash) && catalogRecord.buildArgsHash !== buildArgsHash) {
    return { safeToReuse: false, status: "component-build-mismatch", reason: "catalog-build-args-hash-differs-from-current-plan" };
  }
  return { safeToReuse: true, status: "safe-reuse", reason: "catalog-source-tree-and-current-component-input-match" };
}

async function hashGitPathsIfCommitExists(repoRoot, targetRef, paths, options = {}) {
  try {
    await gitValue(repoRoot, ["rev-parse", "--verify", `${targetRef}^{commit}`]);
    return await hashGitPaths(repoRoot, targetRef, paths, options);
  } catch {
    return null;
  }
}

function text(value) {
  return String(value ?? "").trim();
}

function envReuseCandidate(catalogRecord, envChanged) {
  if (envChanged) return { status: "not-reused", reason: "environment-affected" };
  if (!catalogRecord) return { status: "candidate-no-catalog", reason: "no-previous-artifact-record" };
  const image = environmentImageFromCatalog(catalogRecord.serviceId, catalogRecord);
  const digest = environmentDigestFromCatalog(catalogRecord);
  if (!/^sha256:[a-f0-9]{64}$/u.test(digest ?? "")) {
    return { status: "candidate-unverified-digest", image, digest };
  }
  return {
    status: catalogRecord.environmentInputHash ? "ready" : "candidate-without-environment-metadata",
    image,
    digest,
    reusedFrom: catalogRecord.environmentInputHash ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
  };
}

function environmentImageFromCatalog(serviceId, catalogRecord) {
  const image = catalogRecord?.environmentImage ?? null;
  if (image) return image;
  const fallback = catalogRecord?.image ?? null;
  return typeof fallback === "string" && fallback.includes(`/${serviceId}-env:`) ? fallback : null;
}

function environmentDigestFromCatalog(catalogRecord) {
  return catalogRecord?.environmentDigest ?? (catalogRecord?.environmentImage ? catalogRecord?.digest : null) ?? null;
}

function digestFromImageReference(image) {
  const match = String(image ?? "").match(/@(sha256:[a-f0-9]{64})$/u);
  return match ? match[1] : null;
}

function booleanEnv(value) {
  return ["1", "true", "yes", "on"].includes(String(value ?? "").trim().toLowerCase());
}

async function probeRegistryManifest({ image, digest, timeoutMs = 3000 }) {
  const request = registryManifestRequest(image, digest);
  if (!request) return { status: "invalid-reference", image, digest, reason: "registry-reference-unparseable" };
  const result = await registryManifestHttpProbe(request, "HEAD", timeoutMs);
  if (result.status === "present" || result.status === "missing") return { ...result, image, digest };
  const fallback = await registryManifestHttpProbe(request, "GET", timeoutMs);
  return { ...fallback, image, digest };
}

function registryManifestRequest(image, digest) {
  const parsed = parseTaggedImage(image);
  if (!parsed || !/^sha256:[a-f0-9]{64}$/u.test(digest ?? "")) return null;
  const protocol = parsed.host === "127.0.0.1" || parsed.host.startsWith("127.") || parsed.host === "localhost" || parsed.host.includes(":5000")
    ? "http:"
    : "https:";
  return {
    protocol,
    host: parsed.host,
    path: `/v2/${parsed.repository}/manifests/${digest}`
  };
}

function parseTaggedImage(image) {
  const text = String(image ?? "").trim();
  const slash = text.indexOf("/");
  if (slash <= 0) return null;
  const host = text.slice(0, slash);
  const rest = text.slice(slash + 1);
  const tagSeparator = rest.lastIndexOf(":");
  if (!host || tagSeparator <= 0 || rest.includes("@")) return null;
  return {
    host,
    repository: rest.slice(0, tagSeparator)
  };
}

function registryManifestHttpProbe(request, method, timeoutMs) {
  const client = request.protocol === "http:" ? http : https;
  return new Promise((resolve) => {
    const req = client.request({
      protocol: request.protocol,
      host: request.host.includes(":") ? request.host.slice(0, request.host.lastIndexOf(":")) : request.host,
      port: request.host.includes(":") ? request.host.slice(request.host.lastIndexOf(":") + 1) : undefined,
      method,
      path: request.path,
      timeout: timeoutMs,
      headers: {
        Accept: [
          "application/vnd.oci.image.manifest.v1+json",
          "application/vnd.docker.distribution.manifest.v2+json",
          "application/vnd.oci.image.index.v1+json",
          "application/vnd.docker.distribution.manifest.list.v2+json"
        ].join(", ")
      }
    }, (res) => {
      res.resume();
      res.on("end", () => {
        if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
          resolve({ status: "present", method, statusCode: res.statusCode });
        } else if (res.statusCode === 404) {
          resolve({ status: "missing", method, statusCode: res.statusCode });
        } else {
          resolve({ status: "unknown", method, statusCode: res.statusCode ?? 0, reason: "unexpected-status" });
        }
      });
    });
    req.on("timeout", () => {
      req.destroy();
      resolve({ status: "unknown", method, statusCode: 0, reason: "timeout" });
    });
    req.on("error", (error) => {
      resolve({ status: "unknown", method, statusCode: 0, reason: error.message });
    });
    req.end();
  });
}

function stableHash(value) {
  return createHash("sha256").update(stableJson(value)).digest("hex");
}

function stableJson(value) {
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
  if (value && typeof value === "object") {
    return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
  }
  return JSON.stringify(value);
}

function uniqueSorted(values) {
  return [...new Set(values.filter(Boolean))].sort();
}

function uniquePreserveOrder(values) {
  const seen = new Set();
  const result = [];
  for (const value of values) {
    if (seen.has(value)) continue;
    seen.add(value);
    result.push(value);
  }
  return result;
}
