import { randomUUID } from 'node:crypto';
import { readDiagnosticsConfig, type DiagnosticsConfig } from '../../lib/diagnostics-config.js';

export type CaptureCategory = 'actions' | 'behavior_io' | 'perception_io';
export interface DetailCaptureConfig {
  actions: boolean;
  behavior_io: boolean;
  perception_io: boolean;
  max_record_bytes: number;
}
export function resolveDetailCapture(config?: DiagnosticsConfig, env = process.env): DetailCaptureConfig {
  let selected = config;
  if (!selected) { try { selected = readDiagnosticsConfig(); } catch { /* fail closed */ } }
  const requestedLevel = env.CLAWCLAW_OBSERVABILITY_LEVEL?.trim().toLowerCase();
  const full = (requestedLevel && ['errors', 'telemetry', 'full'].includes(requestedLevel)
    ? requestedLevel : selected?.level) === 'full';
  const enabled = (key: CaptureCategory): boolean => {
    const value = env[`CLAWCLAW_CAPTURE_${key.toUpperCase()}`];
    return value === undefined ? selected?.capture?.[key] ?? full : value.trim() === '1';
  };
  return { actions: enabled('actions'), behavior_io: enabled('behavior_io'), perception_io: enabled('perception_io'),
    max_record_bytes: selected?.capture?.max_record_bytes ?? 256 * 1024 };
}
export function detailCategory(name: string): CaptureCategory | undefined {
  return /^runtime\.detail\.(actions|behavior_io|perception_io)\./.exec(name)?.[1] as CaptureCategory | undefined;
}
type Diagnostic = (name: string, attributes: Readonly<Record<string, unknown>>, severity?: 'debug' | 'info' | 'warn' | 'error') => void;

/** Opt-in, lazy, Owner-side observer. No module API, gameplay Event or cross-Worker payload is added. */
export class DetailCapture {
  readonly enabled: boolean;
  constructor(readonly category: CaptureCategory, private readonly diagnostic?: Diagnostic,
    config?: DetailCaptureConfig) { this.enabled = !!diagnostic && (config ?? resolveDetailCapture())[category]; }
  emit(phase: string, attributes: () => Record<string, unknown>): void {
    if (!this.enabled) return;
    try { this.diagnostic?.(`runtime.detail.${this.category}.${phase}`, attributes(), 'debug'); } catch { /* observational */ }
  }
  run<T>(operation: string, metadata: Record<string, unknown>, input: unknown, invoke: () => Promise<T>, signal?: AbortSignal): Promise<T> {
    if (!this.enabled) return invoke();
    const invocation_id = randomUUID(), started = performance.now();
    const base = { ...metadata, invocation_id, operation };
    this.emit('input', () => ({ ...base, input, stage: 'requested' }));
    let aborted = false;
    const onAbort = () => {
      aborted = true;
      this.emit('cancelled', () => ({ ...base, duration_ms: performance.now() - started,
        error: signal?.reason instanceof Error ? signal.reason.message : 'aborted' }));
    };
    signal?.addEventListener('abort', onAbort, { once: true });
    return (async () => {
      try {
        const output = await invoke();
        this.emit('output', () => ({ ...base, output: output ?? null,
          stage: aborted ? 'late_result' : 'computed_not_yet_runtime_committed', duration_ms: performance.now() - started }));
        return output;
      } catch (error) {
        this.emit('failed', () => ({ ...base, error: error instanceof Error ? { name: error.name, message: error.message } : String(error),
          duration_ms: performance.now() - started }));
        throw error;
      } finally { signal?.removeEventListener('abort', onAbort); }
    })();
  }
}

/** Bounded JSON snapshot: preserves gameplay payloads and IDs, always removes credentials. */
export function captureSnapshot(value: unknown, maxBytes: number): { value: unknown; truncated: boolean } {
  let remaining = maxBytes, nodes = 0, truncated = false;
  const seen = new WeakSet<object>();
  const marker = (): string => { truncated = true; return '[TRUNCATED]'; };
  const secret = /(?:^|_)(?:api_key|claw_key|token|access_token|refresh_token|authorization|cookie|password|passwd|secret|private_key|credential|env|environment)(?:_value)?$/i;
  const visit = (item: unknown, key = '', depth = 0): unknown => {
    if (++nodes > 20000 || remaining <= 0 || depth > 32) return marker();
    remaining -= Buffer.byteLength(key) * 6 + 16;
    if (secret.test(key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[^A-Za-z0-9]+/g, '_'))) return '[REDACTED]';
    if (item === null || item === undefined) return item ?? null;
    if (typeof item === 'boolean' || typeof item === 'number') return item;
    if (typeof item === 'string') {
      const limit = Math.max(0, Math.floor(remaining / 6));
      const cut = item.length > limit;
      const text = item.slice(0, limit)
        .replace(/\bBearer\s+[^\s"']+/gi, 'Bearer [REDACTED]')
        .replace(/\b(?:sk-|claw_|ghp_|github_pat_)[A-Za-z0-9_-]+/g, '[REDACTED]')
        .replace(/([?&](?:token|key|code|api_key|access_token)=)[^&\s"']+/gi, '$1[REDACTED]');
      remaining -= text.length * 6;
      return cut ? text + marker() : text;
    }
    if (typeof item !== 'object') return `[${typeof item}_OMITTED]`;
    if (item instanceof Error) {
      const field = (name: string, fallback: string) => {
        const descriptor = Object.getOwnPropertyDescriptor(item, name);
        return descriptor ? 'value' in descriptor ? descriptor.value : '[ACCESSOR_OMITTED]' : fallback;
      };
      return visit({ name: field('name', 'Error'), message: field('message', '') }, key, depth + 1);
    }
    if (seen.has(item)) return '[CIRCULAR]';
    if (ArrayBuffer.isView(item) || item instanceof ArrayBuffer) return '[BINARY_OMITTED]';
    seen.add(item);
    const out: any = Array.isArray(item) ? [] : {};
    if (Array.isArray(item)) {
      // JSON serializes holes as nulls. Charge them too; copying an isolated
      // high index would create an enormous sparse output despite the budget.
      for (let index = 0; index < item.length; index += 1) {
        if (remaining <= 0 || nodes > 20000) { out.push(marker()); break; }
        const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
        out.push(visit(descriptor ? 'value' in descriptor ? descriptor.value : '[ACCESSOR_OMITTED]' : null, String(index), depth + 1));
      }
      seen.delete(item);
      return out;
    }
    // Do not enumerate getters or run user-defined toJSON methods while logging.
    for (const name in item) {
      if (!Object.prototype.hasOwnProperty.call(item, name)) continue;
      if (name.length > 1024) { if (Array.isArray(out)) out.push(marker()); else out.__capture_truncated = marker(); break; }
      if (remaining <= 0 || nodes > 20000) { if (Array.isArray(out)) out.push(marker()); else out.__capture_truncated = marker(); break; }
      const descriptor = Object.getOwnPropertyDescriptor(item, name);
      const next = visit(descriptor && 'value' in descriptor ? descriptor.value : '[ACCESSOR_OMITTED]', name, depth + 1);
      Object.defineProperty(out, name, { value: next, enumerable: true, configurable: true, writable: true });
    }
    seen.delete(item);
    return out;
  };
  return { value: visit(value), truncated };
}
