import { parentPort, workerData } from 'node:worker_threads';
import { performance } from 'node:perf_hooks';
import {
  PERCEPTION_PLUGIN_API_VERSION,
  type PerceptionApi,
} from '../../../../sdk/perception-plugin/index.js';
import { createSpatialApi, type SpatialFacts } from '../../spatial-api.js';
import type {
  PerceptionAnalysisFailure,
  PerceptionAnalysisJob,
  PerceptionAnalysisResult,
} from './perception-analysis-thread-pool.js';
import type { PerceptionThreadModuleSpec } from './perception-module-loader.js';

interface PerceptionAnalysisWorkerData {
  readonly poolId: string;
  readonly spatialFacts: SpatialFacts;
  readonly modules: readonly PerceptionThreadModuleSpec[];
}

interface PerceptionAnalysisRequest {
  readonly type: 'analyze';
  readonly poolId: string;
  readonly requestId: string;
  readonly jobs: readonly PerceptionAnalysisJob[];
}

interface PerceptionAnalysisResponse {
  readonly type: 'analysis';
  readonly poolId: string;
  readonly requestId: string;
  readonly results: readonly PerceptionAnalysisResult[];
}

interface PerceptionAnalysisWorkerReady {
  readonly type: 'ready';
  readonly poolId: string;
  readonly heapUsedBytes: number;
}

interface PerceptionAnalysisWorkerError {
  readonly type: 'error';
  readonly poolId: string;
  readonly requestId?: string;
  readonly code: string;
  /** Present only when Worker initialization can attribute the failure. */
  readonly moduleId?: string;
}

function roundedElapsed(startedAt: number): number {
  return Math.round(Math.max(0, performance.now() - startedAt) * 1_000) / 1_000;
}

function deepFreeze<T>(value: T, seen = new WeakSet<object>()): T {
  if (!value || typeof value !== 'object') 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 failureDetails(reason: unknown): PerceptionAnalysisFailure {
  const code = reason && typeof reason === 'object' && typeof (reason as any).code === 'string'
    ? (reason as any).code.trim()
    : '';
  const message = reason instanceof Error
    ? reason.message
    : typeof reason === 'string' ? reason : '';
  const prefix = /^([A-Za-z][A-Za-z0-9_.-]{0,119})(?::|$)/.exec(message.trim())?.[1];
  return {
    errorCode: /^[A-Za-z][A-Za-z0-9_.-]{0,119}$/.test(code)
      ? code
      : prefix ?? 'PERCEPTION_MODULE_FAILED',
    messagePresent: message.length > 0,
    ...(message ? { messageLength: message.length } : {}),
  };
}

async function analyzeJob(
  job: PerceptionAnalysisJob,
  api: PerceptionApi,
  modules: ReadonlyMap<string, new() => {
    analyze(input: PerceptionAnalysisJob['input'], api: PerceptionApi): unknown | Promise<unknown>;
  }>,
): Promise<PerceptionAnalysisResult> {
  const startedAt = performance.now();
  try {
    const ModuleClass = modules.get(job.moduleId);
    if (!ModuleClass) throw new Error('perception_analysis_module_unknown');
    const analyzer = new ModuleClass();
    if (typeof analyzer.analyze !== 'function') {
      throw new Error('perception_analysis_module_not_parallel');
    }
    // Every module/frame job receives its own immutable graph. structuredClone
    // at postMessage isolates the Worker from Runtime, but it preserves aliases
    // between jobs inside the same batch unless we split them here.
    const isolatedInput = deepFreeze(structuredClone(job.input));
    const analysis = structuredClone(await analyzer.analyze(isolatedInput, api));
    return {
      moduleId: job.moduleId,
      outcome: 'fulfilled',
      durationMs: roundedElapsed(startedAt),
      analysis,
    };
  } catch (error) {
    return {
      moduleId: job.moduleId,
      outcome: 'rejected',
      durationMs: roundedElapsed(startedAt),
      failure: failureDetails(error),
    };
  }
}

const port = parentPort;
if (!port) throw new Error('perception_analysis_worker_parent_missing');
const data = workerData as Partial<PerceptionAnalysisWorkerData> | undefined;
if (
  !data
  || typeof data.poolId !== 'string'
  || !data.poolId
  || !data.spatialFacts
  || typeof data.spatialFacts !== 'object'
  || !Array.isArray(data.modules)
) throw new Error('perception_analysis_worker_data_invalid');

function send(value: PerceptionAnalysisResponse | PerceptionAnalysisWorkerReady | PerceptionAnalysisWorkerError): void {
  port!.postMessage(value);
}

const api: PerceptionApi = Object.freeze({
  spatial: createSpatialApi(data.spatialFacts),
});
const moduleClasses = new Map<string, new() => {
  analyze(input: PerceptionAnalysisJob['input'], api: PerceptionApi): unknown | Promise<unknown>;
}>();
let startupFailed = false;
for (const spec of data.modules) {
  const moduleId = typeof spec?.moduleId === 'string' && spec.moduleId
    ? spec.moduleId
    : undefined;
  try {
    if (
      !moduleId
      || typeof spec.sourceUrl !== 'string'
      || !spec.sourceUrl
      || moduleClasses.has(moduleId)
    ) throw new Error('perception_analysis_worker_module_invalid');
    const loaded = await import(spec.sourceUrl) as Record<string, unknown>;
    const ModuleClass = loaded.default;
    if (typeof ModuleClass !== 'function') throw new Error('perception_analysis_worker_class_missing');
    const manifest = (ModuleClass as any).manifest;
    if (
      manifest?.apiVersion !== PERCEPTION_PLUGIN_API_VERSION
      || manifest.id !== moduleId
      || manifest.execution !== 'parallel_analysis'
    ) {
      throw new Error('perception_analysis_worker_manifest_mismatch');
    }
    moduleClasses.set(moduleId, ModuleClass as any);
  } catch {
    send({
      type: 'error',
      poolId: data.poolId,
      code: 'perception_analysis_worker_module_load_failed',
      ...(moduleId ? { moduleId } : {}),
    });
    startupFailed = true;
    break;
  }
}
let operationTail = Promise.resolve();

async function handle(value: unknown): Promise<void> {
  if (
    !value
    || typeof value !== 'object'
    || Array.isArray(value)
    || (value as Partial<PerceptionAnalysisRequest>).type !== 'analyze'
  ) {
    send({ type: 'error', poolId: data!.poolId!, code: 'perception_analysis_worker_request_invalid' });
    return;
  }
  const request = value as Partial<PerceptionAnalysisRequest>;
  if (
    request.poolId !== data!.poolId
    || typeof request.requestId !== 'string'
    || !request.requestId
    || !Array.isArray(request.jobs)
  ) {
    send({
      type: 'error',
      poolId: data!.poolId!,
      ...(typeof request.requestId === 'string' ? { requestId: request.requestId } : {}),
      code: 'perception_analysis_worker_request_invalid',
    });
    return;
  }
  const results = await Promise.all(request.jobs.map((job) => analyzeJob(job, api, moduleClasses)));
  send({
    type: 'analysis',
    poolId: data!.poolId!,
    requestId: request.requestId,
    results,
  });
}

if (!startupFailed) {
  port.on('message', (value: unknown) => {
    // A pool slot submits only one frame at a time. Keep this guard in the host
    // so a parent-side protocol regression still cannot overlap two batches in
    // one isolate.
    operationTail = operationTail.then(() => handle(value)).catch(() => {
      send({ type: 'error', poolId: data!.poolId!, code: 'perception_analysis_worker_failed' });
    });
  });

  send({
    type: 'ready',
    poolId: data.poolId,
    heapUsedBytes: process.memoryUsage().heapUsed,
  });
} else {
  // Keep the port alive until the parent retires this failed startup slot.
  // This prevents an exit event from racing ahead of the attributed error.
  port.on('message', () => {});
}
