/**
 * In-memory tool-use trace store, adapted from openclaw-lark's
 * tool-use-trace-store. One trace per agent run; the card builder
 * reads steps from here to render the tool-use panel.
 *
 * The store is per-run (not per-session) because each run has its
 * own card lifecycle. Steps are capped to prevent unbounded memory.
 */

export interface ToolUseTraceStep {
  readonly id: string;
  readonly seq: number;
  readonly toolName: string;
  readonly toolUseId: string | undefined;
  readonly input: unknown;
  readonly result: unknown;
  readonly error: string | undefined;
  readonly durationMs: number | undefined;
  readonly status: "running" | "success" | "error";
  readonly startedAt: number;
  readonly finishedAt: number | undefined;
}

interface RunTraceState {
  nextSeq: number;
  updatedAt: number;
  steps: ToolUseTraceStep[];
}

const MAX_STEPS = 64;
const STEP_RUNNING_TIMEOUT_MS = 5 * 60 * 1000;
const RUN_TTL_MS = 30 * 60 * 1000;
const RESULT_LIMIT = 2048;
const GENERIC_LIMIT = 512;

const runs = new Map<string, RunTraceState>();

export function startToolUseTraceRun(runId: string): void {
  pruneStaleRuns();
  runs.set(runId, { nextSeq: 1, updatedAt: Date.now(), steps: [] });
}

export function clearToolUseTraceRun(runId: string): void {
  runs.delete(runId);
}

export function recordToolUseStart(params: {
  runId: string;
  toolName: string;
  toolUseId: string | undefined;
  input: unknown;
}): void {
  const state = runs.get(params.runId);
  if (state === undefined) return;

  if (state.steps.length >= MAX_STEPS) {
    state.steps.splice(0, state.steps.length - MAX_STEPS + 1);
  }

  const now = Date.now();
  state.steps.push({
    id: `${state.nextSeq}`,
    seq: state.nextSeq,
    toolName: params.toolName,
    toolUseId: params.toolUseId,
    input: sanitizeTraceValue(params.input),
    result: undefined,
    error: undefined,
    durationMs: undefined,
    status: "running",
    startedAt: now,
    finishedAt: undefined,
  });
  state.nextSeq += 1;
  state.updatedAt = now;
}

export function recordToolUseEnd(params: {
  runId: string;
  toolName: string;
  toolUseId: string | undefined;
  result: unknown;
  error: string | undefined;
  durationMs: number | undefined;
}): void {
  const state = runs.get(params.runId);
  if (state === undefined) return;

  const now = Date.now();
  const pendingIndex = findPendingStepIndex(state.steps, params.toolUseId, params.toolName);

  if (pendingIndex >= 0) {
    const step = state.steps[pendingIndex];
    if (step !== undefined) {
      (state.steps as Array<ToolUseTraceStep>)[pendingIndex] = {
        ...step,
        status: params.error !== undefined ? "error" : "success",
        result: sanitizeTraceValue(params.result),
        error: params.error !== undefined ? truncate(params.error, 160) : undefined,
        durationMs: params.durationMs,
        finishedAt: now,
      };
      state.updatedAt = now;
      return;
    }
  }

  // No matching pending step — record as already-completed
  state.steps.push({
    id: `${state.nextSeq}`,
    seq: state.nextSeq,
    toolName: params.toolName,
    toolUseId: params.toolUseId,
    input: undefined,
    result: sanitizeTraceValue(params.result),
    error: params.error !== undefined ? truncate(params.error, 160) : undefined,
    durationMs: params.durationMs,
    status: params.error !== undefined ? "error" : "success",
    startedAt: now,
    finishedAt: now,
  });
  state.nextSeq += 1;
  state.updatedAt = now;
}

export function getToolUseTraceSteps(runId: string): ToolUseTraceStep[] {
  const state = runs.get(runId);
  if (state === undefined) return [];
  if (Date.now() - state.updatedAt > RUN_TTL_MS) {
    runs.delete(runId);
    return [];
  }
  const now = Date.now();
  return state.steps.map((step) => {
    if (step.status === "running" && now - step.startedAt > STEP_RUNNING_TIMEOUT_MS) {
      return { ...step, status: "error" as const, error: "timed out", finishedAt: now };
    }
    return { ...step };
  });
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

function findPendingStepIndex(
  steps: ToolUseTraceStep[],
  toolUseId: string | undefined,
  toolName: string | undefined,
): number {
  if (toolUseId !== undefined) {
    for (let i = steps.length - 1; i >= 0; i -= 1) {
      const step = steps[i];
      if (step !== undefined && step.status === "running" && step.toolUseId === toolUseId) {
        return i;
      }
    }
  }
  if (toolName !== undefined) {
    for (let i = steps.length - 1; i >= 0; i -= 1) {
      const step = steps[i];
      if (step !== undefined && step.status === "running" && step.toolName === toolName) {
        return i;
      }
    }
  }
  return -1;
}

function pruneStaleRuns(): void {
  const now = Date.now();
  for (const [runId, state] of runs) {
    if (now - state.updatedAt > RUN_TTL_MS) {
      runs.delete(runId);
    }
  }
}

function truncate(value: string, maxLength: number): string {
  return value.length <= maxLength ? value : `${value.slice(0, maxLength - 3)}...`;
}

/**
 * Sanitize a tool input or result value for display.
 * Truncates strings, redacts sensitive keys, limits depth.
 */
function sanitizeTraceValue(
  value: unknown,
  depth = 0,
  context: { source: "input" | "result" | undefined; key: string | undefined } = { source: undefined, key: undefined },
): unknown {
  if (value === null || value === undefined) return undefined;

  if (typeof value === "string") {
    const limit = context.source === "result" ? RESULT_LIMIT : GENERIC_LIMIT;
    return truncate(value, limit);
  }

  if (typeof value === "number" || typeof value === "boolean") return value;
  if (depth >= 2) return "[truncated]";

  if (Array.isArray(value)) {
    return value.slice(0, 8).map((item) => sanitizeTraceValue(item, depth + 1, { source: context.source, key: undefined }));
  }

  if (typeof value === "object") {
    const input = value as Record<string, unknown>;
    const output: Record<string, unknown> = {};
    for (const [key, entryValue] of Object.entries(input).slice(0, 12)) {
      output[key] = isSensitiveKey(key)
        ? "[redacted]"
        : sanitizeTraceValue(entryValue, depth + 1, { source: context.source, key });
    }
    return output;
  }

  return truncate(String(value), 180);
}

const SENSITIVE_KEY_RE =
  /token|secret|password|api[_-]?key|authorization|cookie|credential|bearer|session[_-]?id|client[_-]?secret|access[_-]?key/i;

function isSensitiveKey(key: string): boolean {
  return SENSITIVE_KEY_RE.test(key);
}
