/**
 * @fileoverview OpenClaw Gateway WebSocket client.
 *
 * Implements the Gateway WS protocol v3:
 *   1. Connect and receive `connect.challenge`
 *   2. Send `connect` request with auth
 *   3. Receive `hello-ok` (or error)
 *   4. Subscribe to session events (`sessions.subscribe`, `sessions.messages.subscribe`)
 *   5. Listen for real-time events (session changes, messages, tool calls, presence)
 *   6. Send RPC requests (agents.list, sessions.list, sessions.abort, etc.)
 *
 * Uses RxJS subjects for reactive event streaming — the Angular frontend
 * can subscribe to specific event types without callback hell.
 */

import { Subject, Observable, BehaviorSubject, Subscription } from 'rxjs';
import { filter, map, share } from 'rxjs/operators';
import {
  WsFrame,
  WsRequest,
  WsResponse,
  WsEvent,
  ConnectParams,
  HelloOkPayload,
  ConnectChallengePayload,
  GatewayEventName,
  SessionsChangedPayload,
  SessionMessagePayload,
  SessionToolPayload,
  PresencePayload,
  HealthEventPayload,
  OpenClawSession,
} from '../models/types';
import { OpenClawClientConfig } from '../utils/config';

// ─── Connection State ──────────────────────────────────────────────────────

export type WsConnectionState =
  | 'disconnected'
  | 'connecting'
  | 'challenged'
  | 'authenticating'
  | 'connected'
  | 'reconnecting'
  | 'error';

export interface WsConnectionInfo {
  state: WsConnectionState;
  protocol?: number;
  sessionId?: string;
  reconnectAttempt?: number;
  lastError?: string;
}

// ─── RPC Pending Request ──────────────────────────────────────────────────

interface PendingRpc {
  resolve: (payload: unknown) => void;
  reject: (error: Error) => void;
  timeoutId: ReturnType<typeof setTimeout>;
}

// ─── Client ───────────────────────────────────────────────────────────────

export class OpenClawWebSocketClient {
  private ws: WebSocket | null = null;
  private rpcId = 0;
  private pendingRpc = new Map<string, PendingRpc>();
  private challengeNonce = '';
  private reconnectAttempt = 0;
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
  private subscriptions = new Set<string>(); // session keys we've subscribed to
  private sessionSubscriptionsActive = false;

  // ─── RxJS Streams ────────────────────────────────────────────────────

  /** All raw WebSocket events */
  private readonly rawEvent$ = new Subject<WsEvent>();

  /** Connection state changes */
  readonly connectionState$ = new BehaviorSubject<WsConnectionInfo>({
    state: 'disconnected',
  });

  /** Session index changed events */
  readonly sessionsChanged$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'sessions.changed'),
    map((e) => e.payload as SessionsChangedPayload),
    share()
  );

  /** Session message events (for subscribed sessions) */
  readonly sessionMessage$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'session.message'),
    map((e) => e.payload as SessionMessagePayload),
    share()
  );

  /** Session tool events (for subscribed sessions) */
  readonly sessionTool$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'session.tool'),
    map((e) => e.payload as SessionToolPayload),
    share()
  );

  /** Presence events */
  readonly presence$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'presence'),
    map((e) => e.payload as PresencePayload),
    share()
  );

  /** Health events */
  readonly health$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'health'),
    map((e) => e.payload as HealthEventPayload),
    share()
  );

  /** Tick/keepalive events */
  readonly tick$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'tick'),
    share()
  );

  /** Gateway shutdown notification */
  readonly shutdown$ = this.rawEvent$.pipe(
    filter((e) => e.event === 'shutdown'),
    share()
  );

  /** Any event with a specific name */
  on(event: GatewayEventName): Observable<unknown> {
    return this.rawEvent$.pipe(
      filter((e) => e.event === event),
      map((e) => e.payload)
    );
  }

  // ─── Constructor ─────────────────────────────────────────────────────

  constructor(private readonly config: OpenClawClientConfig) {}

  // ─── Connection Lifecycle ───────────────────────────────────────────

  /**
   * Connect to the Gateway WebSocket.
   * Handles the full handshake: challenge → connect → hello-ok.
   */
  connect(): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      return; // already connected
    }

    this.updateState({ state: 'connecting' });
    const url = this.config.gatewayWsUrl;

    try {
      this.ws = new WebSocket(url);
    } catch (err) {
      this.updateState({ state: 'error', lastError: `Failed to create WebSocket: ${err}` });
      this.scheduleReconnect();
      return;
    }

    this.ws.onopen = () => {
      // Waiting for challenge from gateway
      this.updateState({ state: 'challenged' });
    };

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

    this.ws.onerror = (event) => {
      this.updateState({
        state: 'error',
        lastError: `WebSocket error: ${event.type}`,
      });
    };

    this.ws.onclose = (event) => {
      this.updateState({
        state: 'disconnected',
        lastError: event.reason || `WebSocket closed (code: ${event.code})`,
      });
      this.clearPendingRpcs(new Error(`WebSocket closed: ${event.code}`));
      this.scheduleReconnect();
    };
  }

  /**
   * Disconnect from the Gateway WebSocket.
   * Cancels reconnect timer and cleans up.
   */
  disconnect(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    this.reconnectAttempt = 0;

    if (this.ws) {
      this.ws.onclose = null; // prevent reconnect
      this.ws.close(1000, 'Client disconnect');
      this.ws = null;
    }

    this.clearPendingRpcs(new Error('Client disconnected'));
    this.updateState({ state: 'disconnected' });
  }

  /**
   * Check if the WebSocket is currently connected.
   */
  get isConnected(): boolean {
    return this.connectionState$.value.state === 'connected';
  }

  // ─── RPC Methods ─────────────────────────────────────────────────────

  /**
   * Send an RPC request and wait for the response.
   * Returns a promise that resolves with the response payload
   * or rejects on error/timeout.
   *
   * @param method RPC method name (e.g. "sessions.list", "agents.list")
   * @param params Method parameters
   * @param timeoutMs Optional timeout override
   */
  async rpc<T = unknown>(
    method: string,
    params: Record<string, unknown> = {},
    timeoutMs?: number
  ): Promise<T> {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      throw new Error('WebSocket not connected');
    }

    const id = `cc-${++this.rpcId}`;
    const request: WsRequest = {
      type: 'req',
      id,
      method,
      params,
    };

    return new Promise<T>((resolve, reject) => {
      const timeout = setTimeout(() => {
        this.pendingRpc.delete(id);
        reject(new Error(`RPC timeout: ${method} (${timeoutMs || this.config.httpTimeoutMs}ms)`));
      }, timeoutMs || this.config.httpTimeoutMs);

      this.pendingRpc.set(id, {
        resolve: resolve as (p: unknown) => void,
        reject,
        timeoutId: timeout,
      });

      this.ws!.send(JSON.stringify(request));
    });
  }

  // ─── High-Level API ─────────────────────────────────────────────────

  /**
   * Subscribe to session change events for the current WS client.
   * After subscribing, `sessions.changed` events will be emitted.
   */
  async subscribeToSessions(): Promise<void> {
    if (this.sessionSubscriptionsActive) return;
    await this.rpc('sessions.subscribe', {});
    this.sessionSubscriptionsActive = true;
  }

  /**
   * Unsubscribe from session change events.
   */
  async unsubscribeFromSessions(): Promise<void> {
    if (!this.sessionSubscriptionsActive) return;
    await this.rpc('sessions.unsubscribe', {});
    this.sessionSubscriptionsActive = false;
  }

  /**
   * Subscribe to message/transcript events for a specific session.
   * Required before session.message and session.tool events fire.
   *
   * @param sessionKey Full session key, e.g. "agent:otto:telegram:direct:8787451565"
   */
  async subscribeToSessionMessages(sessionKey: string): Promise<void> {
    if (this.subscriptions.has(sessionKey)) return;
    await this.rpc('sessions.messages.subscribe', { sessionKey });
    this.subscriptions.add(sessionKey);
  }

  /**
   * Unsubscribe from message events for a session.
   */
  async unsubscribeFromSessionMessages(sessionKey: string): Promise<void> {
    if (!this.subscriptions.has(sessionKey)) return;
    await this.rpc('sessions.messages.unsubscribe', { sessionKey });
    this.subscriptions.delete(sessionKey);
  }

  /**
   * Fetch the current session list via WS RPC.
   * More complete than the HTTP tools/invoke version.
   */
  async fetchSessions(allAgents = true, activeMinutes?: number): Promise<OpenClawSession[]> {
    const params: Record<string, unknown> = { allAgents };
    if (activeMinutes !== undefined) {
      params.activeMinutes = activeMinutes;
    }

    const result = await this.rpc<{ sessions: OpenClawSession[]; count: number }>(
      'sessions.list',
      params
    );

    return result.sessions || [];
  }

  /**
   * Fetch the agent list via WS RPC.
   */
  async fetchAgents(): Promise<Array<{ id: string; configured: boolean }>> {
    const result = await this.rpc<{ agents: Array<{ id: string; configured: boolean }> }>(
      'agents.list',
      {}
    );
    return result.agents || [];
  }

  /**
   * Fetch agent identity via WS RPC.
   */
  async fetchAgentIdentity(agentId: string): Promise<unknown> {
    return this.rpc('agent.identity.get', { agentId });
  }

  /**
   * Get a transcript preview for a session.
   */
  async fetchSessionPreview(
    sessionKey: string,
    limit = 20
  ): Promise<unknown> {
    return this.rpc('sessions.preview', { sessionKey, limit });
  }

  /**
   * Abort an active session.
   */
  async abortSession(sessionKey: string): Promise<void> {
    await this.rpc('sessions.abort', { sessionKey });
  }

  /**
   * Steer (interrupt + inject message) an active session.
   */
  async steerSession(sessionKey: string, message: string): Promise<void> {
    await this.rpc('sessions.steer', { sessionKey, message });
  }

  /**
   * Fetch gateway status (admin-scoped).
   */
  async fetchStatus(): Promise<unknown> {
    return this.rpc('status', {});
  }

  /**
   * Fetch gateway health snapshot.
   */
  async fetchHealth(): Promise<unknown> {
    return this.rpc('health', {});
  }

  /**
   * Fetch system presence.
   */
  async fetchPresence(): Promise<unknown> {
    return this.rpc('system-presence', {});
  }

  /**
   * Fetch channel status for all configured channels.
   */
  async fetchChannelStatus(): Promise<unknown> {
    return this.rpc('channels.status', {});
  }

  /**
   * Fetch log tail from the gateway.
   */
  async fetchLogTail(
    cursor?: string,
    limit?: number,
    maxBytes?: number
  ): Promise<unknown> {
    const params: Record<string, unknown> = {};
    if (cursor) params.cursor = cursor;
    if (limit) params.limit = limit;
    if (maxBytes) params.maxBytes = maxBytes;
    return this.rpc('logs.tail', params);
  }

  // ─── Internal Handlers ──────────────────────────────────────────────

  private handleMessage(data: string): void {
    let frame: WsFrame;

    try {
      frame = JSON.parse(data);
    } catch {
      console.warn('[OpenClaw WS] Failed to parse frame:', data);
      return;
    }

    switch (frame.type) {
      case 'event':
        this.handleEvent(frame as WsEvent);
        break;
      case 'res':
        this.handleResponse(frame as WsResponse);
        break;
      default:
        console.warn('[OpenClaw WS] Unknown frame type:', frame.type);
    }
  }

  private handleEvent(event: WsEvent): void {
    // Handle connect challenge specially
    if (event.event === 'connect.challenge') {
      this.handleChallenge(event.payload as ConnectChallengePayload);
      return;
    }

    // Emit to all subscribers
    this.rawEvent$.next(event);
  }

  private handleChallenge(challenge: ConnectChallengePayload): void {
    this.challengeNonce = challenge.nonce;
    this.updateState({ state: 'authenticating' });

    // Send connect request
    const connectParams: ConnectParams = {
      minProtocol: this.config.protocolVersion,
      maxProtocol: this.config.protocolVersion,
      client: {
        id: this.config.clientId,
        version: this.config.clientVersion,
        platform: this.config.clientPlatform,
        mode: 'operator',
      },
      role: 'operator',
      scopes: ['operator.read', 'operator.write'],
      auth: {
        token: this.config.authToken,
      },
      locale: 'en-US',
      userAgent: `control-center/${this.config.clientVersion}`,
    };

    this.sendRequest('connect', connectParams);
  }

  private handleResponse(response: WsResponse): void {
    // Handle hello-ok (connect response)
    if (response.id && response.id.startsWith('cc-')) {
      const pending = this.pendingRpc.get(response.id);
      if (pending) {
        clearTimeout(pending.timeoutId);
        this.pendingRpc.delete(response.id);

        if (response.ok) {
          pending.resolve(response.payload);
        } else {
          pending.reject(
            new Error(
              `RPC error: ${response.error?.type || 'unknown'} — ${response.error?.message || 'no details'}`
            )
          );
        }
        return;
      }
    }

    // Handle connect response (sent without cc- prefix)
    if (response.ok && response.payload) {
      const payload = response.payload as HelloOkPayload;
      if (payload.type === 'hello-ok') {
        this.reconnectAttempt = 0;
        this.updateState({
          state: 'connected',
          protocol: payload.protocol,
        });

        // Auto-subscribe to session changes
        this.subscribeToSessions().catch((err) => {
          console.warn('[OpenClaw WS] Failed to subscribe to sessions:', err);
        });

        return;
      }
    }

    // Handle connect error
    if (!response.ok && response.error) {
      this.updateState({
        state: 'error',
        lastError: `Connect failed: ${response.error.type} — ${response.error.message}`,
      });
      this.scheduleReconnect();
    }
  }

  private sendRequest(method: string, params: Record<string, unknown>): void {
    if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
      return;
    }

    const request: WsRequest = {
      type: 'req',
      id: `rpc-${++this.rpcId}`,
      method,
      params,
    };

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

  private updateState(partial: Partial<WsConnectionInfo>): void {
    const current = this.connectionState$.value;
    this.connectionState$.next({ ...current, ...partial } as WsConnectionInfo);
  }

  private scheduleReconnect(): void {
    if (this.reconnectAttempt >= this.config.wsMaxReconnectAttempts) {
      this.updateState({
        state: 'error',
        lastError: `Max reconnect attempts reached (${this.config.wsMaxReconnectAttempts})`,
      });
      return;
    }

    this.reconnectAttempt++;
    const delay = Math.min(
      this.config.wsReconnectDelayMs * Math.pow(1.5, this.reconnectAttempt - 1),
      30_000 // cap at 30s
    );

    this.updateState({
      state: 'reconnecting',
      reconnectAttempt: this.reconnectAttempt,
    });

    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null;
      this.connect();
    }, delay);
  }

  private clearPendingRpcs(error: Error): void {
    for (const [id, pending] of this.pendingRpc.entries()) {
      clearTimeout(pending.timeoutId);
      pending.reject(error);
    }
    this.pendingRpc.clear();
  }

  /**
   * Clean up all subscriptions and disconnect.
   */
  destroy(): void {
    this.disconnect();
    this.rawEvent$.complete();
    this.connectionState$.complete();
    this.subscriptions.clear();
    this.sessionSubscriptionsActive = false;
  }
}