/**
 * WhatsApp channel adapter.
 *
 * Implements the ChannelAdapter interface using whatsapp-web.js with headless Chrome.
 * QR code auth flow: prints QR to console for scanning on first run.
 * Session persistence via LocalAuth strategy with configurable data_dir.
 * Messages are chunked at 4096 chars (same as Telegram).
 */

import WhatsApp from 'whatsapp-web.js';
const { Client, LocalAuth, MessageMedia } = WhatsApp;
import type {
  Attachment,
  InboundMessage,
  OutboundMessage,
  OutboundAttachment,
  ChannelAdapter,
  ChannelStatus,
} from '../types.js';
import {
  allowTrustedOrPairedSender,
  buildResetInboundMessage,
  normalizeResetCommandText,
  shouldIgnoreForMissingMention,
  splitMessage,
} from '../utils.js';
import type { PairingManager } from '../pairing.js';

/** Configuration for the WhatsApp channel adapter. */
export interface WhatsAppAdapterConfig {
  /** Phone numbers allowed to interact. Empty = all numbers. */
  allowedNumbers?: string[];
  /** Group IDs (without @g.us suffix) allowed to interact. Empty = no groups. */
  allowedGroupIds?: string[];
  /** Require bot mention to respond in group chats (default: true). DMs always respond. */
  requireMention?: boolean;
  /** Directory for session persistence (LocalAuth data path). */
  dataDir?: string;
  /** Optional pairing manager for DM pairing codes. */
  pairingManager?: PairingManager;
  /** Allow launching Chromium without sandbox (unsafe; use only in high-trust/containerized setups). */
  allowNoSandbox?: boolean;
}

/** Minimal shape of a whatsapp-web.js message. */
interface WhatsAppMessage {
  id: { id: string; fromMe: boolean };
  from: string;
  body: string;
  timestamp: number;
  fromMe: boolean;
  author?: string;
  _data?: { notifyName?: string };
  /** Whether this message contains media (image, video, audio, document). */
  hasMedia?: boolean;
  /** Message type (e.g. "image", "video", "chat"). */
  type?: string;
  /** Download the media attached to this message. */
  downloadMedia?: () => Promise<{ mimetype: string; data: string; filename?: string } | null>;
  /** Chat handle for typing indicator. */
  getChat?: () => Promise<{ sendStateTyping: () => Promise<void> }>;
  /** Mentioned user IDs in message metadata. */
  mentionedIds?: string[];
}

/**
 * WhatsApp channel adapter backed by whatsapp-web.js.
 *
 * Handles QR code authentication, phone number allowlist filtering,
 * session persistence, and message chunking for 4096-char limit.
 */
export class WhatsAppAdapter implements ChannelAdapter {
  readonly name = 'whatsapp';

  private _status: ChannelStatus = 'disconnected';
  private client: InstanceType<typeof Client> | null = null;
  private messageHandler?: (msg: InboundMessage) => void;
  private config: WhatsAppAdapterConfig;
  private botId?: string;

  get status(): ChannelStatus {
    return this._status;
  }

  constructor(config: WhatsAppAdapterConfig) {
    this.config = config;
  }

  /** Register the inbound message handler. Called by the registry before connect(). */
  onMessage(handler: (msg: InboundMessage) => void): void {
    this.messageHandler = handler;
  }

  /** Create the whatsapp-web.js client, wire up event handlers, and initialize. */
  async connect(): Promise<void> {
    this._status = 'connecting';

    try {
      const authStrategy = new LocalAuth({
        dataPath: this.config.dataDir,
      });

      const puppeteerArgs = this.config.allowNoSandbox
        ? ['--no-sandbox', '--disable-setuid-sandbox']
        : [];
      if (this.config.allowNoSandbox) {
        console.warn('WhatsApp adapter: Chromium sandbox disabled via config (unsafe).');
      }

      this.client = new Client({
        authStrategy,
        puppeteer: {
          headless: true,
          args: puppeteerArgs,
        },
      });
      const client = this.client;
      if (!client) {
        throw new Error('WhatsApp client initialization failed');
      }

      // Promise that resolves on 'ready' or rejects on 'auth_failure'
      const readyPromise = new Promise<void>((resolve, reject) => {
        client.on('ready', () => {
          console.log('WhatsApp bot connected');
          this._status = 'connected';
          // Capture bot's own JID for mention detection
          const clientInfo = client as InstanceType<typeof Client> & {
            info?: { wid?: { _serialized?: string } };
          };
          this.botId = clientInfo.info?.wid?._serialized;
          resolve();
        });

        client.on('auth_failure', (msg: string) => {
          this._status = 'error';
          reject(new Error(`WhatsApp auth failure: ${msg}`));
        });

        client.on('qr', (qr: string) => {
          console.log('WhatsApp QR code received. Scan with your phone:');
          console.log(qr);
        });
      });

      // Register message event handler
      client.on('message', (message: unknown) => {
        this.handleMessage(message as WhatsAppMessage);
      });

      await client.initialize();
      await readyPromise;
    } catch (error) {
      this._status = 'error';
      throw error;
    }
  }

  /** Stop the client and clean up. */
  async disconnect(): Promise<void> {
    if (this.client) {
      try {
        await this.client.destroy();
      } catch {
        // Swallow destroy errors — cleanup must complete
      } finally {
        this.client = null;
      }
    }
    this._status = 'disconnected';
  }

  /** Send an outbound message, automatically chunking if it exceeds 4096 chars. */
  async send(peerId: string, message: OutboundMessage): Promise<void> {
    if (!this.client) {throw new Error('WhatsApp adapter not connected');}

    const text = message.text;

    if (text.length <= 4096) {
      await this.client.sendMessage(peerId, text);
    } else {
      const chunks = splitMessage(text, 4096);
      for (const chunk of chunks) {
        await this.client.sendMessage(peerId, chunk);
      }
    }

    // Send outbound attachments after text
    if (message.attachments && message.attachments.length > 0) {
      for (const attachment of message.attachments) {
        await this.sendAttachment(peerId, attachment);
      }
    }
  }

  /** Send a single outbound attachment via WhatsApp using MessageMedia. */
  private async sendAttachment(peerId: string, attachment: OutboundAttachment): Promise<void> {
    if (!this.client) {return;}

    try {
      if (attachment.data) {
        const media = new MessageMedia(
          attachment.mimeType,
          attachment.data,
          attachment.filename,
        );
        await this.client.sendMessage(peerId, media);
      } else if (attachment.url) {
        // Download from URL and send as MessageMedia
        const media = await MessageMedia.fromUrl(attachment.url);
        await this.client.sendMessage(peerId, media);
      }
    } catch (error) {
      console.error(
        `WhatsApp: failed to send ${attachment.mimeType} attachment:`,
        error instanceof Error ? error.message : 'Unknown error',
      );
    }
  }

  /** Internal: process an inbound WhatsApp message. */
  private async handleMessage(message: WhatsAppMessage): Promise<void> {
    if (!this.messageHandler) {return;}

    // Ignore messages from the bot itself
    if (message.fromMe) {return;}

    const from = message.from;

    // Group message handling
    const isGroup = from.endsWith('@g.us');
    if (isGroup) {
      // Check allowed group IDs
      const groupId = from.replace(/@g\.us$/, '');
      if (
        !this.config.allowedGroupIds ||
        this.config.allowedGroupIds.length === 0 ||
        !this.config.allowedGroupIds.includes(groupId)
      ) {
        return; // Group not allowed (empty list = no groups)
      }

      // Mention requirement in group chats
      if (this.botId && shouldIgnoreForMissingMention({
        requireMention: this.config.requireMention,
        defaultRequireMention: true,
        mentionsBot: message.body?.includes(`@${this.botId.replace(/@c\.us$/, '')}`) ||
          message.mentionedIds?.some((id) => id === this.botId) === true,
      })) {
        // WhatsApp mentions use @phone_number format in body
        // Also check for mentions in the message mentionedIds
        return;
      }
    }

    // Check allowed numbers for DMs (strip @c.us suffix for comparison)
    if (!isGroup) {
      const phoneNumber = from.replace(/@c\.us$/, '');
      if (
        this.config.allowedNumbers &&
        this.config.allowedNumbers.length > 0 &&
        !this.config.allowedNumbers.includes(phoneNumber)
      ) {
        const allowed = await allowTrustedOrPairedSender({
          pairingManager: this.config.pairingManager,
          channel: 'whatsapp',
          senderId: phoneNumber,
          text: message.body ?? '',
          isTrusted: false,
          onPaired: async () => {
            if (!this.client) {return;}
            await this.client.sendMessage(from, 'Pairing successful! You can now chat with Flynn.');
          },
        });
        if (!allowed) {
          return;
        }
      }
    }

    // Send typing indicator without delaying message dispatch.
    void Promise.resolve(message.getChat?.())
      .then(async (chat) => {
        await chat?.sendStateTyping();
      })
      .catch(() => {
        // ignore typing errors
      });

    // Strip bot mention from message body for group messages
    let text = message.body ?? '';
    if (isGroup && this.botId) {
      const botNumber = this.botId.replace(/@c\.us$/, '');
      text = text.replace(new RegExp(`@${botNumber}\\b`, 'g'), '').trim();
    }

    const senderName = message._data?.notifyName;

    // Extract media attachments if the message has media
    const attachments: Attachment[] = [];
    if (message.hasMedia) {
      try {
        const media = await message.downloadMedia?.();
        if (media && typeof media.mimetype === 'string') {
          const mimeType = media.mimetype;
          const isAudio = mimeType.startsWith('audio/');
          const isImage = mimeType.startsWith('image/');
          const isVoice = message.type === 'ptt';

          if (isAudio || isImage || isVoice) {
            attachments.push({
              mimeType: mimeType,
              data: media.data,
              filename: media.filename,
            });
          }
        }
      } catch (error) {
        console.error(
          'Failed to download WhatsApp media:',
          error instanceof Error ? error.message : 'Unknown error',
        );
      }
    }

    text = normalizeResetCommandText(text);

    // Detect reset command
    if (text === '!reset') {
      this.messageHandler(buildResetInboundMessage({
        id: message.id.id,
        channel: 'whatsapp',
        senderId: from,
        senderName,
        attachments,
      }));
      return;
    }

    // Regular message
    this.messageHandler({
      id: message.id.id,
      channel: 'whatsapp',
      senderId: from,
      senderName,
      text,
      timestamp: Date.now(),
      ...(attachments.length > 0 ? { attachments } : {}),
    });
  }
}
