'use strict';

const { hasWalleActivation } = require('./activation');

const DEFAULT_RECONNECT_MS = 5000;
const MAX_RECONNECT_MS = 60000;

function getAppToken(env = process.env) {
  return env.WALLE_SLACK_APP_TOKEN || env.SLACK_APP_TOKEN || env.SLACK_SOCKET_MODE_APP_TOKEN || '';
}

function socketModeDisabled(env = process.env) {
  const value = String(env.WALLE_SLACK_SOCKET_MODE || env.SLACK_SOCKET_MODE || '').toLowerCase();
  return value === '0' || value === 'false' || value === 'off';
}

function isConfigured(env = process.env) {
  const token = getAppToken(env);
  return !socketModeDisabled(env) && /^xapp-/.test(token);
}

function resolveWebSocketCtor() {
  if (typeof WebSocket === 'function') return WebSocket;
  try { return require('ws'); } catch {}
  return null;
}

function extractSlackEvent(envelope) {
  if (!envelope || envelope.type !== 'events_api') return null;
  const payload = envelope.payload || {};
  if (payload.type !== 'event_callback') return null;
  return payload.event || null;
}

function isRealtimeMessageEvent(event) {
  if (!event || !event.type) return false;
  if (event.bot_id || event.subtype) return false;
  if (event.type === 'app_mention') return true;
  if (event.type !== 'message') return false;
  return event.channel_type === 'im' || event.channel_type === 'mpim' || hasWalleActivation(event.text);
}

class SlackSocketModeListener {
  constructor(opts = {}) {
    this.env = opts.env || process.env;
    this.fetchImpl = opts.fetch || global.fetch;
    this.WebSocketCtor = opts.WebSocketCtor || resolveWebSocketCtor();
    this.handleEvent = opts.handleEvent || defaultHandleEvent;
    this.logger = opts.logger || console;
    this.reconnectBaseMs = opts.reconnectBaseMs || DEFAULT_RECONNECT_MS;
    this.maxReconnectMs = opts.maxReconnectMs || MAX_RECONNECT_MS;
    this.started = false;
    this.running = false;
    this.ws = null;
    this.reconnectTimer = null;
    this.reconnectAttempts = 0;
    this.lastConnectedAt = null;
    this.lastEventAt = null;
    this.lastError = null;
    this._seenEventIds = new Map();
  }

  get configured() {
    return isConfigured(this.env);
  }

  getStatus() {
    return {
      configured: this.configured,
      enabled: !socketModeDisabled(this.env),
      running: this.running,
      started: this.started,
      last_connected_at: this.lastConnectedAt,
      last_event_at: this.lastEventAt,
      last_error: this.lastError,
      reconnect_attempts: this.reconnectAttempts,
    };
  }

  start() {
    if (this.started) return this.getStatus();
    this.started = true;
    if (!this.configured) {
      this.logger.log?.('[slack-socket] Not configured; set WALLE_SLACK_APP_TOKEN or SLACK_APP_TOKEN to enable Socket Mode');
      return this.getStatus();
    }
    if (!this.WebSocketCtor) {
      this.lastError = 'No WebSocket implementation available';
      this.logger.warn?.('[slack-socket] No WebSocket implementation available');
      return this.getStatus();
    }
    this._connect().catch((err) => {
      this._recordError(err);
      this._scheduleReconnect();
    });
    return this.getStatus();
  }

  stop() {
    this.started = false;
    this.running = false;
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    if (this.ws) {
      try { this.ws.close(); } catch {}
      this.ws = null;
    }
  }

  async _connect() {
    if (!this.started || !this.configured) return;
    const appToken = getAppToken(this.env);
    if (!this.fetchImpl) throw new Error('fetch is not available');

    const resp = await this.fetchImpl('https://slack.com/api/apps.connections.open', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${appToken}`,
        'Content-Type': 'application/x-www-form-urlencoded',
      },
    });
    const data = await resp.json();
    if (!data.ok || !data.url) {
      throw new Error(`apps.connections.open failed: ${data.error || resp.status}`);
    }

    const ws = new this.WebSocketCtor(data.url);
    this.ws = ws;
    this._bindSocket(ws);
  }

  _bindSocket(ws) {
    const onOpen = () => {
      this.running = true;
      this.reconnectAttempts = 0;
      this.lastConnectedAt = new Date().toISOString();
      this.lastError = null;
      this.logger.log?.('[slack-socket] Connected');
    };
    const onMessage = (eventOrData) => {
      const data = eventOrData && Object.prototype.hasOwnProperty.call(eventOrData, 'data')
        ? eventOrData.data
        : eventOrData;
      this._handleMessage(data).catch((err) => this._recordError(err));
    };
    const onError = (err) => this._recordError(err);
    const onClose = () => {
      this.running = false;
      if (this.ws === ws) this.ws = null;
      if (this.started) this._scheduleReconnect();
    };

    if (typeof ws.on === 'function') {
      ws.on('open', onOpen);
      ws.on('message', onMessage);
      ws.on('error', onError);
      ws.on('close', onClose);
    } else {
      ws.onopen = onOpen;
      ws.onmessage = onMessage;
      ws.onerror = onError;
      ws.onclose = onClose;
    }
  }

  async _handleMessage(data) {
    const text = Buffer.isBuffer(data)
      ? data.toString('utf8')
      : (data instanceof ArrayBuffer)
        ? Buffer.from(data).toString('utf8')
        : ArrayBuffer.isView(data)
          ? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString('utf8')
          : String(data || '');
    if (!text) return;
    let envelope;
    try {
      envelope = JSON.parse(text);
    } catch {
      return;
    }

    if (envelope.envelope_id) this._ack(envelope.envelope_id);
    if (envelope.type === 'hello') return;
    if (envelope.type === 'disconnect') {
      this.lastError = envelope.reason || 'Slack requested disconnect';
      this._scheduleReconnect();
      return;
    }

    const event = extractSlackEvent(envelope);
    if (!isRealtimeMessageEvent(event)) return;
    if (this._seenRecently(envelope.payload?.event_id || event.event_ts || event.ts)) return;

    this.lastEventAt = new Date().toISOString();
    await this.handleEvent(event, { envelope });
  }

  _ack(envelopeId) {
    if (!this.ws || typeof this.ws.send !== 'function') return;
    try {
      this.ws.send(JSON.stringify({ envelope_id: envelopeId }));
    } catch (err) {
      this._recordError(err);
    }
  }

  _seenRecently(id) {
    if (!id) return false;
    const now = Date.now();
    for (const [key, ts] of this._seenEventIds) {
      if (now - ts > 10 * 60_000) this._seenEventIds.delete(key);
    }
    if (this._seenEventIds.has(id)) return true;
    this._seenEventIds.set(id, now);
    return false;
  }

  _recordError(err) {
    const msg = err && err.message ? err.message : String(err || 'unknown error');
    this.lastError = msg.slice(0, 300);
    this.logger.warn?.(`[slack-socket] ${this.lastError}`);
  }

  _scheduleReconnect() {
    if (!this.started || this.reconnectTimer) return;
    this.running = false;
    this.reconnectAttempts += 1;
    const delay = Math.min(this.reconnectBaseMs * Math.pow(2, Math.max(0, this.reconnectAttempts - 1)), this.maxReconnectMs);
    this.reconnectTimer = setTimeout(() => {
      this.reconnectTimer = null;
      this._connect().catch((err) => {
        this._recordError(err);
        this._scheduleReconnect();
      });
    }, delay);
    if (typeof this.reconnectTimer.unref === 'function') this.reconnectTimer.unref();
  }
}

async function defaultHandleEvent(event) {
  const slackMentions = require('../skills/_bundled/slack-mentions/run');
  return slackMentions.processIncomingSlackEvent(event);
}

let singleton = null;

function startSlackSocketModeListener(opts = {}) {
  if (singleton) return singleton;
  singleton = new SlackSocketModeListener(opts);
  singleton.start();
  return singleton;
}

function getSlackSocketModeStatus() {
  if (singleton) return singleton.getStatus();
  return {
    configured: isConfigured(process.env),
    enabled: !socketModeDisabled(process.env),
    running: false,
    started: false,
    last_connected_at: null,
    last_event_at: null,
    last_error: null,
    reconnect_attempts: 0,
  };
}

module.exports = {
  SlackSocketModeListener,
  startSlackSocketModeListener,
  getSlackSocketModeStatus,
  getAppToken,
  isConfigured,
  extractSlackEvent,
  isRealtimeMessageEvent,
};
