/**
 * RemoteEngine - Connect to a remote Hypen app over WebSocket
 * Platform-agnostic client (uses standard WebSocket API)
 */

import type {
  RemoteMessage,
  InitialTreeMessage,
  PatchMessage,
  DispatchActionMessage,
} from "./types.js";
import type { Patch } from "../engine.js";

export type RemoteConnectionState = "disconnected" | "connecting" | "connected" | "error";

export interface RemoteEngineOptions {
  autoReconnect?: boolean;
  reconnectInterval?: number;
  maxReconnectAttempts?: number;
}

/**
 * Client-side engine that connects to a remote Hypen app
 */
export class RemoteEngine {
  private ws: WebSocket | null = null;
  private url: string;
  private state: RemoteConnectionState = "disconnected";
  private options: Required<RemoteEngineOptions>;
  private reconnectAttempts = 0;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;

  // Callbacks
  private patchCallbacks: Array<(patches: Patch[]) => void> = [];
  private stateCallbacks: Array<(state: any) => void> = [];
  private connectionCallbacks: Array<() => void> = [];
  private disconnectionCallbacks: Array<() => void> = [];
  private errorCallbacks: Array<(error: Error) => void> = [];

  // State
  private currentState: any = null;
  private currentRevision = 0;
  private moduleName: string = "";

  constructor(url: string, options: RemoteEngineOptions = {}) {
    this.url = url;
    this.options = {
      autoReconnect: options.autoReconnect ?? true,
      reconnectInterval: options.reconnectInterval ?? 3000,
      maxReconnectAttempts: options.maxReconnectAttempts ?? 10,
    };
  }

  /**
   * Connect to the remote server
   */
  async connect(): Promise<void> {
    if (this.state === "connected" || this.state === "connecting") {
      return;
    }

    this.state = "connecting";

    return new Promise((resolve, reject) => {
      try {
        this.ws = new WebSocket(this.url);

        this.ws.onopen = () => {
          this.state = "connected";
          this.reconnectAttempts = 0;
          this.connectionCallbacks.forEach((cb) => cb());
          resolve();
        };

        this.ws.onmessage = (event) => {
          this.handleMessage(event.data);
        };

        this.ws.onerror = () => {
          this.state = "error";
          const error = new Error("WebSocket error");
          this.errorCallbacks.forEach((cb) => cb(error));
          reject(error);
        };

        this.ws.onclose = () => {
          this.state = "disconnected";
          this.disconnectionCallbacks.forEach((cb) => cb());
          this.attemptReconnect();
        };
      } catch (error) {
        this.state = "error";
        reject(error);
      }
    });
  }

  /**
   * Disconnect from the remote server
   */
  disconnect(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }

    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }

    this.state = "disconnected";
  }

  /**
   * Dispatch an action to the remote server
   */
  dispatchAction(action: string, payload?: any): void {
    if (this.state !== "connected" || !this.ws) {
      console.warn("Cannot dispatch action: not connected");
      return;
    }

    const message: DispatchActionMessage = {
      type: "dispatchAction",
      module: this.moduleName,
      action,
      payload,
    };

    this.ws.send(JSON.stringify(message));
  }

  /**
   * Register callback for patches
   */
  onPatches(callback: (patches: Patch[]) => void): this {
    this.patchCallbacks.push(callback);
    return this;
  }

  /**
   * Register callback for state updates
   */
  onStateUpdate(callback: (state: any) => void): this {
    this.stateCallbacks.push(callback);
    return this;
  }

  /**
   * Register callback for connection
   */
  onConnect(callback: () => void): this {
    this.connectionCallbacks.push(callback);
    return this;
  }

  /**
   * Register callback for disconnection
   */
  onDisconnect(callback: () => void): this {
    this.disconnectionCallbacks.push(callback);
    return this;
  }

  /**
   * Register callback for errors
   */
  onError(callback: (error: Error) => void): this {
    this.errorCallbacks.push(callback);
    return this;
  }

  /**
   * Get current connection state
   */
  getConnectionState(): RemoteConnectionState {
    return this.state;
  }

  /**
   * Get current app state
   */
  getCurrentState(): any {
    return this.currentState;
  }

  /**
   * Get current revision
   */
  getRevision(): number {
    return this.currentRevision;
  }

  private handleMessage(data: string): void {
    try {
      const message = JSON.parse(data) as RemoteMessage;

      switch (message.type) {
        case "initialTree":
          this.handleInitialTree(message as InitialTreeMessage);
          break;

        case "patch":
          this.handlePatch(message as PatchMessage);
          break;

        case "stateUpdate":
          this.currentState = message.state;
          this.stateCallbacks.forEach((cb) => cb(message.state));
          break;
      }
    } catch (error) {
      console.error("Error handling remote message:", error);
      this.errorCallbacks.forEach((cb) =>
        cb(error instanceof Error ? error : new Error(String(error)))
      );
    }
  }

  private handleInitialTree(message: InitialTreeMessage): void {
    this.moduleName = message.module;
    this.currentState = message.state;
    this.currentRevision = message.revision;

    // Apply initial patches
    if (message.patches.length > 0) {
      this.patchCallbacks.forEach((cb) => cb(message.patches));
    }

    // Notify state callbacks
    this.stateCallbacks.forEach((cb) => cb(message.state));
  }

  private handlePatch(message: PatchMessage): void {
    // Check revision ordering
    if (message.revision <= this.currentRevision) {
      console.warn(
        `Out of order patch: expected > ${this.currentRevision}, got ${message.revision}`
      );
      return;
    }

    this.currentRevision = message.revision;

    // Apply patches
    if (message.patches.length > 0) {
      this.patchCallbacks.forEach((cb) => cb(message.patches));
    }
  }

  private attemptReconnect(): void {
    if (!this.options.autoReconnect) {
      return;
    }

    if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
      console.error("Max reconnection attempts reached");
      return;
    }

    this.reconnectAttempts++;

    console.log(
      `Attempting to reconnect (${this.reconnectAttempts}/${this.options.maxReconnectAttempts})...`
    );

    this.reconnectTimer = setTimeout(() => {
      this.connect().catch((error) => {
        console.error("Reconnection failed:", error);
      });
    }, this.options.reconnectInterval);
  }
}
