// Discord provider module implements model/runtime integration.
import { createTransportActivityStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
import { asDateTimestampMs, parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
import { danger, sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
import { attachDiscordGatewayLogging } from "../gateway-logging.js";
import { isFatalGatewayCloseCode } from "../internal/gateway-close-codes.js";
import { GatewayCloseCodes } from "../internal/gateway.js";
import { getDiscordGatewayEmitter, waitForDiscordGatewayStop } from "../monitor.gateway.js";
import type { DiscordVoiceManager } from "../voice/voice-runtime.js";
import {
  DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT,
  type MutableDiscordGateway,
} from "./gateway-handle.js";
import { registerGateway, unregisterGateway } from "./gateway-registry.js";
import {
  DiscordGatewayLifecycleError,
  type DiscordGatewayEvent,
  type DiscordGatewaySupervisor,
} from "./gateway-supervisor.js";
import { createDiscordReadyStatusPatch, type DiscordMonitorStatusSink } from "./status.js";

const DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS = 15_000;
const DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS = 30_000;
const MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS = 120_000;
const DISCORD_GATEWAY_READY_TIMEOUT_ENV = "OPENCLAW_DISCORD_READY_TIMEOUT_MS";
const DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV = "OPENCLAW_DISCORD_RUNTIME_READY_TIMEOUT_MS";
const DISCORD_GATEWAY_READY_POLL_MS = 250;
const DISCORD_GATEWAY_READY_RETRY_BACKOFF_MS = 2_000;
const DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS = 5_000;
const DISCORD_GATEWAY_STARTUP_TERMINATE_CLOSE_TIMEOUT_MS = 1_000;
const DISCORD_GATEWAY_TRANSPORT_ACTIVITY_STATUS_MIN_INTERVAL_MS = 30_000;

type GatewayReadyWaitResult = "ready" | "stopped" | "timeout";

function normalizeGatewayReadyTimeoutMs(value: unknown): number | undefined {
  const numeric = parseStrictPositiveInteger(value);
  if (numeric === undefined) {
    return undefined;
  }
  return Math.min(numeric, MAX_DISCORD_GATEWAY_READY_TIMEOUT_MS);
}

function resolveDiscordGatewayReadyTimeoutMs(params?: { env?: NodeJS.ProcessEnv }): number {
  return (
    normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_READY_TIMEOUT_ENV]) ??
    DEFAULT_DISCORD_GATEWAY_READY_TIMEOUT_MS
  );
}

function resolveDiscordGatewayRuntimeReadyTimeoutMs(params?: { env?: NodeJS.ProcessEnv }): number {
  return (
    normalizeGatewayReadyTimeoutMs(params?.env?.[DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_ENV]) ??
    DEFAULT_DISCORD_GATEWAY_RUNTIME_READY_TIMEOUT_MS
  );
}

async function restartGatewayAfterReadyTimeout(params: {
  gateway?: Pick<MutableDiscordGateway, "connect" | "disconnect" | "ws">;
  abortSignal?: AbortSignal;
  runtime: RuntimeEnv;
}): Promise<void> {
  if (!params.gateway || params.abortSignal?.aborted) {
    return;
  }

  const socket = params.gateway.ws;
  if (!socket) {
    params.gateway.disconnect();
    if (!params.abortSignal?.aborted) {
      params.gateway.connect(false);
    }
    return;
  }

  await new Promise<void>((resolve, reject) => {
    let settled = false;
    let drainTimeout: ReturnType<typeof setTimeout> | undefined;
    let terminateCloseTimeout: ReturnType<typeof setTimeout> | undefined;
    const ignoreSocketError = () => {};
    const clearTimers = () => {
      if (drainTimeout) {
        clearTimeout(drainTimeout);
        drainTimeout = undefined;
      }
      if (terminateCloseTimeout) {
        clearTimeout(terminateCloseTimeout);
        terminateCloseTimeout = undefined;
      }
    };
    const cleanup = () => {
      clearTimers();
      socket.removeListener("close", onClose);
      socket.removeListener("error", ignoreSocketError);
    };
    const finishResolve = () => {
      if (settled) {
        return;
      }
      settled = true;
      cleanup();
      resolve();
    };
    const finishReject = (error: Error) => {
      if (params.abortSignal?.aborted) {
        finishResolve();
        return;
      }
      if (settled) {
        return;
      }
      settled = true;
      cleanup();
      reject(error);
    };
    const onClose = () => {
      finishResolve();
    };

    socket.on("error", ignoreSocketError);
    socket.on("close", onClose);
    params.gateway?.disconnect();

    drainTimeout = setTimeout(() => {
      if (settled) {
        return;
      }
      if (typeof socket.terminate !== "function") {
        finishReject(
          new Error(
            `discord gateway socket did not close within ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms before restart`,
          ),
        );
        return;
      }
      params.runtime.error?.(
        danger(
          `discord: startup restart waiting on a stale gateway socket for ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms; forcing terminate before reconnect`,
        ),
      );
      try {
        socket.terminate();
      } catch {
        finishReject(
          new Error(
            `discord gateway socket did not close within ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms before restart`,
          ),
        );
        return;
      }
      terminateCloseTimeout = setTimeout(() => {
        finishReject(
          new Error(
            `discord gateway socket did not close within ${DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS}ms before restart`,
          ),
        );
      }, DISCORD_GATEWAY_STARTUP_TERMINATE_CLOSE_TIMEOUT_MS);
      terminateCloseTimeout.unref?.();
    }, DISCORD_GATEWAY_STARTUP_DISCONNECT_DRAIN_TIMEOUT_MS);
    drainTimeout.unref?.();
  });

  if (!params.abortSignal?.aborted) {
    params.gateway.connect(false);
  }
}

function parseGatewayCloseCode(message: string): number | undefined {
  const match = /Gateway websocket closed:\s*(\d{3,5})/.exec(message);
  if (!match?.[1]) {
    return undefined;
  }
  const code = Number.parseInt(match[1], 10);
  return Number.isFinite(code) ? code : undefined;
}

function resolveTransportActivityAt(event: unknown): number {
  const at = (event as { at?: unknown } | undefined)?.at;
  const timestampMs = asDateTimestampMs(at);
  return timestampMs !== undefined && timestampMs >= 0 ? timestampMs : Date.now();
}

function createGatewayStatusObserver(params: {
  gateway?: Pick<MutableDiscordGateway, "isConnected">;
  abortSignal?: AbortSignal;
  runtime: RuntimeEnv;
  pushStatus: (patch: Parameters<DiscordMonitorStatusSink>[0]) => void;
  isLifecycleStopping: () => boolean;
  runtimeReadyTimeoutMs: number;
}) {
  let forceStopHandler: ((err: unknown) => void) | undefined;
  let queuedForceStopError: unknown;
  let readyPollId: ReturnType<typeof setInterval> | undefined;
  let readyTimeoutId: ReturnType<typeof setTimeout> | undefined;

  const shouldStop = () => params.abortSignal?.aborted || params.isLifecycleStopping();
  const clearReadyWatch = () => {
    if (readyPollId) {
      clearInterval(readyPollId);
      readyPollId = undefined;
    }
    if (readyTimeoutId) {
      clearTimeout(readyTimeoutId);
      readyTimeoutId = undefined;
    }
  };
  const triggerForceStop = (err: unknown) => {
    if (forceStopHandler) {
      forceStopHandler(err);
      return;
    }
    queuedForceStopError = err;
  };
  const pushConnectedStatus = (at: number) => {
    params.pushStatus(createDiscordReadyStatusPatch(at));
  };
  const startReadyWatch = () => {
    clearReadyWatch();
    const pollConnected = () => {
      if (shouldStop()) {
        clearReadyWatch();
        return;
      }
      if (!params.gateway?.isConnected) {
        return;
      }
      clearReadyWatch();
      pushConnectedStatus(Date.now());
    };

    pollConnected();
    if (!readyTimeoutId) {
      readyPollId = setInterval(pollConnected, DISCORD_GATEWAY_READY_POLL_MS);
      readyPollId.unref?.();
      readyTimeoutId = setTimeout(() => {
        clearReadyWatch();
        if (shouldStop() || params.gateway?.isConnected) {
          return;
        }
        const at = Date.now();
        const error = new Error(
          `discord gateway opened but did not reach READY within ${params.runtimeReadyTimeoutMs}ms`,
        );
        params.pushStatus({
          connected: false,
          lifecycle: "recovering",
          lastEventAt: at,
          lastDisconnect: {
            at,
            error: "runtime-not-ready",
          },
          lastError: "runtime-not-ready",
        });
        params.runtime.error?.(danger(error.message));
        triggerForceStop(error);
      }, params.runtimeReadyTimeoutMs);
      readyTimeoutId.unref?.();
    }
  };

  const onGatewayDebug = (msg: unknown) => {
    if (shouldStop()) {
      return;
    }
    const at = Date.now();
    const message = String(msg);
    if (message.includes("Gateway websocket opened")) {
      params.pushStatus({ connected: false, lastEventAt: at });
      startReadyWatch();
      return;
    }
    if (message.includes("Gateway websocket closed")) {
      clearReadyWatch();
      const code = parseGatewayCloseCode(message);
      // Fatal gateway closes require operator repair. Keep the outer channel supervisor from
      // turning an invalid credential or configuration into an automatic restart loop.
      const terminalDisconnect =
        code !== undefined && isFatalGatewayCloseCode(code as GatewayCloseCodes);
      params.pushStatus({
        connected: false,
        lifecycle: terminalDisconnect ? "blocked" : "recovering",
        ...(terminalDisconnect ? { terminalDisconnect: true, lastError: message } : {}),
        lastEventAt: at,
        lastDisconnect: {
          at,
          ...(code !== undefined ? { status: code } : {}),
        },
      });
      return;
    }
    if (message.includes("Gateway reconnect scheduled in")) {
      clearReadyWatch();
      params.pushStatus({
        connected: false,
        lifecycle: "recovering",
        lastEventAt: at,
        lastError: message,
      });
    }
  };

  return {
    onGatewayDebug,
    clearReadyWatch,
    registerForceStop: (handler: (err: unknown) => void) => {
      forceStopHandler = handler;
      if (queuedForceStopError !== undefined) {
        const err = queuedForceStopError;
        queuedForceStopError = undefined;
        handler(err);
      }
    },
    dispose: () => {
      clearReadyWatch();
      forceStopHandler = undefined;
      queuedForceStopError = undefined;
    },
  };
}

async function waitForGatewayReady(params: {
  gateway?: Pick<MutableDiscordGateway, "connect" | "disconnect" | "isConnected" | "ws">;
  abortSignal?: AbortSignal;
  beforePoll?: () => Promise<"continue" | "stop"> | "continue" | "stop";
  pushStatus?: (patch: Parameters<DiscordMonitorStatusSink>[0]) => void;
  runtime: RuntimeEnv;
  beforeRestart?: () => Promise<void> | void;
  readyTimeoutMs: number;
}): Promise<void> {
  const waitUntilReady = async (): Promise<GatewayReadyWaitResult> => {
    const deadlineAt = Date.now() + params.readyTimeoutMs;
    while (!params.abortSignal?.aborted) {
      if ((await params.beforePoll?.()) === "stop") {
        return "stopped";
      }
      if (params.gateway?.isConnected === true) {
        const at = Date.now();
        params.pushStatus?.(createDiscordReadyStatusPatch(at));
        return "ready";
      }
      if (Date.now() >= deadlineAt) {
        return "timeout";
      }
      await new Promise<void>((resolve) => {
        const timeout = setTimeout(resolve, DISCORD_GATEWAY_READY_POLL_MS);
        timeout.unref?.();
      });
    }
    return "stopped";
  };

  if (!params.gateway) {
    const attempt = await waitUntilReady();
    if (attempt === "timeout") {
      throw new Error(`discord gateway did not reach READY within ${params.readyTimeoutMs}ms`);
    }
    return;
  }

  let attempt = 0;
  while (!params.abortSignal?.aborted) {
    const result = await waitUntilReady();
    if (result !== "timeout") {
      return;
    }

    attempt += 1;
    const restartAt = Date.now();
    params.runtime.error?.(
      danger(
        `discord: gateway READY wait timed out after ${params.readyTimeoutMs}ms; reconnecting with backoff (attempt ${attempt})`,
      ),
    );
    params.pushStatus?.({
      connected: false,
      lifecycle: "recovering",
      lastEventAt: restartAt,
      lastDisconnect: {
        at: restartAt,
        error: "startup-not-ready",
      },
      lastError: "startup-not-ready",
    });
    await params.beforeRestart?.();
    await restartGatewayAfterReadyTimeout({
      gateway: params.gateway,
      abortSignal: params.abortSignal,
      runtime: params.runtime,
    });
    if (params.abortSignal?.aborted) {
      return;
    }
    try {
      await sleepWithAbort(DISCORD_GATEWAY_READY_RETRY_BACKOFF_MS, params.abortSignal, {
        ref: false,
      });
    } catch {
      // Abort is normal lifecycle shutdown; do not enter another reconnect attempt.
      return;
    }
  }
}

export async function runDiscordGatewayLifecycle(params: {
  accountId: string;
  gateway?: MutableDiscordGateway;
  runtime: RuntimeEnv;
  abortSignal?: AbortSignal;
  isDisallowedIntentsError: (err: unknown) => boolean;
  voiceManager: DiscordVoiceManager | null;
  voiceManagerRef: { current: DiscordVoiceManager | null };
  threadBindings: { stop: () => void };
  gatewaySupervisor: DiscordGatewaySupervisor;
  statusSink?: DiscordMonitorStatusSink;
}) {
  const gateway = params.gateway;
  const gatewayReadyAtLifecycleStart = gateway?.isConnected === true;
  if (gateway) {
    registerGateway(params.accountId, gateway);
  }
  const gatewayEmitter = params.gatewaySupervisor.emitter ?? getDiscordGatewayEmitter(gateway);
  const stopGatewayLogging = attachDiscordGatewayLogging({
    emitter: gatewayEmitter,
    runtime: params.runtime,
  });
  let lifecycleStopping = false;

  const pushStatus = (patch: Parameters<DiscordMonitorStatusSink>[0]) => {
    params.statusSink?.(patch);
  };
  const gatewayReadyTimeoutMs = resolveDiscordGatewayReadyTimeoutMs({
    env: process.env,
  });
  const gatewayRuntimeReadyTimeoutMs = resolveDiscordGatewayRuntimeReadyTimeoutMs({
    env: process.env,
  });
  const statusObserver = createGatewayStatusObserver({
    gateway,
    abortSignal: params.abortSignal,
    runtime: params.runtime,
    pushStatus,
    isLifecycleStopping: () => lifecycleStopping,
    runtimeReadyTimeoutMs: gatewayRuntimeReadyTimeoutMs,
  });
  gatewayEmitter?.on("debug", statusObserver.onGatewayDebug);
  let lastTransportActivityStatusAt: number | undefined;
  const onGatewayTransportActivity = (event: unknown) => {
    if (lifecycleStopping || params.abortSignal?.aborted) {
      return;
    }
    const at = resolveTransportActivityAt(event);
    if (
      lastTransportActivityStatusAt !== undefined &&
      at - lastTransportActivityStatusAt < DISCORD_GATEWAY_TRANSPORT_ACTIVITY_STATUS_MIN_INTERVAL_MS
    ) {
      return;
    }
    lastTransportActivityStatusAt = at;
    pushStatus(createTransportActivityStatusPatch(at));
  };
  gatewayEmitter?.on(DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT, onGatewayTransportActivity);

  let sawDisallowedIntents = false;
  const handleGatewayEvent = (event: DiscordGatewayEvent): "continue" | "stop" => {
    if (params.abortSignal?.aborted && event.type === "reconnect-exhausted") {
      lifecycleStopping = true;
      params.runtime.log?.(
        `discord: treating reconnect-exhausted during expected shutdown as clean: ${event.message}`,
      );
      return "continue";
    }
    if (event.type === "disallowed-intents") {
      lifecycleStopping = true;
      sawDisallowedIntents = true;
      params.runtime.error?.(
        danger(
          "discord: gateway closed with code 4014 (missing privileged gateway intents). Enable the required intents in the Discord Developer Portal or disable them in config.",
        ),
      );
      return "stop";
    }
    if (event.shouldStopLifecycle) {
      lifecycleStopping = true;
    }
    params.runtime.error?.(
      danger(
        event.shouldStopLifecycle
          ? `discord gateway ${event.type}: ${event.message}`
          : `discord gateway error: ${event.message}`,
      ),
    );
    return event.shouldStopLifecycle ? "stop" : "continue";
  };
  const drainPendingGatewayErrors = (): "continue" | "stop" =>
    params.gatewaySupervisor.drainPending((event) => {
      const decision = handleGatewayEvent(event);
      if (decision !== "stop") {
        return "continue";
      }
      if (event.type === "disallowed-intents") {
        return "stop";
      }
      throw new DiscordGatewayLifecycleError(event);
    });
  try {
    // Drain gateway errors emitted before lifecycle listeners were attached.
    if (drainPendingGatewayErrors() === "stop") {
      return;
    }

    if (gatewayReadyAtLifecycleStart && params.voiceManager) {
      // READY may precede lazy voice-listener registration. Reconcile only after lifecycle
      // ownership begins so every startup failure still destroys the manager in `finally`.
      void params.voiceManager
        .autoJoin()
        .catch((err: unknown) =>
          params.runtime.error?.(
            danger(`discord voice: autoJoin failed: ${formatErrorMessage(err)}`),
          ),
        );
    }

    await waitForGatewayReady({
      gateway,
      abortSignal: params.abortSignal,
      beforePoll: drainPendingGatewayErrors,
      pushStatus,
      runtime: params.runtime,
      beforeRestart: statusObserver.clearReadyWatch,
      readyTimeoutMs: gatewayReadyTimeoutMs,
    });

    if (drainPendingGatewayErrors() === "stop") {
      return;
    }

    await waitForDiscordGatewayStop({
      gateway: gateway
        ? {
            disconnect: () => gateway.disconnect(),
          }
        : undefined,
      abortSignal: params.abortSignal,
      gatewaySupervisor: params.gatewaySupervisor,
      onGatewayEvent: handleGatewayEvent,
      registerForceStop: statusObserver.registerForceStop,
    });
  } catch (err) {
    if (!sawDisallowedIntents && !params.isDisallowedIntentsError(err)) {
      throw err;
    }
  } finally {
    lifecycleStopping = true;
    params.gatewaySupervisor.detachLifecycle();
    unregisterGateway(params.accountId);
    stopGatewayLogging();
    statusObserver.dispose();
    gatewayEmitter?.removeListener("debug", statusObserver.onGatewayDebug);
    gatewayEmitter?.removeListener(
      DISCORD_GATEWAY_TRANSPORT_ACTIVITY_EVENT,
      onGatewayTransportActivity,
    );
    if (params.voiceManager) {
      await params.voiceManager.destroy();
      const { setDiscordTranscriptsVoiceManager } = await import("../voice/transcripts-source.js");
      setDiscordTranscriptsVoiceManager({
        accountId: params.accountId,
        manager: null,
      });
      params.voiceManagerRef.current = null;
    }
    params.threadBindings.stop();
  }
}
