import WebSocket from 'ws';
import { v4 as uuidv4 } from 'uuid';
import { createLogger, type Logger } from '../utils/logger.js';
import type { IncomingMessage, ChatResponseSender, MediaAttachment } from './types.js';
import { downloadAndDecrypt, wecomMsgtypeToMediaType } from './wecom-media.js';

interface WeComBotConfig {
  botId: string;
  secret: string;
  botProfileId: string;
  getMode: () => 'full' | 'query' | 'collect';
  onMessage: (msg: IncomingMessage, sender: ChatResponseSender) => Promise<void>;
}

const WECOM_WS_URL = 'wss://openws.work.weixin.qq.com';
const HEARTBEAT_INTERVAL_MS = 30_000;
const RECONNECT_MAX_ATTEMPTS = 100;
const RECONNECT_BASE_DELAY_MS = 1000;
// WeCom rate limit: 30 messages/min per conversation.
// Throttle stream updates to avoid hitting the limit.
const STREAM_THROTTLE_MS = 1000;
// WeCom stream message byte limit
const MAX_CONTENT_BYTES = 20_000;

export class WeComBotConnection {
  private log: Logger;
  private ws: WebSocket | null = null;
  private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
  private reconnectAttempts = 0;
  private reconnecting = false;
  private stopped = false;
  private config: WeComBotConfig;
  private readonly botProfileId: string;
  // Pending messages queued while disconnected, flushed on reconnection.
  private pendingQueue: object[] = [];

  constructor(config: WeComBotConfig) {
    this.config = config;
    this.botProfileId = config.botProfileId;
    this.log = createLogger(`WeComBot:${config.botProfileId.substring(0, 8)}`);
  }

  start(): void {
    this.stopped = false;
    this.connect();
  }

  stop(): void {
    this.stopped = true;
    this.cleanup();
  }

  private connect(): void {
    if (this.stopped) return;

    this.log.info(`Connecting to WeCom: ${WECOM_WS_URL}`);
    this.reconnecting = true;
    this.ws = new WebSocket(WECOM_WS_URL);

    this.ws.on('open', () => {
      this.log.info('WebSocket connected, sending subscribe...');
      this.subscribe();
      this.startHeartbeat();
      this.reconnectAttempts = 0;
      this.reconnecting = false;
      // Flush any messages queued while disconnected.
      this.flushPending();
    });

    this.ws.on('message', (data) => {
      this.handleRawMessage(data.toString());
    });

    this.ws.on('close', (code, reason) => {
      this.log.info(`WebSocket closed: code=${code}, reason=${reason.toString()}`);
      this.stopHeartbeat();
      this.reconnecting = true;
      this.scheduleReconnect();
    });

    this.ws.on('error', (err) => {
      this.log.error('WebSocket error', { error: err.message });
    });
  }

  private subscribe(): void {
    this.send({
      cmd: 'aibot_subscribe',
      headers: { req_id: uuidv4() },
      body: {
        bot_id: this.config.botId,
        secret: this.config.secret,
      },
    });
  }

  private startHeartbeat(): void {
    this.stopHeartbeat();
    this.heartbeatTimer = setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ cmd: 'ping', headers: { req_id: uuidv4() } }));
      }
    }, HEARTBEAT_INTERVAL_MS);
  }

  private stopHeartbeat(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  private scheduleReconnect(): void {
    if (this.stopped) return;
    if (this.reconnectAttempts >= RECONNECT_MAX_ATTEMPTS) {
      this.log.error('Max reconnect attempts reached, giving up');
      return;
    }
    const delay = Math.min(
      RECONNECT_BASE_DELAY_MS * Math.pow(2, this.reconnectAttempts),
      60000,
    );
    this.reconnectAttempts++;
    this.log.info(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
    setTimeout(() => this.connect(), delay);
  }

  private async handleRawMessage(raw: string): Promise<void> {
    let msg: any;
    try {
      msg = JSON.parse(raw);
    } catch {
      return;
    }

    const cmd = msg.cmd;

    if (cmd === 'aibot_msg_callback') {
      await this.handleMsgCallback(msg);
    } else if (cmd === 'aibot_event_callback') {
      this.handleEventCallback(msg);
    } else if (msg.errcode !== undefined) {
      if (msg.errcode === 0) {
        this.log.info(`Command response OK`);
      } else {
        this.log.error(`Command error: ${msg.errcode} ${msg.errmsg}`);
      }
    }
  }

  private async handleMsgCallback(msg: any): Promise<void> {
    const userId = msg.body?.from?.userid;
    const msgtype: string = msg.body?.msgtype || 'text';
    const chatType = msg.body?.chattype === 'group' ? 'group' : 'single';
    const chatId = msg.body?.chatid || userId;
    const requestId = msg.headers?.req_id;

    if (!userId) return;

    let content = '';
    let media: MediaAttachment[] | undefined;

    try {
      switch (msgtype) {
        case 'text': {
          const rawContent = msg.body?.text?.content || '';
          content = rawContent.replace(/@[^\s]+\s?/g, '').trim();
          break;
        }

        case 'image': {
          const imageUrl = msg.body?.image?.url;
          const aesKey = msg.body?.image?.aeskey;
          if (imageUrl && aesKey) {
            const { buffer, mimeType } = await downloadAndDecrypt(imageUrl, aesKey);
            media = [{
              type: 'image',
              mimeType,
              base64Data: buffer.toString('base64'),
            }];
            content = '[图片]';
          }
          break;
        }

        case 'voice': {
          // WeCom voice messages include pre-transcribed text in voice.content
          const voiceContent = msg.body?.voice?.content;
          if (voiceContent) {
            content = String(voiceContent).replace(/@[^\s]+\s?/g, '').trim();
          }
          // Also download the audio file for transcription fallback
          const voiceUrl = msg.body?.voice?.url;
          const voiceAesKey = msg.body?.voice?.aeskey;
          if (voiceUrl && voiceAesKey) {
            const { buffer, mimeType } = await downloadAndDecrypt(voiceUrl, voiceAesKey);
            media = [{
              type: 'audio',
              mimeType,
              base64Data: buffer.toString('base64'),
              transcription: content || undefined,
            }];
          }
          if (!content) content = '[语音消息]';
          break;
        }

        case 'file': {
          const fileUrl = msg.body?.file?.url;
          const fileAesKey = msg.body?.file?.aeskey;
          const fileName = msg.body?.file?.filename || 'unknown';
          if (fileUrl && fileAesKey) {
            const { buffer, mimeType } = await downloadAndDecrypt(fileUrl, fileAesKey);
            media = [{
              type: 'document',
              fileName,
              mimeType,
              base64Data: buffer.toString('base64'),
            }];
            content = `[文件: ${fileName}]`;
          }
          break;
        }

        case 'video': {
          const videoUrl = msg.body?.video?.url;
          const videoAesKey = msg.body?.video?.aeskey;
          if (videoUrl && videoAesKey) {
            const { buffer, mimeType } = await downloadAndDecrypt(videoUrl, videoAesKey);
            media = [{
              type: 'video',
              mimeType,
              base64Data: buffer.toString('base64'),
            }];
            content = '[视频]';
          }
          break;
        }

        case 'mixed': {
          // Mixed messages contain multiple items (text + image etc.)
          const items: Array<any> = msg.body?.mixed?.items || [];
          const textParts: string[] = [];
          const mediaList: MediaAttachment[] = [];

          for (const item of items) {
            const itemType: string = item.msgtype;
            if (itemType === 'text' && item.text?.content) {
              textParts.push(String(item.text.content).replace(/@[^\s]+\s?/g, '').trim());
            } else if (itemType === 'image' && item.image?.url && item.image?.aeskey) {
              try {
                const { buffer, mimeType } = await downloadAndDecrypt(item.image.url, item.image.aeskey);
                mediaList.push({
                  type: 'image',
                  mimeType,
                  base64Data: buffer.toString('base64'),
                });
              } catch (err) {
                this.log.warn('Failed to download mixed image item', { error: String(err) });
              }
            }
          }

          content = textParts.join('\n').trim();
          if (mediaList.length > 0) {
            media = mediaList;
            if (!content) content = '[图文消息]';
          }
          if (!content && mediaList.length === 0) return;
          break;
        }

        default:
          this.log.info(`Unsupported WeCom msgtype: ${msgtype}`);
          content = `[不支持的消息类型: ${msgtype}]`;
      }
    } catch (err) {
      this.log.error(`Failed to process WeCom ${msgtype} message`, { error: String(err) });
      content = content || `[消息处理失败: ${msgtype}]`;
    }

    if (!content && (!media || media.length === 0)) return;

    const incoming: IncomingMessage = {
      platform: 'wecom',
      userId,
      content: content || ' ',
      requestId: requestId || uuidv4(),
      chatType: chatType as 'single' | 'group',
      chatId: chatId || userId,
      media,
      botProfileId: this.botProfileId,
    };

    const sender = this.createSender(requestId);
    await this.config.onMessage(incoming, sender);
  }

  private handleEventCallback(msg: any): void {
    const eventType = msg.body?.event?.eventtype;
    const requestId = msg.headers?.req_id;

    if (eventType === 'enter_chat') {
      const mode = this.config.getMode();
      let welcome = '欢迎使用 Sman，输入 //help 查看可用命令。';
      if (mode === 'query') {
        welcome = '你好，我是答疑助手，有什么问题直接问我就行。\n长时间未对话会话会自动重置哦。';
      } else if (mode === 'collect') {
        welcome = '你好，欢迎反馈问题和建议，我会帮你记录。';
      }
      this.send({
        cmd: 'aibot_respond_welcome_msg',
        headers: { req_id: requestId },
        body: {
          msgtype: 'text',
          text: { content: welcome },
        },
      });
    }
  }

  private createSender(requestId?: string): ChatResponseSender {
    const reqId = requestId || uuidv4();
    const streamId = `stream-${Date.now()}-${uuidv4().substring(0, 8)}`;
    let started = false;
    const self = this;

    let thinkingLines: string[] = [];
    let accumulated = '';
    let throttleTimer: ReturnType<typeof setTimeout> | null = null;
    let pendingFlush = false;
    let activeToolName: string | null = null;

    const TOOL_DISPLAY_NAMES: Record<string, string> = {
      Read: '读取文件',
      Write: '写入文件',
      Edit: '编辑文件',
      Bash: '执行命令',
      Glob: '搜索文件',
      Grep: '搜索内容',
      WebSearch: '网络搜索',
      WebFetch: '获取网页',
      Agent: '子 Agent',
      LSP: '代码分析',
      NotebookEdit: '编辑笔记本',
    };

    const getToolDisplayName = (toolName: string): string => {
      return TOOL_DISPLAY_NAMES[toolName] || toolName;
    };

    const buildContent = (): string => {
      const toolLine = activeToolName ? `🔧 ${activeToolName}...\n\n` : '';
      const thinkBlock = thinkingLines.length > 0
        ? `<think>\n${thinkingLines.join('\n')}\n</think>\n\n`
        : '';
      return toolLine + thinkBlock + accumulated;
    };

    const sendPacket = (finish: boolean) => {
      let content = buildContent();

      // Enforce byte limit — evict oldest thinking lines from the top
      while (
        thinkingLines.length > 1 &&
        Buffer.byteLength(content, 'utf8') > MAX_CONTENT_BYTES
      ) {
        thinkingLines.shift();
        const truncatedThink = `<think>\n...\n${thinkingLines.join('\n')}\n</think>\n\n`;
        content = `🔧 ${activeToolName}...\n\n` + truncatedThink + accumulated;
      }

      self.send({
        cmd: 'aibot_respond_msg',
        headers: { req_id: reqId },
        body: {
          msgtype: 'stream',
          stream: { id: streamId, finish, content },
        },
      });
    };

    const flushStream = () => {
      throttleTimer = null;
      if (!pendingFlush) return;
      pendingFlush = false;
      sendPacket(false);
    };

    const scheduleFlush = () => {
      pendingFlush = true;
      if (!throttleTimer) {
        throttleTimer = setTimeout(flushStream, STREAM_THROTTLE_MS);
      }
    };

    return {
      start() { started = true; },
      sendThinking(content: string) {
        if (!started) return;
        thinkingLines.push(`💭 ${content}`);
        scheduleFlush();
      },
      sendToolStatus(toolName: string, status: 'start' | 'end') {
        if (!started) return;
        if (status === 'start') {
          activeToolName = getToolDisplayName(toolName);
        } else {
          activeToolName = null;
        }
        scheduleFlush();
      },
      sendChunk(content: string) {
        if (!started) return;
        accumulated += content;
        scheduleFlush();
      },
      finish(fullContent: string) {
        if (throttleTimer) {
          clearTimeout(throttleTimer);
          throttleTimer = null;
        }
        if (started) {
          // Final message: include thinking block + full answer (no tool status)
          const thinkBlock = thinkingLines.length > 0
            ? `<think>\n${thinkingLines.join('\n')}\n</think>\n\n`
            : '';
          const finalContent = thinkBlock + fullContent;
          activeToolName = null;
          sendPacket(true);
        } else {
          self.send({
            cmd: 'aibot_respond_msg',
            headers: { req_id: reqId },
            body: {
              msgtype: 'markdown',
              markdown: { content: fullContent },
            },
          });
        }
      },
      error(message: string) {
        if (throttleTimer) {
          clearTimeout(throttleTimer);
          throttleTimer = null;
        }
        self.send({
          cmd: 'aibot_respond_msg',
          headers: { req_id: reqId },
          body: {
            msgtype: 'text',
            text: { content: message },
          },
        });
      },
    };
  }

  /**
   * Send a message. If connected → immediate send. If disconnected → queue it,
   * trigger immediate reconnect, flush all queued messages once reconnected.
   */
  private send(data: object): void {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(data));
      return;
    }

    // Disconnected — queue and reconnect now if not already doing so.
    this.pendingQueue.push(data);
    this.log.info(`send: queued message (queue=${this.pendingQueue.length}), triggering reconnect`);
    if (!this.reconnecting) {
      this.reconnecting = true;
      this.connect();
    }
  }

  /** Flush all queued messages after reconnection. */
  private flushPending(): void {
    if (this.pendingQueue.length === 0) return;
    const queue = this.pendingQueue;
    this.pendingQueue = [];
    this.log.info(`Flushing ${queue.length} pending messages`);
    for (const data of queue) {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify(data));
      } else {
        // Re-queue if disconnected again mid-flush.
        this.pendingQueue.push(data);
      }
    }
  }

  private cleanup(): void {
    this.stopHeartbeat();
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}
