import { Buffer } from "node:buffer";
import type { UniDeskConfig } from "./config";
import { applyPk01CaddyManagedBlock, caddyManagedBlockMarkers } from "./pk01-caddy";
import { capture, compactCapture, fingerprintValues, parseJsonOutput, redactText, shQuote } from "./platform-infra-ops-library";
export { capture, compactCapture, fingerprintValues, parseJsonOutput, redactText, shQuote };

export interface PublicServiceExposure {
  enabled: boolean;
  publicBaseUrl: string;
  dns: { hostname: string; expectedA: string; resolvers: string[] };
  frpc: {
    deploymentName: string;
    secretName: string;
    secretKey: string;
    image: string;
    serverAddr: string;
    serverPort: number;
    proxyName: string;
    remotePort: number;
    localIP: string;
    localPort: number;
    tokenSourceRef: string;
    tokenSourceKey: string;
  };
  pk01: {
    route: string;
    caddyConfigPath: string;
    caddyServiceName: string;
    responseHeaderTimeoutSeconds: number;
  };
}

export interface PublicServiceTarget {
  id: string;
  route: string;
  namespace: string;
  replicas: number;
  publicExposure: PublicServiceExposure;
}

export interface FrpcSecretMaterial {
  sourceRef: string;
  sourcePath: string;
  secretName: string;
  secretKey: string;
  frpcToml: string;
  fingerprint: string;
  valuesPrinted: false;
}

export async function applyPk01CaddyBlock(
  config: UniDeskConfig,
  serviceId: string,
  exposure: PublicServiceExposure,
  markers: { start: string; end: string } = caddyManagedBlockMarkers(serviceId),
): Promise<Record<string, unknown>> {
  return await applyPk01CaddyManagedBlock(config, serviceId, exposure, markers);
}

export function prepareFrpcSecret(params: {
  secretRoot: string;
  exposure: PublicServiceExposure;
  sourcePathRedactor: (path: string) => string;
  parseEnvFile: (text: string) => Record<string, string>;
  requiredEnvValue: (values: Record<string, string>, key: string, sourceRef: string) => string;
  readTextFile: (path: string) => string;
}): FrpcSecretMaterial {
  const { exposure } = params;
  const sourcePath = `${params.secretRoot.replace(/\/+$/u, "")}/${exposure.frpc.tokenSourceRef}`;
  const values = params.parseEnvFile(params.readTextFile(sourcePath));
  const token = params.requiredEnvValue(values, exposure.frpc.tokenSourceKey, exposure.frpc.tokenSourceRef);
  const frpcToml = [
    `serverAddr = "${exposure.frpc.serverAddr}"`,
    `serverPort = ${exposure.frpc.serverPort}`,
    "loginFailExit = true",
    `auth.token = "${escapeTomlString(token)}"`,
    "",
    "[[proxies]]",
    `name = "${exposure.frpc.proxyName}"`,
    'type = "tcp"',
    `localIP = "${exposure.frpc.localIP}"`,
    `localPort = ${exposure.frpc.localPort}`,
    `remotePort = ${exposure.frpc.remotePort}`,
    "",
  ].join("\n");
  return {
    sourceRef: exposure.frpc.tokenSourceRef,
    sourcePath: params.sourcePathRedactor(sourcePath),
    secretName: exposure.frpc.secretName,
    secretKey: exposure.frpc.secretKey,
    frpcToml,
    fingerprint: fingerprintValues({ token, frpcToml }, ["token", "frpcToml"]),
    valuesPrinted: false,
  };
}

export function renderFrpcManifest(target: PublicServiceTarget): string {
  const exposure = target.publicExposure;
  if (!exposure.enabled) return "";
  return `---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${exposure.frpc.deploymentName}
  namespace: ${target.namespace}
  labels:
    app.kubernetes.io/name: ${exposure.frpc.deploymentName}
    app.kubernetes.io/component: tunnel
    app.kubernetes.io/part-of: platform-infra
    app.kubernetes.io/managed-by: unidesk
    unidesk.ai/runtime-node: ${target.id}
    unidesk.ai/public-hostname: ${exposure.dns.hostname}
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: ${exposure.frpc.deploymentName}
      app.kubernetes.io/component: tunnel
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ${exposure.frpc.deploymentName}
        app.kubernetes.io/component: tunnel
        app.kubernetes.io/part-of: platform-infra
      annotations:
        unidesk.ai/public-base-url: "${exposure.publicBaseUrl}"
        unidesk.ai/frp-server: "${exposure.frpc.serverAddr}:${exposure.frpc.serverPort}"
        unidesk.ai/frp-remote-port: "${exposure.frpc.remotePort}"
    spec:
      containers:
        - name: frpc
          image: ${exposure.frpc.image}
          imagePullPolicy: IfNotPresent
          args:
            - -c
            - /etc/frp/frpc.toml
          volumeMounts:
            - name: frpc-config
              mountPath: /etc/frp/frpc.toml
              subPath: ${exposure.frpc.secretKey}
              readOnly: true
      volumes:
        - name: frpc-config
          secret:
            secretName: ${exposure.frpc.secretName}
`;
}

export function publicServicePolicyChecks(yaml: string, target: PublicServiceTarget, serviceName: string): Array<Record<string, unknown>> {
  return [
    { name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: `${serviceName} public exposure must use PK01 Caddy+FRP, not Kubernetes Ingress.` },
    { name: "no-nodeport-or-loadbalancer", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Services must stay ClusterIP." },
    { name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not use host network." },
    { name: "no-host-port", ok: !/^\s*hostPort:\s*[0-9]+\s*$/mu.test(yaml), detail: "Pods must not expose host ports." },
    { name: "no-cpu-memory-resources", ok: !/^\s*(cpu|memory):\s*/mu.test(yaml), detail: "No CPU/memory request or limit objects are rendered." },
    { name: "allow-all-network-policy", ok: hasAllowAllNetworkPolicy(yaml, target.namespace), detail: `NetworkPolicy/allow-all exists in ${target.namespace}.` },
  ];
}

export function dryRunManifestScript(params: { yaml: string; target: PublicServiceTarget; fieldManager: string; manifestName: string }): string {
  const encoded = Buffer.from(params.yaml, "utf8").toString("base64");
  return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
manifest="$tmp/${params.manifestName}.k8s.yaml"
printf '%s' '${encoded}' | base64 -d > "$manifest"
kubectl apply --dry-run=client -f "$manifest" >"$tmp/client.out" 2>"$tmp/client.err"
client_rc=$?
if kubectl get namespace ${params.target.namespace} >/dev/null 2>&1; then
  kubectl apply --server-side --dry-run=server --field-manager=${params.fieldManager} -f "$manifest" >"$tmp/server.out" 2>"$tmp/server.err"
  server_rc=$?
  server_disposition=executed
else
  : >"$tmp/server.err"
  printf '%s\\n' 'server dry-run skipped because namespace does not exist yet' >"$tmp/server.out"
  server_rc=0
  server_disposition=skipped-namespace-missing
fi
python3 - "$client_rc" "$server_rc" "$server_disposition" "$tmp/client.out" "$tmp/client.err" "$tmp/server.out" "$tmp/server.err" <<'PY'
import json, sys
client_rc, server_rc = int(sys.argv[1]), int(sys.argv[2])
def text(path):
    try:
        return open(path, encoding="utf-8", errors="replace").read()
    except FileNotFoundError:
        return ""
payload = {
    "ok": client_rc == 0 and server_rc == 0,
    "target": "${params.target.id}",
    "namespace": "${params.target.namespace}",
    "clientDryRun": {"exitCode": client_rc, "stdout": text(sys.argv[4])[-4000:], "stderr": text(sys.argv[5])[-4000:]},
    "serverDryRun": {"exitCode": server_rc, "disposition": sys.argv[3], "stdout": text(sys.argv[6])[-4000:], "stderr": text(sys.argv[7])[-4000:]},
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}

export function publicHttpProbe(baseUrl: string, path: string, options: { headers?: string[] } = {}): Record<string, unknown> {
  const url = `${baseUrl.replace(/\/+$/u, "")}${path}`;
  const args = ["-fsS", "--connect-timeout", "10", "--max-time", "30", "-o", "-", "-w", "\n%{http_code}"];
  for (const header of options.headers ?? []) {
    args.unshift(header);
    args.unshift("-H");
  }
  args.push(url);
  const result = Bun.spawnSync(["curl", ...args], { stdout: "pipe", stderr: "pipe" });
  const stdout = new TextDecoder().decode(result.stdout);
  const stderr = new TextDecoder().decode(result.stderr);
  const lines = stdout.split(/\r?\n/u);
  const statusText = lines.pop() ?? "";
  const status = Number(statusText);
  const body = lines.join("\n");
  return {
    ok: result.exitCode === 0 && Number.isInteger(status) && status >= 200 && status < 500,
    url,
    status: Number.isInteger(status) ? status : null,
    bodyBytes: Buffer.byteLength(body, "utf8"),
    bodyPreview: redactText(body).slice(0, 2000),
    stderrTail: redactText(stderr).slice(-2000),
    headersUsed: options.headers?.length ?? 0,
    valuesPrinted: false,
  };
}

export function escapeTomlString(value: string): string {
  return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
}

function hasAllowAllNetworkPolicy(yaml: string, namespaceName: string): boolean {
  return yaml.split(/^---\s*$/mu).some((document) => /^\s*kind:\s*NetworkPolicy\s*$/mu.test(document)
    && /^\s*name:\s*allow-all\s*$/mu.test(document)
    && new RegExp(`^\\s*namespace:\\s*${escapeRegExp(namespaceName)}\\s*$`, "mu").test(document)
    && /^\s*podSelector:\s*\{\}\s*$/mu.test(document));
}

function escapeRegExp(value: string): string {
  return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
