const WebSocket = require("ws");
const { getSetting, setSetting } = require("./settings");
const { emitLumiEvent } = require("./lumi-events");
const { createLogger } = require("./logger");

const EVENTSUB_URL = "wss://eventsub.wss.twitch.tv/ws?keepalive_timeout_seconds=30";
const eventLog = createLogger("platform:twitch:eventsub", { category: "integration" });

function configuredChannels() {
  const channels = String(getSetting("twitch_channels", ""))
    .split(/[\s,]+/)
    .map((value) => value.replace(/^#/, "").trim().toLowerCase())
    .filter(Boolean);
  if (!channels.length) {
    const authorizedLogin = String(getSetting("twitch_event_login", "")).trim().toLowerCase();
    if (authorizedLogin) channels.push(authorizedLogin);
  }
  return Array.from(new Set(channels));
}

async function twitchRequest(url, options = {}) {
  const response = await fetch(url, options);
  if (!response.ok) {
    const body = await response.text().catch(() => "");
    throw new Error(`Twitch request failed (${response.status})${body ? `: ${body.slice(0, 300)}` : ""}`);
  }
  return response.json();
}

async function refreshAccessToken() {
  const refreshToken = getSetting("twitch_event_refresh_token", "");
  const clientId = getSetting("twitch_client_id", "");
  const clientSecret = getSetting("twitch_client_secret", "");
  if (!refreshToken || !clientId || !clientSecret) return null;
  const params = new URLSearchParams({
    grant_type: "refresh_token",
    refresh_token: refreshToken,
    client_id: clientId,
    client_secret: clientSecret
  });
  const token = await twitchRequest(`https://id.twitch.tv/oauth2/token?${params.toString()}`, { method: "POST" });
  setSetting("twitch_event_oauth", token.access_token);
  if (token.refresh_token) setSetting("twitch_event_refresh_token", token.refresh_token);
  setSetting("twitch_event_expires_at", Date.now() + Number(token.expires_in || 0) * 1000);
  return token.access_token;
}

async function validCredentials() {
  let accessToken = getSetting("twitch_event_oauth", "");
  if (!accessToken) return null;
  let validation;
  try {
    validation = await twitchRequest("https://id.twitch.tv/oauth2/validate", {
      headers: { Authorization: `OAuth ${accessToken}` }
    });
  } catch {
    accessToken = await refreshAccessToken();
    if (!accessToken) return null;
    validation = await twitchRequest("https://id.twitch.tv/oauth2/validate", {
      headers: { Authorization: `OAuth ${accessToken}` }
    });
  }
  return { accessToken, validation, clientId: getSetting("twitch_client_id", "") };
}

class TwitchEventSubManager {
  constructor() {
    this.socket = null;
    this.running = false;
    this.reconnectTimer = null;
    this.reconnectAttempt = 0;
    this.generation = 0;
    this.status = { state: "disconnected", detail: "Not connected", subscriptions: 0 };
  }

  getStatus() {
    return { ...this.status };
  }

  async start() {
    if (this.running) return this.getStatus();
    this.running = true;
    if (!getSetting("twitch_event_oauth", "")) {
      this.status = { state: "needs_authorization", detail: "Connect Twitch events to receive alerts.", subscriptions: 0 };
      return this.getStatus();
    }
    await this.connect(EVENTSUB_URL, false);
    return this.getStatus();
  }

  async restart() {
    await this.stop();
    return this.start();
  }

  async stop() {
    this.running = false;
    this.generation += 1;
    clearTimeout(this.reconnectTimer);
    this.reconnectTimer = null;
    const socket = this.socket;
    this.socket = null;
    if (socket && socket.readyState < WebSocket.CLOSING) socket.close(1000, "Lumi shutdown");
    this.status = { state: "disconnected", detail: "Not connected", subscriptions: 0 };
  }

  async connect(url, preservingSubscriptions) {
    if (!this.running) return;
    const credentials = await validCredentials().catch((error) => {
      eventLog.error("Twitch EventSub credentials could not be validated", error, { event: "eventsub_auth_failed" });
      return null;
    });
    if (!credentials?.accessToken || !credentials.clientId) {
      this.status = { state: "needs_authorization", detail: "Twitch event authorization is missing or expired.", subscriptions: 0 };
      return;
    }
    const generation = ++this.generation;
    this.status = { state: "connecting", detail: "Connecting to Twitch events…", subscriptions: 0 };
    const socket = new WebSocket(url);
    this.socket = socket;
    socket.on("message", (raw) => {
      this.handleMessage(raw, credentials, preservingSubscriptions, generation).catch((error) => {
        eventLog.error("Twitch EventSub message could not be handled", error, { event: "eventsub_message_failed" });
      });
    });
    socket.on("error", (error) => eventLog.error("Twitch EventSub socket error", error, { event: "eventsub_socket_error" }));
    socket.on("close", () => {
      if (!this.running || generation !== this.generation) return;
      this.status = { state: "reconnecting", detail: "Twitch events disconnected; retrying automatically.", subscriptions: 0 };
      this.scheduleReconnect();
    });
  }

  scheduleReconnect() {
    clearTimeout(this.reconnectTimer);
    const delay = Math.min(60000, 1000 * 2 ** Math.min(this.reconnectAttempt++, 6));
    this.reconnectTimer = setTimeout(() => {
      this.connect(EVENTSUB_URL, false).catch((error) => {
        eventLog.error("Twitch EventSub reconnect failed", error, { event: "eventsub_reconnect_failed" });
        if (this.running) this.scheduleReconnect();
      });
    }, delay);
  }

  async handleMessage(raw, credentials, preservingSubscriptions, generation) {
    let message;
    try {
      message = JSON.parse(raw.toString());
    } catch {
      return;
    }
    const type = message.metadata?.message_type;
    if (type === "session_welcome") {
      this.reconnectAttempt = 0;
      this.status = { state: "connected", detail: "Receiving Twitch events.", subscriptions: preservingSubscriptions ? this.status.subscriptions : 0 };
      if (!preservingSubscriptions) {
        await this.subscribe(message.payload.session.id, credentials, generation);
      }
      return;
    }
    if (type === "session_reconnect") {
      const reconnectUrl = message.payload?.session?.reconnect_url;
      if (reconnectUrl) {
        this.connect(reconnectUrl, true).catch((error) => {
          eventLog.error("Twitch requested reconnect failed", error, { event: "eventsub_reconnect_failed" });
          if (this.running) this.scheduleReconnect();
        });
      }
      return;
    }
    if (type === "notification") this.emitNotification(message);
    if (type === "revocation") {
      this.status = { ...this.status, detail: "A Twitch event permission was revoked. Reconnect Twitch events if alerts stop." };
      eventLog.warn("Twitch revoked an EventSub subscription", message.payload?.subscription || {}, { event: "eventsub_revoked" });
    }
  }

  async subscribe(sessionId, credentials, generation) {
    const headers = {
      "Client-Id": credentials.clientId,
      Authorization: `Bearer ${credentials.accessToken}`,
      "Content-Type": "application/json"
    };
    const scopes = new Set(credentials.validation.scopes || []);
    const query = configuredChannels();
    const users = query.length
      ? await twitchRequest(`https://api.twitch.tv/helix/users?${query.map((login) => `login=${encodeURIComponent(login)}`).join("&")}`, { headers })
      : { data: [] };
    const definitions = [];
    for (const broadcaster of users.data || []) {
      definitions.push({ type: "channel.raid", version: "1", condition: { to_broadcaster_user_id: broadcaster.id } });
      if (scopes.has("moderator:read:followers")) {
        definitions.push({ type: "channel.follow", version: "2", condition: { broadcaster_user_id: broadcaster.id, moderator_user_id: credentials.validation.user_id } });
      }
      if (scopes.has("channel:read:subscriptions") && broadcaster.id === credentials.validation.user_id) {
        definitions.push({ type: "channel.subscribe", version: "1", condition: { broadcaster_user_id: broadcaster.id } });
        definitions.push({ type: "channel.subscription.gift", version: "1", condition: { broadcaster_user_id: broadcaster.id } });
      }
    }
    let active = 0;
    for (const definition of definitions) {
      try {
        await twitchRequest("https://api.twitch.tv/helix/eventsub/subscriptions", {
          method: "POST",
          headers,
          body: JSON.stringify({ ...definition, transport: { method: "websocket", session_id: sessionId } })
        });
        active += 1;
      } catch (error) {
        eventLog.error(`Twitch EventSub subscription failed for ${definition.type}`, error, { event: "eventsub_subscribe_failed" });
      }
    }
    if (generation !== this.generation) return;
    this.status = {
      state: "connected",
      detail: active ? `Receiving ${active} Twitch event feed${active === 1 ? "" : "s"}.` : "Connected, but no configured channel events could be subscribed.",
      subscriptions: active
    };
    eventLog.info("Twitch EventSub connected", { channels: users.data?.length || 0, subscriptions: active }, { event: "eventsub_ready" });
  }

  emitNotification(message) {
    const subscription = message.payload?.subscription || {};
    const event = message.payload?.event || {};
    const common = {
      broadcaster_id: event.broadcaster_user_id || event.to_broadcaster_user_id || null,
      broadcaster_name: event.broadcaster_user_name || event.to_broadcaster_user_name || null,
      user_id: event.user_id || event.from_broadcaster_user_id || null,
      user_name: event.user_name || event.from_broadcaster_user_name || null
    };
    const metadata = { id: message.metadata?.message_id, source: "twitch-eventsub", occurredAt: message.metadata?.message_timestamp };
    if (subscription.type === "channel.follow") emitLumiEvent("twitch.follow", { ...common, followed_at: event.followed_at }, metadata);
    if (subscription.type === "channel.raid") emitLumiEvent("twitch.raid", { ...common, viewers: Number(event.viewers || 0) }, metadata);
    if (subscription.type === "channel.subscribe") emitLumiEvent("twitch.subscribe", { ...common, tier: event.tier, gifted: Boolean(event.is_gift) }, metadata);
    if (subscription.type === "channel.subscription.gift") emitLumiEvent("twitch.subscription_gift", { ...common, tier: event.tier, total: Number(event.total || 0), anonymous: Boolean(event.is_anonymous) }, metadata);
  }
}

const twitchEventSubManager = new TwitchEventSubManager();

module.exports = { TwitchEventSubManager, twitchEventSubManager, configuredChannels };
