import { shQuote } from "../platform-infra-public-service";

import type { ExternalActiveSecretMaterial, Sub2ApiConfig, Sub2ApiHostDockerConfig, Sub2ApiTargetConfig } from "./entry";
import { requiredSecretKeys } from "./entry";
import { imageRef, targetDatabase, targetDependencyImages, targetImage } from "./manifest";

export function requireHostDockerConfig(target: Sub2ApiTargetConfig): Sub2ApiHostDockerConfig {
  if (target.hostDocker === null) throw new Error(`target ${target.id} requires hostDocker config when runtimeMode=host-docker`);
  return target.hostDocker;
}

export function hostDockerPlanSummary(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): Record<string, unknown> {
  const host = requireHostDockerConfig(target);
  const database = targetDatabase(sub2api, target);
  return {
    runtimeMode: "host-docker",
    route: target.route,
    projectName: host.projectName,
    workDir: host.workDir,
    composePath: host.composePath,
    envPath: host.envPath,
    appDataDir: host.appDataDir,
    appPort: host.appPort,
    redisPort: host.redisPort,
    databaseHost: database.host,
    databasePort: database.port,
    databaseSslMode: database.sslMode,
    preserveEnvKeys: host.preserveEnvKeys,
    noProxy: host.noProxy,
    valuesPrinted: false,
  };
}

export function hostDockerComposeYaml(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
  const host = requireHostDockerConfig(target);
  const dependencyImages = targetDependencyImages(sub2api, target);
  return `services:
  redis:
    image: ${yamlString(dependencyImages.redis)}
    container_name: ${yamlString(`${host.projectName}-redis`)}
    network_mode: host
    restart: unless-stopped
    command:
      - redis-server
      - --save
      - ""
      - --appendonly
      - "no"
      - --bind
      - "127.0.0.1"
      - --port
      - ${yamlString(String(host.redisPort))}
  app:
    image: ${yamlString(imageRef(sub2api, target))}
    container_name: ${yamlString(`${host.projectName}-app`)}
    network_mode: host
    restart: unless-stopped
    env_file:
      - ${yamlString(host.envPath)}
    volumes:
      - ${yamlString(`${host.appDataDir}:/app/data`)}
    depends_on:
      - redis
`;
}

export function hostDockerEnvFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig, secretMaterial: ExternalActiveSecretMaterial): string {
  const host = requireHostDockerConfig(target);
  const database = targetDatabase(sub2api, target);
  const urlAllowlist = sub2api.security.urlAllowlist;
  const values: Record<string, string> = {
    AUTO_SETUP: String(target.runtime.autoSetup),
    SERVER_HOST: "127.0.0.1",
    SERVER_PORT: String(host.appPort),
    SERVER_MODE: "release",
    RUN_MODE: "standard",
    DATABASE_HOST: database.host,
    DATABASE_PORT: String(database.port),
    DATABASE_USER: database.user,
    DATABASE_DBNAME: database.dbName,
    DATABASE_SSLMODE: database.sslMode,
    DATABASE_PASSWORD: secretMaterial.values.POSTGRES_PASSWORD,
    DATABASE_MAX_OPEN_CONNS: "10",
    DATABASE_MAX_IDLE_CONNS: "2",
    DATABASE_CONN_MAX_LIFETIME_MINUTES: "30",
    DATABASE_CONN_MAX_IDLE_TIME_MINUTES: "5",
    REDIS_HOST: "127.0.0.1",
    REDIS_PORT: String(host.redisPort),
    REDIS_PASSWORD: "",
    REDIS_DB: "0",
    REDIS_POOL_SIZE: "32",
    REDIS_MIN_IDLE_CONNS: "2",
    REDIS_ENABLE_TLS: "false",
    ADMIN_EMAIL: secretMaterial.adminEmail,
    ADMIN_PASSWORD: secretMaterial.values.ADMIN_PASSWORD,
    JWT_SECRET: secretMaterial.values.JWT_SECRET,
    TOTP_ENCRYPTION_KEY: secretMaterial.values.TOTP_ENCRYPTION_KEY,
    JWT_EXPIRE_HOUR: "24",
    TZ: "Asia/Shanghai",
    SECURITY_URL_ALLOWLIST_ENABLED: String(urlAllowlist.enabled),
    SECURITY_URL_ALLOWLIST_ALLOW_INSECURE_HTTP: String(urlAllowlist.allowInsecureHttp),
    SECURITY_URL_ALLOWLIST_ALLOW_PRIVATE_HOSTS: String(urlAllowlist.allowPrivateHosts),
    SECURITY_URL_ALLOWLIST_UPSTREAM_HOSTS: urlAllowlist.upstreamHosts.join(","),
    UPDATE_PROXY_URL: "",
    HTTP_PROXY: "",
    HTTPS_PROXY: "",
    ALL_PROXY: "",
    http_proxy: "",
    https_proxy: "",
    all_proxy: "",
    NO_PROXY: host.noProxy.join(","),
    no_proxy: host.noProxy.join(","),
    GATEWAY_OPENAI_RESPONSE_HEADER_TIMEOUT: "0",
    GATEWAY_OPENAI_HTTP2_ENABLED: "true",
    GATEWAY_OPENAI_HTTP2_ALLOW_PROXY_FALLBACK_TO_HTTP1: "true",
    GATEWAY_OPENAI_HTTP2_FALLBACK_ERROR_THRESHOLD: "2",
    GATEWAY_OPENAI_HTTP2_FALLBACK_WINDOW_SECONDS: "60",
    GATEWAY_OPENAI_HTTP2_FALLBACK_TTL_SECONDS: "600",
    GATEWAY_IMAGE_STREAM_DATA_INTERVAL_TIMEOUT: "900",
    GATEWAY_IMAGE_STREAM_KEEPALIVE_INTERVAL: "10",
    GATEWAY_IMAGE_CONCURRENCY_ENABLED: "false",
    GATEWAY_IMAGE_CONCURRENCY_MAX_CONCURRENT_REQUESTS: "0",
    GATEWAY_IMAGE_CONCURRENCY_OVERFLOW_MODE: "reject",
    GATEWAY_IMAGE_CONCURRENCY_WAIT_TIMEOUT_SECONDS: "30",
    GATEWAY_IMAGE_CONCURRENCY_MAX_WAITING_REQUESTS: "100",
  };
  return `${Object.entries(values).map(([key, value]) => `${key}=${envFileValue(value)}`).join("\n")}\n`;
}

export function hostDockerDryRunScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
  const host = requireHostDockerConfig(target);
  const compose = hostDockerComposeYaml(sub2api, target);
  const composeB64 = Buffer.from(compose, "utf8").toString("base64");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
compose="$tmp/docker-compose.yml"
printf '%s' '${composeB64}' | base64 -d >"$compose"
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
compose_out="$tmp/compose.out"
compose_err="$tmp/compose.err"
ports_out="$tmp/ports.out"
ports_err="$tmp/ports.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
  docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
  docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
  $docker_bin ps --format '{{json .}}' >"$docker_out" 2>"$docker_err"
  docker_rc=$?
  if $docker_bin compose version >"$compose_out" 2>"$compose_err"; then
    compose_rc=0
  elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >"$compose_out" 2>"$compose_err"; then
    compose_rc=0
  else
    compose_rc=0
    printf '%s\\n' 'docker compose is absent; apply will use raw docker run fallback' >"$compose_out"
  fi
else
  docker_rc=1
  compose_rc=1
  : >"$docker_out"
  printf '%s\\n' 'docker daemon is not accessible' >"$docker_err"
  : >"$compose_out"
  printf '%s\\n' 'docker compose is not accessible' >"$compose_err"
fi
{
  for port in ${host.appPort} ${host.redisPort}; do
    state="free"
    if command -v ss >/dev/null 2>&1 && ss -ltn | awk '{print $4}' | grep -E "(:|\\])$port$" >/dev/null 2>&1; then
      state="listening"
    fi
    printf 'port=%s state=%s\\n' "$port" "$state"
  done
} >"$ports_out" 2>"$ports_err"
ports_rc=$?
python3 - "$docker_rc" "$compose_rc" "$ports_rc" "$docker_out" "$docker_err" "$compose_out" "$compose_err" "$ports_out" "$ports_err" <<'PY'
import json
import sys

docker_rc, compose_rc, ports_rc = [int(value) for value in sys.argv[1:4]]
paths = sys.argv[4:]

def text(path, limit=4000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""

payload = {
    "ok": docker_rc == 0 and compose_rc == 0 and ports_rc == 0,
    "target": "${target.id}",
    "runtimeMode": "host-docker",
    "route": "${target.route}",
    "projectName": "${host.projectName}",
    "composePath": "${host.composePath}",
    "envPath": "${host.envPath}",
    "appDataDir": "${host.appDataDir}",
    "image": "${imageRef(sub2api, target)}",
    "dependencyImages": ${JSON.stringify(targetDependencyImages(sub2api, target))},
    "ports": {
        "app": ${host.appPort},
        "redis": ${host.redisPort},
        "observed": text(paths[4], 2000),
    },
    "steps": {
        "docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
        "dockerCompose": {"exitCode": compose_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
        "portObservation": {"exitCode": ports_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

export function hostDockerApplyScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig, secretMaterial: ExternalActiveSecretMaterial | null): string {
  if (secretMaterial === null) throw new Error("host-docker apply requires external-active secret material");
  const host = requireHostDockerConfig(target);
  const compose = hostDockerComposeYaml(sub2api, target);
  const env = hostDockerEnvFile(sub2api, target, secretMaterial);
  const composeB64 = Buffer.from(compose, "utf8").toString("base64");
  const envB64 = Buffer.from(env, "utf8").toString("base64");
  const preserveEnvKeysB64 = Buffer.from(host.preserveEnvKeys.join("\n"), "utf8").toString("base64");
  const image = targetImage(sub2api, target);
  const dependencyImages = targetDependencyImages(sub2api, target);
  const pullCommand = image.pullPolicy === "Always"
    ? `$docker_bin pull ${shQuote(imageRef(sub2api, target))} >"$pull_out" 2>"$pull_err"\napp_pull_rc=$?\n$docker_bin pull ${shQuote(dependencyImages.redis)} >>"$pull_out" 2>>"$pull_err"\nredis_pull_rc=$?\nif [ "$app_pull_rc" -eq 0 ] && [ "$redis_pull_rc" -eq 0 ]; then pull_rc=0; else pull_rc=1; fi`
    : `: >"$pull_out"\n: >"$pull_err"\npull_rc=0`;
  const secretSourceSummary = {
    sourceRef: secretMaterial.sourceRef,
    sourcePath: secretMaterial.sourcePath,
    appSourceRef: secretMaterial.appSourceRef,
    appSourcePath: secretMaterial.appSourcePath,
    sourceAction: secretMaterial.action,
    fingerprint: secretMaterial.fingerprint,
    adminEmail: secretMaterial.adminEmail,
    valuesPrinted: false,
  };
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
compose_tmp="$tmp/docker-compose.yml"
env_tmp="$tmp/sub2api.env"
preserve_keys="$tmp/preserve-env-keys.txt"
existing_env="$tmp/existing.env"
printf '%s' '${composeB64}' | base64 -d >"$compose_tmp"
printf '%s' '${envB64}' | base64 -d >"$env_tmp"
printf '%s' '${preserveEnvKeysB64}' | base64 -d >"$preserve_keys"
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
preserve_out="$tmp/preserve-env.out"
preserve_err="$tmp/preserve-env.err"
install_out="$tmp/install.out"
install_err="$tmp/install.err"
pull_out="$tmp/pull.out"
pull_err="$tmp/pull.err"
legacy_out="$tmp/legacy.out"
legacy_err="$tmp/legacy.err"
up_out="$tmp/up.out"
up_err="$tmp/up.err"
health_out="$tmp/health.out"
health_err="$tmp/health.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
  docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
  docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
  if $docker_bin compose version >"$docker_out" 2>"$docker_err"; then
    compose_cmd="$docker_bin compose"
    compose_mode="plugin"
    docker_rc=0
  elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >"$docker_out" 2>"$docker_err"; then
    compose_cmd="docker-compose"
    compose_mode="standalone"
    docker_rc=0
  else
    compose_cmd=""
    compose_mode="raw-docker"
    $docker_bin version >"$docker_out" 2>"$docker_err"
    docker_rc=$?
  fi
else
  compose_cmd=""
  compose_mode="none"
  docker_rc=1
  : >"$docker_out"
  printf '%s\\n' 'docker daemon or docker compose is not accessible' >"$docker_err"
fi
preserve_rc=1
install_rc=1
if [ "$docker_rc" -eq 0 ]; then
  preserve_rc=0
  : >"$existing_env"
  if [ -s "$preserve_keys" ] && [ -f ${shQuote(host.envPath)} ]; then
    if ! cat ${shQuote(host.envPath)} >"$existing_env" 2>"$preserve_err"; then
      if ! sudo cat ${shQuote(host.envPath)} >"$existing_env" 2>>"$preserve_err"; then
        preserve_rc=1
      fi
    fi
  else
    : >"$preserve_err"
  fi
  if [ "$preserve_rc" -eq 0 ]; then
    python3 - "$env_tmp" "$existing_env" "$preserve_keys" >"$preserve_out" 2>"$preserve_err" <<'PY'
import json
import re
import sys

env_path, existing_path, keys_path = sys.argv[1:]
keys = []
with open(keys_path, encoding="utf-8", errors="replace") as handle:
    for line in handle:
        key = line.strip()
        if key:
            keys.append(key)
existing = {}
try:
    lines = open(existing_path, encoding="utf-8", errors="replace").read().splitlines()
except FileNotFoundError:
    lines = []
for line in lines:
    stripped = line.strip()
    if not stripped or stripped.startswith("#") or "=" not in stripped:
        continue
    key = stripped.split("=", 1)[0].strip()
    if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", key):
        existing[key] = line
current = open(env_path, encoding="utf-8", errors="replace").read()
current_keys = set()
for line in current.splitlines():
    stripped = line.strip()
    if stripped and not stripped.startswith("#") and "=" in stripped:
        current_keys.add(stripped.split("=", 1)[0].strip())
append_lines = []
missing = []
skipped_present = []
for key in keys:
    if key in current_keys:
        skipped_present.append(key)
    elif key in existing:
        append_lines.append(existing[key])
    else:
        missing.append(key)
if append_lines:
    with open(env_path, "a", encoding="utf-8") as handle:
        if not current.endswith("\\n"):
            handle.write("\\n")
        handle.write("\\n".join(append_lines) + "\\n")
print(json.dumps({
    "ok": True,
    "keys": keys,
    "preservedKeys": [line.split("=", 1)[0] for line in append_lines],
    "missingKeys": missing,
    "skippedAlreadyRenderedKeys": skipped_present,
    "valuesPrinted": False,
}, ensure_ascii=False))
PY
    preserve_rc=$?
  else
    printf '{"ok":false,"error":"read-existing-env-failed","valuesPrinted":false}\\n' >"$preserve_out"
  fi
  if [ "$preserve_rc" -eq 0 ]; then
    sudo mkdir -p ${shQuote(host.workDir)} ${shQuote(host.appDataDir)} "$(dirname ${shQuote(host.composePath)})" "$(dirname ${shQuote(host.envPath)})" >"$install_out" 2>"$install_err"
    install_rc=$?
    if [ "$install_rc" -eq 0 ]; then
      sudo install -m 0644 "$compose_tmp" ${shQuote(host.composePath)} >>"$install_out" 2>>"$install_err"
      install_rc=$?
    fi
    if [ "$install_rc" -eq 0 ]; then
      current_uid="$(id -u)"
      current_gid="$(id -g)"
      sudo install -m 0600 -o "$current_uid" -g "$current_gid" "$env_tmp" ${shQuote(host.envPath)} >>"$install_out" 2>>"$install_err"
      install_rc=$?
    fi
  else
    : >"$install_out"
    printf '%s\\n' 'skipped because preserve env failed' >"$install_err"
    install_rc=1
  fi
else
  : >"$preserve_out"
  : >"$preserve_err"
  : >"$install_out"
  : >"$install_err"
fi
if [ "$install_rc" -eq 0 ]; then
${pullCommand}
else
  : >"$pull_out"
  printf '%s\\n' 'skipped because install failed' >"$pull_err"
  pull_rc=1
fi
if [ "$pull_rc" -eq 0 ]; then
  if [ -n "$compose_cmd" ]; then
    legacy_rc=0
    : >"$legacy_out"
    : >"$legacy_err"
    for container_name in ${shQuote(`${host.projectName}-app`)} ${shQuote(`${host.projectName}-redis`)}; do
      if $docker_bin inspect "$container_name" >/dev/null 2>&1; then
        compose_project="$($docker_bin inspect -f '{{ index .Config.Labels "com.docker.compose.project" }}' "$container_name" 2>/dev/null || true)"
        if [ "$compose_project" != ${shQuote(host.projectName)} ]; then
          if $docker_bin rm -f "$container_name" >>"$legacy_out" 2>>"$legacy_err"; then
            printf 'removed legacy container name=%s\\n' "$container_name" >>"$legacy_out"
          else
            legacy_rc=1
          fi
        else
          printf 'kept compose container name=%s project=%s\\n' "$container_name" "$compose_project" >>"$legacy_out"
        fi
      else
        printf 'container absent name=%s\\n' "$container_name" >>"$legacy_out"
      fi
    done
    if [ "$legacy_rc" -eq 0 ]; then
      $compose_cmd -p ${shQuote(host.projectName)} -f ${shQuote(host.composePath)} up -d --remove-orphans >"$up_out" 2>"$up_err"
      up_rc=$?
    else
      : >"$up_out"
      printf '%s\\n' 'skipped because legacy container takeover failed' >"$up_err"
      up_rc=1
    fi
  else
    legacy_rc=0
    printf '%s\\n' 'not-applicable raw-docker mode' >"$legacy_out"
    : >"$legacy_err"
    $docker_bin rm -f ${shQuote(`${host.projectName}-app`)} ${shQuote(`${host.projectName}-redis`)} >"$up_out" 2>"$up_err" || true
    $docker_bin run -d --name ${shQuote(`${host.projectName}-redis`)} \\
      --restart unless-stopped \\
      --network host \\
      ${shQuote(dependencyImages.redis)} \\
      redis-server --save "" --appendonly no --bind 127.0.0.1 --port ${host.redisPort} >>"$up_out" 2>>"$up_err"
    redis_run_rc=$?
    if [ "$redis_run_rc" -eq 0 ]; then
      $docker_bin run -d --name ${shQuote(`${host.projectName}-app`)} \\
        --restart unless-stopped \\
        --network host \\
        --env-file ${shQuote(host.envPath)} \\
        -v ${shQuote(`${host.appDataDir}:/app/data`)} \\
        ${shQuote(imageRef(sub2api, target))} >>"$up_out" 2>>"$up_err"
      app_run_rc=$?
    else
      app_run_rc=1
    fi
    if [ "$redis_run_rc" -eq 0 ] && [ "$app_run_rc" -eq 0 ]; then up_rc=0; else up_rc=1; fi
  fi
else
  : >"$legacy_out"
  printf '%s\\n' 'skipped because pull failed' >"$legacy_err"
  legacy_rc=1
  : >"$up_out"
  printf '%s\\n' 'skipped because pull failed' >"$up_err"
  up_rc=1
fi
health_rc=1
if [ "$up_rc" -eq 0 ]; then
  deadline=$(( $(date +%s) + 180 ))
  while [ "$(date +%s)" -lt "$deadline" ]; do
    if curl -fsS --max-time 5 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"; then
      health_rc=0
      break
    fi
    sleep 3
  done
else
  : >"$health_out"
  printf '%s\\n' 'skipped because compose up failed' >"$health_err"
fi
python3 - "$docker_rc" "$preserve_rc" "$install_rc" "$pull_rc" "$legacy_rc" "$up_rc" "$health_rc" "$compose_mode" "$docker_out" "$docker_err" "$preserve_out" "$preserve_err" "$install_out" "$install_err" "$pull_out" "$pull_err" "$legacy_out" "$legacy_err" "$up_out" "$up_err" "$health_out" "$health_err" <<'PY'
import json
import sys

docker_rc, preserve_rc, install_rc, pull_rc, legacy_rc, up_rc, health_rc = [int(value) for value in sys.argv[1:8]]
docker_mode = sys.argv[8]
paths = sys.argv[9:]

def text(path, limit=4000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""

def parsed(path):
    try:
        return json.load(open(path, encoding="utf-8", errors="replace"))
    except Exception:
        return None

payload = {
    "ok": docker_rc == 0 and preserve_rc == 0 and install_rc == 0 and pull_rc == 0 and legacy_rc == 0 and up_rc == 0 and health_rc == 0,
    "target": "${target.id}",
    "runtimeMode": "host-docker",
    "route": "${target.route}",
    "projectName": "${host.projectName}",
    "dockerMode": docker_mode,
    "composePath": "${host.composePath}",
    "envPath": "${host.envPath}",
    "appDataDir": "${host.appDataDir}",
    "image": "${imageRef(sub2api, target)}",
    "secret": {
        "requiredKeys": ${JSON.stringify(requiredSecretKeys)},
        "externalActiveSource": json.loads(${JSON.stringify(JSON.stringify(secretSourceSummary))}),
        "valuesPrinted": False,
    },
    "steps": {
        "docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
        "preserveEnv": {"exitCode": preserve_rc, "summary": parsed(paths[2]), "stdout": text(paths[2]), "stderr": text(paths[3])},
        "installFiles": {"exitCode": install_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
        "pull": {"exitCode": pull_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
        "legacyTakeover": {"exitCode": legacy_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
        "composeUp": {"exitCode": up_rc, "stdout": text(paths[10]), "stderr": text(paths[11])},
        "localHealth": {"exitCode": health_rc, "stdout": text(paths[12]), "stderr": text(paths[13])},
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

export function hostDockerStatusScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
  const host = requireHostDockerConfig(target);
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
ps_out="$tmp/ps.out"
ps_err="$tmp/ps.err"
health_out="$tmp/health.out"
health_err="$tmp/health.err"
caddy_out="$tmp/caddy.out"
caddy_err="$tmp/caddy.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
  docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
  docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
  $docker_bin version --format '{{json .}}' >"$docker_out" 2>"$docker_err"
  docker_rc=$?
  $docker_bin ps -a --filter label=com.docker.compose.project=${shQuote(host.projectName)} --format '{{json .}}' >"$ps_out" 2>"$ps_err"
  $docker_bin ps -a --filter name=${shQuote(`${host.projectName}-`)} --format '{{json .}}' >>"$ps_out" 2>>"$ps_err"
  ps_rc=$?
else
  docker_rc=1
  ps_rc=1
  : >"$docker_out"
  printf '%s\\n' 'docker daemon is not accessible' >"$docker_err"
  : >"$ps_out"
  : >"$ps_err"
fi
curl -fsS --max-time 5 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"
health_rc=$?
systemctl is-active caddy >"$caddy_out" 2>"$caddy_err"
caddy_rc=$?
python3 - "$docker_rc" "$ps_rc" "$health_rc" "$caddy_rc" "$docker_out" "$docker_err" "$ps_out" "$ps_err" "$health_out" "$health_err" "$caddy_out" "$caddy_err" <<'PY'
import json
import sys

docker_rc, ps_rc, health_rc, caddy_rc = [int(value) for value in sys.argv[1:5]]
paths = sys.argv[5:]

def text(path, limit=4000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""

containers_by_key = {}
for line in text(paths[2], 20000).splitlines():
    try:
        container = json.loads(line)
        key = container.get("ID") or "\\0".join([
            str(container.get("Names") or container.get("names") or ""),
            str(container.get("Image") or container.get("image") or ""),
            str(container.get("CreatedAt") or container.get("createdAt") or ""),
        ])
        if key not in containers_by_key:
            containers_by_key[key] = container
    except json.JSONDecodeError:
        pass
containers = list(containers_by_key.values())

payload = {
    "ok": docker_rc == 0 and ps_rc == 0 and health_rc == 0,
    "target": "${target.id}",
    "runtimeMode": "host-docker",
    "route": "${target.route}",
    "projectName": "${host.projectName}",
    "serviceDns": "host-docker:127.0.0.1:${host.appPort}",
    "containers": containers,
    "secret": {"managedByThisApply": True, "valuesPrinted": False},
    "networkPolicy": {"ok": True, "mode": "not-applicable-host-docker"},
    "egressProxy": {"enabled": False},
    "sentinel": {"enabled": False, "mode": "not-deployed-host-docker"},
    "checks": {
        "docker": {"exitCode": docker_rc, "stdout": text(paths[0]), "stderr": text(paths[1])},
        "composeContainers": {"exitCode": ps_rc, "stdout": text(paths[2], 8000), "stderr": text(paths[3])},
        "localHealth": {"exitCode": health_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
        "caddyService": {"exitCode": caddy_rc, "stdout": text(paths[6]), "stderr": text(paths[7])},
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

export function hostDockerValidateScript(sub2api: Sub2ApiConfig, target: Sub2ApiTargetConfig): string {
  const host = requireHostDockerConfig(target);
  const database = targetDatabase(sub2api, target);
  const exposure = target.publicExposure?.enabled ? target.publicExposure : null;
  const publicProbe = exposure === null ? `
public_rc=0
: >"$tmp/public.out"
: >"$tmp/public.err"
` : `
curl -kfsS --max-time 20 --resolve ${shQuote(`${exposure.dns.hostname}:443:127.0.0.1`)} ${shQuote(`${exposure.publicBaseUrl}/health`)} >"$tmp/public.out" 2>"$tmp/public.err"
public_rc=$?
`;
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
docker_out="$tmp/docker.out"
docker_err="$tmp/docker.err"
app_out="$tmp/app.out"
app_err="$tmp/app.err"
redis_out="$tmp/redis.out"
redis_err="$tmp/redis.err"
pg_out="$tmp/pg.out"
pg_err="$tmp/pg.err"
health_out="$tmp/health.out"
health_err="$tmp/health.err"
docker_bin=""
if docker ps >/dev/null 2>&1; then
  docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
  docker_bin="sudo docker"
fi
if [ -n "$docker_bin" ]; then
  $docker_bin inspect -f '{{.State.Running}}' ${shQuote(`${host.projectName}-app`)} >"$app_out" 2>"$app_err"
  app_rc=$?
  $docker_bin exec ${shQuote(`${host.projectName}-redis`)} redis-cli -h 127.0.0.1 -p ${host.redisPort} ping >"$redis_out" 2>"$redis_err"
  redis_rc=$?
  $docker_bin ps --filter label=com.docker.compose.project=${shQuote(host.projectName)} --format '{{json .}}' >"$docker_out" 2>"$docker_err"
  $docker_bin ps --filter name=${shQuote(`${host.projectName}-`)} --format '{{json .}}' >>"$docker_out" 2>>"$docker_err"
  docker_rc=$?
else
  app_rc=1
  redis_rc=1
  docker_rc=1
  : >"$app_out"
  : >"$redis_out"
  : >"$docker_out"
  printf '%s\\n' 'docker daemon is not accessible' >"$app_err"
  printf '%s\\n' 'docker daemon is not accessible' >"$redis_err"
  printf '%s\\n' 'docker daemon is not accessible' >"$docker_err"
fi
if command -v pg_isready >/dev/null 2>&1; then
  pg_isready -h ${shQuote(database.host)} -p ${database.port} -U ${shQuote(database.user)} -d ${shQuote(database.dbName)} >"$pg_out" 2>"$pg_err"
  pg_rc=$?
else
  pg_rc=127
  : >"$pg_out"
  printf '%s\\n' 'pg_isready is not installed on PK01 host' >"$pg_err"
fi
curl -fsS --max-time 10 http://127.0.0.1:${host.appPort}/health >"$health_out" 2>"$health_err"
health_rc=$?
${publicProbe}
python3 - "$docker_rc" "$app_rc" "$redis_rc" "$pg_rc" "$health_rc" "$public_rc" "$docker_out" "$docker_err" "$app_out" "$app_err" "$redis_out" "$redis_err" "$pg_out" "$pg_err" "$health_out" "$health_err" "$tmp/public.out" "$tmp/public.err" <<'PY'
import json
import sys

docker_rc, app_rc, redis_rc, pg_rc, health_rc, public_rc = [int(value) for value in sys.argv[1:7]]
paths = sys.argv[7:]

def text(path, limit=4000):
    try:
        return open(path, encoding="utf-8", errors="replace").read()[-limit:]
    except FileNotFoundError:
        return ""

payload = {
    "ok": docker_rc == 0 and app_rc == 0 and redis_rc == 0 and pg_rc == 0 and health_rc == 0 and public_rc == 0,
    "target": "${target.id}",
    "runtimeMode": "host-docker",
    "route": "${target.route}",
    "serviceDns": "host-docker:127.0.0.1:${host.appPort}",
    "checks": {
        "dockerComposeProject": {"exitCode": docker_rc, "stdout": text(paths[0], 8000), "stderr": text(paths[1])},
        "appContainerRunning": {"exitCode": app_rc, "stdout": text(paths[2]), "stderr": text(paths[3])},
        "redisPing": {"exitCode": redis_rc, "stdout": text(paths[4]), "stderr": text(paths[5])},
        "postgresLocalPgIsReady": {"exitCode": pg_rc, "host": "${database.host}", "port": ${database.port}, "user": "${database.user}", "dbName": "${database.dbName}", "stdout": text(paths[6]), "stderr": text(paths[7])},
        "sub2apiLocalHealth": {"exitCode": health_rc, "stdout": text(paths[8]), "stderr": text(paths[9])},
        "sub2apiPublicHealthViaPk01Caddy": {"exitCode": public_rc, "stdout": text(paths[10]), "stderr": text(paths[11])},
        "sentinel": {"ok": True, "mode": "not-deployed-host-docker"},
        "egressProxy": {"ok": True, "mode": "not-deployed-host-docker"},
    },
    "valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

function yamlString(value: string): string {
  return JSON.stringify(value);
}

function envFileValue(value: string): string {
  if (/^[A-Za-z0-9_./:@,+-]*$/u.test(value)) return value;
  return JSON.stringify(value);
}
