/**
 * ZCLAW WebSocket Client
 * 
 * Typed WebSocket client for OpenClaw Gateway protocol.
 * Handles:
 * - Connection + handshake (connect challenge/response)
 * - Request/response pattern (with timeout)
 * - Event subscription
 * - Auto-reconnect
 * - Agent streaming
 */

import WebSocket from 'ws';
import { EventEmitter } from 'events';
import * as crypto from 'crypto';

// === Protocol Types ===

export interface ConnectParams {
  minProtocol?: number;
  maxProtocol?: number;
  client: {
    id: string;
    version: string;
    platform: string;
    mode: 'operator' | 'node';
  };
  role: 'operator' | 'node';
  scopes: string[];
  auth?: { token?: string };
  locale?: string;
  userAgent?: string;
  device?: {
    id: string;
    publicKey?: string;
    signature?: string;
    signedAt?: number;
    nonce?: string;
  };
}

export interface GatewayRequest {
  type: 'req';
  id: string;
  method: string;
  params?: Record<string, any>;
}

export interface GatewayResponse {
  type: 'res';
  id: string;
  ok: boolean;
  payload?: any;
  error?: any;
}

export interface GatewayEvent {
  type: 'event';
  event: string;
  payload?: any;
  seq?: number;
  stateVersion?: number;
}

export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent;

// Agent stream events
export interface AgentStreamEvent {
  stream: 'assistant' | 'tool' | 'lifecycle';
  delta?: string;
  content?: string;
  tool?: string;
  phase?: 'start' | 'end' | 'error';
  runId?: string;
  error?: string;
}

export interface WsClientOptions {
  /** Gateway WebSocket URL (default: ws://127.0.0.1:18789) */
  url?: string;
  /** Auth token */
  token?: string;
  /** Client identifier */
  clientId?: string;
  /** Client version */
  clientVersion?: string;
  /** Auto-reconnect on disconnect */
  autoReconnect?: boolean;
  /** Reconnect interval ms */
  reconnectInterval?: number;
  /** Max reconnect attempts (0 = infinite) */
  maxReconnectAttempts?: number;
  /** Request timeout ms */
  requestTimeout?: number;
}

export type ConnectionState = 'disconnected' | 'connecting' | 'handshaking' | 'connected' | 'reconnecting';

export class GatewayWsClient extends EventEmitter {
  private ws: WebSocket | null = null;
  private state: ConnectionState = 'disconnected';
  private requestId: number = 0;
  private pendingRequests = new Map<string, {
    resolve: (value: any) => void;
    reject: (reason: any) => void;
    timer: ReturnType<typeof setTimeout>;
  }>();
  private reconnectAttempts: number = 0;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private deviceId: string;
  private options: Required<WsClientOptions>;

  constructor(opts: WsClientOptions = {}) {
    super();
    this.options = {
      url: opts.url || 'ws://127.0.0.1:18789',
      token: opts.token || '',
      clientId: opts.clientId || 'zclaw-tauri',
      clientVersion: opts.clientVersion || '0.1.0',
      autoReconnect: opts.autoReconnect ?? true,
      reconnectInterval: opts.reconnectInterval || 3000,
      maxReconnectAttempts: opts.maxReconnectAttempts || 0,
      requestTimeout: opts.requestTimeout || 30000,
    };
    this.deviceId = crypto.randomBytes(16).toString('hex');
  }

  /** Current connection state */
  getState(): ConnectionState {
    return this.state;
  }

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

    this.setState('connecting');

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

        this.ws.on('open', () => {
          this.setState('handshaking');
          // Wait for connect.challenge event
        });

        this.ws.on('message', (data: Buffer) => {
          try {
            const frame: GatewayFrame = JSON.parse(data.toString());
            this.handleFrame(frame, resolve);
          } catch (err: any) {
            this.emit('error', new Error(`Failed to parse frame: ${err.message}`));
          }
        });

        this.ws.on('close', (code: number, reason: Buffer) => {
          const wasConnected = this.state === 'connected';
          this.cleanup();

          if (wasConnected && this.options.autoReconnect) {
            this.scheduleReconnect();
          }

          this.emit('close', { code, reason: reason.toString() });
        });

        this.ws.on('error', (err: Error) => {
          if (this.state === 'connecting') {
            this.cleanup();
            reject(err);
          }
          this.emit('error', err);
        });
      } catch (err) {
        this.cleanup();
        reject(err);
      }
    });
  }

  /** Disconnect from Gateway */
  disconnect() {
    this.cancelReconnect();
    this.options.autoReconnect = false;

    if (this.ws) {
      this.ws.close(1000, 'Client disconnect');
    }
    this.cleanup();
  }

  /** Send a request and wait for response */
  async request(method: string, params?: Record<string, any>): Promise<any> {
    if (this.state !== 'connected') {
      throw new Error(`Cannot send request in state: ${this.state}`);
    }

    const id = `req_${++this.requestId}`;
    const frame: GatewayRequest = { type: 'req', id, method, params: params || {} };

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pendingRequests.delete(id);
        reject(new Error(`Request ${method} timed out after ${this.options.requestTimeout}ms`));
      }, this.options.requestTimeout);

      this.pendingRequests.set(id, { resolve, reject, timer });
      this.send(frame);
    });
  }

  /** Send agent message (trigger agent loop) */
  async sendAgentMessage(message: string, opts?: {
    sessionKey?: string;
    model?: string;
    agentId?: string;
  }): Promise<{ runId: string; acceptedAt: string }> {
    return this.request('agent', {
      message,
      sessionKey: opts?.sessionKey,
      model: opts?.model,
      agentId: opts?.agentId,
    });
  }

  /** Wait for agent run to complete */
  async waitForAgent(runId: string): Promise<{ status: string; startedAt: string; endedAt: string; error?: string }> {
    return this.request('agent.wait', { runId });
  }

  /** Send message through IM channel */
  async sendChannelMessage(channel: string, chatId: string, text: string): Promise<any> {
    return this.request('send', { channel, chatId, text });
  }

  /** Get Gateway health */
  async getHealth(): Promise<any> {
    return this.request('health');
  }

  /** Get Gateway status */
  async getStatus(): Promise<any> {
    return this.request('status');
  }

  // === ZCLAW Custom RPC Methods ===

  async listClones(): Promise<any> {
    return this.request('zclaw.clones.list');
  }

  async createClone(opts: { name: string; role?: string; nickname?: string; scenarios?: string[]; model?: string }): Promise<any> {
    return this.request('zclaw.clones.create', opts);
  }

  async getUsageStats(): Promise<any> {
    return this.request('zclaw.stats.usage');
  }

  async getSessionStats(): Promise<any> {
    return this.request('zclaw.stats.sessions');
  }

  async getWorkspaceInfo(): Promise<any> {
    return this.request('zclaw.workspace.info');
  }

  async getPluginStatus(): Promise<any> {
    return this.request('zclaw.plugins.status');
  }

  async getQuickConfig(): Promise<any> {
    return this.request('zclaw.config.quick', { get: true });
  }

  async saveQuickConfig(config: Record<string, any>): Promise<any> {
    return this.request('zclaw.config.quick', config);
  }

  // === Internal ===

  private handleFrame(frame: GatewayFrame, connectResolve?: (value: void) => void) {
    switch (frame.type) {
      case 'event':
        this.handleEvent(frame as GatewayEvent, connectResolve);
        break;
      case 'res':
        this.handleResponse(frame as GatewayResponse);
        break;
      default:
        // Ignore unexpected frame types
        break;
    }
  }

  private handleEvent(event: GatewayEvent, connectResolve?: (value: void) => void) {
    if (event.event === 'connect.challenge' && this.state === 'handshaking') {
      // Respond to challenge with connect request
      this.performHandshake(event.payload?.nonce, connectResolve);
      return;
    }

    // Emit typed events
    this.emit('event', event);
    this.emit(`event:${event.event}`, event.payload);

    // Specific agent stream events
    if (event.event === 'agent') {
      this.emit('agent:stream', event.payload as AgentStreamEvent);
    }
  }

  private performHandshake(nonce: string, connectResolve?: (value: void) => void) {
    const platform = process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'macos' : 'linux';

    const connectReq: GatewayRequest = {
      type: 'req',
      id: `connect_${Date.now()}`,
      method: 'connect',
      params: {
        minProtocol: 3,
        maxProtocol: 3,
        client: {
          id: this.options.clientId,
          version: this.options.clientVersion,
          platform,
          mode: 'operator',
        },
        role: 'operator',
        scopes: ['operator.read', 'operator.write'],
        auth: this.options.token ? { token: this.options.token } : {},
        locale: 'zh-CN',
        userAgent: `zclaw-tauri/${this.options.clientVersion}`,
        device: {
          id: this.deviceId,
        },
      },
    };

    // Listen for the connect response
    const handler = (data: Buffer) => {
      try {
        const frame = JSON.parse(data.toString());
        if (frame.type === 'res' && frame.id === connectReq.id) {
          this.ws?.removeListener('message', handler);
          if (frame.ok) {
            this.setState('connected');
            this.reconnectAttempts = 0;
            this.emit('connected', frame.payload);
            connectResolve?.();
          } else {
            const err = new Error(`Handshake failed: ${JSON.stringify(frame.error)}`);
            this.emit('error', err);
            this.cleanup();
          }
        }
      } catch { /* ignore parse errors during handshake */ }
    };

    this.ws?.on('message', handler);
    this.send(connectReq);
  }

  private handleResponse(res: GatewayResponse) {
    const pending = this.pendingRequests.get(res.id);
    if (pending) {
      clearTimeout(pending.timer);
      this.pendingRequests.delete(res.id);
      if (res.ok) {
        pending.resolve(res.payload);
      } else {
        pending.reject(new Error(JSON.stringify(res.error)));
      }
    }
  }

  private send(frame: GatewayFrame) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(frame));
    }
  }

  private setState(state: ConnectionState) {
    const prev = this.state;
    this.state = state;
    if (prev !== state) {
      this.emit('state', { state, previous: prev });
    }
  }

  private cleanup() {
    // Clear all pending requests
    for (const [id, pending] of this.pendingRequests) {
      clearTimeout(pending.timer);
      pending.reject(new Error('Connection closed'));
    }
    this.pendingRequests.clear();

    if (this.ws) {
      this.ws.removeAllListeners();
      if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
        try { this.ws.close(); } catch { /* ignore */ }
      }
      this.ws = null;
    }

    this.setState('disconnected');
  }

  private scheduleReconnect() {
    if (this.options.maxReconnectAttempts > 0 && this.reconnectAttempts >= this.options.maxReconnectAttempts) {
      this.emit('reconnect:failed', { attempts: this.reconnectAttempts });
      return;
    }

    this.reconnectAttempts++;
    this.setState('reconnecting');

    const delay = Math.min(
      this.options.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
      30000
    );

    this.reconnectTimer = setTimeout(async () => {
      try {
        await this.connect();
        this.emit('reconnect:success', { attempts: this.reconnectAttempts });
      } catch {
        // Will trigger another reconnect via the close handler
      }
    }, delay);
  }

  private cancelReconnect() {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }
}
