// tiktok-push.js
// 抖音小程序 WebSocket 推送模块（完整修复版）
// 支持频道订阅、心跳保活、自动重连、私有频道认证

// ==================== 辅助函数 ====================
function getGlobalObject() {
  if (typeof global !== 'undefined') return global;
  if (typeof window !== 'undefined') return window;
  return this;
}

// ==================== CallbackRegistry ====================
function CallbackRegistry() {
  this._callbacks = {};
}

function prefix(name) {
  return '_' + name;
}

CallbackRegistry.prototype.get = function (name) {
  var key = prefix(name);
  return this._callbacks[key] || [];
};

CallbackRegistry.prototype.add = function (name, callback, context) {
  var key = prefix(name);
  if (!this._callbacks[key]) {
    this._callbacks[key] = [];
  }
  this._callbacks[key].push({
    fn: callback,
    context: context
  });
};

CallbackRegistry.prototype.remove = function (name, callback, context) {
  if (!name && !callback && !context) {
    this._callbacks = {};
    return;
  }
  var names = name ? [prefix(name)] : Object.keys(this._callbacks);
  for (var i = 0; i < names.length; i++) {
    var key = names[i];
    if (!this._callbacks[key]) continue;
    if (callback || context) {
      var filtered = [];
      for (var j = 0; j < this._callbacks[key].length; j++) {
        var cb = this._callbacks[key][j];
        if ((callback && callback !== cb.fn) || (context && context !== cb.context)) {
          filtered.push(cb);
        }
      }
      if (filtered.length === 0) {
        delete this._callbacks[key];
      } else {
        this._callbacks[key] = filtered;
      }
    } else {
      delete this._callbacks[key];
    }
  }
};

// ==================== Dispatcher ====================
function Dispatcher(failThrough) {
  this.callbacks = new CallbackRegistry();
  this.global_callbacks = [];
  this.failThrough = failThrough;
}

Dispatcher.prototype.on = function (eventName, callback, context) {
  this.callbacks.add(eventName, callback, context);
  return this;
};

Dispatcher.prototype.on_global = function (callback) {
  if (typeof callback === 'function') {
    this.global_callbacks.push(callback);
  }
  return this;
};

Dispatcher.prototype.off = function (eventName, callback, context) {
  this.callbacks.remove(eventName, callback, context);
  return this;
};

Dispatcher.prototype.emit = function (eventName, data) {
  // 全局回调
  if (Array.isArray(this.global_callbacks)) {
    for (var i = 0; i < this.global_callbacks.length; i++) {
      var gc = this.global_callbacks[i];
      if (typeof gc === 'function') {
        try {
          gc(eventName, data);
        } catch (e) {
          console.error('global callback error:', e);
        }
      }
    }
  }

  // 获取事件回调列表
  var callbacks = null;
  try {
    callbacks = this.callbacks && this.callbacks.get ? this.callbacks.get(eventName) : null;
  } catch (e) {
    console.warn('get callbacks error:', e);
  }

  if (callbacks && Array.isArray(callbacks) && callbacks.length > 0) {
    for (var j = 0; j < callbacks.length; j++) {
      var item = callbacks[j];
      if (item && typeof item.fn === 'function') {
        var ctx = item.context || getGlobalObject();
        try {
          item.fn.call(ctx, data);
        } catch (e) {
          console.error('callback error for event "' + eventName + '":', e);
        }
      }
    }
  } else if (this.failThrough && typeof this.failThrough === 'function') {
    this.failThrough(eventName, data);
  }

  return this;
};

// ==================== Channel ====================
function Channel(connection, channel_name) {
  this.subscribed = false;
  this.dispatcher = new Dispatcher();
  this.connection = connection;
  this.channelName = channel_name;
  this.subscribeCb = null;
  this.queue = [];

  // 绑定 dispatcher 方法，确保 this 指向 dispatcher
  this.on = this.dispatcher.on.bind(this.dispatcher);
  this.off = this.dispatcher.off.bind(this.dispatcher);
  this.emit = this.dispatcher.emit.bind(this.dispatcher);
  this.on_global = this.dispatcher.on_global.bind(this.dispatcher);
}

Channel.prototype.processSubscribe = function () {
  if (this.connection.state !== 'connected') {
    return;
  }
  if (this.subscribeCb) {
    this.subscribeCb();
  }
};

Channel.prototype.processQueue = function () {
  if (this.connection.state !== 'connected' || !this.subscribed) {
    return;
  }
  for (var i = 0; i < this.queue.length; i++) {
    try {
      this.queue[i]();
    } catch (e) {
      console.error('processQueue error:', e);
    }
  }
  this.queue = [];
};

Channel.prototype.trigger = function (event, data) {
  if (event.indexOf('client-') !== 0) {
    throw new Error("Event '" + event + "' should start with 'client-'");
  }
  var self = this;
  this.queue.push(function () {
    self.connection.send(JSON.stringify({ event: event, data: data, channel: self.channelName }));
  });
  this.processQueue();
};

// ==================== Connection ====================
function Connection(options) {
  this.dispatcher = new Dispatcher();

  // 绑定 dispatcher 方法
  this.on = this.dispatcher.on.bind(this.dispatcher);
  this.off = this.dispatcher.off.bind(this.dispatcher);
  this.emit = this.dispatcher.emit.bind(this.dispatcher);
  this.on_global = this.dispatcher.on_global.bind(this.dispatcher);

  this.options = options;
  this.state = 'initialized';
  this.doNotConnect = 0;
  this.reconnectInterval = 1000;
  this.socketTask = null;
  this.reconnectTimer = 0;
  this.socket_id = null;

  this.connect();
}

Connection.prototype.updateNetworkState = function (state) {
  var old_state = this.state;
  this.state = state;
  if (old_state !== state) {
    this.emit('state_change', { previous: old_state, current: state });
  }
};

Connection.prototype.connect = function () {
  var self = this;
  this.doNotConnect = 0;

  if (this.state === 'connected') {
    console.log('networkState is "' + this.state + '" , no need to connect');
    return;
  }

  if (this.reconnectTimer) {
    clearTimeout(this.reconnectTimer);
    this.reconnectTimer = 0;
  }

  this.closeAndClean();

  var url = this.options.url;
  var app_key = this.options.app_key;
  var fullUrl = url + '/app/' + app_key;

  this.socketTask = tt.connectSocket({
    url: fullUrl,
    success: function (res) {
      console.log('WebSocket connect success, socketTaskId:', res.socketTaskId);
    },
    fail: function (err) {
      console.error('WebSocket connect fail:', err);
      self.updateNetworkState('disconnected');
      if (!self.doNotConnect) {
        self.waitReconnect();
      }
      if (self.options.onError) {
        self.options.onError(err);
      }
    }
  });

  this.socketTask.onOpen(function (res) {
    self.reconnectInterval = 1000;
    if (self.doNotConnect) {
      self.updateNetworkState('disconnected');
      self.socketTask.close();
      return;
    }
    self.updateNetworkState('connected');
    if (self.options.onOpen) {
      self.options.onOpen(res);
    }
  });

  this.socketTask.onMessage(function (res) {
    if (self.options.onMessage) {
      self.options.onMessage(res);
    }
  });

  this.socketTask.onClose(function (res) {
	  console.log('WebSocket closed, code:', res);
    self.updateNetworkState('disconnected');
    if (!self.doNotConnect) {
      self.waitReconnect();
    }
    if (self.options.onClose) {
      self.options.onClose(res);
    }
  });

  this.socketTask.onError(function (res) {
	  console.log('WebSocket error, code:', res);
    self.updateNetworkState('disconnected');
    if (!self.doNotConnect) {
      self.waitReconnect();
    }
    if (self.options.onError) {
      self.options.onError(res);
    }
  });

  this.updateNetworkState('connecting');
};

Connection.prototype.closeAndClean = function () {
  if (this.socketTask) {
    try {
      this.socketTask.close();
    } catch (e) {
      console.warn('closeAndClean error:', e);
    }
    this.socketTask = null;
  }
  this.updateNetworkState('disconnected');
};

Connection.prototype.waitReconnect = function () {
  if (this.state === 'connected' || this.state === 'connecting') {
    return;
  }
  if (!this.doNotConnect) {
    this.updateNetworkState('connecting');
    var self = this;
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
    }
    this.reconnectTimer = setTimeout(function () {
      self.connect();
    }, this.reconnectInterval);
    if (this.reconnectInterval < 1000) {
      this.reconnectInterval = 1000;
    } else {
      this.reconnectInterval = Math.min(this.reconnectInterval * 2, 2000);
    }
  }
};

Connection.prototype.send = function (data) {
  if (this.state !== 'connected') {
    console.warn('state is "' + this.state + '", cannot send:', data);
    return;
  }
  this.socketTask.send({
    data: data,
    fail: function (err) {
      console.error('send fail:', err);
    }
  });
};

Connection.prototype.close = function () {
  this.doNotConnect = 1;
  this.updateNetworkState('disconnected');
  if (this.socketTask) {
    this.socketTask.close();
  }
};

// ==================== Push 客户端 ====================
function Push(options) {
  this.doNotConnect = 0;
  options = options || {};
  options.heartbeat = options.heartbeat || 25000;
  options.pingTimeout = options.pingTimeout || 10000;
  this.config = options;
  this.uid = 0;
  this.channels = {};
  this.connection = null;
  this.pingTimeoutTimer = 0;
  Push.instances.push(this);
  this.createConnection();
}

Push.instances = [];

Push.prototype.checkoutPing = function () {
  var self = this;
  setTimeout(function () {
    if (self.connection.state === 'connected') {
      self.connection.send('hi');
      if (self.pingTimeoutTimer) {
        clearTimeout(self.pingTimeoutTimer);
        self.pingTimeoutTimer = 0;
      }
      self.pingTimeoutTimer = setTimeout(function () {
        self.connection.closeAndClean();
        if (!self.connection.doNotConnect) {
          self.connection.waitReconnect();
        }
      }, self.config.pingTimeout);
    }
  }, this.config.heartbeat);
};

Push.prototype.channel = function (name) {
  return this.channels[name];
};

Push.prototype.allChannels = function () {
  return this.channels;
};

Push.prototype.createConnection = function () {
  if (this.connection) {
    throw new Error('Connection already exists');
  }

  var self = this;
  var url = this.config.url;
  var app_key = this.config.app_key;

  function updateSubscribed() {
    for (var i in self.channels) {
      if (self.channels[i]) {
        self.channels[i].subscribed = false;
      }
    }
  }

  this.connection = new Connection({
    url: url,
    app_key: app_key,
    onOpen: function () {
      self.connection.state = 'connected';
      self.checkoutPing();
    },
    onMessage: function (res) {
      if (self.pingTimeoutTimer) {
        clearTimeout(self.pingTimeoutTimer);
        self.pingTimeoutTimer = 0;
      }

      var params;
      try {
        params = JSON.parse(res.data);
      } catch (e) {
        console.warn('parse message error:', e, res.data);
        return;
      }

      var event = params.event;
      var channel_name = params.channel;

      if (event === 'pusher:pong') {
        self.checkoutPing();
        return;
      }
      if (event === 'pusher:error') {
        throw new Error(params.data && params.data.message);
      }

      var data = params.data;
      var channel;

      if (event === 'pusher_internal:subscription_succeeded') {
        channel = self.channels[channel_name];
        if (channel) {
          channel.subscribed = true;
          channel.processQueue();
          channel.emit('pusher:subscription_succeeded');
        }
        return;
      }

      if (event === 'pusher:connection_established') {
        self.connection.socket_id = data.socket_id;
        self.connection.state = 'connected';
        self.subscribeAll();
        return;
      }

      if (event && event.indexOf('pusher_internal') !== -1) {
        console.log('Event "' + event + '" not implemented');
        return;
      }

      channel = self.channels[channel_name];
      if (channel) {
        if (typeof data === 'string') {
          try {
            data = JSON.parse(data);
          } catch (e) {}
        }
        channel.emit(event, data);
      }
    },
    onClose: function () {
      updateSubscribed();
    },
    onError: function () {
      updateSubscribed();
    }
  });
};

Push.prototype.disconnect = function () {
  this.connection.doNotConnect = 1;
  this.connection.close();
};

Push.prototype.subscribeAll = function () {
  if (this.connection.state !== 'connected') {
    return;
  }
  for (var channel_name in this.channels) {
    if (this.channels[channel_name]) {
      this.channels[channel_name].processSubscribe();
    }
  }
};

Push.prototype.unsubscribe = function (channel_name) {
  if (this.channels[channel_name]) {
    delete this.channels[channel_name];
    if (this.connection.state === 'connected') {
      this.connection.send(JSON.stringify({ event: 'pusher:unsubscribe', data: { channel: channel_name } }));
    }
  }
};

Push.prototype.unsubscribeAll = function () {
  for (var channel_name in this.channels) {
    this.unsubscribe(channel_name);
  }
  this.channels = {};
};

Push.prototype.subscribe = function (channel_name) {
  if (this.channels[channel_name]) {
    return this.channels[channel_name];
  }
  if (channel_name.indexOf('private-') === 0) {
    return createPrivateChannel(channel_name, this);
  }
  if (channel_name.indexOf('presence-') === 0) {
    return createPresenceChannel(channel_name, this);
  }
  return createChannel(channel_name, this);
};

// ==================== 频道创建辅助函数 ====================
function createChannel(channel_name, push) {
  var channel = new Channel(push.connection, channel_name);
  push.channels[channel_name] = channel;
  channel.subscribeCb = function () {
    push.connection.send(JSON.stringify({ event: 'pusher:subscribe', data: { channel: channel_name } }));
  };
  channel.processSubscribe();
  return channel;
}

function createPrivateChannel(channel_name, push) {
  var channel = new Channel(push.connection, channel_name);
  push.channels[channel_name] = channel;
  channel.subscribeCb = function () {
    tt.request({
      url: push.config.auth,
      method: 'POST',
      data: { channel_name: channel_name, socket_id: push.connection.socket_id },
      success: function (res) {
        var data;
        try {
          data = res.data;
        } catch (e) {
          console.error('parse auth response error:', e);
          return;
        }
        data.channel = channel_name;
        push.connection.send(JSON.stringify({ event: 'pusher:subscribe', data: data }));
      },
      fail: function (err) {
        console.error('auth request fail:', err);
        throw new Error(err.errMsg);
      }
    });
  };
  channel.processSubscribe();
  return channel;
}

function createPresenceChannel(channel_name, push) {
  return createPrivateChannel(channel_name, push);
}

// ==================== 导出 ====================
module.exports = {
  Push: Push
};