import type { AgentRunLaneSpec } from "../agentrun-lanes";

import { cleanupRunnersFactsNodeScript } from "./secrets";
import { shQuote } from "./utils";

export interface RunnerJobObservationIdentity {
  readonly runnerJobId: string;
  readonly runId: string;
  readonly commandId: string;
  readonly runnerId: string;
  readonly namespace: string;
  readonly jobName: string;
}

export function runnerJobObservationScript(spec: AgentRunLaneSpec, identity: RunnerJobObservationIdentity): string {
  const identityJsonB64 = Buffer.from(JSON.stringify(identity), "utf8").toString("base64");
  return [
    "set -eu",
    `namespace=${shQuote(spec.runtime.namespace)}`,
    `manager_deployment=${shQuote(spec.runtime.managerDeployment)}`,
    `runner_job_id=${shQuote(identity.runnerJobId)}`,
    `job_name=${shQuote(identity.jobName)}`,
    `identity_json_b64=${shQuote(identityJsonB64)}`,
    "tmp_dir=$(mktemp -d)",
    "trap 'rm -rf \"$tmp_dir\"' EXIT",
    "facts_exit=0",
    "set +e",
    "kubectl -n \"$namespace\" exec -i deploy/\"$manager_deployment\" -- env RETENTION_NAMESPACE=\"$namespace\" RETENTION_RUNNER_JOB_ID=\"$runner_job_id\" sh -lc 'cat >/tmp/agentrun-runner-observation.mjs && bun /tmp/agentrun-runner-observation.mjs' > \"$tmp_dir/runner-facts.json\" 2> \"$tmp_dir/runner-facts.err\" <<'NODE'",
    cleanupRunnersFactsNodeScript(),
    "NODE",
    "facts_exit=$?",
    "set -e",
    "if [ \"$facts_exit\" -ne 0 ]; then printf '%s\\n' '{\"ok\":false,\"items\":[],\"failureKind\":\"manager-facts-unavailable\",\"valuesPrinted\":false}' > \"$tmp_dir/runner-facts.json\"; fi",
    "job_exit=0",
    "set +e",
    "kubectl -n \"$namespace\" get job \"$job_name\" -o json > \"$tmp_dir/job.json\" 2> \"$tmp_dir/job.err\"",
    "job_exit=$?",
    "set -e",
    "if [ \"$job_exit\" -ne 0 ]; then printf '%s\\n' '{}' > \"$tmp_dir/job.json\"; fi",
    "pods_exit=0",
    "set +e",
    "kubectl -n \"$namespace\" get pods -l \"job-name=$job_name\" -o json > \"$tmp_dir/pods.json\" 2> \"$tmp_dir/pods.err\"",
    "pods_exit=$?",
    "set -e",
    "if [ \"$pods_exit\" -ne 0 ]; then printf '%s\\n' '{\"items\":[]}' > \"$tmp_dir/pods.json\"; fi",
    "node -e 'const p=JSON.parse(require(\"node:fs\").readFileSync(process.argv[1],\"utf8\")); for(const x of Array.isArray(p.items)?p.items.slice(0,8):[]){const n=x?.metadata?.name;if(typeof n===\"string\"&&n)console.log(n)}' \"$tmp_dir/pods.json\" > \"$tmp_dir/pod-names.txt\"",
    "events_failed=0",
    "event_index=0",
    "while IFS= read -r pod_name; do",
    "  [ -n \"$pod_name\" ] || continue",
    "  event_file=\"$tmp_dir/event-$event_index.json\"",
    "  set +e",
    "  kubectl -n \"$namespace\" get events --field-selector \"involvedObject.kind=Pod,involvedObject.name=$pod_name,type=Warning\" -o json > \"$event_file\" 2> \"$tmp_dir/event-$event_index.err\"",
    "  event_exit=$?",
    "  set -e",
    "  if [ \"$event_exit\" -ne 0 ]; then events_failed=$((events_failed + 1)); printf '%s\\n' '{\"items\":[]}' > \"$event_file\"; fi",
    "  event_index=$((event_index + 1))",
    "done < \"$tmp_dir/pod-names.txt\"",
    "IDENTITY_JSON_B64=\"$identity_json_b64\" FACTS_EXIT=\"$facts_exit\" JOB_EXIT=\"$job_exit\" PODS_EXIT=\"$pods_exit\" EVENTS_FAILED=\"$events_failed\" TMP_DIR=\"$tmp_dir\" NAMESPACE=\"$namespace\" node <<'NODE'",
    runnerJobObservationNodeScript(),
    "NODE",
  ].join("\n");
}

function runnerJobObservationNodeScript(): string {
  return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const tmp = process.env.TMP_DIR;
const expected = JSON.parse(Buffer.from(process.env.IDENTITY_JSON_B64, "base64").toString("utf8"));
const factsExit = Number(process.env.FACTS_EXIT);
const jobExit = Number(process.env.JOB_EXIT);
const podsExit = Number(process.env.PODS_EXIT);
const eventsFailed = Number(process.env.EVENTS_FAILED);

function readJson(name) {
  return JSON.parse(fs.readFileSync(path.join(tmp, name), "utf8"));
}

function iso(value) {
  if (typeof value !== "string" || value.length === 0) return null;
  const ms = Date.parse(value);
  return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}

function ageMs(value) {
  const ms = Date.parse(value || "");
  return Number.isFinite(ms) ? Math.max(0, Date.now() - ms) : null;
}

function bounded(value, max = 320) {
  if (typeof value !== "string" || value.length === 0) return null;
  const oneLine = value.replace(/\s+/g, " ").trim();
  return oneLine.length <= max ? oneLine : oneLine.slice(0, max);
}

function annotationsOf(value) {
  return value?.metadata?.annotations && typeof value.metadata.annotations === "object" ? value.metadata.annotations : {};
}

function stateKind(state) {
  if (state?.running) return "running";
  if (state?.terminated) return "terminated";
  if (state?.waiting) return "waiting";
  return "unknown";
}

function waitingFromStatus(status, source) {
  const waiting = status?.state?.waiting;
  if (!waiting || typeof waiting.reason !== "string" || waiting.reason.length === 0) return [];
  return [{
    source,
    name: typeof status.name === "string" ? status.name : null,
    reason: waiting.reason,
    message: bounded(waiting.message),
    valuesPrinted: false,
  }];
}

function warningFromEvent(event) {
  if (event?.type !== "Warning" || typeof event?.reason !== "string" || event.reason.length === 0) return null;
  return {
    source: "event",
    name: typeof event?.metadata?.name === "string" ? event.metadata.name : null,
    reason: event.reason,
    message: bounded(event.message),
    count: Number.isFinite(event.count) ? event.count : null,
    lastObservedAt: iso(event?.series?.lastObservedTime || event?.eventTime || event?.lastTimestamp || event?.firstTimestamp),
    valuesPrinted: false,
  };
}

function identityMatches(value) {
  const annotations = annotationsOf(value);
  const optionalRunnerJobId = annotations["agentrun.pikastech.local/runner-job-id"];
  return annotations["agentrun.pikastech.local/run-id"] === expected.runId
    && annotations["agentrun.pikastech.local/command-id"] === expected.commandId
    && annotations["agentrun.pikastech.local/runner-id"] === expected.runnerId
    && (optionalRunnerJobId === undefined || optionalRunnerJobId === expected.runnerJobId);
}

const facts = readJson("runner-facts.json");
const factItems = Array.isArray(facts.items) ? facts.items : [];
const fact = factItems.find((item) => item?.id === expected.runnerJobId) || null;
const job = readJson("job.json");
const podsPayload = readJson("pods.json");
const pods = Array.isArray(podsPayload.items) ? podsPayload.items.slice(0, 8) : [];
const eventDocuments = fs.readdirSync(tmp)
  .filter((name) => /^event-\d+\.json$/u.test(name))
  .sort()
  .map(readJson);
const warningEvents = eventDocuments.flatMap((value) => Array.isArray(value.items) ? value.items : []);
const warningsByPod = new Map();
for (const event of warningEvents) {
  const podName = event?.involvedObject?.kind === "Pod" && typeof event?.involvedObject?.name === "string"
    ? event.involvedObject.name
    : null;
  if (podName === null) continue;
  const items = warningsByPod.get(podName) || [];
  items.push(event);
  warningsByPod.set(podName, items);
}

const podObservations = pods.map((pod) => {
  const statuses = Array.isArray(pod?.status?.containerStatuses) ? pod.status.containerStatuses : [];
  const initStatuses = Array.isArray(pod?.status?.initContainerStatuses) ? pod.status.initContainerStatuses : [];
  const runner = statuses.find((status) => status?.name === "runner") || null;
  const conditions = Array.isArray(pod?.status?.conditions) ? pod.status.conditions : [];
  const eventWarnings = (warningsByPod.get(pod?.metadata?.name || "") || []).map(warningFromEvent).filter(Boolean);
  const waiting = [
    ...statuses.flatMap((status) => waitingFromStatus(status, "container")),
    ...initStatuses.flatMap((status) => waitingFromStatus(status, "init-container")),
    ...conditions.flatMap((condition) => condition?.status === "False" && typeof condition?.reason === "string" && condition.reason.length > 0
      ? [{ source: "condition", name: condition.type || null, reason: condition.reason, message: bounded(condition.message), valuesPrinted: false }]
      : []),
    ...eventWarnings,
  ];
  const ready = conditions.some((condition) => condition?.type === "Ready" && condition?.status === "True");
  return {
    name: pod?.metadata?.name || null,
    uid: pod?.metadata?.uid || null,
    resourceVersion: pod?.metadata?.resourceVersion || null,
    phase: pod?.status?.phase || null,
    createdAt: iso(pod?.metadata?.creationTimestamp),
    ageMs: ageMs(pod?.metadata?.creationTimestamp),
    ready,
    identityMatches: identityMatches(pod),
    runnerContainerState: stateKind(runner?.state || {}),
    runnerContainerReady: runner?.ready === true,
    runnerContainerRestartCount: Number(runner?.restartCount || 0),
    waiting: waiting.slice(0, 8),
    waitingOmittedCount: Math.max(0, waiting.length - 8),
    eventFactsAvailable: eventsFailed === 0,
    valuesPrinted: false,
  };
});

const managerIdentityMatches = fact !== null
  && fact.runId === expected.runId
  && fact.commandId === expected.commandId
  && fact.runnerId === expected.runnerId
  && fact.namespace === expected.namespace
  && fact.jobName === expected.jobName;
const jobPresent = jobExit === 0 && job?.metadata?.name === expected.jobName;
const jobIdentityMatches = jobPresent && identityMatches(job);
const podIdentityMatches = podObservations.length > 0 && podObservations.every((pod) => pod.identityMatches === true);
const podPhases = podObservations.map((pod) => pod.phase).filter((value) => typeof value === "string");
const waitingReasons = [...new Set(podObservations.flatMap((pod) => pod.waiting.map((item) => item.reason)).filter(Boolean))].slice(0, 8);
const state = podPhases.includes("Pending")
  ? "Pending"
  : podPhases.includes("Running")
    ? "Running"
    : podPhases.includes("Failed")
      ? "Failed"
      : podPhases.includes("Succeeded")
        ? "Succeeded"
        : jobPresent
          ? "Unknown"
          : "NotFound";
const classification = state === "Pending" && waitingReasons.includes("FailedMount")
  ? "pending-failed-mount"
  : state.toLowerCase();
const warningEventsAvailable = podObservations.length > 0 && eventsFailed === 0;
const ok = factsExit === 0
  && facts.ok === true
  && factItems.length === 1
  && managerIdentityMatches
  && jobPresent
  && jobIdentityMatches
  && podsExit === 0
  && podIdentityMatches
  && warningEventsAvailable;
const reason = ok
  ? null
  : factsExit !== 0 || facts.ok !== true
    ? "manager-runner-facts-query-failed"
    : factItems.length !== 1 || !managerIdentityMatches
      ? "manager-runner-identity-mismatch"
      : !jobPresent
        ? "runner-job-not-found"
        : !jobIdentityMatches
          ? "runner-job-identity-mismatch"
          : podsExit !== 0 || !podIdentityMatches
            ? "runner-pod-observation-incomplete"
            : "runner-warning-event-observation-incomplete";

console.log(JSON.stringify({
  ok,
  reason,
  observedAt: new Date().toISOString(),
  target: { namespace: process.env.NAMESPACE },
  managerFacts: {
    ok: factsExit === 0 && facts.ok === true && factItems.length === 1,
    queryMode: facts.queryMode || null,
    runnerJobId: expected.runnerJobId,
    itemCount: factItems.length,
    identityMatches: managerIdentityMatches,
    valuesPrinted: false,
  },
  kubernetesFacts: {
    jobQuery: "exact-name",
    podQuery: "exact-job-name-label",
    warningEventQuery: "exact-pod-name",
    jobPresent,
    podCount: podObservations.length,
    warningEventCount: warningEvents.length,
    warningEventsAvailable,
    valuesPrinted: false,
  },
  runner: {
    name: expected.jobName,
    namespace: expected.namespace,
    runId: expected.runId,
    commandId: expected.commandId,
    runnerJobId: expected.runnerJobId,
    runnerId: expected.runnerId,
    resourceUid: job?.metadata?.uid || null,
    resourceVersion: job?.metadata?.resourceVersion || null,
    createdAt: iso(job?.metadata?.creationTimestamp),
    lastActiveAt: fact?.runnerHeartbeatAt || fact?.runUpdatedAt || fact?.runnerJobUpdatedAt || null,
    lastActiveAgeMs: ageMs(fact?.runnerHeartbeatAt || fact?.runUpdatedAt || fact?.runnerJobUpdatedAt),
    runStatus: fact?.runStatus || null,
    runClaimedBy: fact?.runClaimedBy || null,
    commandState: fact?.commandState || null,
    heartbeatAt: fact?.runnerHeartbeatAt || null,
    heartbeatAgeMs: ageMs(fact?.runnerHeartbeatAt),
    jobStatus: {
      active: Number(job?.status?.active || 0),
      succeeded: Number(job?.status?.succeeded || 0),
      failed: Number(job?.status?.failed || 0),
      ready: Number(job?.status?.ready || 0),
    },
    podPhases,
    waitingReasons,
    podObservations: podObservations.slice(0, 4),
    podObservationOmittedCount: Math.max(0, podObservations.length - 4),
    identityComplete: managerIdentityMatches && jobIdentityMatches && podIdentityMatches,
    managerFactsComplete: factsExit === 0 && facts.ok === true && factItems.length === 1,
    podFactsComplete: podsExit === 0 && podIdentityMatches && warningEventsAvailable,
    state,
    classification,
    cleanupAuthority: "agentrun-manager-pre-create-retention",
    cleanupDecisionComputed: false,
    mutation: false,
    valuesPrinted: false,
  },
  mutation: false,
  valuesPrinted: false,
}));
`;
}
