const MAX_LOGGED_CHARS = 8_000;
const MAX_LOGGED_LINES = 20;
const MAX_LINE_CHARS = 2_000;
const MAX_PENDING_LINE_CHARS = 16_000;

interface DiagnosticLogger {
  warn(bindings: Readonly<Record<string, unknown>>, message: string): void;
}

export interface AgentSdkStderrSink {
  readonly write: (data: string) => void;
  readonly flush: () => void;
}

/**
 * Turn the SDK subprocess stderr stream into bounded, run-correlated logs.
 * Complete lines are buffered so a credential split across chunks is still
 * removed before anything reaches the logger.
 */
export function createAgentSdkStderrSink(input: {
  readonly logger: DiagnosticLogger;
  readonly runId: string;
  readonly projectId: string;
  readonly sensitiveValues: readonly (string | undefined)[];
}): AgentSdkStderrSink {
  const secrets = [...new Set(input.sensitiveValues.filter((value): value is string => value !== undefined && value !== ""))]
    .sort((left, right) => right.length - left.length);
  let pending = "";
  let droppingOversizedLine = false;
  let remainingChars = MAX_LOGGED_CHARS;
  let remainingLines = MAX_LOGGED_LINES;
  let limitReported = false;

  const reportLimit = (): void => {
    if (limitReported) return;
    limitReported = true;
    input.logger.warn(
      { runId: input.runId, projectId: input.projectId },
      "agent SDK stderr log limit reached; further diagnostics omitted",
    );
  };

  const emit = (line: string): void => {
    if (line.trim() === "") return;
    if (remainingChars <= 0 || remainingLines <= 0) {
      reportLimit();
      return;
    }
    const redacted = redactDiagnostic(line, secrets);
    const allowed = Math.min(MAX_LINE_CHARS, remainingChars);
    const diagnostic = boundedDiagnostic(redacted, allowed);
    remainingChars -= diagnostic.length;
    remainingLines--;
    input.logger.warn(
      { runId: input.runId, projectId: input.projectId, sdkStderr: diagnostic },
      "agent SDK stderr",
    );
    if (redacted.length > allowed || remainingChars <= 0 || remainingLines <= 0) reportLimit();
  };

  const append = (fragment: string): void => {
    if (fragment === "" || droppingOversizedLine) return;
    if (pending.length + fragment.length > MAX_PENDING_LINE_CHARS) {
      pending = "";
      droppingOversizedLine = true;
      emit("[oversized stderr line omitted]");
      return;
    }
    pending += fragment;
  };

  const write = (data: string): void => {
    let lineStart = 0;
    for (let index = 0; index < data.length; index++) {
      const character = data[index];
      if (character !== "\n" && character !== "\r") continue;
      append(data.slice(lineStart, index));
      if (!droppingOversizedLine) emit(pending);
      pending = "";
      droppingOversizedLine = false;
      if (character === "\r" && data[index + 1] === "\n") index++;
      lineStart = index + 1;
    }
    append(data.slice(lineStart));
  };

  const flush = (): void => {
    if (droppingOversizedLine) {
      droppingOversizedLine = false;
    } else {
      emit(pending);
    }
    pending = "";
  };

  return { write, flush };
}

function boundedDiagnostic(value: string, limit: number): string {
  if (value.length <= limit) return value;
  const marker = "…[truncated]";
  if (limit <= marker.length) return marker.slice(0, limit);
  return `${value.slice(0, limit - marker.length)}${marker}`;
}

function redactDiagnostic(value: string, secrets: readonly string[]): string {
  let redacted = value;
  for (const secret of secrets) redacted = redacted.replaceAll(secret, "[REDACTED]");
  return redacted
    .replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "")
    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, " ")
    .replace(/\b(postgres(?:ql)?):\/\/([^:\s/@]+):([^@\s]+)@/gi, "$1://$2:[REDACTED]@")
    .replace(/\b((?:api[_-]?key|token|secret|password)\s*[:=]\s*)\S+/gi, "$1[REDACTED]")
    .trim();
}
