import { AIChooseMoveOptions, AIDecisionProgress, AIMove, chooseMove } from './ai';
import {
  AIWorkerErrorMessage,
  AIWorkerRequestMessage,
  AIWorkerResponseMessage,
} from './ai-worker-protocol';
import { CardTracker } from './card-tracker';
import type { Difficulty, GameState, PlayerIndex } from './types';

export interface AIWorkerClientLike {
  chooseMove(
    state: GameState,
    playerIdx: PlayerIndex,
    difficulty?: Difficulty,
    tracker?: CardTracker,
    onProgress?: (progress: AIDecisionProgress) => void,
    options?: AIChooseMoveOptions,
  ): Promise<AIMove>;
  dispose(): void;
}

export type AIWorkerFactory = () => Worker;

interface PendingRequest {
  state: GameState;
  playerIdx: PlayerIndex;
  difficulty: Difficulty;
  tracker?: CardTracker;
  onProgress?: (progress: AIDecisionProgress) => void;
  options?: AIChooseMoveOptions;
  resolve: (move: AIMove) => void;
  reject: (error: Error) => void;
}

function defaultWorkerFactory(): Worker {
  return new Worker(new URL('./ai.worker.ts', import.meta.url), { type: 'module' });
}

function toError(message: string, cause?: unknown): Error {
  if (cause instanceof Error) {
    return cause;
  }

  return new Error(message);
}

function fromWorkerError(message: AIWorkerErrorMessage): Error {
  const error = new Error(message.error.message);
  error.name = message.error.name;
  if (message.error.stack) {
    error.stack = message.error.stack;
  }
  return error;
}

export class AIWorkerClient implements AIWorkerClientLike {
  private worker: Worker | null = null;
  private disposed = false;
  private workerUnavailable = false;
  private requestCounter = 0;
  private readonly pending = new Map<string, PendingRequest>();

  constructor(private readonly workerFactory: AIWorkerFactory = defaultWorkerFactory) {}

  async chooseMove(
    state: GameState,
    playerIdx: PlayerIndex,
    difficulty: Difficulty = 'advanced',
    tracker?: CardTracker,
    onProgress?: (progress: AIDecisionProgress) => void,
    options?: AIChooseMoveOptions,
  ): Promise<AIMove> {
    if (this.disposed) {
      throw new Error('AIWorkerClient has been disposed');
    }

    const worker = this.getOrCreateWorker();
    if (!worker) {
      return chooseMove(state, playerIdx, difficulty, tracker, onProgress);
    }

    const requestId = `ai-request-${this.requestCounter++}`;

    return new Promise<AIMove>((resolve, reject) => {
      const pending: PendingRequest = {
        state,
        playerIdx,
        difficulty,
        tracker,
        onProgress,
        options,
        resolve,
        reject,
      };

      this.pending.set(requestId, pending);

      const message: AIWorkerRequestMessage = {
        type: 'choose-move',
        requestId,
        state,
        playerIdx,
        difficulty,
        trackerSnapshot: tracker ? tracker.toSnapshot() : null,
        inferenceSnapshot: options?.inference?.toSnapshot() ?? null,
      };

      try {
        worker.postMessage(message);
      } catch (error) {
        this.pending.delete(requestId);
        this.disableWorker();
        void this.runFallback(pending);
      }
    });
  }

  dispose(): void {
    if (this.disposed) {
      return;
    }

    this.disposed = true;

    if (this.worker) {
      this.worker.removeEventListener('message', this.handleWorkerMessage);
      this.worker.removeEventListener('error', this.handleWorkerFailure);
      this.worker.removeEventListener('messageerror', this.handleWorkerMessageError);
      this.worker.terminate();
      this.worker = null;
    }

    const error = new Error('AIWorkerClient has been disposed');
    for (const pending of this.pending.values()) {
      pending.reject(error);
    }
    this.pending.clear();
  }

  private getOrCreateWorker(): Worker | null {
    if (this.workerUnavailable) {
      return null;
    }

    if (this.worker) {
      return this.worker;
    }

    if (typeof Worker === 'undefined') {
      this.workerUnavailable = true;
      return null;
    }

    try {
      this.worker = this.workerFactory();
      this.worker.addEventListener('message', this.handleWorkerMessage);
      this.worker.addEventListener('error', this.handleWorkerFailure);
      this.worker.addEventListener('messageerror', this.handleWorkerMessageError);
      return this.worker;
    } catch {
      this.workerUnavailable = true;
      this.worker = null;
      return null;
    }
  }

  private disableWorker(): void {
    if (this.worker) {
      this.worker.removeEventListener('message', this.handleWorkerMessage);
      this.worker.removeEventListener('error', this.handleWorkerFailure);
      this.worker.removeEventListener('messageerror', this.handleWorkerMessageError);
      this.worker.terminate();
      this.worker = null;
    }

    this.workerUnavailable = true;
  }

  private async runFallback(pending: PendingRequest): Promise<void> {
    try {
      const move = await chooseMove(
        pending.state,
        pending.playerIdx,
        pending.difficulty,
        pending.tracker,
        pending.onProgress,
        pending.options,
      );
      pending.resolve(move);
    } catch (error) {
      pending.reject(toError('Fallback AI move failed', error));
    }
  }

  private readonly handleWorkerMessage = (event: MessageEvent<AIWorkerResponseMessage>): void => {
    const message = event.data;
    const pending = this.pending.get(message.requestId);
    if (!pending) {
      return;
    }

    if (message.type === 'progress') {
      pending.onProgress?.(message.progress);
      return;
    }

    this.pending.delete(message.requestId);

    if (message.type === 'result') {
      pending.resolve(message.move);
      return;
    }

    pending.reject(fromWorkerError(message));
  };

  private readonly handleWorkerFailure = (_event: Event): void => {
    this.failoverPendingRequests(new Error('AI worker failed'));
  };

  private readonly handleWorkerMessageError = (_event: MessageEvent): void => {
    this.failoverPendingRequests(new Error('AI worker could not deserialize a message'));
  };

  private failoverPendingRequests(_error: Error): void {
    const pendingRequests = [...this.pending.values()];
    this.pending.clear();
    this.disableWorker();

    for (const pending of pendingRequests) {
      void this.runFallback(pending);
    }
  }
}