import { readFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
import https from "node:https";

const namespace = process.env.NAMESPACE || "";
const pipelineRun = process.env.PIPELINERUN || "";
const shouldWait = process.env.WAIT === "true";
const timeoutSeconds = requiredPositiveNumber("TIMEOUT_SECONDS");
const pollIntervalSeconds = requiredPositiveNumber("POLL_INTERVAL_SECONDS");
const logsTailLines = Number(process.env.LOGS_TAIL_LINES || "240");
const maxLogBytes = Number(process.env.MAX_LOG_BYTES || "32000");
const host = process.env.KUBERNETES_SERVICE_HOST;
const port = Number(process.env.KUBERNETES_SERVICE_PORT || "443");
const token = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/token", "utf8").trim();
const ca = readFileSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
const manifest = JSON.parse(Buffer.from(readFileSync(0, "utf8").replace(/\s+/g, ""), "base64").toString("utf8"));
const startedAt = Date.now();
const traceContext = resolveTraceContext();

function request(method, path, body, contentType = "application/json") {
  return new Promise((resolve, reject) => {
    const headers = { authorization: `Bearer ${token}`, traceparent: traceContext.traceparent };
    if (traceContext.tracestate !== null) headers.tracestate = traceContext.tracestate;
    const payload = body === undefined ? null : typeof body === "string" ? body : JSON.stringify(body);
    if (payload !== null) {
      headers["content-type"] = contentType;
      headers["content-length"] = Buffer.byteLength(payload);
    }
    const req = https.request({ host, port, path, method, ca, headers }, (res) => {
      let text = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => { text += chunk; });
      res.on("end", () => resolve({ status: res.statusCode || 0, text }));
    });
    req.on("error", reject);
    if (payload !== null) req.write(payload);
    req.end();
  });
}

function parseBody(result) {
  if (!result.text) return null;
  try {
    return JSON.parse(result.text);
  } catch {
    return null;
  }
}

async function getPipelineRun() {
  const result = await request("GET", `/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/pipelineruns/${encodeURIComponent(pipelineRun)}`);
  if (result.status === 404) return { found: false, object: null, status: result.status, text: result.text };
  if (result.status < 200 || result.status >= 300) throw new Error(result.text || `kube api GET pipelinerun status ${result.status}`);
  return { found: true, object: parseBody(result), status: result.status, text: result.text };
}

function succeededCondition(object) {
  const conditions = Array.isArray(object?.status?.conditions) ? object.status.conditions : [];
  return conditions.find((item) => item && item.type === "Succeeded") || null;
}

function compact(object) {
  const condition = succeededCondition(object);
  return {
    name: object?.metadata?.name || pipelineRun,
    namespace: object?.metadata?.namespace || namespace,
    generation: object?.metadata?.generation ?? null,
    startTime: object?.status?.startTime || null,
    completionTime: object?.status?.completionTime || null,
    conditionStatus: condition?.status || null,
    conditionReason: condition?.reason || null,
    conditionMessage: condition?.message || null,
  };
}

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

let created = false;
let reused = false;
let latest = await getPipelineRun();
if (latest.found) {
  reused = true;
} else {
  const result = await request("POST", `/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/pipelineruns`, manifest);
  if (result.status === 409) {
    reused = true;
  } else if (result.status >= 200 && result.status < 300) {
    created = true;
  } else {
    const error = kubernetesApiError(result, "pipelinerun-create");
    const firstBreak = { code: "kubernetes-api-request-failed", phase: "pipelinerun-create", reason: error.message, valuesRedacted: true };
    process.stdout.write(JSON.stringify({
      ok: false,
      submitted: false,
      created: false,
      reused: false,
      wait: shouldWait,
      polls: 0,
      completed: false,
      failed: true,
      terminal: true,
      stillRunning: false,
      timedOutWait: false,
      elapsedMs: Date.now() - startedAt,
      pipelineRun: compact(null),
      traceId: traceContext.traceId,
      traceparent: traceContext.traceparent,
      firstBreak,
      error,
      statusAuthority: "kubernetes-api-serviceaccount",
      parsedDownstreamCliOutput: false,
      warning: false,
      blocking: true,
      valuesRedacted: true,
    }));
    await emitSpan("cicd.tekton.pipelinerun.create", false, { error, firstBreak, httpStatus: error.httpStatus, exitCode: 1 });
    process.exit(1);
  }
  latest = await getPipelineRun();
}

const deadline = Date.now() + timeoutSeconds * 1000;
let polls = 0;
while (shouldWait) {
  const condition = succeededCondition(latest.object);
  if (condition?.status === "True" || condition?.status === "False") break;
  if (Date.now() >= deadline) break;
  polls += 1;
  process.stderr.write(JSON.stringify({ event: "cicd.branch-follower.native-tekton.wait", pipelineRun, namespace, polls, conditionStatus: condition?.status || null, valuesRedacted: true }) + "\n");
  await delay(pollIntervalSeconds * 1000);
  latest = await getPipelineRun();
}

const condition = succeededCondition(latest.object);
const completed = condition?.status === "True";
const failed = condition?.status === "False";
const terminal = completed || failed;
const logsTail = terminal ? await pipelineRunLogsTail() : "";
const output = {
  ok: !failed,
  submitted: true,
  created,
  reused,
  wait: shouldWait,
  polls,
  completed,
  failed,
  terminal,
  stillRunning: !terminal,
  timedOutWait: shouldWait && !terminal,
  elapsedMs: Date.now() - startedAt,
  traceId: traceContext.traceId,
  traceparent: traceContext.traceparent,
  pipelineRun: compact(latest.object),
  logsTail,
  statusAuthority: "kubernetes-api-serviceaccount",
  parsedDownstreamCliOutput: false,
  valuesRedacted: true,
};
process.stdout.write(JSON.stringify(output));
await emitSpan("cicd.tekton.pipelinerun.wait", !failed, {
  phase: failed ? "pipelinerun-terminal-failed" : completed ? "pipelinerun-terminal-succeeded" : "pipelinerun-wait",
  status: condition?.status || null,
  durationMs: Date.now() - startedAt,
  exitCode: failed ? 1 : 0,
});
if (failed) process.exit(1);

function requiredPositiveNumber(name) {
  const value = Number(process.env[name]);
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
  return value;
}

function resolveTraceContext() {
  const incoming = process.env.TRACEPARENT || process.env.OTEL_TRACEPARENT || "";
  const matched = incoming.match(/^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i);
  const traceId = matched?.[1]?.toLowerCase() || randomBytes(16).toString("hex");
  const parentSpanId = matched?.[2]?.toLowerCase() || null;
  const spanId = randomBytes(8).toString("hex");
  return {
    traceId,
    parentSpanId,
    spanId,
    traceparent: `00-${traceId}-${spanId}-${matched?.[3]?.toLowerCase() || "01"}`,
    tracestate: boundedHeader(process.env.TRACESTATE),
  };
}

function boundedHeader(value) {
  return typeof value === "string" && value.length > 0 && value.length <= 512 ? value : null;
}

function kubernetesApiError(result, phase) {
  const body = parseBody(result);
  const code = safeToken(body?.reason) || `HTTP_${result.status || 0}`;
  const message = redactSummary(body?.message || result.text || `Kubernetes API request failed with HTTP ${result.status || 0}`);
  return {
    type: "KubernetesApiError",
    code,
    phase,
    httpStatus: result.status || null,
    exitCode: 1,
    stderrSummary: message,
    message,
    valuesRedacted: true,
  };
}

function safeToken(value) {
  return typeof value === "string" && /^[A-Za-z0-9_.-]{1,80}$/.test(value) ? value : null;
}

function redactSummary(value) {
  return String(value || "")
    .replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, "$1[REDACTED]")
    .replace(/(token|password|secret)(\s*[:=]\s*)\S+/gi, "$1$2[REDACTED]")
    .replace(/https?:\/\/[^\s/]+@/gi, "https://[REDACTED]@")
    .replace(/([?&][^=\s]+)=([^&\s]+)/g, "$1=[REDACTED]")
    .replace(/\s+/g, " ")
    .trim()
    .slice(0, 480);
}

async function emitSpan(name, ok, attributes) {
  const endpoint = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
  if (!endpoint) return;
  const started = BigInt(startedAt) * 1_000_000n;
  const ended = BigInt(Date.now()) * 1_000_000n;
  const payload = {
    resourceSpans: [{
      resource: { attributes: otelAttributes({
        "service.name": process.env.OTEL_SERVICE_NAME || "unidesk-cicd",
        "deployment.environment": process.env.UNIDESK_LANE || null,
        "unidesk.node": process.env.UNIDESK_NODE || null,
        "unidesk.values_redacted": true,
      }) },
      scopeSpans: [{
        scope: { name: "unidesk.cicd.submit_pipelinerun", version: "1" },
        spans: [{
          traceId: traceContext.traceId,
          spanId: traceContext.spanId,
          ...(traceContext.parentSpanId === null ? {} : { parentSpanId: traceContext.parentSpanId }),
          name,
          kind: 3,
          startTimeUnixNano: started.toString(),
          endTimeUnixNano: ended.toString(),
          attributes: otelAttributes({
            "cicd.consumer": process.env.UNIDESK_PAC_CONSUMER || null,
            "cicd.pipeline_run": pipelineRun,
            "k8s.namespace.name": namespace,
            "cicd.source_commit": process.env.SOURCE_COMMIT || null,
            "cicd.phase": attributes.phase || null,
            "cicd.status": attributes.status || null,
            "cicd.duration_ms": attributes.durationMs || null,
            "process.exit.code": attributes.exitCode ?? null,
            "http.response.status_code": attributes.httpStatus ?? null,
            "error.type": attributes.error?.type || null,
            "error.code": attributes.error?.code || null,
            "cicd.first_break": attributes.firstBreak?.code || null,
            "warning": false,
            "blocking": attributes.exitCode === 1,
            "valuesRedacted": true,
          }),
          status: { code: ok ? 1 : 2 },
        }],
      }],
    }],
  };
  try {
    const controller = new AbortController();
    const timeoutMs = exporterTimeoutMs();
    const timeout = setTimeout(() => controller.abort(), timeoutMs);
    const response = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json", traceparent: traceContext.traceparent }, body: JSON.stringify(payload), signal: controller.signal });
    clearTimeout(timeout);
    if (!response.ok) process.stderr.write(JSON.stringify({ event: "cicd.otel.export", warning: true, blocking: false, httpStatus: response.status, traceId: traceContext.traceId, valuesRedacted: true }) + "\n");
  } catch (error) {
    process.stderr.write(JSON.stringify({ event: "cicd.otel.export", warning: true, blocking: false, errorType: error instanceof Error ? error.name : "Error", timedOut: error instanceof Error && error.name === "AbortError", timeoutMs: exporterTimeoutMs(), traceId: traceContext.traceId, valuesRedacted: true }) + "\n");
  }
}

function exporterTimeoutMs() {
  const value = Number(process.env.OTEL_EXPORTER_TIMEOUT_MS || "300");
  return Number.isInteger(value) && value >= 50 && value <= 2000 ? value : 300;
}

function otelAttributes(values) {
  return Object.entries(values).filter(([, value]) => value !== null && value !== undefined).map(([key, value]) => ({ key, value: otelValue(value) }));
}

function otelValue(value) {
  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) };
}

async function pipelineRunLogsTail() {
  const selector = encodeURIComponent(`tekton.dev/pipelineRun=${pipelineRun}`);
  const podsResult = await request("GET", `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${selector}`);
  if (podsResult.status < 200 || podsResult.status >= 300) return "";
  const pods = parseBody(podsResult);
  const chunks = [];
  for (const pod of Array.isArray(pods?.items) ? pods.items : []) {
    const podName = pod?.metadata?.name;
    const containers = [
      ...(Array.isArray(pod?.spec?.initContainers) ? pod.spec.initContainers : []),
      ...(Array.isArray(pod?.spec?.containers) ? pod.spec.containers : []),
    ];
    for (const container of containers) {
      if (!podName || !container?.name) continue;
      const path = `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(podName)}/log?container=${encodeURIComponent(container.name)}&tailLines=${Math.max(1, logsTailLines)}`;
      const log = await request("GET", path);
      if (log.status >= 200 && log.status < 300 && log.text) chunks.push(log.text);
    }
  }
  return chunks.join("\n").slice(-Math.max(1, maxLogBytes));
}
