import { EventEmitter } from 'node:events';
import WebSocket from 'ws';
import type { SigningConfig } from './signing';
import { signRequest } from './signing';
import { randomUUID } from 'node:crypto';

export interface PacificaWebSocketConfig {
  url: string;
  apiKey?: string;
  secret?: string;
  subaccount?: string;
  reconnectIntervalMs?: number;
  maxReconnectIntervalMs?: number;
  heartbeatIntervalMs?: number;
}

type Subscription = {
  channel: string;
  params?: Record<string, unknown>;
  auth?: SigningConfig;
};

type SubscribePayload = {
  method: 'subscribe';
  params: Record<string, unknown>;
};

const PRIVATE_CHANNEL_PREFIXES = ['orders.', 'fills.', 'account.'] as const;
const DEFAULT_AUTH_METHOD = 'SUBSCRIBE';
const DEFAULT_AUTH_PATH = '/ws';

export class PacificaWebSocket extends EventEmitter {
  private socket?: WebSocket;
  private reconnectTimer?: NodeJS.Timeout;
  private heartbeatTimer?: NodeJS.Timeout;
  private manuallyClosed = false;
  private reconnectAttempts = 0;
  private readonly subscriptions: Subscription[] = [];
  private readonly pendingRequests = new Map<
    string,
    {
      resolve: (value: any) => void;
      reject: (error: Error) => void;
      timer: NodeJS.Timeout;
    }
  >();

  constructor(private readonly config: PacificaWebSocketConfig) {
    super();
  }

  connect(): void {
    this.manuallyClosed = false;
    this.openSocket();
  }

  disconnect(): void {
    this.manuallyClosed = true;
    this.clearHeartbeat();
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = undefined;
    }
    this.socket?.close();
    this.socket = undefined;
  }

  subscribe(
    channel: string,
    params?: Record<string, unknown>,
    authOverride?: SigningConfig
  ): void {
    const auth = this.resolveAuth(channel, authOverride);
    if (!auth && this.requiresAuthentication(channel)) {
      throw new Error(`Missing credentials for private Pacifica channel ${channel}`);
    }
    const payload: Subscription = { channel, params, auth };
    this.subscriptions.push(payload);
    this.flushSubscriptions();
  }

  subscribeAuthenticated(
    channel: string,
    params?: Record<string, unknown>,
    authOverride?: SigningConfig
  ): void {
    this.subscribe(channel, params, authOverride ?? this.configCredentials());
  }

  sendRaw(data: unknown): void {
    if (this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(data));
    }
  }

  private openSocket(): void {
    this.socket = new WebSocket(this.config.url);
    this.socket.on('open', () => {
      this.reconnectAttempts = 0;
      this.emit('open');
      this.flushSubscriptions(true);
      this.startHeartbeat();
    });

    this.socket.on('message', message => {
      this.handleMessage(message);
    });

    this.socket.on('error', error => {
      this.emit('error', error);
    });

    this.socket.on('close', () => {
      this.emit('close');
      this.clearHeartbeat();
      this.rejectPendingRequests(new Error('Pacifica WebSocket closed'));
      if (!this.manuallyClosed) {
        this.scheduleReconnect();
      }
    });
  }

  private flushSubscriptions(force = false): void {
    if (!force && this.socket?.readyState !== WebSocket.OPEN) return;
    for (const sub of this.subscriptions) {
      const baseParams: Record<string, unknown> = sub.params
        ? { ...sub.params, channel: sub.channel }
        : { channel: sub.channel };
      const payload: SubscribePayload = { method: 'subscribe', params: baseParams };
      if (sub.auth) {
        const authParams = this.buildAuthParams(sub.auth, payload);
        const paramsWithAuth = {
          ...baseParams,
          auth: authParams
        };
        this.sendRaw({ method: 'subscribe', params: paramsWithAuth });
      } else {
        this.sendRaw(payload);
      }
    }
  }

  private startHeartbeat(): void {
    this.clearHeartbeat();
    const interval = this.config.heartbeatIntervalMs ?? 30_000;
    this.heartbeatTimer = setInterval(() => {
      this.sendRaw({ method: 'ping' });
    }, interval);
  }

  private clearHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = undefined;
    }
  }

  private scheduleReconnect(): void {
    if (this.reconnectTimer) return;
    this.reconnectAttempts += 1;
    const base = this.config.reconnectIntervalMs ?? 1_000;
    const max = this.config.maxReconnectIntervalMs ?? 30_000;
    const delay = Math.min(base * 2 ** (this.reconnectAttempts - 1), max);
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = undefined;
      this.emit('reconnected', { attempt: this.reconnectAttempts });
      this.openSocket();
    }, delay);
  }

  private requiresAuthentication(channel: string): boolean {
    return PRIVATE_CHANNEL_PREFIXES.some(prefix => channel.startsWith(prefix));
  }

  private configCredentials(): SigningConfig {
    return {
      apiKey: this.config.apiKey,
      secret: this.config.secret,
      subaccount: this.config.subaccount
    };
  }

  private resolveAuth(channel: string, override?: SigningConfig): SigningConfig | undefined {
    const combined: SigningConfig = {
      apiKey: override?.apiKey ?? this.config.apiKey,
      secret: override?.secret ?? this.config.secret,
      subaccount: override?.subaccount ?? this.config.subaccount
    };
    const hasCredentials = Boolean(combined.apiKey && combined.secret);
    if (!hasCredentials) {
      return undefined;
    }
    if (override) {
      return combined;
    }
    return this.requiresAuthentication(channel) ? combined : undefined;
  }

  private buildAuthParams(signing: SigningConfig, payload: SubscribePayload): Record<string, unknown> {
    if (!signing.apiKey || !signing.secret) {
      throw new Error('Pacifica WebSocket auth requires both apiKey and secret');
    }

    const { headers, timestamp } = signRequest(
      signing,
      DEFAULT_AUTH_METHOD,
      DEFAULT_AUTH_PATH,
      payload
    );

    const signature = headers['X-Pacific-Signature'];
    if (!signature) {
      throw new Error('Pacifica signing result missing X-Pacific-Signature header');
    }

    const auth: Record<string, unknown> = {
      key: signing.apiKey,
      timestamp,
      signature
    };

    const subaccount = signing.subaccount;
    if (subaccount) {
      auth.subaccount = subaccount;
    }

    return auth;
  }

  async waitForOpen(): Promise<void> {
    if (this.socket?.readyState === WebSocket.OPEN) return;
    if (this.socket?.readyState === WebSocket.CONNECTING) {
      await new Promise<void>(resolve => this.once('open', resolve));
      return;
    }
    throw new Error('Pacifica WebSocket is not connected');
  }

  async sendRpc(action: string, params: Record<string, unknown>, timeoutMs = 10_000): Promise<any> {
    await this.waitForOpen();
    const id = randomUUID();
    const message = {
      id,
      params: {
        [action]: params
      }
    };

    return new Promise((resolve, reject) => {
      const timer = setTimeout(() => {
        this.pendingRequests.delete(id);
        reject(new Error(`Pacifica WebSocket request ${action} timed out`));
      }, timeoutMs);

      this.pendingRequests.set(id, { resolve, reject, timer });
      this.sendRaw(message);
    });
  }

  private handleMessage(message: WebSocket.RawData): void {
    this.emit('message', message);
    let parsed: any;
    try {
      const text = typeof message === 'string' ? message : message.toString();
      parsed = JSON.parse(text);
    } catch {
      return;
    }

    if (!parsed || typeof parsed !== 'object' || !parsed.id) {
      return;
    }

    const pending = this.pendingRequests.get(parsed.id as string);
    if (!pending) return;
    this.pendingRequests.delete(parsed.id as string);
    clearTimeout(pending.timer);

    if (parsed.error) {
      const errMsg = typeof parsed.error === 'string'
        ? parsed.error
        : parsed.error.message || JSON.stringify(parsed.error);
      pending.reject(new Error(errMsg));
      return;
    }

    if ('result' in parsed) {
      pending.resolve(parsed.result);
      return;
    }

    if ('data' in parsed) {
      pending.resolve(parsed.data);
      return;
    }

    pending.resolve(parsed);
  }

  private rejectPendingRequests(error: Error): void {
    for (const [, entry] of this.pendingRequests) {
      clearTimeout(entry.timer);
      entry.reject(error);
    }
    this.pendingRequests.clear();
  }
}
