import { parentPort, workerData } from 'node:worker_threads';
import {
  PERCEPTION_PLUGIN_API_VERSION,
  type ClawclawPerception,
  type ClawclawPerceptionClass,
  type PerceptionAnalysisInput,
  type PerceptionBoundary,
  type PerceptionFrame,
} from '../../../../sdk/perception-plugin/index.js';
import { createSpatialApi, type SpatialFacts } from '../../spatial-api.js';
import type { PerceptionThreadModuleSpec } from './perception-module-loader.js';
import { normalizePerceptionEvents } from './perception-module-output.js';
import { assertRuntimeModuleOutputSize } from '../../../framework/modules/runtime-module-output.js';

type ResetReason = Exclude<PerceptionBoundary['kind'], 'events_requested'> | 'replay';
type ReducerOperation = 'update' | 'commit' | 'flush' | 'reset';

interface PerceptionReducerWorkerData {
  readonly poolId: string;
  readonly laneId: number;
  readonly modules: readonly PerceptionThreadModuleSpec[];
  readonly spatialFacts: SpatialFacts;
  readonly diagnosticsEnabled?: boolean;
}

interface PerceptionReducerRequest {
  readonly type: 'operate';
  readonly poolId: string;
  readonly requestId: string;
  readonly moduleId: string;
  readonly operation: ReducerOperation;
  readonly frame?: PerceptionFrame;
  readonly analysis?: unknown;
  readonly boundary?: PerceptionBoundary;
  readonly reason?: ResetReason;
}

function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
  if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
  const object = value as object;
  if (seen.has(object)) return value;
  seen.add(object);
  for (const child of Object.values(object)) deepFreeze(child, seen);
  return Object.freeze(value);
}

function errorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

function errorCode(error: unknown): string {
  const message = errorMessage(error).trim();
  return /^([A-Za-z][A-Za-z0-9_.-]{0,119})(?::|$)/.exec(message)?.[1]
    ?? 'PERCEPTION_REDUCER_OPERATION_FAILED';
}

const port = parentPort;
if (!port) throw new Error('perception_reducer_worker_parent_missing');
const data = workerData as Partial<PerceptionReducerWorkerData> | undefined;
if (
  !data
  || typeof data.poolId !== 'string'
  || !data.poolId
  || typeof data.laneId !== 'number'
  || !Array.isArray(data.modules)
  || data.modules.length === 0
  || !data.spatialFacts
  || typeof data.spatialFacts !== 'object'
) throw new Error('perception_reducer_worker_data_invalid');

function diagnosticMemory(): Readonly<Record<string, number>> {
  return data?.diagnosticsEnabled === true
    ? { heapUsedBytes: process.memoryUsage().heapUsed }
    : {};
}

function send(value: Readonly<Record<string, unknown>>): void {
  port!.postMessage({
    ...value,
    poolId: data!.poolId!,
    laneId: data!.laneId!,
    ...diagnosticMemory(),
  });
}

function validateClass(
  spec: PerceptionThreadModuleSpec,
  loaded: Record<string, unknown>,
): ClawclawPerceptionClass {
  if (typeof loaded.default !== 'function') {
    throw new Error('perception_reducer_worker_class_missing');
  }
  const ModuleClass = loaded.default as unknown as ClawclawPerceptionClass;
  const manifest = ModuleClass.manifest;
  if (
    !manifest
    || manifest.apiVersion !== PERCEPTION_PLUGIN_API_VERSION
    || manifest.id !== (spec.declaredId ?? spec.moduleId)
    || (manifest.execution ?? 'ordered_update')
      !== (spec.manifest.execution ?? 'ordered_update')
    || JSON.stringify(manifest.produces) !== JSON.stringify(spec.manifest.produces)
  ) throw new Error('perception_reducer_worker_manifest_mismatch');
  return ModuleClass;
}

function validateInstance(
  spec: PerceptionThreadModuleSpec,
  instance: ClawclawPerception,
): void {
  const execution = spec.manifest.execution ?? 'ordered_update';
  if (
    !instance
    || typeof instance !== 'object'
    || (execution === 'ordered_update' && typeof instance.update !== 'function')
    || (execution === 'parallel_analysis' && (
      typeof (instance as any).analyze !== 'function'
      || typeof (instance as any).commit !== 'function'
    ))
  ) throw new Error('perception_reducer_worker_contract_invalid');
}

const api = Object.freeze({ spatial: createSpatialApi(data.spatialFacts) });
const specs = new Map<string, PerceptionThreadModuleSpec>();
const classes = new Map<string, ClawclawPerceptionClass>();
const modules = new Map<string, ClawclawPerception>();
const startupFailures: Array<Readonly<Record<string, unknown>>> = [];

for (const spec of data.modules) {
  try {
    if (
      !spec
      || typeof spec.moduleId !== 'string'
      || !spec.moduleId
      || typeof spec.sourceUrl !== 'string'
      || !spec.sourceUrl
      || specs.has(spec.moduleId)
    ) throw new Error('perception_reducer_worker_module_invalid');
    const ModuleClass = validateClass(
      spec,
      await import(spec.sourceUrl) as Record<string, unknown>,
    );
    const instance = new ModuleClass();
    validateInstance(spec, instance);
    specs.set(spec.moduleId, spec);
    classes.set(spec.moduleId, ModuleClass);
    modules.set(spec.moduleId, instance);
  } catch (error) {
    startupFailures.push({
      moduleId: typeof spec?.moduleId === 'string' ? spec.moduleId : '',
      code: errorCode(error),
      messagePresent: errorMessage(error).length > 0,
      messageLength: errorMessage(error).length,
    });
  }
}

function safeDebugSnapshot(module: ClawclawPerception): unknown {
  try {
    const snapshot = structuredClone(module.debugSnapshot?.() ?? null);
    assertRuntimeModuleOutputSize(snapshot, 'perception_debug_snapshot');
    return snapshot;
  } catch {
    return null;
  }
}

async function operate(request: PerceptionReducerRequest): Promise<void> {
  if (
    request.poolId !== data!.poolId
    || typeof request.requestId !== 'string'
    || !request.requestId
    || typeof request.moduleId !== 'string'
    || !['update', 'commit', 'flush', 'reset'].includes(request.operation)
  ) throw new Error('perception_reducer_worker_request_invalid');
  const spec = specs.get(request.moduleId);
  const ModuleClass = classes.get(request.moduleId);
  const module = modules.get(request.moduleId);
  if (!spec || !ModuleClass || !module) throw new Error('perception_reducer_worker_module_unknown');

  try {
    let value;
    if (request.operation === 'update') {
      if ((spec.manifest.execution ?? 'ordered_update') !== 'ordered_update' || !request.frame) {
        throw new Error('perception_reducer_update_invalid');
      }
      value = await module.update(deepFreeze(request.frame), api);
    } else if (request.operation === 'commit') {
      if (spec.manifest.execution !== 'parallel_analysis' || !request.frame) {
        throw new Error('perception_reducer_commit_invalid');
      }
      value = await (module as ClawclawPerception & {
        commit(
          frame: Readonly<PerceptionFrame>,
          analysis: unknown,
          capability: typeof api,
        ): unknown | Promise<unknown>;
      }).commit(
        deepFreeze(request.frame),
        deepFreeze(request.analysis),
        api,
      );
    } else if (request.operation === 'flush') {
      if (!request.boundary) throw new Error('perception_reducer_flush_invalid');
      value = await module.flush?.(deepFreeze(request.boundary), api);
    } else {
      if (typeof request.reason !== 'string') throw new Error('perception_reducer_reset_invalid');
      await module.reset?.(request.reason);
    }

    const events = request.operation === 'reset'
      ? []
      : normalizePerceptionEvents(
          value as Parameters<typeof normalizePerceptionEvents>[0],
          spec.manifest,
        );
    send({
      type: 'result',
      requestId: request.requestId,
      moduleId: request.moduleId,
      events,
      debugSnapshot: safeDebugSnapshot(module),
    });
  } catch (error) {
    // commit/reset may have partially mutated private reducer state. Match the
    // direct runner contract by replacing that instance before the next frame.
    if (request.operation === 'commit' || request.operation === 'reset') {
      const replacement = new ModuleClass();
      validateInstance(spec, replacement);
      modules.set(request.moduleId, replacement);
    }
    send({
      type: 'request_error',
      requestId: request.requestId,
      moduleId: request.moduleId,
      code: errorCode(error),
      messagePresent: errorMessage(error).length > 0,
      messageLength: errorMessage(error).length,
      debugSnapshot: safeDebugSnapshot(modules.get(request.moduleId)!),
    });
  }
}

let operationTail = Promise.resolve();
port.on('message', (request: PerceptionReducerRequest) => {
  operationTail = operationTail.then(() => operate(request)).catch((error) => {
    send({
      type: 'error',
      requestId: typeof request?.requestId === 'string' ? request.requestId : undefined,
      moduleId: typeof request?.moduleId === 'string' ? request.moduleId : undefined,
      code: errorCode(error),
    });
  });
});

send({
  type: 'ready',
  moduleIds: [...modules.keys()],
  startupFailures,
  debugSnapshots: Object.fromEntries(
    [...modules].map(([moduleId, module]) => [moduleId, safeDebugSnapshot(module)]),
  ),
});
