// Discord plugin module implements receive recovery behavior.
import { OpusError } from "libopus-wasm";
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";

export const DECRYPT_FAILURE_WINDOW_MS = 30_000;
const DECRYPT_FAILURE_RECONNECT_THRESHOLD = 3;
const DECRYPT_FAILURE_MARKER = "DecryptionFailed(";
const DAVE_PASSTHROUGH_DISABLED_MARKER = "UnencryptedWhenPassthroughDisabled";
const WASM_MEMORY_ACCESS_MARKER = "memory access out of bounds";
const OPUS_INVALID_PACKET_CODE = -4;

export const DAVE_RECEIVE_PASSTHROUGH_INITIAL_EXPIRY_SECONDS = 30;
export const DAVE_RECEIVE_PASSTHROUGH_REARM_EXPIRY_SECONDS = 15;

export type VoiceReceiveRecoveryState = {
  decryptFailureCount: number;
  lastDecryptFailureAt: number;
  decryptRecoveryInFlight: boolean;
};

type VoiceReceiveErrorAnalysis = {
  message: string;
  isAbortLike: boolean;
  isDecodeCorruption: boolean;
  shouldAttemptPassthrough: boolean;
  countsAsDecryptFailure: boolean;
};

type DavePassthroughTarget = {
  guildId: string;
  channelId: string;
  connection: {
    state: {
      status: unknown;
      networking?: {
        state?: {
          code?: unknown;
          dave?: {
            lastTransitionId?: number;
            reinitializing?: boolean;
            recoverFromInvalidTransition?: (transitionId: number) => void;
            session?: {
              setPassthroughMode: (passthrough: boolean, expirySeconds: number) => void;
            };
          };
        };
      };
    };
  };
};

type DavePassthroughSdk = {
  VoiceConnectionStatus: {
    Ready: unknown;
  };
  NetworkingStatusCode: {
    Ready: unknown;
    Resuming: unknown;
  };
};

export function createVoiceReceiveRecoveryState(): VoiceReceiveRecoveryState {
  return {
    decryptFailureCount: 0,
    lastDecryptFailureAt: 0,
    decryptRecoveryInFlight: false,
  };
}

function isAbortLikeReceiveError(err: unknown): boolean {
  if (!err || typeof err !== "object") {
    return false;
  }
  const name =
    "name" in err && typeof (err as { name?: unknown }).name === "string"
      ? (err as { name: string }).name
      : "";
  const message =
    "message" in err && typeof (err as { message?: unknown }).message === "string"
      ? (err as { message: string }).message
      : "";
  return (
    name === "AbortError" ||
    message === "Premature close" ||
    message.includes("The operation was aborted") ||
    message.includes("aborted")
  );
}

function isOpusDecodeInvalidPacketError(err: unknown): boolean {
  if (!err || typeof err !== "object") {
    return false;
  }
  const maybeOpusError = err as {
    name?: unknown;
    code?: unknown;
    codeName?: unknown;
    operation?: unknown;
  };
  const isDecodeOperation =
    maybeOpusError.operation === "decode" || maybeOpusError.operation === "decodeFloat";
  const isInvalidPacket =
    maybeOpusError.code === OPUS_INVALID_PACKET_CODE || maybeOpusError.codeName === "InvalidPacket";
  return (
    isDecodeOperation &&
    isInvalidPacket &&
    (err instanceof OpusError || maybeOpusError.name === "OpusError")
  );
}

export function analyzeVoiceReceiveError(err: unknown): VoiceReceiveErrorAnalysis {
  const message = formatErrorMessage(err);
  const normalizedMessage = message.toLowerCase();
  const shouldAttemptPassthrough = message.includes(DAVE_PASSTHROUGH_DISABLED_MARKER);
  const isWasmMemoryAccessFailure = normalizedMessage.includes(WASM_MEMORY_ACCESS_MARKER);
  return {
    message,
    isAbortLike: isAbortLikeReceiveError(err),
    isDecodeCorruption: isOpusDecodeInvalidPacketError(err),
    shouldAttemptPassthrough,
    countsAsDecryptFailure:
      message.includes(DECRYPT_FAILURE_MARKER) ||
      shouldAttemptPassthrough ||
      isWasmMemoryAccessFailure,
  };
}

export function noteVoiceDecryptFailure(
  state: VoiceReceiveRecoveryState,
  now: number = Date.now(),
): {
  firstFailure: boolean;
  shouldRecover: boolean;
} {
  if (now - state.lastDecryptFailureAt > DECRYPT_FAILURE_WINDOW_MS) {
    state.decryptFailureCount = 0;
  }
  state.lastDecryptFailureAt = now;
  state.decryptFailureCount += 1;
  const firstFailure = state.decryptFailureCount === 1;
  if (
    state.decryptFailureCount < DECRYPT_FAILURE_RECONNECT_THRESHOLD ||
    state.decryptRecoveryInFlight
  ) {
    return { firstFailure, shouldRecover: false };
  }
  state.decryptRecoveryInFlight = true;
  resetVoiceReceiveRecoveryState(state);
  return { firstFailure, shouldRecover: true };
}

export function resetVoiceReceiveRecoveryState(state: VoiceReceiveRecoveryState): void {
  state.decryptFailureCount = 0;
  state.lastDecryptFailureAt = 0;
}

export function finishVoiceDecryptRecovery(state: VoiceReceiveRecoveryState): void {
  state.decryptRecoveryInFlight = false;
}

function isDaveReinitializing(session: { reinitializing?: boolean }): boolean {
  return session.reinitializing === true;
}

export function recoverDaveZeroTransition(params: {
  target: DavePassthroughTarget;
  sdk: DavePassthroughSdk;
  onWarn: (message: string) => void;
}): "not-attempted" | "recovered" | "failed" {
  const { target, sdk, onWarn } = params;
  const networkingState = target.connection.state.networking?.state;
  const daveSession = networkingState?.dave;
  if (
    target.connection.state.status !== sdk.VoiceConnectionStatus.Ready ||
    networkingState?.code !== sdk.NetworkingStatusCode.Ready ||
    daveSession?.lastTransitionId !== 0 ||
    daveSession.reinitializing !== false ||
    typeof daveSession.recoverFromInvalidTransition !== "function"
  ) {
    return "not-attempted";
  }

  try {
    // The upstream DAVE recovery guard treats transition zero as falsy.
    daveSession.recoverFromInvalidTransition(0);
    return "recovered";
  } catch (err) {
    onWarn(
      `discord voice: failed to recover DAVE transition 0 guild=${target.guildId} channel=${target.channelId}: ${formatErrorMessage(err)}`,
    );
    // Upstream marks the session reinitializing before gateway/native work can fail.
    return isDaveReinitializing(daveSession) ? "failed" : "not-attempted";
  }
}

export function enableDaveReceivePassthrough(params: {
  target: DavePassthroughTarget;
  sdk: DavePassthroughSdk;
  reason: string;
  expirySeconds: number;
  onVerbose: (message: string) => void;
  onWarn: (message: string) => void;
}): boolean {
  const { target, sdk, reason, expirySeconds, onVerbose, onWarn } = params;
  const networkingState = target.connection.state.networking?.state;
  if (
    target.connection.state.status !== sdk.VoiceConnectionStatus.Ready ||
    !networkingState ||
    (networkingState.code !== sdk.NetworkingStatusCode.Ready &&
      networkingState.code !== sdk.NetworkingStatusCode.Resuming)
  ) {
    return false;
  }
  const daveSession = networkingState.dave?.session;
  if (!daveSession) {
    return false;
  }
  try {
    daveSession.setPassthroughMode(true, expirySeconds);
    onVerbose(
      `enabled DAVE receive passthrough: guild ${target.guildId} channel ${target.channelId} expiry=${expirySeconds}s reason=${reason}`,
    );
    return true;
  } catch (err) {
    onWarn(
      `discord voice: failed to enable DAVE passthrough guild=${target.guildId} channel=${target.channelId} reason=${reason}: ${formatErrorMessage(err)}`,
    );
    return false;
  }
}
