// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-07-12-p0-admission-failure-identity.
// Responsibility: Normalize typed AgentRun dispatch failures before durable admission.

// rollout 仅接受上游类型化 failureKind 作为权威；HTTP 状态、耗时和展示文案都不能推断 rollout。
const FAILURE_REASON_RULES = Object.freeze([
  rule("rollout-in-progress", ["rollout-in-progress", "rollout-progressing", "deployment-in-progress", "runtime-starting", "service-starting"]),
  rule("manager-service-unavailable", ["agentrun-connect-failed", "agentrun-manager-fetch-failed", "agentrun-request-failed", "agentrun-timeout", "agentrun-unreachable", "manager-unavailable", "service-unavailable", "upstream-unavailable", "http-502", "http-503", "http-504", "fetch-failed", "network-error", "timeout"]),
  rule("admission-policy-denied", ["admission-denied", "policy-denied", "forbidden", "permission-denied", "rbac-denied", "unauthorized"]),
  rule("runner-capacity-exhausted", ["runner-capacity", "runner-capacity-exhausted", "capacity-exhausted", "no-runner-capacity", "runner-unavailable", "runner-quota-exceeded"]),
  rule("provider-profile-unconfigured", ["provider-profile-unconfigured", "provider-profile-missing", "provider-not-configured", "profile-not-configured", "provider-unconfigured"]),
  rule("secret-unavailable", ["secret-unavailable", "secret-missing", "secret-not-found", "credential-unavailable", "credential-missing"]),
  rule("image-unavailable", ["image-unavailable", "image-not-found", "image-pull-failed", "image-pull-backoff", "invalid-image"]),
  rule("workspace-materialization-failed", ["workspace-materialization-failed", "resource-bundle-materialization-failed", "workspace-unavailable", "gitbundle-unavailable", "workspace-checkout-failed"]),
  rule("lease-idempotency-conflict", ["lease-conflict", "lease-lost", "idempotency-conflict", "command-conflict", "dispatch-intent-conflict", "already-claimed"]),
  rule("dispatcher-runtime-misconfigured", ["schema-invalid", "config-invalid", "configuration-invalid", "dispatcher-misconfigured", "runtime-misconfigured", "agentrun-internal-url-required", "invalid-dispatch-contract"])
]);

const FAILURE_PRESENTATIONS = Object.freeze({
  "rollout-in-progress": {
    userMessage: "AgentRun 正在滚动更新，本次请求尚未接纳；可稍后重试。",
    recoveryAction: "wait-for-rollout"
  },
  "manager-service-unavailable": {
    userMessage: "AgentRun 管理服务暂时不可用，本次请求尚未接纳；请等待服务恢复后重试。",
    recoveryAction: "retry-when-agentrun-service-ready"
  },
  "admission-policy-denied": {
    userMessage: "AgentRun 接纳策略拒绝了本次请求；请检查身份、租户与执行策略。",
    recoveryAction: "review-admission-policy"
  },
  "runner-capacity-exhausted": {
    userMessage: "AgentRun 当前没有可用 Runner 容量，本次请求尚未接纳；请释放或扩容后重试。",
    recoveryAction: "release-or-add-runner-capacity"
  },
  "provider-profile-unconfigured": {
    userMessage: "AgentRun Provider/Profile 未配置，本次请求无法接纳；请先完成对应 Profile 配置。",
    recoveryAction: "configure-provider-profile"
  },
  "secret-unavailable": {
    userMessage: "AgentRun 所需 Secret 不可用，本次请求无法接纳；请恢复 YAML 引用的 Secret。",
    recoveryAction: "restore-referenced-secret"
  },
  "image-unavailable": {
    userMessage: "AgentRun Runner 镜像不可用，本次请求无法接纳；请发布或修正目标镜像。",
    recoveryAction: "publish-or-correct-runner-image"
  },
  "workspace-materialization-failed": {
    userMessage: "AgentRun Workspace/Resource Bundle 物化失败，本次请求无法继续；请检查仓库与资源包引用。",
    recoveryAction: "repair-workspace-materialization"
  },
  "lease-idempotency-conflict": {
    userMessage: "AgentRun Lease/Idempotency 发生冲突；请核对已有 Run/Command 后再重试。",
    recoveryAction: "inspect-existing-lease-or-idempotent-command"
  },
  "dispatcher-runtime-misconfigured": {
    userMessage: "AgentRun 调度契约或运行面配置不匹配，本次请求未进入实时事件流；请展开诊断修复配置。",
    recoveryAction: "repair-agentrun-dispatch-contract"
  },
  "unknown-dispatch-failure": {
    userMessage: "AgentRun 返回了未识别的调度失败，本次请求尚未接纳；请展开诊断定位。",
    recoveryAction: "inspect-agentrun-diagnostic"
  }
});

export function classifyCodeAgentDispatchFailure(error = {}) {
  const upstream = record(error?.agentRunError);
  const nested = record(upstream?.error);
  const failureKind = normalizedFailureKind(firstText(error?.failureKind, upstream?.failureKind, nested?.failureKind, error?.code, upstream?.code, nested?.code, "agentrun-dispatch-error"));
  const statusCode = firstInteger(error?.statusCode, error?.status, upstream?.statusCode, upstream?.status, nested?.statusCode);
  const reason = dispatchFailureReason(failureKind, statusCode);
  const presentation = FAILURE_PRESENTATIONS[reason] ?? FAILURE_PRESENTATIONS["unknown-dispatch-failure"];
  const retryable = firstBoolean(error?.retryable, upstream?.retryable, nested?.retryable, defaultRetryable(reason, statusCode));
  return {
    failureKind,
    reason,
    retryable,
    userMessage: presentation.userMessage,
    recoveryAction: presentation.recoveryAction,
    message: firstText(error?.message, upstream?.message, nested?.message, "AgentRun dispatch failed before durable admission."),
    statusCode,
    dispatchStage: firstText(error?.dispatchStage, error?.stage, upstream?.dispatchStage, upstream?.stage),
    upstreamTraceId: firstText(upstream?.traceId, nested?.traceId),
    runId: firstText(error?.runId, upstream?.runId, upstream?.run?.id, nested?.runId),
    commandId: firstText(error?.commandId, upstream?.commandId, upstream?.command?.id, nested?.commandId),
    dispatchIntentId: firstText(error?.dispatchIntentId, upstream?.dispatchIntentId, upstream?.dispatchIntent?.id, nested?.dispatchIntentId),
    valuesPrinted: false
  };
}

export function dispatchFailureReason(failureKind, statusCode = null) {
  const normalized = normalizedFailureKind(failureKind);
  const matched = FAILURE_REASON_RULES.find((candidate) => candidate.kinds.has(normalized));
  if (matched) return matched.reason;
  if (statusCode === 401 || statusCode === 403) return "admission-policy-denied";
  if (statusCode === 409) return "lease-idempotency-conflict";
  if ([502, 503, 504].includes(statusCode)) return "manager-service-unavailable";
  return "unknown-dispatch-failure";
}

function rule(reason, kinds) {
  return Object.freeze({ reason, kinds: new Set(kinds) });
}

function defaultRetryable(reason, statusCode) {
  if (["rollout-in-progress", "manager-service-unavailable", "runner-capacity-exhausted", "lease-idempotency-conflict"].includes(reason)) return true;
  if (["admission-policy-denied", "provider-profile-unconfigured", "secret-unavailable", "image-unavailable", "workspace-materialization-failed", "dispatcher-runtime-misconfigured"].includes(reason)) return false;
  return Number.isInteger(statusCode) && statusCode >= 500;
}

function normalizedFailureKind(value) {
  const text = String(value ?? "").trim().toLowerCase().replace(/_/gu, "-").replace(/[^a-z0-9.-]+/gu, "-").replace(/^-+|-+$/gu, "");
  return text || "agentrun-dispatch-error";
}

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

function firstText(...values) {
  for (const value of values) {
    if (typeof value === "string" && value.trim()) return value.trim();
  }
  return null;
}

function firstInteger(...values) {
  for (const value of values) {
    const number = Number(value);
    if (Number.isInteger(number) && number > 0) return number;
  }
  return null;
}

function firstBoolean(...values) {
  for (const value of values) if (typeof value === "boolean") return value;
  return false;
}
