import { bytesToHex } from '@/lib/encoding';
import type { FactoryOperation } from './contract';

export type FactoryCommand = {
  runId: string;
  requestId: string;
  operation: FactoryOperation;
  input: unknown;
};

export type FactoryEntity = {
  runId: string;
  kind: string;
  entityId: string;
  operation: string;
  dependencyOrder: number;
  createdAt: Date;
  deletedAt: Date | null;
};

type FactoryExecution = {
  entity?: { kind: string; id: string; operation?: string };
  result: unknown;
};

export type FactoryOperationHandler = {
  dependencyOrder: number;
  reserve?(input: unknown): { kind: string; id: string };
  execute(context: { runId: string; input: unknown }): Promise<FactoryExecution>;
  read(context: { runId: string; entity: FactoryEntity }): Promise<unknown>;
  cleanup(context: { runId: string; entity: FactoryEntity }): Promise<void>;
};

type ClaimResult =
  | { status: 'claimed' }
  | { status: 'replay'; result: unknown }
  | { status: 'conflict' };

export interface FactoryStore {
  claim(command: FactoryCommand, bodyHash: string): Promise<ClaimResult>;
  complete(runId: string, requestId: string, result: unknown): Promise<void>;
  register(entity: FactoryEntity): Promise<void>;
  findOwned(runId: string, kind: string, entityId: string): Promise<FactoryEntity | null>;
  listOwned(runId: string): Promise<FactoryEntity[]>;
  markDeleted(runId: string, kind: string, entityId: string): Promise<void>;
}

export class FactoryConflictError extends Error {}
export class FactoryOwnershipError extends Error {}
export class FactoryOperationError extends Error {}

function stableValue(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(stableValue);
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value as Record<string, unknown>)
        .sort(([left], [right]) => left.localeCompare(right))
        .map(([key, item]) => [key, stableValue(item)]),
    );
  }
  return value;
}

async function commandHash(command: FactoryCommand): Promise<string> {
  const body = JSON.stringify(stableValue(command));
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
  return bytesToHex(digest);
}

export function createFactoryEngine(
  store: FactoryStore,
  handlers: Record<string, FactoryOperationHandler>,
) {
  return {
    async execute(command: FactoryCommand): Promise<unknown> {
      const handler = handlers[command.operation];
      if (!handler)
        throw new FactoryOperationError(`Unsupported factory operation: ${command.operation}`);

      const claim = await store.claim(command, await commandHash(command));
      if (claim.status === 'conflict') throw new FactoryConflictError('requestId body conflict');
      if (claim.status === 'replay') return claim.result;

      const reserved = handler.reserve?.(command.input);
      if (reserved) {
        await store.register({
          runId: command.runId,
          kind: reserved.kind,
          entityId: reserved.id,
          operation: command.operation,
          dependencyOrder: handler.dependencyOrder,
          createdAt: new Date(),
          deletedAt: null,
        });
      }
      const execution = await handler.execute({ runId: command.runId, input: command.input });
      if (execution.entity && !reserved) {
        const existing = await store.findOwned(
          command.runId,
          execution.entity.kind,
          execution.entity.id,
        );
        if (!existing) {
          await store.register({
            runId: command.runId,
            kind: execution.entity.kind,
            entityId: execution.entity.id,
            operation: execution.entity.operation ?? command.operation,
            dependencyOrder: handler.dependencyOrder,
            createdAt: new Date(),
            deletedAt: null,
          });
        }
      }
      await store.complete(command.runId, command.requestId, execution.result);
      return execution.result;
    },

    async readback(input: { runId: string; kind: string; id: string }): Promise<unknown> {
      const entity = await store.findOwned(input.runId, input.kind, input.id);
      if (!entity) throw new FactoryOwnershipError('Entity is not owned by run');
      const handler = handlers[entity.operation];
      if (!handler) throw new FactoryOperationError(`Missing handler: ${entity.operation}`);
      return handler.read({ runId: input.runId, entity });
    },

    async cleanupRun(runId: string): Promise<{ deleted: Record<string, number> }> {
      const entities = (await store.listOwned(runId)).sort(
        (left, right) => right.dependencyOrder - left.dependencyOrder,
      );
      const deleted: Record<string, number> = {};
      for (const entity of entities) {
        const handler = handlers[entity.operation];
        if (!handler) throw new FactoryOperationError(`Missing handler: ${entity.operation}`);
        await handler.cleanup({ runId, entity });
        await store.markDeleted(runId, entity.kind, entity.entityId);
        deleted[entity.kind] = (deleted[entity.kind] ?? 0) + 1;
      }
      return { deleted };
    },
  };
}

export function createMemoryFactoryStore(): FactoryStore {
  const commands = new Map<string, { hash: string; result?: unknown; complete: boolean }>();
  const entities = new Map<string, FactoryEntity>();
  const commandKey = (runId: string, requestId: string) => `${runId}:${requestId}`;
  const entityKey = (runId: string, kind: string, id: string) => `${runId}:${kind}:${id}`;

  return {
    async claim(command, bodyHash) {
      const key = commandKey(command.runId, command.requestId);
      const existing = commands.get(key);
      if (!existing) {
        commands.set(key, { hash: bodyHash, complete: false });
        return { status: 'claimed' };
      }
      if (existing.hash !== bodyHash || !existing.complete) return { status: 'conflict' };
      return { status: 'replay', result: existing.result };
    },
    async complete(runId, requestId, result) {
      const record = commands.get(commandKey(runId, requestId));
      if (!record) throw new Error('Factory command was not claimed');
      record.result = result;
      record.complete = true;
    },
    async register(entity) {
      entities.set(entityKey(entity.runId, entity.kind, entity.entityId), entity);
    },
    async findOwned(runId, kind, entityId) {
      const entity = entities.get(entityKey(runId, kind, entityId));
      return entity && !entity.deletedAt ? entity : null;
    },
    async listOwned(runId) {
      return [...entities.values()].filter((entity) => entity.runId === runId && !entity.deletedAt);
    },
    async markDeleted(runId, kind, entityId) {
      const entity = entities.get(entityKey(runId, kind, entityId));
      if (entity) entity.deletedAt = new Date();
    },
  };
}
