import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { hwlabRuntimeLaneSpecForNode } from "../../src/hwlab-node-lanes";
import { loadSentinelCicdState } from "../../src/hwlab-node-web-sentinel-cicd";
import {
  sentinelImageBuildProxyEnv,
  sentinelPublishImageBuildShell,
  sentinelPublishShell,
  sentinelPublishSourceShell,
} from "../../src/hwlab-node-web-sentinel-cicd-jobs";
import { exportCicdOtelPayload, loadCicdOtelRuntimeConfig } from "./otel-runtime-config";
import { renderPacFeatureConfigSchemaStage } from "../../src/pac-feature-config-schema-stage";

const options = parseOptions(process.argv.slice(2));
const sourceAuthority = options.sourceAuthority === "gitea-snapshot" ? "gitea-snapshot" : fail("--source-authority must be gitea-snapshot");
const state = loadSentinelCicdState(
  hwlabRuntimeLaneSpecForNode(options.lane, options.node),
  options.sentinel,
  30,
  "cached",
  {
    commit: options.sourceCommit,
    stageRef: options.sourceStageRef,
    mirrorCommit: options.sourceCommit,
    sourceAuthority,
  },
);
const outputDir = resolve(options.outputDir);
mkdirSync(outputDir, { recursive: true });
const otelConfig = loadCicdOtelRuntimeConfig(options.repositoryId);
const otelSetup = [
  `export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=${shellQuote(otelConfig.endpoint)}`,
  `export OTEL_EXPORTER_TIMEOUT_MS=${shellQuote(String(otelConfig.timeoutMs))}`,
  `export OTEL_SERVICE_NAME=${shellQuote(otelConfig.serviceName)}`,
  `export OTEL_TRACES_SAMPLER_ARG=${shellQuote(String(otelConfig.samplingRatio))}`,
].join("\n");
const traceSetup = [
  "if [ -f /workspace/meta/traceparent ]; then TRACEPARENT=$(cat /workspace/meta/traceparent); export TRACEPARENT; fi",
  "if [ -f /workspace/meta/tracestate ]; then TRACESTATE=$(cat /workspace/meta/tracestate); export TRACESTATE; fi",
].join("\n");
const proxySetup = sentinelImageBuildProxyEnv(state).map(({ name, value }) => `export ${name}=${shellQuote(value)}`).join("\n");
const featureConfigSchemaStage = renderPacFeatureConfigSchemaStage({
  repositoryId: options.repositoryId,
  consumer: options.repositoryId,
  repoDirExpression: "'/workspace/source'",
  renderedRootExpression: '"$gitops_worktree"',
});
writeExecutable("source.sh", [
  "#!/bin/sh",
  "set -eu",
  otelSetup,
  `incoming_traceparent=${shellQuote(process.env.TRACEPARENT ?? "")}`,
  `incoming_tracestate=${shellQuote(process.env.TRACESTATE ?? "")}`,
  sentinelPublishSourceShell(state, options.pipelineRun),
  "if [ -n \"$incoming_traceparent\" ]; then printf '%s' \"$incoming_traceparent\" > /workspace/meta/traceparent; fi",
  "if [ -n \"$incoming_tracestate\" ]; then printf '%s' \"$incoming_tracestate\" > /workspace/meta/tracestate; fi",
].join("\n"));
writeExecutable("image-build.sh", ["#!/bin/sh", "set -eu", otelSetup, traceSetup, proxySetup, sentinelPublishImageBuildShell(state, options.pipelineRun)].join("\n"));
writeExecutable("publish.sh", ["#!/bin/sh", "set -eu", otelSetup, traceSetup, proxySetup, sentinelPublishShell(state, options.pipelineRun, true, featureConfigSchemaStage)].join("\n"));
emitConfigFallbackWarning();
await emitOuterSpan();
process.stdout.write(`${JSON.stringify({ ok: true, action: "render-sentinel-publish-task", node: options.node, lane: options.lane, sentinel: options.sentinel, pipelineRun: options.pipelineRun, sourceCommit: options.sourceCommit, outputDir, traceId: traceId(process.env.TRACEPARENT), valuesRedacted: true })}\n`);

function writeExecutable(name: string, content: string): void {
  const path = resolve(outputDir, name);
  writeFileSync(path, `${content}\n`, "utf8");
  chmodSync(path, 0o755);
}

function parseOptions(args: string[]): Record<"node" | "lane" | "repositoryId" | "sentinel" | "pipelineRun" | "sourceCommit" | "sourceStageRef" | "sourceAuthority" | "outputDir", string> {
  const values = new Map<string, string>();
  for (let index = 0; index < args.length; index += 2) {
    const key = args[index];
    const value = args[index + 1];
    if (key === undefined || value === undefined || !key.startsWith("--")) fail("render-sentinel-publish-task requires key/value options");
    values.set(key.slice(2), value);
  }
  const required = (key: string): string => values.get(key) || fail(`--${key} is required`);
  const result = {
    node: required("node"),
    lane: required("lane"),
    repositoryId: required("repository-id"),
    sentinel: required("sentinel"),
    pipelineRun: required("pipeline-run"),
    sourceCommit: required("source-commit"),
    sourceStageRef: required("source-stage-ref"),
    sourceAuthority: required("source-authority"),
    outputDir: required("output-dir"),
  };
  if (!/^[0-9a-f]{40}$/u.test(result.sourceCommit)) fail("--source-commit must be a 40-character lowercase Git commit");
  for (const key of ["node", "lane", "repositoryId", "sentinel", "pipelineRun"] as const) if (!/^[A-Za-z0-9._-]+$/u.test(result[key])) fail(`--${key.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)} must be a simple id`);
  if (!result.sourceStageRef.startsWith("refs/unidesk/snapshots/")) fail("--source-stage-ref must be an immutable UniDesk snapshot ref");
  return result;
}

function traceId(traceparent: string | undefined): string | null {
  return traceparent?.match(/^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/iu)?.[1]?.toLowerCase() ?? null;
}

async function emitOuterSpan(): Promise<void> {
  const context = process.env.TRACEPARENT?.match(/^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/iu);
  if (context === undefined || context === null || !shouldSample(context[1] ?? "", otelConfig.samplingRatio)) return;
  const startedAtMs = Number(process.env.CICD_TRACE_STARTED_AT_MS ?? Date.now());
  const payload = {
    resourceSpans: [{
      resource: { attributes: attributes({ "service.name": otelConfig.serviceName, "unidesk.node": options.node, "hwlab.lane": options.lane, "unidesk.values_redacted": true }) },
      scopeSpans: [{
        scope: { name: "unidesk.cicd.pac_outer", version: "1" },
        spans: [{
          traceId: context[1]?.toLowerCase(),
          spanId: context[2]?.toLowerCase(),
          name: "cicd.pac.outer_event",
          kind: 2,
          startTimeUnixNano: (BigInt(startedAtMs) * 1_000_000n).toString(),
          endTimeUnixNano: (BigInt(Date.now()) * 1_000_000n).toString(),
          attributes: attributes({
            "cicd.target": options.node,
            "cicd.lane": options.lane,
            "cicd.consumer": `sentinel-${options.node.toLowerCase()}-v03`,
            "cicd.repository": "pikasTech/unidesk",
            "cicd.branch": "master",
            "cicd.source_commit": options.sourceCommit,
            "cicd.pipeline_run": options.pipelineRun,
            "cicd.delivery_class": "outer-pac-event",
            "cicd.phase": "task-rendered",
            "warning": false,
            "blocking": false,
            "valuesRedacted": true,
          }),
          status: { code: 1 },
        }],
      }],
    }],
  };
  const warning = await exportCicdOtelPayload(otelConfig, process.env.TRACEPARENT ?? "", context[1]?.toLowerCase() ?? "", payload);
  if (warning !== null) process.stderr.write(`${JSON.stringify(warning)}\n`);
}

function emitConfigFallbackWarning(): void {
  if (otelConfig.fallbackCodes.length === 0) return;
  process.stderr.write(`${JSON.stringify({ event: "cicd.otel.config", warning: true, blocking: false, causeCodes: otelConfig.fallbackCodes, endpointHostname: otelConfig.endpointHostname, valuesRedacted: true })}\n`);
}

function shouldSample(traceIdValue: string, ratio: number): boolean {
  if (ratio <= 0) return false;
  if (ratio >= 1) return true;
  const bucket = Number.parseInt(traceIdValue.slice(0, 8), 16) / 0xffffffff;
  return bucket < ratio;
}

function attributes(values: Record<string, unknown>): Array<Record<string, unknown>> {
  return Object.entries(values).filter(([, value]) => value !== null && value !== undefined).map(([key, value]) => ({ key, value: attributeValue(value) }));
}

function attributeValue(value: unknown): Record<string, unknown> {
  if (typeof value === "boolean") return { boolValue: value };
  if (typeof value === "number" && Number.isFinite(value)) return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value };
  return { stringValue: typeof value === "string" ? value : JSON.stringify(value) };
}

function shellQuote(value: string): string {
  return `'${value.replaceAll("'", `'"'"'`)}'`;
}

function fail(message: string): never {
  throw new Error(message);
}
