import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadConfig } from "./config.ts";
import { PanelClient } from "./api/PanelClient.ts";
import { SessionVault } from "./session/SessionVault.ts";
import { OfficialKickApiAdapter } from "./kick/adapters/OfficialKickApiAdapter.ts";
import { parseInboundPayload, toChatMessage } from "./kick/KickWebhookEvent.ts";
import { OutboundQueue } from "./queue/OutboundQueue.ts";
import { LegacyGameCoordinator } from "./games/LegacyGameCoordinator.ts";
import type { ChannelStatus, ChatMessage, EngineEvent, InboundKickEvent } from "./types.ts";

const config = loadConfig();
const panel = new PanelClient(config);
const vault = new SessionVault(config.vaultPath, config.oauthMasterKey);
const adapter = new OfficialKickApiAdapter(config, vault);
const channels = new Map<number, ChannelStatus>();
let stopping = false;
let adapterReady = false;
let botKickUserId = config.botKickUserId;
let channelsSnapshot = 0;
let eventBuffer: EngineEvent[] = [];
let eventFlush: NodeJS.Timeout | null = null;

const flushEvents = async (strict = false): Promise<void> => {
  if (!eventBuffer.length) return;
  const batch = eventBuffer.splice(0, 100);
  try {
    await panel.events(batch);
  } catch (error) {
    eventBuffer.unshift(...batch);
    if (eventBuffer.length > 2_000) eventBuffer = eventBuffer.slice(-2_000);
    if (strict) throw error;
  }
};

const enqueueEvent = (event: EngineEvent): void => {
  eventBuffer.push(event);
  if (eventFlush) return;
  eventFlush = setTimeout(() => {
    eventFlush = null;
    void flushEvents();
  }, 200);
  eventFlush.unref();
};

const games = new LegacyGameCoordinator(config, enqueueEvent);

const handleMessage = (message: ChatMessage): void => {
  if (
    (botKickUserId && message.sender.kick_user_id === botKickUserId)
    || message.sender.username.toLocaleLowerCase("fr") === config.botUsername.toLocaleLowerCase("fr")
  ) return;
  enqueueEvent({
    id: message.event_id,
    channel_id: message.channel_id,
    type: "chat.message",
    payload: {
      content: message.content,
      sender: message.sender,
      kick_channel_id: message.kick_channel_id,
      kick_chatroom_id: message.kick_chatroom_id,
      created_at: message.created_at
    }
  });
  games.handleMessage(message);
};

const queue = new OutboundQueue(panel, adapter, (channelId) => channels.get(channelId));

const initializeSession = async (): Promise<void> => {
  try {
    await adapter.initialize();
    const session = await adapter.testSession();
    adapterReady = session.valid;
    if (session.valid && session.kickUserId) botKickUserId = session.kickUserId;
    if (!adapterReady) queue.openCircuit(120_000);
    await panel.sessionStatus(session.valid ? "valid" : "invalid", {
      kick_user_id: session.kickUserId,
      expires_at: session.expiresAt,
      error: session.error ?? ""
    });
  } catch (error) {
    adapterReady = false;
    queue.openCircuit(120_000);
    const detail = error instanceof Error ? error.message : "Autorisation OAuth absente";
    await panel.sessionStatus("invalid", { error: detail }).catch(() => undefined);
  }
};

const syncChannels = async (signal: AbortSignal): Promise<void> => {
  const nextChannels = await panel.channels(signal);
  channels.clear();
  for (const channel of nextChannels) channels.set(channel.channel_id, channel);
  channelsSnapshot = nextChannels.length;
  if (adapterReady) await adapter.ensureEventSubscriptions(nextChannels);
  for (const channel of nextChannels) {
    const runtimeConfig = await panel.channelConfig(channel.channel_id, signal).catch(() => null);
    await games.sync(channel, (runtimeConfig?.active_game as Parameters<typeof games.sync>[1]) ?? null);
    if (channel.moderator_status === "checking" && adapterReady) {
      const status = await adapter.checkModeratorRole(channel);
      await panel.moderatorStatus(channel.channel_id, status, botKickUserId);
    }
  }
};

const processInboundEvent = async (event: InboundKickEvent): Promise<void> => {
  const channel = channels.get(event.channel_id);
  if (!channel) throw new Error(`Chaîne ${event.channel_id} non chargée.`);
  const payload = parseInboundPayload(event);
  const chatMessage = toChatMessage(event, channel, payload);
  if (chatMessage) {
    handleMessage(chatMessage);
    return;
  }
  enqueueEvent({
    id: event.kick_message_id,
    channel_id: event.channel_id,
    type: event.event_type,
    payload
  });
};

const pollInboundEvents = async (signal: AbortSignal): Promise<void> => {
  const events = await panel.inboundEvents(signal);
  for (const event of events) {
    try {
      await processInboundEvent(event);
      await flushEvents(true);
      await panel.acknowledgeInboundEvent(event.id, "processed");
    } catch (error) {
      const detail = error instanceof Error ? error.message : "Webhook Kick invalide";
      await panel.acknowledgeInboundEvent(event.id, event.attempts >= 5 ? "failed" : "retry", detail);
    }
  }
};

const processWorkerCommands = async (signal: AbortSignal): Promise<void> => {
  const commands = await panel.workerCommands(signal);
  for (const command of commands) {
    let status: "completed" | "failed" = "completed";
    let result = "";
    try {
      if (command.command_type === "test-session") {
        const session = await adapter.testSession();
        result = session.valid ? "Autorisation OAuth FiIyx valide." : (session.error ?? "Autorisation invalide.");
        if (!session.valid) status = "failed";
      } else if (command.command_type === "revoke-session") {
        await adapter.close();
        await vault.revoke();
        adapterReady = false;
        queue.openCircuit(24 * 60 * 60 * 1_000);
        result = "Autorisation OAuth chiffrée révoquée localement.";
      } else if (command.command_type === "open-session-manager") {
        if (process.env.FILYX_ALLOW_INTERACTIVE_SESSION_MANAGER !== "true") {
          throw new Error("L’assistant OAuth doit être lancé manuellement sur la machine du moteur.");
        }
        const here = path.dirname(fileURLToPath(import.meta.url));
        const child = spawn(process.execPath, ["--experimental-strip-types", path.join(here, "session", "session-manager.ts")], {
          detached: true,
          stdio: "ignore",
          windowsHide: false,
          env: process.env
        });
        child.unref();
        result = "Assistant OAuth lancé.";
      } else if (command.command_type === "restart-engine") {
        result = "Redémarrage demandé.";
        await panel.completeWorkerCommand(command.id, status, result);
        setTimeout(() => process.exit(0), 200).unref();
        continue;
      } else {
        throw new Error("Commande worker non prise en charge.");
      }
    } catch (error) {
      status = "failed";
      result = error instanceof Error ? error.message : "Erreur inconnue";
    }
    await panel.completeWorkerCommand(command.id, status, result);
  }
};

const loop = async (): Promise<void> => {
  const controller = new AbortController();
  const heartbeat = setInterval(() => {
    void panel.heartbeat({
      active_channels: channelsSnapshot,
      websocket_connections: 0,
      queue_depth: eventBuffer.length,
      metadata: {
        adapter_ready: adapterReady,
        queue_circuit_open: queue.circuitOpen,
        kick_transport: "official-api-webhooks"
      }
    }).catch(() => undefined);
  }, config.heartbeatIntervalMs);
  heartbeat.unref();

  let lastChannelSync = 0;
  let lastCommandPoll = 0;
  while (!stopping) {
    const started = Date.now();
    try {
      if (started - lastChannelSync > 15_000) {
        await syncChannels(controller.signal);
        lastChannelSync = started;
      }
      await pollInboundEvents(controller.signal);
      if (adapterReady) await queue.poll(controller.signal);
      if (started - lastCommandPoll > 4_000) {
        await processWorkerCommands(controller.signal);
        lastCommandPoll = started;
      }
      await flushEvents();
    } catch (error) {
      const message = error instanceof Error ? error.message : "Erreur moteur";
      process.stderr.write(`[FilyX] ${message}\n`);
    }
    const elapsed = Date.now() - started;
    await new Promise((resolve) => setTimeout(resolve, Math.max(100, config.pollIntervalMs - elapsed)));
  }
  controller.abort();
  clearInterval(heartbeat);
};

const shutdown = async (): Promise<void> => {
  if (stopping) return;
  stopping = true;
  if (eventFlush) clearTimeout(eventFlush);
  await flushEvents().catch(() => undefined);
  channels.clear();
  games.stop();
  await adapter.close();
};

process.once("SIGINT", () => void shutdown());
process.once("SIGTERM", () => void shutdown());

await initializeSession();
await loop();
