/**
 * ZCLAW Gateway Client (Browser/Tauri side)
 * 
 * WebSocket client for OpenClaw Gateway protocol, designed to run
 * in the Tauri React frontend. Uses native browser WebSocket API.
 */

// === Protocol Types ===

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;
}

export type GatewayFrame = GatewayRequest | GatewayResponse | GatewayEvent;

export interface AgentStreamDelta {
  stream: 'assistant' | 'tool' | 'lifecycle';
  delta?: string;
  content?: string;
  tool?: string;
  toolInput?: string;
  toolOutput?: string;
  phase?: 'start' | 'end' | 'error';
  runId?: string;
  error?: string;
}

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

type EventCallback = (payload: any) => void;

// === Client ===

export class GatewayClient {
  private ws: WebSocket | null = null;
  private state: ConnectionState = 'disconnected';
  private requestId = 0;
  private pendingRequests = new Map<string, {
    resolve: (value: any) => void;
    reject: (reason: any) => void;
    timer: number;
  }>();
  private eventListeners = new Map<string, Set<EventCallback>>();
  private reconnectAttempts = 0;
  private reconnectTimer: number | null = null;
  private deviceId: string;

  // Options
  private url: string;
  private token: string;
  private autoReconnect: boolean;
  private reconnectInterval: number;
  private requestTimeout: number;

  // State change callbacks
  onStateChange?: (state: ConnectionState) => void;
  onLog?: (level: string, message: string) => void;

  constructor(opts?: {
    url?: string;
    token?: string;
    autoReconnect?: boolean;
    reconnectInterval?: number;
    requestTimeout?: number;
  }) {
    this.url = opts?.url || 'ws://127.0.0.1:18789';
    this.token = opts?.token || '';
    this.autoReconnect = opts?.autoReconnect ?? true;
    this.reconnectInterval = opts?.reconnectInterval || 3000;
    this.requestTimeout = opts?.requestTimeout || 30000;
    this.deviceId = crypto.randomUUID?.() || `zclaw_${Date.now().toString(36)}`;
  }

  getState(): ConnectionState {
    return this.state;
  }

  // === Connection ===

  connect(): Promise<void> {
    if (this.state === 'connected' || this.state === 'connecting') {
      return Promise.resolve();
    }

    this.setState('connecting');

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

        this.ws.onopen = () => {
          this.setState('handshaking');
        };

        this.ws.onmessage = (evt) => {
          try {
            const frame: GatewayFrame = JSON.parse(evt.data);
            this.handleFrame(frame, resolve);
          } catch (err: any) {
            this.log('error', `Parse error: ${err.message}`);
          }
        };

        this.ws.onclose = (evt) => {
          const wasConnected = this.state === 'connected';
          this.cleanup();

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

          this.emitEvent('close', { code: evt.code, reason: evt.reason });
        };

        this.ws.onerror = () => {
          if (this.state === 'connecting') {
            this.cleanup();
            reject(new Error('WebSocket connection failed'));
          }
        };
      } catch (err) {
        this.cleanup();
        reject(err);
      }
    });
  }

  disconnect() {
    this.autoReconnect = false;
    this.cancelReconnect();

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

  // === Request/Response ===

  async request(method: string, params?: Record<string, any>): Promise<any> {
    if (this.state !== 'connected') {
      throw new Error(`Not connected (state: ${this.state})`);
    }

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

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

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

  // === High-level API ===

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

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

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

  // ZCLAW custom methods
  async listClones(): Promise<any> { return this.request('zclaw.clones.list'); }
  async createClone(opts: { name: string; role?: string; scenarios?: string[] }): Promise<any> { return this.request('zclaw.clones.create', opts); }
  async updateClone(id: string, updates: Record<string, any>): Promise<any> { return this.request('zclaw.clones.update', { id, updates }); }
  async deleteClone(id: string): Promise<any> { return this.request('zclaw.clones.delete', { id }); }
  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); }
  async listChannels(): Promise<any> { return this.request('channels.list'); }
  async getFeishuStatus(): Promise<any> { return this.request('feishu.status'); }
  async listScheduledTasks(): Promise<any> { return this.request('heartbeat.tasks'); }

  // === Event Subscription ===

  /** Subscribe to a Gateway event (e.g., 'agent', 'chat', 'heartbeat') */
  on(event: string, callback: EventCallback): () => void {
    if (!this.eventListeners.has(event)) {
      this.eventListeners.set(event, new Set());
    }
    this.eventListeners.get(event)!.add(callback);

    // Return unsubscribe function
    return () => {
      this.eventListeners.get(event)?.delete(callback);
    };
  }

  /** Subscribe to agent stream events */
  onAgentStream(callback: (delta: AgentStreamDelta) => void): () => void {
    return this.on('agent', callback);
  }

  // === Internal ===

  private handleFrame(frame: GatewayFrame, connectResolve?: (value: void) => void) {
    if (frame.type === 'event') {
      this.handleEvent(frame, connectResolve);
    } else if (frame.type === 'res') {
      this.handleResponse(frame);
    }
  }

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

    // Dispatch to listeners
    this.emitEvent(event.event, event.payload);
  }

  private performHandshake(_nonce: string, connectResolve?: (value: void) => void) {
    const connectId = `connect_${Date.now()}`;

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

    // Temporarily intercept the connect response
    const originalHandler = this.ws!.onmessage;
    this.ws!.onmessage = (evt) => {
      try {
        const frame = JSON.parse(evt.data);
        if (frame.type === 'res' && frame.id === connectId) {
          // Restore normal message handler
          this.ws!.onmessage = originalHandler;
          if (frame.ok) {
            this.setState('connected');
            this.reconnectAttempts = 0;
            this.emitEvent('connected', frame.payload);
            this.log('info', 'Connected to Gateway');
            connectResolve?.();
          } else {
            this.log('error', `Handshake failed: ${JSON.stringify(frame.error)}`);
            this.cleanup();
          }
        } else {
          // Pass through non-connect frames
          originalHandler?.call(this.ws!, evt);
        }
      } catch { /* ignore */ }
    };

    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 emitEvent(event: string, payload: any) {
    const listeners = this.eventListeners.get(event);
    if (listeners) {
      for (const cb of listeners) {
        try { cb(payload); } catch { /* ignore listener errors */ }
      }
    }
    // Also emit wildcard
    const wildcardListeners = this.eventListeners.get('*');
    if (wildcardListeners) {
      for (const cb of wildcardListeners) {
        try { cb({ event, payload }); } catch { /* ignore */ }
      }
    }
  }

  private setState(state: ConnectionState) {
    this.state = state;
    this.onStateChange?.(state);
    this.emitEvent('state', state);
  }

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

    if (this.ws) {
      this.ws.onopen = null;
      this.ws.onmessage = null;
      this.ws.onclose = null;
      this.ws.onerror = null;
      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() {
    this.reconnectAttempts++;
    this.setState('reconnecting');
    const delay = Math.min(this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1), 30000);

    this.reconnectTimer = window.setTimeout(async () => {
      try {
        await this.connect();
      } catch { /* close handler will trigger another reconnect */ }
    }, delay);
  }

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

  private detectPlatform(): string {
    const ua = navigator.userAgent.toLowerCase();
    if (ua.includes('win')) return 'windows';
    if (ua.includes('mac')) return 'macos';
    return 'linux';
  }

  private log(level: string, message: string) {
    this.onLog?.(level, message);
  }
}

// Singleton instance
let _client: GatewayClient | null = null;

export function getGatewayClient(opts?: ConstructorParameters<typeof GatewayClient>[0]): GatewayClient {
  if (!_client) {
    _client = new GatewayClient(opts);
  }
  return _client;
}
