/**
 * Exec approval request client.
 * Registers two-phase approval requests with the gateway, waits for decisions,
 * and builds host/node payloads with optional command highlighting.
 */
import {
  asDateTimestampMs,
  resolveExpiresAtMsFromDurationMs,
} from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString as parseString } from "@openclaw/normalization-core/string-coerce";
import { isApprovalNotFoundError } from "../infra/approval-errors.js";
import type {
  ExecApprovalCommandSpan,
  ExecApprovalUnavailableDecision,
  ExecAsk,
  ExecSecurity,
  SystemRunApprovalPlan,
} from "../infra/exec-approvals.js";
import { normalizeExecutableToken } from "../infra/exec-wrapper-tokens.js";
import {
  isShellWrapperExecutable,
  POSIX_PARSEABLE_SHELL_WRAPPERS,
  resolveShellWrapperTransportArgv,
} from "../infra/shell-wrapper-resolution.js";
import { createLazyPromise } from "../shared/lazy-runtime.js";
import {
  DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS,
  DEFAULT_APPROVAL_TIMEOUT_MS,
} from "./bash-tools.exec-runtime.js";
import { callGatewayTool } from "./tools/gateway.js";

const POSIX_COMMAND_HIGHLIGHT_SHELLS: ReadonlySet<string> = POSIX_PARSEABLE_SHELL_WRAPPERS;

const loadExecApprovalCommandSpansRuntime = createLazyPromise(
  () => import("./bash-tools.exec-approval-request.runtime.js"),
  { cacheRejections: true },
);

/** Gateway payload fields used to register or wait for an exec approval decision. */
type RequestExecApprovalDecisionParams = {
  id: string;
  command?: string;
  commandArgv?: string[];
  systemRunPlan?: SystemRunApprovalPlan;
  env?: Record<string, string>;
  cwd: string | undefined;
  nodeId?: string;
  host: "gateway" | "node";
  security: ExecSecurity;
  ask: ExecAsk;
  warningText?: string;
  commandSpans?: ExecApprovalCommandSpan[];
  unavailableDecisions?: readonly ExecApprovalUnavailableDecision[];
  agentId?: string;
  resolvedPath?: string;
  sessionKey?: string;
  sessionId?: string;
  runId?: string;
  toolCallId?: string;
  turnSourceChannel?: string;
  turnSourceTo?: string;
  turnSourceAccountId?: string;
  turnSourceThreadId?: string | number;
  approvalReviewerDeviceIds?: string[];
  requireDeliveryRoute?: boolean;
  suppressDelivery?: boolean;
};

type ExecApprovalRequestToolParams = RequestExecApprovalDecisionParams & {
  timeoutMs: number;
  twoPhase: true;
};

function buildExecApprovalRequestToolParams(
  params: RequestExecApprovalDecisionParams,
): ExecApprovalRequestToolParams {
  return {
    id: params.id,
    ...(params.command ? { command: params.command } : {}),
    ...(params.commandArgv ? { commandArgv: params.commandArgv } : {}),
    systemRunPlan: params.systemRunPlan,
    env: params.env,
    cwd: params.cwd,
    nodeId: params.nodeId,
    host: params.host,
    security: params.security,
    ask: params.ask,
    warningText: params.warningText,
    commandSpans: params.commandSpans,
    ...(params.unavailableDecisions?.length
      ? { unavailableDecisions: params.unavailableDecisions }
      : {}),
    agentId: params.agentId,
    resolvedPath: params.resolvedPath,
    sessionKey: params.sessionKey,
    sessionId: params.sessionId,
    runId: params.runId,
    toolCallId: params.toolCallId,
    turnSourceChannel: params.turnSourceChannel,
    turnSourceTo: params.turnSourceTo,
    turnSourceAccountId: params.turnSourceAccountId,
    turnSourceThreadId: params.turnSourceThreadId,
    approvalReviewerDeviceIds: params.approvalReviewerDeviceIds,
    requireDeliveryRoute: params.requireDeliveryRoute,
    suppressDelivery: params.suppressDelivery,
    timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
    twoPhase: true,
  };
}

type ParsedDecision = { present: boolean; value: string | null };

function parseDecision(value: unknown): ParsedDecision {
  if (!value || typeof value !== "object") {
    return { present: false, value: null };
  }
  // Distinguish "field missing" from "field present but null/invalid".
  // Registration responses intentionally omit `decision`; decision waits can include it.
  if (!Object.hasOwn(value, "decision")) {
    return { present: false, value: null };
  }
  const decision = (value as { decision?: unknown }).decision;
  return { present: true, value: typeof decision === "string" ? decision : null };
}

function parseExpiresAtMs(value: unknown): number | undefined {
  return asDateTimestampMs(value);
}

function resolveDefaultExecApprovalExpiresAtMs(): number {
  return resolveExpiresAtMsFromDurationMs(DEFAULT_APPROVAL_TIMEOUT_MS) ?? 0;
}

/** Registration result returned before an approval decision is available. */
export type ExecApprovalRegistration = {
  id: string;
  expiresAtMs: number;
  finalDecision?: string | null;
};

class ExecApprovalRunAbortedError extends Error {
  constructor() {
    super("Exec approval cancelled because its run was aborted");
    this.name = "ExecApprovalRunAbortedError";
  }
}

export function isExecApprovalRunAbortedError(error: unknown): boolean {
  return error instanceof ExecApprovalRunAbortedError;
}

/** Registers a two-phase exec approval request with the gateway. */
async function registerExecApprovalRequest(
  params: RequestExecApprovalDecisionParams,
): Promise<ExecApprovalRegistration> {
  // Two-phase registration is critical: the ID must be registered server-side
  // before exec returns `approval-pending`, otherwise `/approve` can race and orphan.
  const registrationResult = await callGatewayTool(
    "exec.approval.request",
    { timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
    buildExecApprovalRequestToolParams(params),
    { expectFinal: false },
  );
  const decision = parseDecision(registrationResult);
  const id = parseString(registrationResult?.id) ?? params.id;
  const expiresAtMs =
    parseExpiresAtMs(registrationResult?.expiresAtMs) ?? resolveDefaultExecApprovalExpiresAtMs();
  if (decision.present) {
    return { id, expiresAtMs, finalDecision: decision.value };
  }
  return { id, expiresAtMs };
}

/** Uses a pre-resolved decision or waits for the registered approval id. */
export async function resolveRegisteredExecApprovalDecision(params: {
  approvalId: string;
  preResolvedDecision: string | null | undefined;
}): Promise<string | null> {
  if (params.preResolvedDecision !== undefined) {
    return params.preResolvedDecision ?? null;
  }
  try {
    const decisionResult = await callGatewayTool<{ decision: string }>(
      "exec.approval.waitDecision",
      { timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
      { id: params.approvalId },
    );
    if (
      decisionResult &&
      typeof decisionResult === "object" &&
      (decisionResult as { terminalReason?: unknown }).terminalReason === "run-aborted"
    ) {
      throw new ExecApprovalRunAbortedError();
    }
    return parseDecision(decisionResult).value;
  } catch (err) {
    // Timeout/cleanup path: treat missing/expired as no decision so askFallback applies.
    if (isApprovalNotFoundError(err)) {
      return null;
    }
    throw err;
  }
}

type HostExecApprovalParams = {
  approvalId: string;
  command?: string;
  commandArgv?: string[];
  systemRunPlan?: SystemRunApprovalPlan;
  env?: Record<string, string>;
  workdir: string | undefined;
  host: "gateway" | "node";
  nodeId?: string;
  security: ExecSecurity;
  ask: ExecAsk;
  warningText?: string;
  commandSpans?: ExecApprovalCommandSpan[];
  unavailableDecisions?: readonly ExecApprovalUnavailableDecision[];
  commandHighlighting?: boolean;
  agentId?: string;
  resolvedPath?: string;
  sessionKey?: string;
  sessionId?: string;
  runId?: string;
  toolCallId?: string;
  turnSourceChannel?: string;
  turnSourceTo?: string;
  turnSourceAccountId?: string;
  turnSourceThreadId?: string | number;
  approvalReviewerDeviceIds?: string[];
  requireDeliveryRoute?: boolean;
  suppressDelivery?: boolean;
};

type ExecApprovalRequesterContext = {
  agentId?: string;
  sessionKey?: string;
};

/** Builds requester identity context for an approval payload. */
export function buildExecApprovalRequesterContext(params: ExecApprovalRequesterContext): {
  agentId?: string;
  sessionKey?: string;
} {
  return {
    agentId: params.agentId,
    sessionKey: params.sessionKey,
  };
}

type ExecApprovalTurnSourceContext = {
  turnSourceChannel?: string;
  turnSourceTo?: string;
  turnSourceAccountId?: string;
  turnSourceThreadId?: string | number;
};

/** Builds originating channel context for approval delivery/routing. */
export function buildExecApprovalTurnSourceContext(
  params: ExecApprovalTurnSourceContext,
): ExecApprovalTurnSourceContext {
  return {
    turnSourceChannel: params.turnSourceChannel,
    turnSourceTo: params.turnSourceTo,
    turnSourceAccountId: params.turnSourceAccountId,
    turnSourceThreadId: params.turnSourceThreadId,
  };
}

async function resolveCommandSpans(
  command: string | undefined,
): Promise<ExecApprovalCommandSpan[] | undefined> {
  if (!command) {
    return undefined;
  }
  try {
    const { resolveExecApprovalCommandSpans } = await loadExecApprovalCommandSpansRuntime();
    return await resolveExecApprovalCommandSpans(command);
  } catch {
    return undefined;
  }
}

function hasUnsupportedShellArgv(argv: readonly string[] | undefined): boolean {
  if (!argv?.length) {
    return false;
  }
  const shellWrapperArgv = resolveShellWrapperTransportArgv([...argv]) ?? argv;
  const executable = shellWrapperArgv[0];
  if (!executable) {
    return false;
  }
  const normalizedExecutable = normalizeExecutableToken(executable);
  return (
    isShellWrapperExecutable(normalizedExecutable) &&
    !POSIX_COMMAND_HIGHLIGHT_SHELLS.has(normalizedExecutable)
  );
}

function shouldSkipGeneratedCommandSpans(params: HostExecApprovalParams): boolean {
  if (params.host === "gateway" && process.platform === "win32") {
    return true;
  }
  const argv = params.commandArgv?.length ? params.commandArgv : params.systemRunPlan?.argv;
  return hasUnsupportedShellArgv(argv);
}

async function buildHostApprovalDecisionParams(
  params: HostExecApprovalParams,
): Promise<RequestExecApprovalDecisionParams> {
  const commandSpans =
    params.commandHighlighting === true
      ? (params.commandSpans ??
        (shouldSkipGeneratedCommandSpans(params)
          ? undefined
          : await resolveCommandSpans(params.command ?? params.systemRunPlan?.commandText)))
      : undefined;
  return {
    id: params.approvalId,
    command: params.command,
    commandArgv: params.commandArgv,
    systemRunPlan: params.systemRunPlan,
    env: params.env,
    cwd: params.workdir,
    nodeId: params.nodeId,
    host: params.host,
    security: params.security,
    ask: params.ask,
    warningText: params.warningText,
    commandSpans,
    unavailableDecisions: params.unavailableDecisions,
    ...buildExecApprovalRequesterContext({
      agentId: params.agentId,
      sessionKey: params.sessionKey,
    }),
    resolvedPath: params.resolvedPath,
    sessionId: params.sessionId,
    runId: params.runId,
    toolCallId: params.toolCallId,
    requireDeliveryRoute: params.requireDeliveryRoute,
    suppressDelivery: params.suppressDelivery,
    approvalReviewerDeviceIds: params.approvalReviewerDeviceIds,
    ...buildExecApprovalTurnSourceContext(params),
  };
}

/** Registers a host/node approval request without waiting for a decision. */
async function registerExecApprovalRequestForHost(
  params: HostExecApprovalParams,
): Promise<ExecApprovalRegistration> {
  return await registerExecApprovalRequest(await buildHostApprovalDecisionParams(params));
}

/** Registers a host/node approval request and wraps failures for exec callers. */
export async function registerExecApprovalRequestForHostOrThrow(
  params: HostExecApprovalParams,
): Promise<ExecApprovalRegistration> {
  try {
    return await registerExecApprovalRequestForHost(params);
  } catch (err) {
    throw new Error(`Exec approval registration failed: ${String(err)}`, { cause: err });
  }
}
