import { existsSync, readFileSync } from "node:fs";
import https from "node:https";
import { spawnSync } from "node:child_process";

const namespace = requiredEnv("TEKTON_NAMESPACE");
const query = requiredEnv("TASKRUN_QUERY");
const pipelineRunName = process.env.PIPELINE_RUN_NAME || "";
const pipelineRunPrefix = process.env.PIPELINE_RUN_PREFIX || "";
const logsTailLines = positiveInt(process.env.LOGS_TAIL_LINES, "LOGS_TAIL_LINES");
const maxLogBytes = positiveInt(process.env.MAX_LOG_BYTES, "MAX_LOG_BYTES");
const maxMessageBytes = positiveInt(process.env.MAX_MESSAGE_BYTES, "MAX_MESSAGE_BYTES");
const maxContainers = positiveInt(process.env.MAX_CONTAINERS, "MAX_CONTAINERS");

const useServiceAccount = Boolean(process.env.KUBERNETES_SERVICE_HOST && process.env.KUBERNETES_SERVICE_PORT)
  && existsSync("/var/run/secrets/kubernetes.io/serviceaccount/token")
  && existsSync("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");

class KubeReadError extends Error {
  constructor(reason, message, details = {}) {
    super(shortText(message) || reason);
    this.name = "KubeReadError";
    this.reason = reason;
    this.statusCode = details.statusCode ?? null;
    this.notFound = details.notFound === true;
    this.path = details.path || null;
  }
}

main().catch((error) => {
  process.stderr.write(error?.message || String(error));
  process.exit(1);
});

async function main() {
  const resolution = await resolveTaskRun();
  if (resolution.taskRun === null) {
    console.log(JSON.stringify({
      ok: false,
      degradedReason: resolution.degradedReason || "taskrun-not-found",
      message: resolution.message || null,
      query: { namespace, taskRun: query, pipelineRun: pipelineRunName || null, pipelineRunPrefix: pipelineRunPrefix || null },
      candidates: resolution.candidates,
      resolution: {
        mode: resolution.mode,
        bounded: true,
        namespaceTaskRunList: false,
      },
      statusAuthority: useServiceAccount ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw",
      parsedDownstreamCliOutput: false,
    }));
    return;
  }
  const taskRun = resolution.taskRun;
  const metadata = taskRun.metadata || {};
  const status = taskRun.status || {};
  const condition = conditionByType(status, "Succeeded");
  const podName = status.podName || await resolvePodName(taskRun);
  const pod = podName ? await getJson(`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(podName)}`, false) : null;
  const podStatuses = podStatusRows(pod);
  const taskRunSteps = stepRows(status);
  const containers = mergeContainerRows(taskRunSteps, podStatuses).slice(0, maxContainers);
  const logContainers = selectLogContainers(containers);
  const includeLogTails = process.env.INCLUDE_LOG_TAILS === "true" || condition?.status !== "True";
  const logs = [];
  const perContainerBytes = Math.max(1, Math.floor(maxLogBytes / Math.max(1, logContainers.length)));
  for (const container of logContainers) {
    const name = container.containerName || container.name;
    if (!podName || !name) continue;
    const read = await readPodLog(podName, name, logsTailLines, perContainerBytes);
    const text = read.tail || "";
    logs.push({
      ok: read.ok,
      degradedReason: read.degradedReason,
      message: read.message,
      pod: podName,
      container: name,
      lineCount: text.length === 0 ? 0 : text.split(/\r?\n/u).filter((line) => line.length > 0).length,
      bytes: Buffer.byteLength(text, "utf8"),
      ...(includeLogTails || read.ok === false ? { tail: text } : {}),
      nodeCicdTiming: lastNodeCicdTiming(text),
    });
  }
  const timing = lastTiming(logs);
  const logFailures = logs.filter((item) => item.ok === false);
  console.log(JSON.stringify({
    ok: logFailures.length === 0,
    degradedReason: logFailures.length === 0 ? null : "log-read-failed",
    errors: logFailures.map((item) => ({
      pod: item.pod,
      container: item.container,
      degradedReason: item.degradedReason,
      message: item.message,
    })),
    taskRun: {
      name: metadata.name || null,
      namespace: metadata.namespace || namespace,
      pipelineRun: metadata.labels?.["tekton.dev/pipelineRun"] || null,
      pipelineTask: metadata.labels?.["tekton.dev/pipelineTask"] || metadata.labels?.["tekton.dev/task"] || null,
      podName,
      condition: {
        status: condition?.status || null,
        reason: condition?.reason || null,
        message: shortText(condition?.message || null),
      },
      startTime: status.startTime || null,
      completionTime: status.completionTime || null,
      durationSeconds: durationSeconds(status.startTime, status.completionTime),
    },
    pod: pod === null ? null : {
      name: pod.metadata?.name || podName,
      phase: pod.status?.phase || null,
      containerCount: podStatuses.length,
      initContainerCount: Array.isArray(pod.status?.initContainerStatuses) ? pod.status.initContainerStatuses.length : 0,
      startTime: pod.status?.startTime || null,
    },
    containers,
    logs,
    nodeCicdTiming: timing,
    resolution: {
      mode: resolution.mode,
      candidates: resolution.candidates,
    },
    statusAuthority: useServiceAccount ? "kubernetes-api-serviceaccount" : "target-node-kubectl-raw",
    parsedDownstreamCliOutput: false,
  }));
}

async function resolveTaskRun() {
  const direct = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/taskruns/${encodeURIComponent(query)}`, false);
  if (direct !== null) return { mode: "direct-name", taskRun: direct, candidates: [] };
  if (!pipelineRunName) {
    const fullNameMiss = pipelineRunPrefix.length > 0 && query.startsWith(`${pipelineRunPrefix}-`);
    return {
      mode: fullNameMiss ? "direct-name-not-found" : "pipeline-run-required",
      degradedReason: fullNameMiss ? "taskrun-not-found" : "pipeline-run-required",
      message: fullNameMiss
        ? `TaskRun ${query} was not found in namespace ${namespace}`
        : "--pipeline-run is required for pipeline-task alias lookup; namespace-wide TaskRun listing is disabled",
      taskRun: null,
      candidates: [],
    };
  }
  const selectors = [];
  selectors.push(`tekton.dev/pipelineRun=${pipelineRunName},tekton.dev/pipelineTask=${query}`);
  selectors.push(`tekton.dev/pipelineRun=${pipelineRunName},tekton.dev/task=${query}`);
  for (const selector of selectors) {
    const list = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/taskruns?labelSelector=${encodeURIComponent(selector)}`, false);
    const candidates = taskRunCandidates(list);
    const filtered = candidates.filter((item) => pipelineRunPrefix.length === 0 || String(item.pipelineRun || "").startsWith(pipelineRunPrefix));
    const picked = filtered[0] || null;
    if (picked !== null) {
      const item = await getJson(`/apis/tekton.dev/v1/namespaces/${encodeURIComponent(namespace)}/taskruns/${encodeURIComponent(picked.name)}`, true);
      return { mode: "pipeline-task-label", taskRun: item, candidates: filtered.slice(0, 8) };
    }
    if (candidates.length > 0) return { mode: "pipeline-task-label-no-prefix-match", taskRun: null, candidates: candidates.slice(0, 8) };
  }
  return { mode: "not-found", taskRun: null, candidates: [] };
}

async function resolvePodName(taskRun) {
  const uid = taskRun?.metadata?.uid || "";
  const name = taskRun?.metadata?.name || query;
  const selectors = uid ? [`tekton.dev/taskRunUID=${uid}`, `tekton.dev/taskRun=${name}`] : [`tekton.dev/taskRun=${name}`];
  for (const selector of selectors) {
    const list = await getJson(`/api/v1/namespaces/${encodeURIComponent(namespace)}/pods?labelSelector=${encodeURIComponent(selector)}`, false);
    const items = Array.isArray(list?.items) ? list.items : [];
    const pod = items[0];
    if (pod?.metadata?.name) return pod.metadata.name;
  }
  return null;
}

function taskRunCandidates(list) {
  const items = Array.isArray(list?.items) ? list.items : [];
  return items.map((item) => ({
    name: item?.metadata?.name || null,
    pipelineRun: item?.metadata?.labels?.["tekton.dev/pipelineRun"] || null,
    pipelineTask: item?.metadata?.labels?.["tekton.dev/pipelineTask"] || item?.metadata?.labels?.["tekton.dev/task"] || null,
    startTime: item?.status?.startTime || item?.metadata?.creationTimestamp || null,
    status: conditionByType(item?.status || {}, "Succeeded")?.status || null,
    reason: conditionByType(item?.status || {}, "Succeeded")?.reason || null,
  })).filter((item) => item.name !== null).sort((left, right) => String(right.startTime || "").localeCompare(String(left.startTime || "")));
}

function stepRows(status) {
  const steps = Array.isArray(status?.steps) ? status.steps : [];
  return steps.map((step) => ({
    source: "taskrun-step",
    name: step.name || null,
    containerName: step.container || (step.name ? `step-${step.name}` : null),
    terminated: compactTerminated(step.terminated),
    waiting: compactWaiting(step.waiting),
    running: step.running ? { startedAt: step.running.startedAt || null } : null,
  }));
}

function podStatusRows(pod) {
  const status = pod?.status || {};
  return [
    ...statusArray(status.initContainerStatuses, "pod-init-container"),
    ...statusArray(status.containerStatuses, "pod-container"),
    ...statusArray(status.ephemeralContainerStatuses, "pod-ephemeral-container"),
  ];
}

function statusArray(value, source) {
  return (Array.isArray(value) ? value : []).map((item) => ({
    source,
    name: item.name || null,
    containerName: item.name || null,
    ready: item.ready === true,
    restartCount: Number.isInteger(item.restartCount) ? item.restartCount : null,
    terminated: compactTerminated(item.state?.terminated || item.lastState?.terminated),
    waiting: compactWaiting(item.state?.waiting),
    running: item.state?.running ? { startedAt: item.state.running.startedAt || null } : null,
  }));
}

function mergeContainerRows(steps, podStatuses) {
  const rows = [];
  const byName = new Map(podStatuses.map((item) => [item.containerName, item]));
  for (const step of steps) rows.push({ ...step, ...(byName.get(step.containerName) || {}) });
  const seen = new Set(rows.map((item) => item.containerName));
  for (const item of podStatuses) {
    if (!seen.has(item.containerName)) rows.push(item);
  }
  return rows.map((item) => ({
    source: item.source || null,
    name: item.name || null,
    containerName: item.containerName || null,
    ready: item.ready === undefined ? null : item.ready,
    restartCount: Number.isInteger(item.restartCount) ? item.restartCount : null,
    terminated: item.terminated || null,
    waiting: item.waiting || null,
    running: item.running || null,
  }));
}

function selectLogContainers(containers) {
  const failed = containers.filter((item) => item.terminated?.exitCode !== null && item.terminated?.exitCode !== 0);
  const waiting = containers.filter((item) => item.waiting !== null);
  const step = containers.filter((item) => String(item.containerName || "").startsWith("step-"));
  const selected = [];
  for (const item of [...failed, ...waiting, ...step, ...containers]) {
    if (selected.some((existing) => existing.containerName === item.containerName)) continue;
    selected.push(item);
    if (selected.length >= maxContainers) break;
  }
  return selected;
}

async function readPodLog(podName, container, tailLines, limitBytes) {
  const path = `/api/v1/namespaces/${encodeURIComponent(namespace)}/pods/${encodeURIComponent(podName)}/log?container=${encodeURIComponent(container)}&tailLines=${tailLines}&limitBytes=${limitBytes}`;
  try {
    const text = tailBytes(await getText(path, false), limitBytes);
    return { ok: true, degradedReason: null, message: null, tail: text };
  } catch (error) {
    return {
      ok: false,
      degradedReason: isNotFoundError(error) ? "log-not-found" : error instanceof KubeReadError ? error.reason : "log-read-failed",
      message: shortText(error?.message || String(error)),
      tail: "",
    };
  }
}

async function getJson(path, required) {
  let text = "";
  try {
    text = await getText(path, required);
  } catch (error) {
    if (!required && isNotFoundError(error)) return null;
    throw error;
  }
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new KubeReadError("invalid-json", `kube api returned non-JSON for ${path}: ${error?.message || String(error)}`, { path });
  }
}

async function getText(path, required) {
  if (useServiceAccount) return kubeApiGet(path, required);
  const result = spawnSync("kubectl", ["get", "--raw", path], { encoding: "utf8", maxBuffer: Math.max(maxLogBytes * 2, 1024 * 1024) });
  if (result.error) {
    throw new KubeReadError("transport-error", `kubectl get --raw transport error for ${path}: ${result.error.message}`, { path });
  }
  if (result.status === 0) return result.stdout;
  const body = result.stderr || result.stdout || `kubectl get --raw failed with exit ${result.status}`;
  throw new KubeReadError(isNotFoundText(body) ? "not-found" : "kube-api-error", `kubectl get --raw failed for ${path}: ${body}`, {
    path,
    notFound: isNotFoundText(body),
  });
}

function kubeApiGet(path, required) {
  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");
  return new Promise((resolve, reject) => {
    const req = https.request({ host, port, path, method: "GET", ca, headers: { authorization: `Bearer ${token}` } }, (res) => {
      let body = "";
      res.setEncoding("utf8");
      res.on("data", (chunk) => { body += chunk; });
      res.on("end", () => {
        const code = res.statusCode || 0;
        if (code >= 200 && code < 300) resolve(body);
        else reject(new KubeReadError(code === 404 || isNotFoundText(body) ? "not-found" : "kube-api-error", `kube api GET ${path} status ${code}: ${body || "-"}`, {
          path,
          statusCode: code,
          notFound: code === 404 || isNotFoundText(body),
        }));
      });
    });
    req.on("error", (error) => reject(new KubeReadError("transport-error", `kube api GET ${path} transport error: ${error?.message || String(error)}`, { path })));
    req.end();
  });
}

function isNotFoundError(error) {
  return error instanceof KubeReadError && error.notFound === true;
}

function isNotFoundText(value) {
  return /\b404\b|not found|NotFound/u.test(String(value || ""));
}

function compactTerminated(value) {
  if (!value) return null;
  return {
    reason: value.reason || null,
    exitCode: Number.isInteger(value.exitCode) ? value.exitCode : null,
    message: shortText(value.message || null),
    startedAt: value.startedAt || null,
    finishedAt: value.finishedAt || null,
  };
}

function compactWaiting(value) {
  if (!value) return null;
  return {
    reason: value.reason || null,
    message: shortText(value.message || null),
  };
}

function lastNodeCicdTiming(text) {
  const lines = text.split(/\r?\n/u).filter((line) => line.includes("node-cicd-timing"));
  for (let index = lines.length - 1; index >= 0; index -= 1) {
    const parsed = parseTimingLine(lines[index]);
    if (parsed !== null) return parsed;
  }
  return null;
}

function parseTimingLine(line) {
  const candidates = [line.trim()];
  const brace = line.indexOf("{");
  if (brace >= 0) candidates.push(line.slice(brace).trim());
  for (const candidate of candidates) {
    try {
      const parsed = JSON.parse(candidate);
      if (parsed && typeof parsed === "object" && JSON.stringify(parsed).includes("node-cicd-timing")) return parsed;
    } catch {
      // Keep trying the next bounded candidate.
    }
  }
  return { rawTail: shortText(line) };
}

function lastTiming(logs) {
  for (let index = logs.length - 1; index >= 0; index -= 1) {
    if (logs[index].nodeCicdTiming !== null) return logs[index].nodeCicdTiming;
  }
  return null;
}

function conditionByType(status, type) {
  const conditions = Array.isArray(status?.conditions) ? status.conditions : [];
  return conditions.find((item) => item?.type === type) || null;
}

function timestampMs(value) {
  const parsed = Date.parse(String(value || ""));
  return Number.isFinite(parsed) ? parsed : null;
}

function durationSeconds(start, end) {
  const s = timestampMs(start);
  const e = timestampMs(end);
  return s === null || e === null || e < s ? null : Math.round((e - s) / 1000);
}

function shortText(value) {
  if (value === null || value === undefined) return null;
  const text = String(value).replace(/\s+/gu, " ").trim();
  return text.length <= maxMessageBytes ? text : `${text.slice(0, maxMessageBytes - 3)}...`;
}

function tailBytes(value, maxBytes) {
  const buffer = Buffer.from(value, "utf8");
  if (buffer.length <= maxBytes) return value;
  return buffer.subarray(buffer.length - maxBytes).toString("utf8");
}

function positiveInt(value, name) {
  const parsed = Number(value);
  if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);
  return parsed;
}

function requiredEnv(name) {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required`);
  return value;
}
