/**
 * Dialogs Web Chat - Plain script SDK (first milestone).
 * Requires: Socket.IO client loaded before this script (e.g. script src="https://cdn.socket.io/4.x/socket.io.min.js")
 * Backend: channel-web REST + guest Socket.IO (see WEBCHAT_FLOW_AND_INTEGRATION.md).
 */
(function (global) {
  'use strict';

  var STORAGE_KEY_VISITOR = 'dialogs_webchat_visitor_id';
  var DEFAULT_ROOT_PATH = '';

  function generateVisitorId() {
    var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    var id = '';
    for (var i = 0; i < 24; i++) {
      id += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return id;
  }

  function getOrCreateVisitorId() {
    try {
      var stored = global.localStorage && global.localStorage.getItem(STORAGE_KEY_VISITOR);
      if (stored && stored.length >= 24) return stored;
    } catch (e) {}
    var id = generateVisitorId();
    try {
      if (global.localStorage) global.localStorage.setItem(STORAGE_KEY_VISITOR, id);
    } catch (e) {}
    return id;
  }

  function createEmitter() {
    var listeners = {};
    return {
      on: function (event, fn) {
        if (!listeners[event]) listeners[event] = [];
        listeners[event].push(fn);
      },
      off: function (event, fn) {
        if (!listeners[event]) return;
        var i = listeners[event].indexOf(fn);
        if (i !== -1) listeners[event].splice(i, 1);
      },
      emit: function (event, data) {
        if (!listeners[event]) return;
        listeners[event].slice().forEach(function (fn) {
          try {
            fn(data);
          } catch (err) {
            console.error('DialogsWebChat listener error:', err);
          }
        });
      }
    };
  }

  function DialogsWebChat() {
    var self = this;
    var config = {};
    var reconnectConfig = { autoReconnect: false, maxAttempts: 5, delayMs: 2000 };
    var socket = null;
    var webSessionId = null;
    var currentConversationId = null;
    var connectResolve = null;
    var reconnectAttempts = 0;
    var reconnectTimer = null;
    var emitter = createEmitter();

    function baseUrl() {
      var host = config.host || '';
      var root = config.ROOT_PATH != null ? config.ROOT_PATH : DEFAULT_ROOT_PATH;
      return host + root + '/api/v1/bots/' + (config.botId || '') + '/mod/channel-web';
    }

    function api(method, path, body) {
      var url = baseUrl() + path;
      var opts = {
        method: method,
        headers: { 'Content-Type': 'application/json' }
      };
      if (body) opts.body = JSON.stringify(body);
      return global.fetch(url, opts);
    }

    /** Returns a rejected Promise if socket is disconnected; otherwise null. Use to block session APIs when disconnected. */
    function requireConnected() {
      if (!(socket && socket.connected && webSessionId)) {
        return Promise.reject(new Error('Not connected. Call connect() first.'));
      }
      return null;
    }

    this.init = function (userConfig) {
      config = {
        host: userConfig.host || '',
        botId: userConfig.botId || '',
        ROOT_PATH: userConfig.ROOT_PATH != null ? userConfig.ROOT_PATH : DEFAULT_ROOT_PATH
      };
      if (userConfig.reconnect !== undefined) {
        reconnectConfig.autoReconnect = !!userConfig.reconnect.autoReconnect;
        reconnectConfig.maxAttempts = Math.max(1, parseInt(userConfig.reconnect.maxAttempts, 10) || 5);
        reconnectConfig.delayMs = Math.max(500, parseInt(userConfig.reconnect.delayMs, 10) || 2000);
      }
      return self;
    };

    /**
     * Optional: validate that the current origin is allowed to use this bot (e.g. domain whitelist).
     * Calls GET {host}/lite/{botId}/validation/?domainName={origin}. Returns Promise that resolves
     * if response.ok, rejects otherwise. origin defaults to window.location.origin.
     * Use before connect() if you want the same client-side gate as the official embed.
     */
    this.validateHost = function (origin) {
      var host = config.host || '';
      var botId = config.botId || '';
      var domain = origin != null ? origin : (global.location && global.location.origin) || '';
      var url = host + '/lite/' + encodeURIComponent(botId) + '/validation/?domainName=' + encodeURIComponent(domain);
      return global.fetch(url).then(function (res) {
        if (!res.ok) throw new Error('Domain not allowed for this bot (validation failed)');
      });
    };

    function clearReconnectTimer() {
      if (reconnectTimer) {
        clearTimeout(reconnectTimer);
        reconnectTimer = null;
      }
    }

    function tryAutoReconnect() {
      clearReconnectTimer();
      if (!reconnectConfig.autoReconnect || reconnectAttempts >= reconnectConfig.maxAttempts) {
        emitter.emit('reconnect_failed', { attempts: reconnectAttempts });
        return;
      }
      reconnectAttempts += 1;
      emitter.emit('reconnecting', { attempt: reconnectAttempts, maxAttempts: reconnectConfig.maxAttempts });
      reconnectTimer = setTimeout(function () {
        reconnectTimer = null;
        self.connect().then(
          function () {
            reconnectAttempts = 0;
            emitter.emit('reconnected', { webSessionId: webSessionId });
          },
          function () {
            tryAutoReconnect();
          }
        );
      }, reconnectConfig.delayMs);
    }

    /**
     * Connect to guest socket. Must be called before createConversation/sendMessage.
     * Returns a Promise that resolves when connected (with webSessionId available).
     */
    this.connect = function () {
      if (socket && socket.connected) {
        return Promise.resolve(webSessionId);
      }
      if (!global.io) {
        return Promise.reject(new Error('Socket.IO not loaded. Add: <script src="https://cdn.socket.io/4.x/socket.io.min.js"></script>'));
      }
      var host = config.host || '';
      var root = config.ROOT_PATH != null ? config.ROOT_PATH : DEFAULT_ROOT_PATH;
      var path = (root || '') + '/socket.io';
      var visitorId = getOrCreateVisitorId();
      var botId = config.botId || '';

      return new Promise(function (resolve, reject) {
        if (socket) {
          socket.removeAllListeners();
          socket.disconnect();
          socket = null;
        }
        socket = global.io(host + '/guest', {
          path: path || '/socket.io',
          query: { visitorId: visitorId, botId: botId },
          transports: ['websocket', 'polling']
        });

        socket.on('connect', function () {
          webSessionId = socket.id;
          reconnectAttempts = 0;
          clearReconnectTimer();
          if (connectResolve) {
            connectResolve(webSessionId);
            connectResolve = null;
          }
          resolve(webSessionId);
        });

        socket.on('disconnect', function (reason) {
          webSessionId = null;
          emitter.emit('disconnect', { reason: reason });
          if (reconnectConfig.autoReconnect) {
            tryAutoReconnect();
          }
        });

        socket.on('connect_error', function (err) {
          if (connectResolve) {
            connectResolve = null;
            reject(err);
          }
        });

        socket.on('event', function (payload) {
          var name = payload && payload.name;
          var data = payload && payload.data;
          if (name === 'guest.webchat.message') {
            emitter.emit('message', data);
          } else if (name === 'guest.webchat.typing') {
            emitter.emit('typing', data);
          } else if (name === 'guest.webchat.clear') {
            emitter.emit('clear', data);
          }
        });
      });
    };

    /**
     * Manually reconnect: disconnect (if connected) and call connect() again.
     * Returns the same Promise as connect(). Resets auto-reconnect attempt count.
     */
    this.reconnect = function () {
      clearReconnectTimer();
      reconnectAttempts = 0;
      if (socket) {
        socket.removeAllListeners();
        socket.disconnect();
        socket = null;
      }
      webSessionId = null;
      return self.connect();
    };

    /**
     * Disable auto-reconnect and clear any pending reconnect timer.
     */
    this.disableAutoReconnect = function () {
      reconnectConfig.autoReconnect = false;
      clearReconnectTimer();
    };

    /**
     * Close the socket connection. Clears reconnect timer. Use connect() or reconnect() to connect again.
     */
    this.disconnect = function () {
      clearReconnectTimer();
      if (socket) {
        socket.removeAllListeners();
        socket.disconnect();
        socket = null;
      }
      webSessionId = null;
    };

    /**
     * List conversations for the current user. Requires connect() first.
     * Optional startupData: { name, email, phone, custom_id, isEmulator }.
     * Returns Promise<{ conversations: Array<{ id, userId, createdOn, lastMessage? }>, ... }>.
     */
    this.listConversations = function (startupData) {
      var err = requireConnected();
      if (err) return err;
      var body = { webSessionId: webSessionId };
      if (startupData) body.startupData = startupData;
      return api('POST', '/conversations/list', body)
        .then(function (res) {
          if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
          return res.json();
        });
    };

    /**
     * Get one conversation with its message history. Requires connect() first.
     * Returns Promise<{ id, userId, createdOn, messages: Array<{ id, conversationId, authorId, sentOn, payload }> }>.
     */
    this.getConversation = function (conversationId) {
      var err = requireConnected();
      if (err) return err;
      return api('POST', '/conversations/get', {
        webSessionId: webSessionId,
        conversationId: conversationId
      })
        .then(function (res) {
          if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
          return res.json();
        })
        .then(function (convo) {
          if (convo && convo.id) currentConversationId = convo.id;
          return convo;
        });
    };

    /**
     * Create a new conversation. Requires connect() first.
     * Returns Promise<conversationId> (convoId from API).
     */
    this.createConversation = function () {
      var err = requireConnected();
      if (err) return err;
      return api('POST', '/conversations/new', { webSessionId: webSessionId })
        .then(function (res) {
          if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
          return res.json();
        })
        .then(function (json) {
          var id = json.convoId || json.conversationId;
          if (id) currentConversationId = id;
          return id;
        });
    };

    /**
     * Reset a conversation: clear dialog session and optionally clear UI. Requires connect() first.
     * conversationId optional; uses current conversation if omitted.
     * Server may emit guest.webchat.clear; listen for chat.on('clear') to clear the message list.
     * Returns Promise<void>. After success, currentConversationId is cleared; call createConversation() for a new thread.
     */
    this.resetConversation = function (conversationId) {
      var err = requireConnected();
      if (err) return err;
      var convoId = conversationId || currentConversationId;
      if (!convoId) {
        return Promise.reject(new Error('No conversation. Provide conversationId or ensure a conversation is active.'));
      }
      return api('POST', '/conversations/reset', {
        webSessionId: webSessionId,
        conversationId: convoId
      }).then(function (res) {
        if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
        currentConversationId = null;
      });
    };

    /**
     * Fetch bot metadata (name, description, theme, startup_message, etc.). Does not require connect().
     * Uses config.botId from init(). Returns Promise<botInfo>.
     */
    this.getBotInfo = function () {
      var url = baseUrl() + '/botInfo';
      return global.fetch(url).then(function (res) {
        if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
        return res.json();
      });
    };

    /**
     * Send a message. payload should be e.g. { type: 'text', text: 'Hello' }.
     * If conversationId is omitted, uses the id from createConversation() or last known.
     */
    this.sendMessage = function (conversationId, payload) {
      var err = requireConnected();
      if (err) return err;
      var body = {
        webSessionId: webSessionId,
        payload: payload || { type: 'text', text: '' },
        isEmulator: false
      };
      if (conversationId) body.conversationId = conversationId;
      else if (currentConversationId) body.conversationId = currentConversationId;

      return api('POST', '/messages', body).then(function (res) {
        if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
      });
    };

    /**
     * Upload a file (image, document, etc.). Uses POST /messages/files (multipart/form-data).
     * conversationId is optional; uses current if omitted.
     * file: File or Blob. optionalPayload: optional object (e.g. { caption: '...' }) sent as JSON string in form.
     */
    this.sendFile = function (conversationId, file, optionalPayload) {
      var err = requireConnected();
      if (err) return err;
      var convoId = conversationId || currentConversationId;
      if (!convoId) {
        return Promise.reject(new Error('No conversation. Provide conversationId or create a conversation first.'));
      }
      if (!file) {
        return Promise.reject(new Error('file is required (File or Blob).'));
      }
      var formData = new global.FormData();
      formData.append('file', file, (file.name != null ? file.name : 'file'));
      formData.append('webSessionId', webSessionId);
      formData.append('conversationId', convoId);
      formData.append('payload', typeof optionalPayload === 'string' ? optionalPayload : JSON.stringify(optionalPayload || {}));
      var url = baseUrl() + '/messages/files';
      return global.fetch(url, {
        method: 'POST',
        body: formData
      }).then(function (res) {
        if (!res.ok) return res.text().then(function (t) { throw new Error(t || res.status); });
      });
    };

    this.getConversationId = function () {
      return currentConversationId;
    };

    this.setConversationId = function (id) {
      currentConversationId = id;
    };

    this.on = function (event, callback) {
      emitter.on(event, callback);
      return self;
    };

    this.off = function (event, callback) {
      emitter.off(event, callback);
      return self;
    };

    this.isConnected = function () {
      return !!(socket && socket.connected && webSessionId);
    };

    this.getWebSessionId = function () {
      return webSessionId;
    };
  }

  global.DialogsWebChat = DialogsWebChat;
  global.createDialogsChat = function (config) {
    var client = new DialogsWebChat();
    if (config) client.init(config);
    return client;
  };
})(typeof window !== 'undefined' ? window : this);
