import { createHash } from "node:crypto";

import type { RenderedCliResult } from "../output";
import { normalizeEventItems, renderMachine, renderedCliResult } from "./render";
import type { AgentRunResourceOptions } from "./utils";
import { record, stringOrNull } from "./utils";

const EVENT_DETAIL_TEXT_CHARS = 240;
const EVENT_DETAIL_FIELD_LIMIT = 32;

export function renderEventDetail(
  command: string,
  raw: Record<string, unknown>,
  options: AgentRunResourceOptions,
  runId: string,
  item: Record<string, unknown>,
): RenderedCliResult {
  const payload = compactEventDetail(runId, item, options);
  if (options.output === "json" || options.output === "yaml") {
    return renderMachine(command, payload, options.output, raw.ok !== false);
  }
  return renderedCliResult(raw.ok !== false, command, renderEventDetailHuman(payload));
}

export function compactEventDetail(
  runId: string,
  item: Record<string, unknown>,
  options: Pick<AgentRunResourceOptions, "node" | "lane"> = { node: null, lane: null },
): Record<string, unknown> {
  const rawPayload = record(item.payload);
  const summary = normalizeEventItems({ items: [item] }, runId)[0] ?? {};
  const seq = typeof summary.seq === "number" ? summary.seq : item.seq;
  const targetArgs = [
    options.node === null ? null : `--node ${options.node}`,
    options.lane === null ? null : `--lane ${options.lane}`,
  ].filter((value): value is string => value !== null).join(" ");
  const suffix = targetArgs.length === 0 ? "" : ` ${targetArgs}`;
  const afterSeq = typeof seq === "number" && Number.isSafeInteger(seq) ? Math.max(0, seq - 1) : "<seq-1>";
  const evidence = compactEventEvidence(item, rawPayload);
  return {
    kind: "EventDetail",
    source: `run/${runId}`,
    identity: summary.identity ?? `run/${runId}/seq/${String(seq)}`,
    seq,
    type: summary.type ?? null,
    phase: summary.phase ?? null,
    status: summary.status ?? null,
    correlation: compactDefined({
      runId,
      commandId: nullableSummaryValue(summary.commandId),
      taskId: firstBoundedString(item.taskId, rawPayload.taskId),
      attemptId: firstBoundedString(item.attemptId, rawPayload.attemptId),
      sessionId: firstBoundedString(item.sessionId, rawPayload.sessionId),
      runnerId: firstBoundedString(item.runnerId, rawPayload.runnerId),
      runnerJobId: firstBoundedString(item.runnerJobId, rawPayload.runnerJobId),
      itemId: firstBoundedString(item.itemId, rawPayload.itemId),
    }),
    ...(summary.tool === undefined ? {} : { tool: summary.tool }),
    ...(summary.exitCode === undefined ? {} : { exitCode: summary.exitCode }),
    ...(summary.durationMs === undefined ? {} : { durationMs: summary.durationMs }),
    ...(summary.outputBytes === undefined ? {} : { outputBytes: summary.outputBytes }),
    ...(summary.outputSha256 === undefined ? {} : { outputSha256: summary.outputSha256 }),
    ...(summary.outputTruncated === undefined ? {} : { outputTruncated: summary.outputTruncated }),
    ...(summary.outputOmitted === undefined ? {} : { outputOmitted: summary.outputOmitted }),
    summary: safeEventDetailSummary(summary, rawPayload),
    evidence,
    disclosure: {
      projection: "bounded render-only durable event detail",
      payload: "omitted; selected scalars, counts, bytes and hashes only",
      full: `bun scripts/cli.ts agentrun events run/${runId} --after-seq ${afterSeq} --limit 1 --full -o json${suffix}`,
      raw: `bun scripts/cli.ts agentrun events run/${runId} --after-seq ${afterSeq} --limit 1 --raw${suffix}`,
    },
    redaction: {
      credentialValues: "omitted",
      urlCredentials: "removed",
      secretRefsOnly: true,
      valuesPrinted: false,
    },
    valuesPrinted: false,
  };
}

function compactEventEvidence(item: Record<string, unknown>, payload: Record<string, unknown>): Record<string, unknown> {
  const phase = firstBoundedString(item.phase, payload.phase);
  const generic = {
    metadata: compactEventMetadata(payload),
    content: compactEventContent(payload),
    payloadShape: {
      fieldCount: Object.keys(payload).length,
      fields: Object.keys(payload).sort().slice(0, EVENT_DETAIL_FIELD_LIMIT).map((key) => boundedText(key, 80)),
      omittedFieldCount: Math.max(0, Object.keys(payload).length - EVENT_DETAIL_FIELD_LIMIT),
      valuesOmitted: true,
      valuesPrinted: false,
    },
  };
  if (phase !== "resource-bundle-materialized") return { ...generic, valuesPrinted: false };
  return {
    ...generic,
    ...compactResourceBundleMaterializedEvidence(payload),
    valuesPrinted: false,
  };
}

function safeEventDetailSummary(summary: Record<string, unknown>, payload: Record<string, unknown>): string {
  const parts = [
    boundedScalar(summary.phase),
    boundedScalar(summary.type),
    boundedScalar(summary.status),
    boundedScalar(summary.tool),
  ].filter((value): value is string | number | boolean => value !== null);
  for (const [name, value] of [
    ["bundles", payload.bundles],
    ["tools", payload.tools],
    ["requiredSkills", payload.requiredSkills],
    ["promptRefs", payload.promptRefs],
  ] as const) {
    const count = collectionCount(value);
    if (count !== null) parts.push(`${name}=${count}`);
  }
  return boundedText(parts.map(String).filter((value, index, values) => values.indexOf(value) === index).join("; "), EVENT_DETAIL_TEXT_CHARS);
}

function compactResourceBundleMaterializedEvidence(payload: Record<string, unknown>): Record<string, unknown> {
  const primary = record(payload.primaryWorkspace);
  const source = Object.keys(primary).length > 0 ? primary : payload;
  const resolutionAuthority = projectedResolutionAuthority(source, payload);
  const primaryWorkspace = {
    ...compactDefined({
      status: boundedScalar(source.status),
      sourceContract: boundedScalar(source.sourceContract),
      repoUrl: redactedUrl(source.repoUrl ?? payload.repoUrl),
      fetchRepoUrl: redactedUrl(source.fetchRepoUrl ?? payload.fetchRepoUrl),
      mirrorUsed: booleanOrNull(source.mirrorUsed ?? payload.mirrorUsed),
      requestedRef: boundedScalar(source.requestedRef ?? payload.requestedRef),
      requestedCommitId: boundedScalar(source.requestedCommitId ?? payload.requestedCommitId),
      sourceCommit: boundedScalar(source.sourceCommit ?? payload.commitId),
      headCommit: boundedScalar(source.headCommit),
      headIsFrozenSource: booleanOrNull(source.headIsFrozenSource),
      treeId: boundedScalar(source.treeId ?? payload.treeId),
      headTreeId: boundedScalar(source.headTreeId),
      sourceBranch: boundedScalar(source.sourceBranch),
      branch: boundedScalar(source.branch),
      targetPath: boundedScalar(source.targetPath),
      workspaceLifecycle: boundedScalar(source.workspaceLifecycle ?? payload.workspaceLifecycle),
      writable: booleanOrNull(source.writable),
      assemblyContractHash: boundedScalar(source.assemblyContractHash),
      bundleContractHash: boundedScalar(source.bundleContractHash),
      bundleProvenanceHash: boundedScalar(source.bundleProvenanceHash),
      provenanceHash: boundedScalar(source.provenanceHash),
    }),
    resolutionAuthority: resolutionAuthority.durableValue,
    resolutionAuthoritySource: "durable-event",
    resolutionAuthorityRedacted: resolutionAuthority.durableValueRedacted,
    ...(resolutionAuthority.modeInference === null
      ? {}
      : { resolutionModeInference: resolutionAuthority.modeInference }),
    valuesPrinted: false,
  };
  const counts = compactDefined({
    bundles: collectionCount(payload.bundles),
    tools: collectionCount(payload.tools),
    skillDirs: collectionCount(payload.skillDirs),
    requiredSkills: collectionCount(payload.requiredSkills),
    promptRefs: collectionCount(payload.promptRefs),
  });
  const initialPrompt = record(payload.initialPrompt);
  const promptSummary = Object.keys(initialPrompt).length === 0
    ? null
    : compactDefined({
        available: booleanOrNull(initialPrompt.available),
        bytes: finiteNumberOrNull(initialPrompt.bytes),
        sha256: boundedScalar(initialPrompt.sha256),
        promptRefCount: finiteNumberOrNull(initialPrompt.promptRefCount),
        skillCount: finiteNumberOrNull(initialPrompt.skillCount),
        contentOmitted: true,
        valuesPrinted: false,
      });
  return {
    primaryWorkspace,
    counts,
    ...(promptSummary === null ? {} : { initialPrompt: promptSummary }),
  };
}

function projectedResolutionAuthority(source: Record<string, unknown>, payload: Record<string, unknown>): {
  durableValue: string | null;
  durableValueRedacted: boolean;
  modeInference: Record<string, unknown> | null;
} {
  const durable = stringOrNull(source.resolutionAuthority) ?? stringOrNull(payload.resolutionAuthority);
  if (durable !== null && durable !== "REDACTED") {
    return {
      durableValue: boundedText(durable, EVENT_DETAIL_TEXT_CHARS),
      durableValueRedacted: false,
      modeInference: null,
    };
  }
  const owner = Object.hasOwn(source, "requestedCommitId")
    ? source
    : Object.hasOwn(payload, "requestedCommitId")
      ? payload
      : null;
  if (durable === "REDACTED" && owner !== null) {
    const requestedCommitId = owner.requestedCommitId;
    if (requestedCommitId === null || stringOrNull(requestedCommitId) !== null) {
      return {
        durableValue: null,
        durableValueRedacted: true,
        modeInference: {
          mode: requestedCommitId === null ? "runner-first-materialization" : "declared-commit",
          source: "durable-event requestedCommitId",
          authoritative: false,
          valuesPrinted: false,
        },
      };
    }
  }
  return {
    durableValue: null,
    durableValueRedacted: durable === "REDACTED",
    modeInference: null,
  };
}

function compactEventMetadata(payload: Record<string, unknown>): Record<string, unknown> {
  const keys = [
    "phase",
    "kind",
    "method",
    "status",
    "failureKind",
    "willRetry",
    "exitCode",
    "durationMs",
    "namespace",
    "jobName",
    "serviceAccountName",
    "sourceCommit",
    "bootCommit",
    "commitId",
    "treeId",
  ];
  const result: Record<string, unknown> = {};
  for (const key of keys) {
    const value = payload[key];
    if (typeof value === "string") result[key] = boundedText(value, EVENT_DETAIL_TEXT_CHARS);
    else if (typeof value === "number" && Number.isFinite(value)) result[key] = value;
    else if (typeof value === "boolean") result[key] = value;
  }
  result.valuesPrinted = false;
  return result;
}

function compactEventContent(payload: Record<string, unknown>): Record<string, unknown> {
  const keys = ["command", "stdout", "stderr", "output", "outputSummary", "summary", "text", "message", "reason", "prompt", "content"];
  const result: Record<string, unknown> = {};
  for (const key of keys) {
    if (payload[key] !== undefined && payload[key] !== null) result[key] = contentMetadata(payload[key]);
  }
  result.valuesPrinted = false;
  return result;
}

function contentMetadata(value: unknown): Record<string, unknown> {
  const text = typeof value === "string" ? value : stableJson(value);
  return {
    bytes: Buffer.byteLength(text, "utf8"),
    sha256: `sha256:${createHash("sha256").update(text).digest("hex")}`,
    omitted: true,
    valuesPrinted: false,
  };
}

function stableJson(value: unknown): string {
  try {
    return JSON.stringify(value) ?? String(value);
  } catch {
    return String(value);
  }
}

function collectionCount(value: unknown): number | null {
  if (Array.isArray(value)) return value.length;
  const object = record(value);
  const count = finiteNumberOrNull(object.count);
  if (count !== null) return count;
  return Array.isArray(object.items) ? object.items.length : null;
}

function redactedUrl(value: unknown): string | null {
  const raw = stringOrNull(value);
  if (raw === null) return null;
  if (raw.includes("://")) {
    try {
      const parsed = new URL(raw);
      if (!["git:", "git+ssh:", "http:", "https:", "ssh:"].includes(parsed.protocol) || parsed.hostname.length === 0) {
        return nonstandardRemoteFingerprint("nonstandard-url-remote", raw);
      }
      parsed.username = "";
      parsed.password = "";
      parsed.search = "";
      parsed.hash = "";
      return boundedText(parsed.toString(), EVENT_DETAIL_TEXT_CHARS);
    } catch {
      return nonstandardRemoteFingerprint("invalid-url", raw);
    }
  }
  const scpLike = /^(?:[^@\s]+@)?[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?:[^\r\n]+$/u.test(raw);
  if (scpLike) return nonstandardRemoteFingerprint("scp-like-remote", raw);
  return nonstandardRemoteFingerprint("nonstandard-remote", raw);
}

function nonstandardRemoteFingerprint(kind: string, raw: string): string {
  const fingerprint = createHash("sha256").update(raw).digest("hex");
  return `<${kind};path-omitted;sha256:${fingerprint}>`;
}

function renderEventDetailHuman(payload: Record<string, unknown>): string {
  const correlation = record(payload.correlation);
  const lines = [
    `Event:   ${String(payload.identity ?? "-")}`,
    `Run:     ${String(correlation.runId ?? "-")}`,
    `Seq:     ${String(payload.seq ?? "-")}`,
    `Type:    ${String(payload.type ?? "-")}`,
    `Phase:   ${String(payload.phase ?? "-")}`,
    `Status:  ${String(payload.status ?? "-")}`,
    `Command: ${String(correlation.commandId ?? "-")}`,
    `Summary: ${String(payload.summary ?? "-")}`,
    "",
    "Evidence:",
    JSON.stringify(payload.evidence ?? {}, null, 2),
    "",
    "Disclosure:",
    `  Full: ${String(record(payload.disclosure).full ?? "-")}`,
    `  Raw:  ${String(record(payload.disclosure).raw ?? "-")}`,
    "  Values: credential and content values omitted",
  ];
  return lines.join("\n");
}

function compactDefined(value: Record<string, unknown>): Record<string, unknown> {
  return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== null && item !== undefined));
}

function firstBoundedString(...values: unknown[]): string | null {
  for (const value of values) {
    const text = stringOrNull(value);
    if (text !== null) return boundedText(text, EVENT_DETAIL_TEXT_CHARS);
  }
  return null;
}

function nullableSummaryValue(value: unknown): unknown {
  return value === "-" ? null : value;
}

function boundedScalar(value: unknown): string | number | boolean | null {
  if (typeof value === "string") return boundedText(value, EVENT_DETAIL_TEXT_CHARS);
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "boolean") return value;
  return null;
}

function boundedText(value: string, maxChars: number): string {
  const oneLine = value.replace(/\s+/gu, " ").trim();
  return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, Math.max(0, maxChars - 3))}...`;
}

function booleanOrNull(value: unknown): boolean | null {
  return typeof value === "boolean" ? value : null;
}

function finiteNumberOrNull(value: unknown): number | null {
  return typeof value === "number" && Number.isFinite(value) ? value : null;
}
