/**
 * WhatsApp Service - Manages WhatsApp Web connection via whatsapp-web.js
 *
 * Purpose:
 * - Handle WhatsApp pairing/linking via QR code
 * - Maintain persistent session state
 * - Provide data access APIs for the WhatsApp tool
 * - Manage allowed chats/groups access control
 */

import fs from 'fs';
import path from 'path';

// Lazy-load whatsapp-web.js — loaded on first connect(), not at import time
let Client, LocalAuth, MessageMedia;
async function _ensureWhatsApp() {
  if (Client) return;
  try {
    const pkg = await import('whatsapp-web.js');
    const mod = pkg.default || pkg;
    Client = mod.Client;
    LocalAuth = mod.LocalAuth;
    MessageMedia = mod.MessageMedia;
  } catch (err) {
    throw new Error('whatsapp-web.js is not installed. Install it with: npm install whatsapp-web.js');
  }
}
import { getUserDataDir } from '../utilities/userDataDir.js';

// Connection states
const WA_STATUS = {
  DISCONNECTED: 'disconnected',
  CONNECTING: 'connecting',
  QR_READY: 'qr_ready',
  AUTHENTICATING: 'authenticating',
  CONNECTED: 'connected',
  FAILED: 'failed'
};

class WhatsAppService {
  constructor(logger = null) {
    this.logger = logger;
    this.client = null;
    this.status = WA_STATUS.DISCONNECTED;
    this.qrCode = null;
    this.currentUser = null;
    this.webSocketManager = null;

    // Allowed chats/groups (empty = all allowed)
    this.allowedChats = [];

    // Persisted config path
    this.dataDir = path.join(getUserDataDir(), 'whatsapp');
    this.configPath = path.join(this.dataDir, 'whatsapp-config.json');
    this.sessionDir = path.join(this.dataDir, 'session');

    // Ensure directories exist
    this._ensureDirs();

    // Load saved config
    this._loadConfig();
  }

  // ========================
  // Lifecycle
  // ========================

  /**
   * Check if a previous session exists and auto-reconnect if so.
   * Called once after the WebSocket manager is wired up.
   */
  async autoReconnect() {
    if (this._hasExistingSession()) {
      this.logger?.info('WhatsApp: Found existing session, auto-reconnecting...');
      try {
        await this.connect();
      } catch (e) {
        this.logger?.warn('WhatsApp auto-reconnect failed', { error: e.message });
      }
    }
  }

  /**
   * Check if a saved session directory exists (non-empty)
   */
  _hasExistingSession() {
    try {
      if (!fs.existsSync(this.sessionDir)) return false;
      const entries = fs.readdirSync(this.sessionDir);
      return entries.length > 0;
    } catch {
      return false;
    }
  }

  /**
   * Initialize and connect the WhatsApp client
   */
  async connect() {
    try {
      await _ensureWhatsApp();
    } catch (err) {
      return {
        success: false,
        error: err.message,
        status: WA_STATUS.FAILED
      };
    }

    if (this.status === WA_STATUS.CONNECTED) {
      return { success: true, status: this.status, user: this.currentUser };
    }

    if (this.status === WA_STATUS.CONNECTING || this.status === WA_STATUS.QR_READY) {
      return { success: true, status: this.status, qrCode: this.qrCode };
    }

    this.status = WA_STATUS.CONNECTING;
    this._broadcast('whatsapp_status', { status: this.status });

    try {
      this.client = new Client({
        authStrategy: new LocalAuth({ dataPath: this.sessionDir }),
        puppeteer: {
          headless: true,
          args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
        }
      });

      this._attachClientEvents();
      await this.client.initialize();

      return { success: true, status: this.status, qrCode: this.qrCode };
    } catch (error) {
      this.status = WA_STATUS.FAILED;
      this.logger?.error('WhatsApp connection failed', { error: error.message });
      this._broadcast('whatsapp_status', { status: this.status, error: error.message });
      return { success: false, error: error.message, status: this.status };
    }
  }

  /**
   * Disconnect the WhatsApp client
   */
  async disconnect() {
    if (!this.client) {
      this.status = WA_STATUS.DISCONNECTED;
      return { success: true };
    }

    try {
      await this.client.destroy();
    } catch (e) {
      this.logger?.warn('WhatsApp disconnect error', { error: e.message });
    }

    this.client = null;
    this.status = WA_STATUS.DISCONNECTED;
    this.qrCode = null;
    this.currentUser = null;
    this._broadcast('whatsapp_status', { status: this.status });
    return { success: true };
  }

  /**
   * Logout — clears session so re-pairing is required
   */
  async logout() {
    if (this.client) {
      try {
        await this.client.logout();
      } catch (e) {
        this.logger?.warn('WhatsApp logout error', { error: e.message });
      }
      await this.disconnect();
    }

    // Remove session files
    try {
      if (fs.existsSync(this.sessionDir)) {
        fs.rmSync(this.sessionDir, { recursive: true, force: true });
      }
    } catch (e) {
      this.logger?.warn('Failed to remove session dir', { error: e.message });
    }

    return { success: true };
  }

  /**
   * Get current connection status
   */
  getStatus() {
    return {
      status: this.status,
      connected: this.status === WA_STATUS.CONNECTED,
      qrCode: this.qrCode,
      user: this.currentUser,
      allowedChats: this.allowedChats
    };
  }

  // ========================
  // Access Control
  // ========================

  /**
   * Update allowed chats list
   * @param {Array<{id: string, name: string}>} chats
   */
  setAllowedChats(chats) {
    this.allowedChats = chats || [];
    this._saveConfig();
    return { success: true, allowedChats: this.allowedChats };
  }

  /**
   * Check if a chat is allowed
   */
  isChatAllowed(chatId) {
    if (this.allowedChats.length === 0) return true; // empty = all allowed
    return this.allowedChats.some(c => c.id === chatId);
  }

  // ========================
  // Data Access (used by WhatsAppTool)
  // ========================

  /**
   * Resolve a chat identifier — accepts either a WhatsApp ID (e.g. "1234@g.us")
   * or a human-readable name (e.g. "Work Group"). Returns the serialized chat ID.
   * Throws if no match or multiple ambiguous matches are found.
   * @param {string} chatIdOrName
   * @returns {Promise<string>} Resolved serialized chat ID
   */
  async resolveChatId(chatIdOrName) {
    if (!chatIdOrName) throw new Error('chatId or chat name is required');

    // If it looks like a WhatsApp ID already, return as-is
    if (chatIdOrName.includes('@')) {
      return chatIdOrName;
    }

    // Otherwise search by name (case-insensitive)
    this._ensureConnected();
    const allChats = await this.client.getChats();
    const needle = chatIdOrName.toLowerCase().trim();

    // Exact match first
    const exact = allChats.find(c =>
      (c.name || c.id.user || '').toLowerCase() === needle
    );
    if (exact) return exact.id._serialized;

    // Partial/substring match
    const partial = allChats.filter(c =>
      (c.name || c.id.user || '').toLowerCase().includes(needle)
    );

    if (partial.length === 0) {
      throw new Error(`No chat found matching "${chatIdOrName}". Use list-chats to see available chats.`);
    }
    if (partial.length === 1) {
      return partial[0].id._serialized;
    }

    // Multiple matches — return a helpful error
    const matches = partial.slice(0, 5).map(c => `"${c.name || c.id.user}" (${c.id._serialized})`).join(', ');
    throw new Error(
      `Multiple chats match "${chatIdOrName}": ${matches}. ` +
      `Please use a more specific name or the exact chat ID.`
    );
  }

  /**
   * List chats with optional filtering and pagination
   * @param {Object} options
   * @param {string} options.filter - Filter by name (substring, case-insensitive)
   * @param {string} options.type - Filter by type: "group", "individual", or "all" (default)
   * @param {number} options.page - Page number, 1-based (default: 1)
   * @param {number} options.pageSize - Items per page (default: 20)
   * @param {boolean} options.unreadOnly - Only show chats with unread messages
   */
  async listChats(options = {}) {
    this._ensureConnected();
    const { filter, type = 'all', page = 1, pageSize = 20, unreadOnly = false } = options;

    const rawChats = await this.client.getChats();

    let chats = rawChats.map(chat => ({
      id: chat.id._serialized,
      name: chat.name || chat.id.user,
      isGroup: chat.isGroup,
      unreadCount: chat.unreadCount,
      lastMessage: chat.lastMessage ? {
        body: chat.lastMessage.body?.substring(0, 200),
        timestamp: chat.lastMessage.timestamp,
        fromMe: chat.lastMessage.fromMe
      } : null,
      allowed: this.isChatAllowed(chat.id._serialized)
    }));

    // Apply filters
    if (type === 'group') {
      chats = chats.filter(c => c.isGroup);
    } else if (type === 'individual') {
      chats = chats.filter(c => !c.isGroup);
    }

    if (filter) {
      const needle = filter.toLowerCase();
      chats = chats.filter(c => c.name.toLowerCase().includes(needle));
    }

    if (unreadOnly) {
      chats = chats.filter(c => c.unreadCount > 0);
    }

    // Pagination
    const totalChats = chats.length;
    const totalPages = Math.ceil(totalChats / pageSize) || 1;
    const safePage = Math.max(1, Math.min(page, totalPages));
    const startIdx = (safePage - 1) * pageSize;
    const pageChats = chats.slice(startIdx, startIdx + pageSize);

    return {
      chats: pageChats,
      pagination: {
        page: safePage,
        pageSize,
        totalChats,
        totalPages,
        hasMore: safePage < totalPages
      }
    };
  }

  /**
   * Get messages from a specific chat
   * @param {string} chatId
   * @param {Object} options - { limit, before, after, searchTerm }
   */
  async getMessages(chatId, options = {}) {
    this._ensureConnected();
    chatId = await this.resolveChatId(chatId);
    this._ensureChatAllowed(chatId);

    const { limit = 50, before, after, searchTerm } = options;

    const chat = await this.client.getChatById(chatId);
    if (!chat) throw new Error(`Chat not found: ${chatId}`);

    // Fetch messages — whatsapp-web.js uses fetchMessages with limit
    const fetchLimit = searchTerm ? Math.max(limit * 5, 200) : limit;
    const messages = await chat.fetchMessages({ limit: fetchLimit });

    let filtered = messages;

    // Date filtering
    if (before) {
      const beforeTs = new Date(before).getTime() / 1000;
      filtered = filtered.filter(m => m.timestamp <= beforeTs);
    }
    if (after) {
      const afterTs = new Date(after).getTime() / 1000;
      filtered = filtered.filter(m => m.timestamp >= afterTs);
    }

    // Text search
    if (searchTerm) {
      const term = searchTerm.toLowerCase();
      filtered = filtered.filter(m =>
        m.body?.toLowerCase().includes(term)
      );
    }

    // Apply final limit
    filtered = filtered.slice(-limit);

    return {
      chatId,
      chatName: chat.name || chat.id.user,
      messageCount: filtered.length,
      messages: await Promise.all(filtered.map(m => this._formatMessage(m)))
    };
  }

  /**
   * Search across multiple chats
   * @param {string} searchTerm
   * @param {Object} options - { chatIds, limit, before, after }
   */
  async searchMessages(searchTerm, options = {}) {
    this._ensureConnected();
    if (!searchTerm) throw new Error('searchTerm is required');

    const { chatIds, limit = 30, before, after } = options;

    // Determine which chats to search (resolve names to IDs)
    let chatsToSearch;
    if (chatIds && chatIds.length > 0) {
      chatsToSearch = await Promise.all(chatIds.map(id => this.resolveChatId(id)));
    } else if (this.allowedChats.length > 0) {
      chatsToSearch = this.allowedChats.map(c => c.id);
    } else {
      // Search all chats — get up to 20 most recent
      const allChats = await this.client.getChats();
      chatsToSearch = allChats.slice(0, 20).map(c => c.id._serialized);
    }

    const results = [];
    for (const chatId of chatsToSearch) {
      try {
        const chatResults = await this.getMessages(chatId, {
          limit: Math.ceil(limit / chatsToSearch.length) || 10,
          before,
          after,
          searchTerm
        });
        if (chatResults.messages.length > 0) {
          results.push(chatResults);
        }
      } catch (e) {
        this.logger?.debug('Search skip chat', { chatId, error: e.message });
      }
    }

    return {
      searchTerm,
      totalMatches: results.reduce((s, r) => s + r.messageCount, 0),
      chatsSearched: chatsToSearch.length,
      results
    };
  }

  /**
   * Get media/file from a message
   * @param {string} chatId
   * @param {string} messageId - serialized message ID
   */
  async getMedia(chatId, messageId) {
    this._ensureConnected();
    chatId = await this.resolveChatId(chatId);
    this._ensureChatAllowed(chatId);

    const chat = await this.client.getChatById(chatId);
    if (!chat) throw new Error(`Chat not found: ${chatId}`);

    // Fetch a batch of messages to find the target one
    const messages = await chat.fetchMessages({ limit: 100 });
    const message = messages.find(m => m.id._serialized === messageId);

    if (!message) throw new Error(`Message not found: ${messageId}`);
    if (!message.hasMedia) throw new Error('Message has no media');

    const media = await message.downloadMedia();
    if (!media) throw new Error('Failed to download media');

    // Save to temp dir
    const ext = this._mimeToExt(media.mimetype);
    const filename = `wa-media-${Date.now()}${ext}`;
    const outputDir = path.join(this.dataDir, 'media');
    if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });

    const filePath = path.join(outputDir, filename);
    fs.writeFileSync(filePath, media.data, 'base64');

    return {
      filename,
      mimetype: media.mimetype,
      filePath,
      size: Buffer.from(media.data, 'base64').length
    };
  }

  /**
   * Get chat info (participants, description, etc.)
   */
  async getChatInfo(chatId) {
    this._ensureConnected();
    chatId = await this.resolveChatId(chatId);
    this._ensureChatAllowed(chatId);

    const chat = await this.client.getChatById(chatId);
    if (!chat) throw new Error(`Chat not found: ${chatId}`);

    const info = {
      id: chat.id._serialized,
      name: chat.name || chat.id.user,
      isGroup: chat.isGroup,
      unreadCount: chat.unreadCount,
      timestamp: chat.timestamp,
      muteExpiration: chat.muteExpiration
    };

    if (chat.isGroup) {
      info.description = chat.groupMetadata?.desc || '';
      info.participants = (chat.groupMetadata?.participants || []).map(p => ({
        id: p.id._serialized,
        isAdmin: p.isAdmin || false,
        isSuperAdmin: p.isSuperAdmin || false
      }));
      info.participantCount = info.participants.length;
    }

    return info;
  }

  // ========================
  // Internal helpers
  // ========================

  setWebSocketManager(wsManager) {
    this.webSocketManager = wsManager;
  }

  _attachClientEvents() {
    this.client.on('qr', (qr) => {
      this.qrCode = qr;
      this.status = WA_STATUS.QR_READY;
      this.logger?.info('WhatsApp QR code generated');
      this._broadcast('whatsapp_qr', { qrCode: qr });
      this._broadcast('whatsapp_status', { status: this.status });
    });

    this.client.on('authenticated', () => {
      this.status = WA_STATUS.AUTHENTICATING;
      this.qrCode = null;
      this.logger?.info('WhatsApp authenticated');
      this._broadcast('whatsapp_status', { status: this.status });
    });

    this.client.on('ready', async () => {
      this.status = WA_STATUS.CONNECTED;
      this.qrCode = null;

      try {
        const info = this.client.info;
        this.currentUser = {
          id: info.wid._serialized,
          name: info.pushname,
          phone: info.wid.user
        };
      } catch (e) {
        this.currentUser = { name: 'Connected' };
      }

      this.logger?.info('WhatsApp ready', { user: this.currentUser });
      this._broadcast('whatsapp_status', { status: this.status, user: this.currentUser });
    });

    this.client.on('disconnected', (reason) => {
      this.status = WA_STATUS.DISCONNECTED;
      this.qrCode = null;
      this.currentUser = null;
      this.logger?.warn('WhatsApp disconnected', { reason });
      this._broadcast('whatsapp_status', { status: this.status, reason });
    });

    this.client.on('auth_failure', (msg) => {
      this.status = WA_STATUS.FAILED;
      this.logger?.error('WhatsApp auth failure', { message: msg });
      this._broadcast('whatsapp_status', { status: this.status, error: msg });
    });
  }

  async _formatMessage(msg) {
    const formatted = {
      id: msg.id._serialized,
      body: msg.body,
      timestamp: msg.timestamp,
      date: new Date(msg.timestamp * 1000).toISOString(),
      fromMe: msg.fromMe,
      author: msg.author || msg.from,
      type: msg.type, // chat, image, video, audio, document, sticker, etc.
      hasMedia: msg.hasMedia,
      isForwarded: msg.isForwarded || false
    };

    // Include contact name if available
    try {
      const contact = await msg.getContact();
      formatted.authorName = contact.pushname || contact.name || contact.number;
    } catch {
      formatted.authorName = formatted.author;
    }

    // Media metadata (without downloading)
    if (msg.hasMedia) {
      formatted.mediaType = msg.type;
      if (msg._data?.mimetype) formatted.mimetype = msg._data.mimetype;
      if (msg._data?.filename) formatted.fileName = msg._data.filename;
      if (msg._data?.size) formatted.fileSize = msg._data.size;
      formatted.caption = msg._data?.caption || null;
    }

    return formatted;
  }

  _ensureConnected() {
    if (this.status !== WA_STATUS.CONNECTED || !this.client) {
      throw new Error('WhatsApp is not connected. Please link your account first in Settings.');
    }
  }

  _ensureChatAllowed(chatId) {
    if (!this.isChatAllowed(chatId)) {
      throw new Error(`Access denied: Chat ${chatId} is not in the allowed list. Update allowed chats in Settings.`);
    }
  }

  _broadcast(type, data) {
    if (this.webSocketManager) {
      // Broadcast to all sessions
      this.webSocketManager.broadcastToSession(null, {
        type,
        data,
        timestamp: new Date().toISOString()
      });
    }
  }

  _ensureDirs() {
    for (const dir of [this.dataDir, this.sessionDir]) {
      if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
      }
    }
  }

  _loadConfig() {
    try {
      if (fs.existsSync(this.configPath)) {
        const raw = fs.readFileSync(this.configPath, 'utf-8');
        const config = JSON.parse(raw);
        this.allowedChats = config.allowedChats || [];
      }
    } catch (e) {
      this.logger?.warn('Failed to load WhatsApp config', { error: e.message });
    }
  }

  _saveConfig() {
    try {
      fs.writeFileSync(this.configPath, JSON.stringify({
        allowedChats: this.allowedChats,
        updatedAt: new Date().toISOString()
      }, null, 2));
    } catch (e) {
      this.logger?.warn('Failed to save WhatsApp config', { error: e.message });
    }
  }

  _mimeToExt(mime) {
    const map = {
      'image/jpeg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/webp': '.webp',
      'video/mp4': '.mp4', 'audio/ogg': '.ogg', 'audio/mpeg': '.mp3',
      'application/pdf': '.pdf', 'application/msword': '.doc',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
      'application/vnd.ms-excel': '.xls',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx'
    };
    return map[mime] || '.bin';
  }
}

export default WhatsAppService;
export { WA_STATUS };
