import { spawnSync } from "node:child_process";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import type { RenderedCliResult } from "./output";
import { applyPk01CaddySiteBlock, caddyManagedBlockMarkers, renderSimpleReverseProxyCaddySiteBlock } from "./pk01-caddy";
import {
  asRecord,
  booleanField,
  integerField,
  parseOpsApplyOptions,
  parseOpsCommonOptions,
  readYamlRecord,
  recordField,
  stringField,
} from "./platform-infra-ops-library";

const configPath = rootPath("config", "frontend.yaml");
const configLabel = "config/frontend.yaml";
const serviceName = "unidesk-frontend";

interface FrontendExposureConfig {
  metadata: { name: string };
  publicExposure: {
    enabled: boolean;
    publicBaseUrl: string;
    dns: { hostname: string; expectedA: string; resolvers: string[] };
    upstream: { host: string; port: number };
    pk01: {
      route: string;
      caddyConfigPath: string;
      caddyServiceName: string;
      responseHeaderTimeoutSeconds: number;
    };
    probes: { healthPath: string; rootPath: string };
  };
}

export async function runFrontendPublicExposureCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
  const [scope = "public-exposure", action = "plan", ...rest] = args;
  if (scope !== "public-exposure") return { ok: false, error: "unsupported-frontend-command", args, help: frontendPublicExposureHelp() };
  if (action === "plan") {
    const result = plan();
    return wantsFull(rest) ? result : renderPlan(result);
  }
  if (action === "apply") {
    const options = parseOpsApplyOptions(rest);
    const result = await apply(config, options.confirm);
    return options.full || options.raw ? result : renderApply(result);
  }
  if (action === "status") {
    const options = parseCommon(rest);
    const result = status();
    return options.full || options.raw ? result : renderStatus(result);
  }
  if (action === "validate") {
    const options = parseCommon(rest);
    const result = validate();
    return options.full || options.raw ? result : renderValidate(result);
  }
  return { ok: false, error: "unsupported-frontend-public-exposure-command", args, help: frontendPublicExposureHelp() };
}

export function frontendPublicExposureHelp(): Record<string, unknown> {
  return {
    command: "frontend public-exposure plan|apply|status|validate",
    examples: [
      "bun scripts/cli.ts frontend public-exposure plan",
      "bun scripts/cli.ts frontend public-exposure apply --dry-run",
      "bun scripts/cli.ts frontend public-exposure apply --confirm",
      "bun scripts/cli.ts frontend public-exposure status",
      "bun scripts/cli.ts frontend public-exposure validate",
    ],
    configPath: configLabel,
    description: "Expose the YAML-declared UniDesk frontend through PK01 Caddy HTTPS using a managed Caddy block.",
  };
}

function plan(): Record<string, unknown> {
  const frontend = readFrontendExposureConfig();
  const exposure = frontend.publicExposure;
  return {
    ok: true,
    service: serviceName,
    configPath: configLabel,
    metadata: frontend.metadata,
    publicExposure: exposureSummary(frontend),
    caddy: {
      marker: caddyManagedBlockMarkers(serviceName).start,
      siteBlockFingerprint: fingerprint(renderCaddySiteBlock(frontend)),
      siteBlockPreview: renderCaddySiteBlock(frontend),
    },
    policy: policyChecks(frontend),
    next: {
      dryRun: "bun scripts/cli.ts frontend public-exposure apply --dry-run",
      apply: "bun scripts/cli.ts frontend public-exposure apply --confirm",
      status: "bun scripts/cli.ts frontend public-exposure status",
      validate: "bun scripts/cli.ts frontend public-exposure validate",
    },
    valuesPrinted: false,
    publicBaseUrl: exposure.publicBaseUrl,
  };
}

async function apply(config: UniDeskConfig, confirm: boolean): Promise<Record<string, unknown>> {
  const frontend = readFrontendExposureConfig();
  const policy = policyChecks(frontend);
  const failedPolicy = policy.filter((item) => item.ok === false);
  if (!confirm) {
    return {
      ok: failedPolicy.length === 0,
      mode: "dry-run",
      publicExposure: exposureSummary(frontend),
      caddy: {
        marker: caddyManagedBlockMarkers(serviceName).start,
        siteBlockFingerprint: fingerprint(renderCaddySiteBlock(frontend)),
        siteBlockPreview: renderCaddySiteBlock(frontend),
      },
      policy,
      valuesPrinted: false,
    };
  }
  if (failedPolicy.length > 0) {
    return { ok: false, mode: "policy-blocked", publicExposure: exposureSummary(frontend), policy, valuesPrinted: false };
  }

  const exposure = frontend.publicExposure;
  const upstreamProbe = httpProbe(upstreamBaseUrl(frontend), exposure.probes.healthPath);
  const caddy = exposure.enabled
    ? await applyPk01CaddySiteBlock(
        config,
        serviceName,
        {
          enabled: exposure.enabled,
          dns: { hostname: exposure.dns.hostname },
          frpc: { remotePort: exposure.upstream.port },
          pk01: exposure.pk01,
        },
        renderCaddySiteBlock(frontend),
        caddyManagedBlockMarkers(serviceName),
      )
    : { ok: true, action: "not-enabled" };
  const publicHealthProbe = exposure.enabled ? httpProbeWithRetry(() => httpProbe(exposure.publicBaseUrl, exposure.probes.healthPath), 8, 1000) : { ok: true, skipped: true };
  const publicRootProbe = exposure.enabled ? httpProbeWithRetry(() => httpProbe(exposure.publicBaseUrl, exposure.probes.rootPath), 4, 1000) : { ok: true, skipped: true };
  return {
    ok: upstreamProbe.ok === true && caddy.ok !== false && publicHealthProbe.ok === true && publicRootProbe.ok === true,
    mode: "confirmed",
    publicExposure: exposureSummary(frontend),
    upstreamProbe,
    pk01Caddy: caddy,
    publicHealthProbe,
    publicRootProbe,
    valuesPrinted: false,
  };
}

function status(): Record<string, unknown> {
  const frontend = readFrontendExposureConfig();
  const exposure = frontend.publicExposure;
  const upstreamHealthProbe = httpProbe(upstreamBaseUrl(frontend), exposure.probes.healthPath);
  const publicHealthProbe = exposure.enabled ? httpProbe(exposure.publicBaseUrl, exposure.probes.healthPath) : { ok: true, skipped: true };
  const publicRootProbe = exposure.enabled ? httpProbe(exposure.publicBaseUrl, exposure.probes.rootPath) : { ok: true, skipped: true };
  return {
    ok: upstreamHealthProbe.ok === true && publicHealthProbe.ok === true && publicRootProbe.ok === true,
    publicExposure: exposureSummary(frontend),
    upstreamHealthProbe,
    publicHealthProbe,
    publicRootProbe,
    valuesPrinted: false,
  };
}

function validate(): Record<string, unknown> {
  const frontend = readFrontendExposureConfig();
  const exposure = frontend.publicExposure;
  const dns = dnsProbe(exposure.dns.hostname);
  const upstreamHealthProbe = httpProbe(upstreamBaseUrl(frontend), exposure.probes.healthPath);
  const publicHealthProbe = exposure.enabled ? httpProbe(exposure.publicBaseUrl, exposure.probes.healthPath) : { ok: true, skipped: true };
  const publicRootProbe = exposure.enabled ? httpProbe(exposure.publicBaseUrl, exposure.probes.rootPath) : { ok: true, skipped: true };
  return {
    ok: dns.addresses.includes(exposure.dns.expectedA) && upstreamHealthProbe.ok === true && publicHealthProbe.ok === true && publicRootProbe.ok === true,
    publicExposure: exposureSummary(frontend),
    dns,
    upstreamHealthProbe,
    publicHealthProbe,
    publicRootProbe,
    valuesPrinted: false,
  };
}

function readFrontendExposureConfig(): FrontendExposureConfig {
  const yaml = readYamlRecord(configPath, "UniDeskFrontendConfig");
  const metadata = recordField(yaml, "metadata", "frontend");
  const exposure = recordField(yaml, "publicExposure", "frontend");
  const dns = recordField(exposure, "dns", "frontend.publicExposure");
  const upstream = recordField(exposure, "upstream", "frontend.publicExposure");
  const pk01 = recordField(exposure, "pk01", "frontend.publicExposure");
  const probes = recordField(exposure, "probes", "frontend.publicExposure");
  const config: FrontendExposureConfig = {
    metadata: { name: stringField(metadata, "name", "frontend.metadata") },
    publicExposure: {
      enabled: booleanField(exposure, "enabled", "frontend.publicExposure"),
      publicBaseUrl: stringField(exposure, "publicBaseUrl", "frontend.publicExposure"),
      dns: {
        hostname: stringField(dns, "hostname", "frontend.publicExposure.dns"),
        expectedA: stringField(dns, "expectedA", "frontend.publicExposure.dns"),
        resolvers: stringArray(dns.resolvers, "frontend.publicExposure.dns.resolvers"),
      },
      upstream: {
        host: stringField(upstream, "host", "frontend.publicExposure.upstream"),
        port: portField(upstream, "port", "frontend.publicExposure.upstream"),
      },
      pk01: {
        route: stringField(pk01, "route", "frontend.publicExposure.pk01"),
        caddyConfigPath: stringField(pk01, "caddyConfigPath", "frontend.publicExposure.pk01"),
        caddyServiceName: stringField(pk01, "caddyServiceName", "frontend.publicExposure.pk01"),
        responseHeaderTimeoutSeconds: integerField(pk01, "responseHeaderTimeoutSeconds", "frontend.publicExposure.pk01"),
      },
      probes: {
        healthPath: pathField(probes, "healthPath", "frontend.publicExposure.probes"),
        rootPath: pathField(probes, "rootPath", "frontend.publicExposure.probes"),
      },
    },
  };
  validateFrontendExposureConfig(config);
  return config;
}

function validateFrontendExposureConfig(config: FrontendExposureConfig): void {
  const exposure = config.publicExposure;
  const url = new URL(exposure.publicBaseUrl);
  if (url.protocol !== "https:") throw new Error(`${configLabel}.publicExposure.publicBaseUrl must use https://`);
  if (url.hostname !== exposure.dns.hostname) throw new Error(`${configLabel}.publicExposure.publicBaseUrl hostname must match publicExposure.dns.hostname`);
  if (!/^[0-9.]+$/u.test(exposure.dns.expectedA)) throw new Error(`${configLabel}.publicExposure.dns.expectedA must be an IPv4 address`);
  if (exposure.pk01.responseHeaderTimeoutSeconds < 1) throw new Error(`${configLabel}.publicExposure.pk01.responseHeaderTimeoutSeconds must be >= 1`);
}

function policyChecks(config: FrontendExposureConfig): Array<Record<string, unknown> & { ok: boolean }> {
  const exposure = config.publicExposure;
  return [
    { name: "enabled", ok: exposure.enabled, detail: "publicExposure.enabled must be true for public closeout" },
    { name: "public-https", ok: exposure.publicBaseUrl.startsWith("https://"), detail: exposure.publicBaseUrl },
    { name: "dns-matches-url", ok: new URL(exposure.publicBaseUrl).hostname === exposure.dns.hostname, detail: exposure.dns.hostname },
    { name: "pk01-managed-caddy", ok: exposure.pk01.route.length > 0 && exposure.pk01.caddyConfigPath.startsWith("/"), detail: `${exposure.pk01.route}:${exposure.pk01.caddyConfigPath}` },
    { name: "upstream-declared", ok: exposure.upstream.host.length > 0 && exposure.upstream.port > 0, detail: `${exposure.upstream.host}:${exposure.upstream.port}` },
  ];
}

function renderCaddySiteBlock(config: FrontendExposureConfig): string {
  const exposure = config.publicExposure;
  return renderSimpleReverseProxyCaddySiteBlock({
    hostname: exposure.dns.hostname,
    upstream: `${exposure.upstream.host}:${exposure.upstream.port}`,
    responseHeaderTimeoutSeconds: exposure.pk01.responseHeaderTimeoutSeconds,
  });
}

function exposureSummary(config: FrontendExposureConfig): Record<string, unknown> {
  const exposure = config.publicExposure;
  return {
    enabled: exposure.enabled,
    publicBaseUrl: exposure.publicBaseUrl,
    hostname: exposure.dns.hostname,
    expectedA: exposure.dns.expectedA,
    resolvers: exposure.dns.resolvers,
    upstream: `${exposure.upstream.host}:${exposure.upstream.port}`,
    pk01Route: exposure.pk01.route,
    marker: caddyManagedBlockMarkers(serviceName).start,
    probes: exposure.probes,
    owner: configLabel,
  };
}

function upstreamBaseUrl(config: FrontendExposureConfig): string {
  return `http://${config.publicExposure.upstream.host}:${config.publicExposure.upstream.port}`;
}

function httpProbeWithRetry(run: () => Record<string, unknown>, attempts: number, delayMs: number): Record<string, unknown> {
  let latest = run();
  for (let attempt = 1; attempt < attempts && latest.ok !== true; attempt += 1) {
    spawnSync("sleep", [(delayMs / 1000).toFixed(3)]);
    latest = run();
  }
  return { ...latest, attempts };
}

function httpProbe(baseUrl: string, path: string): Record<string, unknown> {
  const url = `${baseUrl.replace(/\/+$/u, "")}${path}`;
  const result = spawnSync("curl", ["-fsS", "-L", "--connect-timeout", "5", "--max-time", "15", "-o", "/dev/null", "-w", "%{http_code}", url], { encoding: "utf8" });
  const status = Number(result.stdout.trim());
  return {
    ok: result.status === 0 && Number.isInteger(status) && status >= 200 && status < 500,
    url,
    status: Number.isInteger(status) ? status : null,
    exitCode: result.status,
    stderrTail: result.status === 0 ? "" : result.stderr.slice(-1000),
    valuesPrinted: false,
  };
}

function dnsProbe(hostname: string): Record<string, unknown> & { addresses: string[] } {
  const result = spawnSync("getent", ["hosts", hostname], { encoding: "utf8" });
  const addresses = result.stdout
    .split(/\r?\n/u)
    .map((line) => line.trim().split(/\s+/u)[0] ?? "")
    .filter((value) => /^\d+\.\d+\.\d+\.\d+$/u.test(value));
  return { ok: result.status === 0 && addresses.length > 0, hostname, addresses, exitCode: result.status };
}

function parseCommon(args: string[]): ReturnType<typeof parseOpsCommonOptions> {
  return parseOpsCommonOptions(args);
}

function wantsFull(args: string[]): boolean {
  return args.includes("--full") || args.includes("--raw");
}

function portField(record: Record<string, unknown>, key: string, path: string): number {
  const value = integerField(record, key, path);
  if (value < 1 || value > 65535) throw new Error(`${path}.${key} must be a TCP port`);
  return value;
}

function pathField(record: Record<string, unknown>, key: string, path: string): string {
  const value = stringField(record, key, path);
  if (!value.startsWith("/")) throw new Error(`${path}.${key} must start with /`);
  return value;
}

function stringArray(value: unknown, path: string): string[] {
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path} must be an array of strings`);
  return value as string[];
}

function fingerprint(value: string): string {
  const result = spawnSync("sha256sum", { input: value, encoding: "utf8" });
  return `sha256:${(result.stdout.split(/\s+/u)[0] ?? "").trim()}`;
}

function renderPlan(result: Record<string, unknown>): RenderedCliResult {
  const exposure = rec(result.publicExposure);
  const failed = arr(result.policy).filter((item) => item.ok === false);
  const lines = [
    "FRONTEND PUBLIC-EXPOSURE PLAN",
    ...table(["FIELD", "VALUE", "DETAIL", "VALUE"], [
      ["CONFIG", str(result.configPath), "owner", str(exposure.owner)],
      ["PUBLIC", bool(exposure.enabled), "url", str(exposure.publicBaseUrl)],
      ["DNS", str(exposure.hostname), "expectedA", str(exposure.expectedA)],
      ["UPSTREAM", str(exposure.upstream), "pk01", str(exposure.pk01Route)],
      ["CADDY", str(exposure.marker), "block", str(rec(result.caddy).siteBlockFingerprint)],
      ["POLICY", failed.length === 0 ? "ok" : `failed=${failed.length}`, "valuesPrinted", "false"],
    ]),
    "",
    "NEXT",
    "  dry-run: bun scripts/cli.ts frontend public-exposure apply --dry-run",
    "  apply: bun scripts/cli.ts frontend public-exposure apply --confirm",
    "  status: bun scripts/cli.ts frontend public-exposure status",
  ];
  return rendered(result, "frontend public-exposure plan", lines);
}

function renderApply(result: Record<string, unknown>): RenderedCliResult {
  const exposure = rec(result.publicExposure);
  const upstreamProbe = rec(result.upstreamProbe);
  const publicHealthProbe = rec(result.publicHealthProbe);
  const publicRootProbe = rec(result.publicRootProbe);
  const caddy = rec(result.pk01Caddy);
  const lines = [
    "FRONTEND PUBLIC-EXPOSURE APPLY",
    ...table(["PUBLIC", "MODE", "STATUS"], [[str(exposure.publicBaseUrl), str(result.mode), result.ok === false ? "failed" : "ok"]]),
    "",
    "CHECKS",
    ...table(["CHECK", "OK", "DETAIL"], [
      ["upstream-health", bool(upstreamProbe.ok), str(upstreamProbe.url, "-")],
      ["pk01-caddy", bool(caddy.ok), str(caddy.action, str(caddy.hostname, "-"))],
      ["public-health", bool(publicHealthProbe.ok), str(publicHealthProbe.url, "-")],
      ["public-root", bool(publicRootProbe.ok), str(publicRootProbe.url, "-")],
    ]),
    "",
    "NEXT",
    "  status: bun scripts/cli.ts frontend public-exposure status",
    "  validate: bun scripts/cli.ts frontend public-exposure validate",
  ];
  return rendered(result, "frontend public-exposure apply", lines);
}

function renderStatus(result: Record<string, unknown>): RenderedCliResult {
  const exposure = rec(result.publicExposure);
  const upstreamHealthProbe = rec(result.upstreamHealthProbe);
  const publicHealthProbe = rec(result.publicHealthProbe);
  const publicRootProbe = rec(result.publicRootProbe);
  const lines = [
    "FRONTEND PUBLIC-EXPOSURE STATUS",
    ...table(["PUBLIC", "UPSTREAM", "STATUS"], [[str(exposure.publicBaseUrl), str(exposure.upstream), result.ok === false ? "failed" : "ok"]]),
    "",
    "CHECKS",
    ...table(["CHECK", "OK", "DETAIL"], [
      ["upstream-health", bool(upstreamHealthProbe.ok), str(upstreamHealthProbe.url, "-")],
      ["public-health", bool(publicHealthProbe.ok), str(publicHealthProbe.url, "-")],
      ["public-root", bool(publicRootProbe.ok), str(publicRootProbe.url, "-")],
    ]),
  ];
  return rendered(result, "frontend public-exposure status", lines);
}

function renderValidate(result: Record<string, unknown>): RenderedCliResult {
  const dns = rec(result.dns);
  const upstreamHealthProbe = rec(result.upstreamHealthProbe);
  const publicHealthProbe = rec(result.publicHealthProbe);
  const publicRootProbe = rec(result.publicRootProbe);
  const lines = [
    "FRONTEND PUBLIC-EXPOSURE VALIDATE",
    ...table(["CHECK", "OK", "DETAIL"], [
      ["dns", bool(dns.ok), values(dns.addresses).map(String).join(",") || "-"],
      ["upstream-health", bool(upstreamHealthProbe.ok), str(upstreamHealthProbe.url, "-")],
      ["public-health", bool(publicHealthProbe.ok), str(publicHealthProbe.url, "-")],
      ["public-root", bool(publicRootProbe.ok), str(publicRootProbe.url, "-")],
    ]),
  ];
  return rendered(result, "frontend public-exposure validate", lines);
}

function rendered(result: Record<string, unknown>, command: string, lines: string[]): RenderedCliResult {
  return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain" };
}

function rec(value: unknown): Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function arr(value: unknown): Record<string, unknown>[] {
  return Array.isArray(value) ? value.map(rec) : [];
}

function values(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

function str(value: unknown, fallback = "-"): string {
  return value === undefined || value === null || value === "" ? fallback : String(value);
}

function bool(value: unknown): string {
  return value === true ? "yes" : value === false ? "no" : "-";
}

function table(headers: string[], rows: string[][]): string[] {
  const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
  const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join("  ").trimEnd();
  return [renderRow(headers), ...rows.map(renderRow)];
}
