import { Injectable, Logger } from '@nestjs/common';
import {
  WSClient,
  EventDispatcher,
  LoggerLevel,
} from '@larksuiteoapi/node-sdk';
import { FeishuBot } from './entities/feishu-bot.entity';
import { ConnectionState, ConnectionStatus } from './dto/ws-status.dto';

interface BotConnection {
  client: WSClient;
  status: ConnectionStatus;
}

@Injectable()
export class FeishuWsManager {
  private readonly logger = new Logger(FeishuWsManager.name);
  private connections: Map<string, BotConnection> = new Map();
  private reconnectAttempts: Map<string, number> = new Map();
  private readonly MAX_RECONNECT_ATTEMPTS = 5;
  private readonly RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000];

  // Injected after construction to avoid circular dep
  private _feishuService: any;

  setFeishuService(service: any): void {
    this._feishuService = service;
  }

  /**
   * Start WebSocket connection for a bot
   */
  async connect(bot: FeishuBot): Promise<void> {
    const botId = bot.id;

    // Check if already connected or connecting
    const existing = this.connections.get(botId);
    if (
      existing &&
      (existing.status.state === ConnectionState.CONNECTED ||
        existing.status.state === ConnectionState.CONNECTING)
    ) {
      this.logger.warn(`Bot ${botId} is already connecting or connected`);
      return;
    }

    // Mark as connecting immediately to prevent race conditions
    this.connections.set(botId, {
      client: null as any,
      status: { botId, state: ConnectionState.CONNECTING },
    });

    try {
      const wsClient = new WSClient({
        appId: bot.appId,
        appSecret: bot.appSecret,
        loggerLevel: LoggerLevel.info,
        logger: {
          debug: (msg: string) => this.logger.debug(msg),
          info: (msg: string) => this.logger.log(msg),
          warn: (msg: string) => this.logger.warn(msg),
          error: (msg: string) => this.logger.error(msg),
          trace: (msg: string) => this.logger.verbose(msg),
        },
      });

      // Register the main message event handler
      wsClient.start({
        eventDispatcher: new EventDispatcher({}).register({
          'im.message.receive_v1': async (data: any) => {
            await this._handleMessage(bot, data);
          },
        }),
      });

      const conn = this.connections.get(botId);
      if (conn) {
        conn.client = wsClient;
        conn.status = {
          botId,
          state: ConnectionState.CONNECTED,
          connectedAt: new Date(),
        };
      }

      this.reconnectAttempts.set(botId, 0);
      this.logger.log(`WebSocket connected for bot ${botId}`);
    } catch (error: any) {
      this.logger.error(`Failed to connect WebSocket for bot ${botId}`, error);
      this._setStatus(botId, {
        botId,
        state: ConnectionState.ERROR,
        error: error?.message || 'Connection failed',
      });
      throw error;
    }
  }

  /**
   * Disconnect WebSocket for a bot
   */
  async disconnect(botId: string): Promise<void> {
    const connection = this.connections.get(botId);
    if (!connection) {
      this.logger.warn(`No connection found for bot ${botId}`);
      return;
    }

    try {
      // Lark.WSClient does not expose a public stop() in all versions;
      // we simply remove the reference and let GC clean up.
      this.connections.delete(botId);
      this.reconnectAttempts.delete(botId);
      this.logger.log(`WebSocket disconnected for bot ${botId}`);
    } catch (error) {
      this.logger.error(`Error disconnecting bot ${botId}`, error);
      throw error;
    }
  }

  /**
   * Get connection status for a specific bot
   */
  getStatus(botId: string): ConnectionStatus | null {
    return this.connections.get(botId)?.status ?? null;
  }

  /**
   * Get all active connection statuses
   */
  getAllStatuses(): ConnectionStatus[] {
    return Array.from(this.connections.values()).map((c) => c.status);
  }

  /**
   * Returns true if the bot has an active connection
   */
  isConnected(botId: string): boolean {
    return (
      this.connections.get(botId)?.status.state === ConnectionState.CONNECTED
    );
  }

  // ─── Private Helpers ──────────────────────────────────────────────────────

  private _setStatus(botId: string, status: ConnectionStatus): void {
    const connection = this.connections.get(botId);
    if (connection) {
      connection.status = status;
    }
    // If connection not stored yet (during initial connect), we just skip —
    // the status will be set when the connection map entry is created.
  }

  /**
   * Handle an incoming im.message.receive_v1 event from Feishu WebSocket.
   */
  private async _handleMessage(bot: FeishuBot, data: any): Promise<void> {
    this.logger.log(`Received WS message for bot ${bot.id}`);

    try {
      const event = data?.event ?? data;
      const message = event?.message;

      if (!message) {
        this.logger.warn('No message field in WS event');
        return;
      }

      const messageId: string = message.message_id;
      const openId: string | undefined = event?.sender?.sender_id?.open_id;

      if (!openId) {
        this.logger.warn('No sender open_id in WS event');
        return;
      }

      let userText = '';
      try {
        const content = JSON.parse(message.content || '{}');
        userText = content.text || '';
      } catch {
        this.logger.warn('Failed to parse WS message content');
        return;
      }

      if (!userText.trim()) return;

      if (this._feishuService) {
        await this._feishuService.handleIncomingMessage(
          bot,
          openId,
          messageId,
          userText,
        );
      } else {
        this.logger.error('FeishuService not injected into FeishuWsManager');
      }
    } catch (error) {
      this.logger.error('Error in _handleMessage', error);
    }
  }

  /**
   * Schedule an auto-reconnect with exponential backoff.
   */
  async attemptReconnect(bot: FeishuBot): Promise<void> {
    const botId = bot.id;
    const attempts = this.reconnectAttempts.get(botId) ?? 0;

    if (attempts >= this.MAX_RECONNECT_ATTEMPTS) {
      this.logger.error(`Max reconnect attempts reached for bot ${botId}`);
      this._setStatus(botId, {
        botId,
        state: ConnectionState.ERROR,
        error: 'Max reconnect attempts reached',
      });
      return;
    }

    const delay =
      this.RECONNECT_DELAYS[attempts] ??
      this.RECONNECT_DELAYS[this.RECONNECT_DELAYS.length - 1];
    this.logger.log(
      `Reconnecting bot ${botId} in ${delay}ms (attempt ${attempts + 1})`,
    );
    this.reconnectAttempts.set(botId, attempts + 1);

    setTimeout(async () => {
      try {
        await this.connect(bot);
      } catch (error) {
        this.logger.error(`Reconnect failed for bot ${botId}`, error);
      }
    }, delay);
  }
}
