import { createHash } from 'node:crypto';
import type { GameSyncPerceptionReport } from '../../../../game-sync-protocol.js';
import type { CanonicalEvent } from '../../../../framework/events/index.js';
import type {
  ClawclawMailboxAdapterDescriptor,
  ClawclawMailboxDriver,
} from '../mailbox-plugin.js';

const GAME_SYNC_PERCEPTION_REPORT_BATCH_SIZE = 20;
const PERCEPTION_REPORT_UUID_NAMESPACE = Buffer.from(
  '3c45fc594dd54a59a22185085c1096d1',
  'hex',
);

export interface GameSyncPerceptionDescriptor {
  readonly perceptionId: string;
  readonly name: string;
}

export interface GameSyncPerceptionReportAdapterOptions {
  readonly send: (reports: readonly GameSyncPerceptionReport[]) => boolean;
  readonly descriptor: (eventType: string) => GameSyncPerceptionDescriptor | undefined;
  readonly producerPluginId: string;
  readonly diagnostic?: (
    name: string,
    attributes?: Readonly<Record<string, unknown>>,
  ) => void;
}

function uuidForEvent(event: CanonicalEvent): string {
  const bytes = createHash('sha1')
    .update(PERCEPTION_REPORT_UUID_NAMESPACE)
    .update(event.sessionId)
    .update('\0')
    .update(event.eventId)
    .digest()
    .subarray(0, 16);
  bytes[6] = (bytes[6]! & 0x0f) | 0x50;
  bytes[8] = (bytes[8]! & 0x3f) | 0x80;
  const hex = bytes.toString('hex');
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

function sourceTick(event: CanonicalEvent): number | undefined {
  for (const candidate of [event.payload.to_tick, event.payload.source_tick, event.payload.tick]) {
    if (Number.isSafeInteger(candidate) && Number(candidate) >= 0) return Number(candidate);
  }
  return undefined;
}

/** Best-effort Mailbox fan-out to the active GameSync connection. */
export class GameSyncPerceptionReportAdapter implements ClawclawMailboxDriver {
  readonly adapter: ClawclawMailboxAdapterDescriptor = {
    id: 'clawclaw.mailbox.adapter.game-sync-perception-report',
    acknowledgement: 'none',
  };

  private stopped = false;

  constructor(private readonly options: GameSyncPerceptionReportAdapterOptions) {}

  observeEvents(events: readonly CanonicalEvent[]): void {
    if (this.stopped) return;
    let skipped = 0;
    const reports: GameSyncPerceptionReport[] = [];
    for (const event of events) {
      if (
        event.producerPluginId !== this.options.producerPluginId
        || event.deliveryClass === 'internal_only'
      ) continue;
      const descriptor = this.options.descriptor(event.type);
      const output = typeof event.payload.text === 'string' ? event.payload.text.trim() : '';
      const tick = sourceTick(event);
      if (!descriptor || !descriptor.name.trim() || !output || tick === undefined) {
        skipped += 1;
        continue;
      }
      reports.push({
        id: uuidForEvent(event),
        name: descriptor.name.trim(),
        output,
        source_tick: tick,
      });
    }
    if (skipped > 0) {
      this.options.diagnostic?.('runtime.mailbox.perception_report_skipped', {
        event_count: skipped,
        error_code: 'PERCEPTION_REPORT_FIELDS_MISSING',
      });
    }
    for (let index = 0; index < reports.length; index += GAME_SYNC_PERCEPTION_REPORT_BATCH_SIZE) {
      const batch = reports.slice(index, index + GAME_SYNC_PERCEPTION_REPORT_BATCH_SIZE);
      if (!this.options.send(batch)) {
        this.options.diagnostic?.('runtime.mailbox.perception_report_dropped', {
          event_count: batch.length,
          error_code: 'GAME_SYNC_NOT_CONNECTED',
        });
      }
    }
  }

  async flush(): Promise<void> {}

  async stop(): Promise<void> {
    this.stopped = true;
  }
}
