/**
 * botClient.js
 * Consolidated Webex client for standard bot operations.
 *
 * This is the primary client for normal ServChan bot activities:
 * - Creating rooms
 * - Posting messages
 * - Adding members
 * - Basic room helpers (getWOByRoom, getStoreByRoom)
 *
 * For privileged operations (DECT, telephony, etc.), use adminClient.js instead.
 */

import axios from 'axios';
import { logger } from '../../utils/logger.js';
import FormData from 'form-data';

class ServChanBotClient {
  static #instance = null;

  constructor() {
    if (ServChanBotClient.#instance) {
      return ServChanBotClient.#instance;
    }

    const token = process.env.WEBEX_BOT_TOKEN;
    const baseURL = process.env.WEBEX_BASE_URL || 'https://webexapis.com/v1';

    if (!token) {
      logger('webex:botClient', 'WEBEX_BOT_TOKEN missing – bot will not work', 'error');
      throw new Error('Missing WEBEX_BOT_TOKEN');
    }

    this.axios = axios.create({
      baseURL,
      timeout: 15000,
      headers: {
        Authorization: `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
    });

    this.axios.interceptors.response.use(
      (res) => res,
      (err) => {
        const msg = err.response
          ? `${err.response.status} - ${JSON.stringify(err.response.data)}`
          : err.message;
        logger('webex:botClient', `API error: ${msg}`, 'error');
        return Promise.reject(err);
      }
    );

    ServChanBotClient.#instance = this;
    logger('webex:botClient', 'Initialized (bot token)');
  }

  // ──────────────────────────────────────────────
  // Core messaging & room operations
  // ──────────────────────────────────────────────

  async createRoom(title, teamId = null) {
    const payload = { title, type: 'group' };
    if (teamId) payload.teamId = teamId;

    const res = await this.axios.post('/rooms', payload);
    logger('webex:bot:createRoom', `Created ${res.data.id}`);
    return res.data;
  }

  async addMember(roomId, personEmail) {
    const res = await this.axios.post('/memberships', { roomId, personEmail });
    logger('webex:bot:addMember', `Added ${personEmail}`);
    return res.data;
  }

  async sendMarkdown(roomId, markdown, textFallback = null) {
    if (!roomId || !markdown) {
      logger('webex:bot:sendMarkdown', 'Missing roomId or markdown', 'warn');
      return null;
    }

    const payload = { roomId, markdown };
    if (textFallback) payload.text = textFallback;

    const res = await this.axios.post('/messages', payload);
    return res.data;
  }

  async sendWithAttachment(roomId, buffer, fileName, contentType = 'application/octet-stream', text = 'Attached file', fileUrl = null) {
    if (!roomId || !fileName) {
      logger('webex:bot:attachment', 'Missing required params', 'warn');
      return null;
    }

    if (fileUrl) {
      const payload = { roomId, markdown: text, files: [fileUrl] };
      const res = await this.axios.post('/messages', payload);
      return res.data;
    }

    if (buffer) {
      const form = new FormData();
      form.append('roomId', roomId);
      form.append('markdown', text);
      form.append('files', buffer, { filename: fileName, contentType });

      const res = await this.axios.post('/messages', form, {
        headers: form.getHeaders(),
      });
      return res.data;
    }

    throw new Error('Either buffer or fileUrl must be provided');
  }

  async deleteRoom(roomId) {
    if (!roomId) throw new Error('roomId is required');
    await this.axios.delete(`/rooms/${roomId}`);
    logger('webex:bot:deleteRoom', `Deleted ${roomId}`);
  }

  // ──────────────────────────────────────────────
  // Helpers used by commands
  // ──────────────────────────────────────────────

  async getRoom(roomId) {
    const res = await this.axios.get(`/rooms/${roomId}`);
    return res.data;
  }

  async getWOByRoom(roomId) {
    try {
      const room = await this.getRoom(roomId);
      return this.#extractWoNumber(room?.title);
    } catch (err) {
      logger('webex:bot:getWOByRoom', `Failed for ${roomId}: ${err.message}`, 'warn');
      return null;
    }
  }

  async getStoreByRoom(roomId) {
    try {
      const room = await this.getRoom(roomId);
      return this.#extractStoreNumber(room?.title);
    } catch (err) {
      logger('webex:bot:getStoreByRoom', `Failed for ${roomId}: ${err.message}`, 'warn');
      return null;
    }
  }

  #extractWoNumber(str) {
    if (!str) return null;
    const m = str.match(/WO[-–—]?\s*(\d+)/i);
    return m ? m[1] : null;
  }

  #extractStoreNumber(str) {
    if (!str || typeof str !== 'string') return null;

    const storeRegex = /Store\s*[#:=-]?\s*(\d{3,5})/i;
    let m = str.match(storeRegex);
    if (m) return m[1];

    const fallback = /WO[-–—]?\s*\d+\s*[\|]\s*(\d{3,5})/i;
    m = str.match(fallback);
    if (m) return m[1];

    const numbers = [...str.matchAll(/\b(\d{4,5})\b/g)].map(x => x[1]);
    for (const n of numbers) {
      if (n.length >= 6) continue;
      return n;
    }
    return null;
  }

  // ──────────────────────────────────────────────
  // Inbound message file helpers
  // ──────────────────────────────────────────────

  async getMessage(messageId) {
    if (!messageId) throw new Error('messageId is required');
    const res = await this.axios.get(`/messages/${encodeURIComponent(messageId)}`);
    return res.data;
  }

  #parseFilenameFromHeaders(headers = {}) {
    const disposition = headers['content-disposition'] || headers['Content-Disposition'];
    if (!disposition) return null;

    const utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
    if (utf8Match) {
      try {
        return decodeURIComponent(utf8Match[1].trim());
      } catch {
        return utf8Match[1].trim();
      }
    }

    const quotedMatch = disposition.match(/filename="([^"]+)"/i);
    if (quotedMatch) return quotedMatch[1];

    const plainMatch = disposition.match(/filename=([^;]+)/i);
    return plainMatch ? plainMatch[1].trim() : null;
  }

  #extensionFromContentType(contentType = '') {
    const ct = String(contentType).split(';')[0].trim().toLowerCase();
    const map = {
      'image/jpeg': 'jpg',
      'image/png': 'png',
      'image/gif': 'gif',
      'image/bmp': 'bmp',
      'image/webp': 'webp',
      'image/heic': 'heic',
      'image/heif': 'heif',
      'application/pdf': 'pdf',
      'text/plain': 'txt',
    };
    return map[ct] || null;
  }

  async inspectMessageFile(fileUrl) {
    if (!fileUrl) throw new Error('fileUrl is required');

    const res = await axios.head(fileUrl, {
      headers: { Authorization: this.axios.defaults.headers.Authorization },
      timeout: 30000,
      validateStatus: (status) => status < 500,
    });

    if (res.status >= 400) {
      throw new Error(`File inspect failed: HTTP ${res.status}`);
    }

    const contentType = res.headers['content-type']?.split(';')[0]?.trim()
      || 'application/octet-stream';
    const fileName = this.#parseFilenameFromHeaders(res.headers)
      || `attachment.${this.#extensionFromContentType(contentType) || 'bin'}`;

    return { fileName, contentType };
  }

  async downloadMessageFile(fileUrl, fileNameHint = null) {
    if (!fileUrl) throw new Error('fileUrl is required');

    let fileName = fileNameHint;
    let contentType = 'application/octet-stream';

    if (!fileName) {
      try {
        const inspected = await this.inspectMessageFile(fileUrl);
        fileName = inspected.fileName;
        contentType = inspected.contentType;
      } catch (err) {
        logger('webex:bot:downloadMessageFile', `HEAD failed, continuing with GET: ${err.message}`, 'warn');
        fileName = 'attachment.bin';
      }
    }

    const res = await axios.get(fileUrl, {
      headers: { Authorization: this.axios.defaults.headers.Authorization },
      responseType: 'arraybuffer',
      timeout: 120000,
      maxBodyLength: Infinity,
      maxContentLength: Infinity,
    });

    const headerType = res.headers['content-type']?.split(';')[0]?.trim();
    if (headerType) contentType = headerType;

    const headerName = this.#parseFilenameFromHeaders(res.headers);
    if (headerName) fileName = headerName;

    const buffer = Buffer.from(res.data);
    logger('webex:bot:downloadMessageFile', `Downloaded ${fileName} (${buffer.length} bytes)`);
    return { buffer, contentType, fileName };
  }

  // ──────────────────────────────────────────────
  // Adaptive Card + person helpers (for approval flow)
  // ──────────────────────────────────────────────

  /**
   * Send an Adaptive Card (v1.3) message.
   * card = plain JS object matching Adaptive Card schema.
   * The Webex /messages endpoint accepts attachments with contentType for cards.
   */
  async sendAdaptiveCard(roomId, card, fallbackText = "This message contains an interactive card. Please view in a supported Webex client.") {
    if (!roomId || !card) {
      logger("webex:bot:sendAdaptiveCard", "Missing roomId or card", "warn");
      return null;
    }
    const payload = {
      roomId,
      text: fallbackText,
      attachments: [
        {
          contentType: "application/vnd.microsoft.card.adaptive",
          content: card,
        },
      ],
    };
    const res = await this.axios.post("/messages", payload);
    return res.data;
  }

  /** Delete a message posted by this bot (e.g. remove an approval card after submit). */
  async deleteMessage(messageId) {
    if (!messageId) {
      logger('webex:bot:deleteMessage', 'Missing messageId', 'warn');
      return null;
    }
    await this.axios.delete(`/messages/${encodeURIComponent(messageId)}`);
    logger('webex:bot:deleteMessage', `Deleted message ${messageId}`);
    return true;
  }

  /**
   * Fetch person details (for approver attribution from attachmentAction.personId).
   */
  async getPersonDetails(personId) {
    if (!personId) return { displayName: "Unknown User", emails: [] };
    try {
      const res = await this.axios.get("/people/" + encodeURIComponent(personId));
      return res.data;
    } catch (err) {
      logger("webex:bot:getPersonDetails", "Failed for " + personId + ": " + err.message, "warn");
      return { displayName: personId, emails: [] };
    }
  }
}

export default new ServChanBotClient();
