import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";

const provenanceKeys = {
  configRef: "unidesk.ai/owning-config-ref",
  effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
  renderer: "unidesk.ai/source-artifact-renderer",
  mode: "unidesk.ai/source-artifact-mode",
};

const sourceCommitKeys = [
  "pipelinesascode.tekton.dev/sha",
  "pipelinesascode.tekton.dev/commit",
  "agentrun.pikastech.local/source-commit",
  "unidesk.ai/source-commit",
  "hwlab.pikastech.local/source-commit",
];

const knownDriftPathKeys = new Set([
  "accessModes", "annotations", "apiVersion", "args", "command", "computeResources", "default", "description", "dnsPolicy", "env", "envFrom",
  "executionMode", "finally", "fsGroup", "generateName", "hostNetwork", "image", "kind", "labels", "length", "metadata", "mode", "mountPath", "name", "namespace", "onError", "operator",
  "optional", "params", "pipeline", "pipelineRef", "pipelineSpec", "podTemplate", "readinessProbe", "readOnly", "requests", "resources",
  "results", "retries", "runAfter", "script", "secret", "secretName", "securityContext", "serviceAccountName", "sidecars", "spec", "steps",
  "storage", "subPath", "taskRunTemplate", "taskSpec", "tasks", "timeout", "timeouts", "type", "value", "values", "volumeClaimTemplate",
  "volumeMounts", "volumes", "when", "workspaces", "workingDir", "wrapper",
]);

function record(value) {
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value !== null && typeof value === "object") {
    return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
  }
  if (value === undefined) return { __type: "undefined" };
  if (typeof value === "bigint") return { __type: "bigint", value: String(value) };
  return value;
}

function valueEvidence(value) {
  const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
  const serialized = JSON.stringify(stableValue(value));
  const text = serialized === undefined ? String(value) : serialized;
  const length = typeof value === "string"
    ? Buffer.byteLength(value, "utf8")
    : Array.isArray(value)
      ? value.length
      : value !== null && typeof value === "object"
        ? Object.keys(value).length
        : Buffer.byteLength(text, "utf8");
  const sha256 = createHash("sha256").update(text).digest("hex");
  return `type=${type},length=${length},sha256=${sha256}`;
}

function safeRuntimeReason(reason) {
  const text = typeof reason === "string" ? reason : String(reason);
  const fixed = new Set([
    "source-commit-not-specified",
    "source-commit-run-not-found",
    "source-commit-identity-conflict",
    "pipeline-get-not-found",
    "pipelinerun-list-not-found",
    "pipelinerun-list-invalid-shape",
  ]);
  if (fixed.has(text)) return text;
  if (/^source-commit-run-conflict:\d+$/u.test(text)) return text;
  if (/^(?:pipeline-get|pipelinerun-list)-command-failed:(?:unknown|\d+):type=string,length=\d+,sha256=[0-9a-f]{64}$/u.test(text)) return text;
  if (/^runtime-observer-input-error:type=string,length=\d+,sha256=[0-9a-f]{64}$/u.test(text)) return text;
  return `runtime-observer-reason:${valueEvidence(text)}`;
}

function unavailable(reason, sourceCommit = null, candidateCount = 0) {
  return {
    status: "unavailable",
    name: null,
    canonicalSha256: null,
    firstDrift: null,
    provenanceAligned: false,
    configRef: null,
    effectiveConfigSha256: null,
    sourceCommit,
    reason: safeRuntimeReason(reason),
    candidateCount,
  };
}

function missing(reason, sourceCommit = null) {
  return { ...unavailable(reason, sourceCommit, 0), status: "missing" };
}

function isAdmissionDefault(path, value) {
  const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
  const emptyRecord = value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
  if ([
    "spec.workspaces.*.volumeClaimTemplate.metadata",
    "spec.workspaces.*.volumeClaimTemplate.status",
  ].includes(normalized)) return emptyRecord;
  const atTaskSpec = (suffix) => [
    `tasks.*.taskSpec.${suffix}`,
    `spec.tasks.*.taskSpec.${suffix}`,
    `spec.pipelineSpec.tasks.*.taskSpec.${suffix}`,
  ].includes(normalized);
  if (atTaskSpec("metadata")) return emptyRecord;
  if (atTaskSpec("spec")) return value === null;
  if (atTaskSpec("params.*.type")) return value === "string";
  if (atTaskSpec("results.*.type")) return value === "string";
  if (atTaskSpec("steps.*.computeResources")) return emptyRecord;
  if (atTaskSpec("sidecars.*.computeResources")) return emptyRecord;
  return false;
}

export function canonicalizePacPipelineSpec(value, path = []) {
  if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
  const duration = canonicalTektonPipelineTimeout(path, value);
  if (duration !== null) return duration;
  if (value === null || typeof value !== "object") return value;
  const output = {};
  for (const key of Object.keys(value).sort()) {
    const child = value[key];
    const childPath = [...path, key];
    if (isAdmissionDefault(childPath, child)) continue;
    output[key] = canonicalizePacPipelineSpec(child, childPath);
  }
  return output;
}

function canonicalTektonPipelineTimeout(path, value) {
  if (path.join(".") !== "spec.timeouts.pipeline" || typeof value !== "string") return null;
  const matchSign = /^([+-]?)(.*)$/u.exec(value);
  if (matchSign === null) return null;
  const source = matchSign[2] ?? "";
  const units = {
    h: 3_600_000_000_000n,
    m: 60_000_000_000n,
    s: 1_000_000_000n,
    ms: 1_000_000n,
    us: 1_000n,
    "µs": 1_000n,
    "μs": 1_000n,
    ns: 1n,
  };
  const segment = /([0-9]+)(ns|us|µs|μs|ms|s|m|h)/uy;
  let offset = 0;
  let nanoseconds = 0n;
  let count = 0;
  while (offset < source.length) {
    segment.lastIndex = offset;
    const matched = segment.exec(source);
    if (matched === null) return null;
    nanoseconds += BigInt(matched[1] ?? "0") * (units[matched[2] ?? ""] ?? 0n);
    offset = segment.lastIndex;
    count += 1;
  }
  if (count === 0) return null;
  if (matchSign[1] === "-") nanoseconds = -nanoseconds;
  return `tekton-duration-ns:${nanoseconds}`;
}

export function canonicalPacPipelineSha256(value) {
  return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
}

function firstCanonicalDrift(expected, actual, path = "$") {
  if (Object.is(expected, actual)) return null;
  if (Array.isArray(expected) || Array.isArray(actual)) {
    if (!Array.isArray(expected) || !Array.isArray(actual)) return { path, expected: valueEvidence(expected), actual: valueEvidence(actual) };
    if (expected.length !== actual.length) return { path: `${path}.length`, expected: valueEvidence(expected.length), actual: valueEvidence(actual.length) };
    for (let index = 0; index < expected.length; index += 1) {
      const drift = firstCanonicalDrift(expected[index], actual[index], `${path}[${index}]`);
      if (drift !== null) return drift;
    }
    return null;
  }
  const expectedRecord = expected !== null && typeof expected === "object";
  const actualRecord = actual !== null && typeof actual === "object";
  if (expectedRecord || actualRecord) {
    if (!expectedRecord || !actualRecord) return { path, expected: valueEvidence(expected), actual: valueEvidence(actual) };
    const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort();
    for (const key of keys) {
      const expectedOwnsKey = Object.prototype.hasOwnProperty.call(expected, key);
      const actualOwnsKey = Object.prototype.hasOwnProperty.call(actual, key);
      const childPath = knownDriftPathKeys.has(key) ? `${path}.${key}` : `${path}.[key:${valueEvidence(key)}]`;
      if (!expectedOwnsKey || !actualOwnsKey) {
        return { path: childPath, expected: valueEvidence(expected[key]), actual: valueEvidence(actual[key]) };
      }
      const drift = firstCanonicalDrift(expected[key], actual[key], childPath);
      if (drift !== null) return drift;
    }
    return null;
  }
  return { path, expected: valueEvidence(expected), actual: valueEvidence(actual) };
}

function firstDrift(expected, actual) {
  return firstCanonicalDrift(canonicalizePacPipelineSpec(expected), canonicalizePacPipelineSpec(actual));
}

function kubectlJson(args, label) {
  try {
    return {
      kind: "ok",
      value: JSON.parse(execFileSync("kubectl", args, { encoding: "utf8", timeout: 30_000, maxBuffer: 64 * 1024 * 1024 })),
    };
  } catch (error) {
    const stderr = typeof error?.stderr === "string" ? error.stderr : Buffer.isBuffer(error?.stderr) ? error.stderr.toString("utf8") : "";
    const stdout = typeof error?.stdout === "string" ? error.stdout : Buffer.isBuffer(error?.stdout) ? error.stdout.toString("utf8") : "";
    const message = stderr.trim().split(/\r?\n/u)[0] || stdout.trim().split(/\r?\n/u)[0] || String(error?.message ?? "command failed");
    if (/\bNotFound\b|\bnot found\b/u.test(message)) return { kind: "not-found", reason: `${label}-not-found` };
    const status = Number.isInteger(error?.status) ? error.status : "unknown";
    return { kind: "unavailable", reason: `${label}-command-failed:${status}:${valueEvidence(message)}` };
  }
}

function manifestProvenance(manifest) {
  const annotations = record(record(manifest?.metadata).annotations);
  return {
    configRef: typeof annotations[provenanceKeys.configRef] === "string" ? annotations[provenanceKeys.configRef] : null,
    effectiveConfigSha256: typeof annotations[provenanceKeys.effectiveConfigSha256] === "string" ? annotations[provenanceKeys.effectiveConfigSha256] : null,
    renderer: typeof annotations[provenanceKeys.renderer] === "string" ? annotations[provenanceKeys.renderer] : null,
    mode: typeof annotations[provenanceKeys.mode] === "string" ? annotations[provenanceKeys.mode] : null,
  };
}

function manifestSourceCommits(manifest) {
  const metadata = record(manifest?.metadata);
  const labels = record(metadata.labels);
  const annotations = record(metadata.annotations);
  return [...new Set(sourceCommitKeys.flatMap((key) => [labels[key], annotations[key]])
    .filter((value) => typeof value === "string" && /^[0-9a-f]{40}$/iu.test(value))
    .map((value) => value.toLowerCase()))];
}

function safeManifestName(value) {
  return typeof value === "string"
    && value.length <= 253
    && /^[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/u.test(value)
    ? value
    : null;
}

function safeAlignedConfigRef(observed, expected) {
  return observed === expected
    && typeof expected === "string"
    && expected.length <= 512
    && /^[A-Za-z0-9_./-]+#[A-Za-z0-9_.-]+$/u.test(expected)
    ? expected
    : null;
}

function safeAlignedSha256(observed, expected) {
  return observed === expected && typeof expected === "string" && /^sha256:[0-9a-f]{64}$/u.test(expected) ? expected : null;
}

function pipelineRunWrapper(manifest) {
  const spec = record(manifest?.spec);
  const hasPipelineSpec = Object.prototype.hasOwnProperty.call(spec, "pipelineSpec");
  const hasPipelineRef = Object.prototype.hasOwnProperty.call(spec, "pipelineRef");
  const observedMode = manifestProvenance(manifest).mode;
  const mode = observedMode === "embedded-pipeline-spec" || observedMode === "remote-pipeline-annotation" ? observedMode : "invalid";
  const executionMode = hasPipelineSpec && !hasPipelineRef ? "pipeline-spec" : "invalid";
  const wrapperSpec = { ...spec };
  delete wrapperSpec.pipelineSpec;
  delete wrapperSpec.pipelineRef;
  return { mode, executionMode, spec: wrapperSpec };
}

function observedItem(input, manifest, spec, sourceCommit = null, candidateCount = 0, wrapper = null) {
  const observedProvenance = manifestProvenance(manifest);
  const provenanceAligned = observedProvenance.configRef === input.provenance.configRef
    && observedProvenance.effectiveConfigSha256 === input.provenance.effectiveConfigSha256
    && observedProvenance.renderer === input.provenance.renderer
    && observedProvenance.mode === input.provenance.mode;
  const comparePipelineSpec = wrapper === null || wrapper.executionMode === "pipeline-spec";
  const specDrift = comparePipelineSpec ? firstDrift(input.desiredSpec, spec) : null;
  const wrapperDrift = wrapper === null || input.desiredWrapper === null
    ? null
    : firstDrift(input.desiredWrapper, wrapper);
  const drift = specDrift ?? wrapperDrift;
  return {
    status: drift === null ? "aligned" : "drifted",
    name: safeManifestName(record(manifest.metadata).name),
    canonicalSha256: canonicalPacPipelineSha256(
      wrapper === null
        ? spec
        : wrapper.executionMode === "pipeline-spec"
          ? { pipelineSpec: spec, wrapper }
          : { wrapper },
    ),
    firstDrift: drift,
    provenanceAligned,
    configRef: safeAlignedConfigRef(observedProvenance.configRef, input.provenance.configRef),
    effectiveConfigSha256: safeAlignedSha256(observedProvenance.effectiveConfigSha256, input.provenance.effectiveConfigSha256),
    renderer: observedProvenance.renderer === input.provenance.renderer ? input.provenance.renderer : null,
    mode: observedProvenance.mode === input.provenance.mode ? input.provenance.mode : null,
    sourceCommit,
    reason: null,
    candidateCount,
  };
}

export function selectPipelineRunBySourceCommit(items, pipelineRunPrefix, sourceCommit) {
  if (sourceCommit === null) return { kind: "unavailable", reason: "source-commit-not-specified", run: null, candidates: [] };
  const candidates = items.filter((run) => {
    const name = String(record(run?.metadata).name ?? "");
    return name.startsWith(pipelineRunPrefix) && manifestSourceCommits(run).includes(sourceCommit);
  });
  if (candidates.length === 0) return { kind: "missing", reason: "source-commit-run-not-found", run: null, candidates: [] };
  if (candidates.some((run) => {
    const commits = manifestSourceCommits(run);
    return commits.length !== 1 || commits[0] !== sourceCommit;
  })) return { kind: "unavailable", reason: "source-commit-identity-conflict", run: null, candidates };
  const ordered = [...candidates].sort((left, right) => {
    const time = String(record(right?.metadata).creationTimestamp ?? "").localeCompare(String(record(left?.metadata).creationTimestamp ?? ""));
    return time !== 0 ? time : String(record(right?.metadata).name ?? "").localeCompare(String(record(left?.metadata).name ?? ""));
  });
  return { kind: "ok", reason: null, run: ordered[0], candidates: ordered };
}

export function observePacSourceArtifactRuntime(input, readKubectlJson = kubectlJson) {
  const liveResult = readKubectlJson(["get", "pipeline", input.pipeline, "-n", input.namespace, "-o", "json"], "pipeline-get");
  const live = liveResult.kind === "ok"
    ? observedItem(input, liveResult.value, record(liveResult.value.spec))
    : liveResult.kind === "not-found"
      ? missing(liveResult.reason)
      : unavailable(liveResult.reason);

  let embedded;
  if (input.sourceCommit === null) {
    embedded = unavailable("source-commit-not-specified");
  } else {
    const runsResult = readKubectlJson(["get", "pipelinerun", "-n", input.namespace, "-o", "json"], "pipelinerun-list");
    if (runsResult.kind !== "ok") {
      embedded = runsResult.kind === "not-found" ? missing(runsResult.reason, input.sourceCommit) : unavailable(runsResult.reason, input.sourceCommit);
    } else {
      const items = Array.isArray(runsResult.value?.items) ? runsResult.value.items : null;
      if (items === null) {
        embedded = unavailable("pipelinerun-list-invalid-shape", input.sourceCommit);
      } else {
        const selected = selectPipelineRunBySourceCommit(items, input.pipelineRunPrefix, input.sourceCommit);
        if (selected.kind === "ok") {
          const signatures = selected.candidates.map((run) => JSON.stringify({
            canonicalSha256: (() => {
              const wrapper = pipelineRunWrapper(run);
              return canonicalPacPipelineSha256(wrapper.executionMode === "pipeline-spec"
                ? { pipelineSpec: record(record(run.spec).pipelineSpec), wrapper }
                : { wrapper });
            })(),
            provenance: manifestProvenance(run),
          }));
          embedded = new Set(signatures).size === 1
            ? observedItem(
              input,
              selected.run,
              record(record(selected.run.spec).pipelineSpec),
              input.sourceCommit,
              selected.candidates.length,
              pipelineRunWrapper(selected.run),
            )
            : unavailable(`source-commit-run-conflict:${selected.candidates.length}`, input.sourceCommit, selected.candidates.length);
        } else {
          embedded = selected.kind === "missing"
            ? missing(selected.reason, input.sourceCommit)
            : unavailable(selected.reason, input.sourceCommit, selected.candidates.length);
        }
      }
    }
  }
  return { live, embedded };
}

if (typeof process.env.PAC_SOURCE_ARTIFACT_INPUT_FILE === "string") {
  try {
    const inputText = readFileSync(process.env.PAC_SOURCE_ARTIFACT_INPUT_FILE, "utf8");
    const input = JSON.parse(inputText);
    process.stdout.write(JSON.stringify(observePacSourceArtifactRuntime(input)));
  } catch (error) {
    const evidence = valueEvidence(String(error?.message ?? error));
    process.stdout.write(JSON.stringify({
      live: unavailable(`runtime-observer-input-error:${evidence}`),
      embedded: unavailable(`runtime-observer-input-error:${evidence}`),
    }));
  }
}
