/*
* Thin wrapper over the Agora Web SDKs — RTM 2.x (agora-rtm-sdk) is the
* control plane for every medium per the Consultation API Handbook
* (chat text, the accept handshake, the hangup signal, the ending
* warning all ride it), RTC (agora-rtc-sdk-ng) additionally carries
* audio for the AUDIO medium. Tokens/channel/uid always come from the
* backend (start-chat, accept, agora-token) — never construct
* channel_name, rtm_account or rtc_uid client-side.
*
* Confirmed live: RTM 2.x consumes the AccessToken2 ("007…") tokens
* this API mints — this package version was pinned deliberately
* (agora-rtm-sdk@^2), don't let it drift to 1.x.
*/

// Both SDKs touch `window`/browser globals at module-evaluation time
// (typical of WebRTC packages), not just when their functions run —
// confirmed live, a hard navigation/refresh straight to a consultation
// page crashes with "window is not defined" during Next's server
// render if these are static top-level imports. Load them lazily,
// client-side only, the first time a function below actually needs
// them; `import type` is erased at compile time so it's safe to keep
// static.
import type { IAgoraRTCClient, IMicrophoneAudioTrack } from "agora-rtc-sdk-ng";

let rtmModulePromise: Promise<typeof import("agora-rtm-sdk")> | null = null;
let rtcModulePromise: Promise<typeof import("agora-rtc-sdk-ng")> | null = null;
let chatModulePromise: Promise<typeof import("agora-chat")> | null = null;

function loadAgoraRtm() {
  if (!rtmModulePromise) rtmModulePromise = import("agora-rtm-sdk");
  return rtmModulePromise;
}

function loadAgoraRtc() {
  if (!rtcModulePromise) rtcModulePromise = import("agora-rtc-sdk-ng");
  return rtcModulePromise;
}

function loadAgoraChat() {
  if (!chatModulePromise) chatModulePromise = import("agora-chat");
  return chatModulePromise;
}

export function getAgoraAppId(): string {
  const appId = process.env.NEXT_PUBLIC_AGORA_APP_ID;

  if (!appId) {
    throw new Error("NEXT_PUBLIC_AGORA_APP_ID is not configured.");
  }

  return appId;
}

/*
|--------------------------------------------------------------------------
| RTM (chat control plane — used for CHAT text and as the signalling
| channel alongside RTC audio for AUDIO)
|--------------------------------------------------------------------------
*/

export interface RtmMessagePayload {
  clientMessageId: string;
  body: string;
  sentAt: string;
  // Optional structured data riding along with a real message — used
  // by the consultee auto-details message (see app/lib/consultation.ts)
  // so the receiving side can read it directly instead of parsing it
  // back out of `body`. Never persisted by messages/sync, live-RTM
  // only — see loadPriorMessages()'s parseAutoDetailsMessage() fallback
  // for the reload case.
  meta?: Record<string, any>;
}

// Tracks the one RTM client this tab is currently logged in with, so a
// fast remount (React re-running an effect before its own unmount
// cleanup lands, or a customer/astrologer navigating back into a
// session) logs the old local instance out before creating a new one,
// instead of leaving two live clients for the same account.
let activeRtmClient: any = null;

export async function createAndLoginRtm(userAccount: string, token: string) {
  const { default: AgoraRTM } = await loadAgoraRtm();

  if (activeRtmClient) {
    await logoutRtm(activeRtmClient);
  }

  const client = new AgoraRTM.RTM(getAgoraAppId(), userAccount);

  // Confirmed live: RTM error -10027 ("the user ID is already in use by
  // another active RTM instance with the same app ID") fires reliably
  // when resuming a session after a hard refresh, a crashed tab, or a
  // reload mid-call — there's no reliable way to run an async
  // client.logout() during page unload, so the previous instance is
  // still "active" from Agora's side for a short window afterwards.
  // That stale session times out on its own; retry with a short delay
  // rather than failing the whole join on the first attempt.
  const attemptLogin = async (retriesLeft: number): Promise<void> => {
    try {
      await client.login({ token });
    } catch (error: any) {
      if (error?.code === -10027 && retriesLeft > 0) {
        await new Promise((resolve) => setTimeout(resolve, 2500));
        return attemptLogin(retriesLeft - 1);
      }
      throw error;
    }
  };

  await attemptLogin(4);
  activeRtmClient = client;
  return client;
}

export async function subscribeRtmChannel(client: any, channelName: string) {
  await client.subscribe(channelName, { withMessage: true });
}

export function onRtmMessage(
  client: any,
  handler: (payload: RtmMessagePayload, publisher: string) => void
) {
  const listener = (event: any) => {
    try {
      const parsed = JSON.parse(event.message);
      handler(parsed, event.publisher);
    } catch {
      // Ignore anything that isn't our own JSON envelope.
    }
  };

  client.addEventListener("message", listener);
  return () => client.removeEventListener("message", listener);
}

export async function publishRtmMessage(
  client: any,
  channelName: string,
  payload: RtmMessagePayload
) {
  await client.publish(channelName, JSON.stringify(payload), { channelType: "MESSAGE" });
}

export async function logoutRtm(client: any) {
  try {
    await client.logout();
  } catch {
    // Best-effort — the session is ending either way.
  } finally {
    if (activeRtmClient === client) activeRtmClient = null;
  }
}

/*
|--------------------------------------------------------------------------
| RTC (audio for the AUDIO medium — confirmed live: an audio token
| carries no publish-video privilege, so this stays audio-only)
|--------------------------------------------------------------------------
*/

export interface RtcAudioSession {
  client: IAgoraRTCClient;
  localAudioTrack: IMicrophoneAudioTrack;
}

export async function joinRtcAudio(params: {
  channel: string;
  token: string;
  uid: number;
  onRemoteAudio: (playRemote: () => void) => void;
}): Promise<RtcAudioSession> {
  const { default: AgoraRTC } = await loadAgoraRtc();

  const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });

  client.on("user-published", async (user, mediaType) => {
    await client.subscribe(user, mediaType);

    if (mediaType === "audio" && user.audioTrack) {
      params.onRemoteAudio(() => user.audioTrack?.play());
    }
  });

  await client.join(getAgoraAppId(), params.channel, params.token, params.uid);

  const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
  await client.publish([localAudioTrack]);

  return { client, localAudioTrack };
}

export function toggleMic(session: RtcAudioSession, muted: boolean) {
  session.localAudioTrack.setEnabled(!muted);
}

export async function leaveRtcAudio(session: RtcAudioSession | null) {
  if (!session) return;

  try {
    session.localAudioTrack.close();
    await session.client.leave();
  } catch {
    // Best-effort — the session is ending either way.
  }
}

/*
|--------------------------------------------------------------------------
| Chat (image/voice attachments — a separate real-time connection from
| RTM above, not a replacement for it: RTM stays the durable, already-
| proven carrier for plain text; Chat is used only for the message
| types RTM was never meant for. Confirmed live end-to-end: SDK login,
| REST user provisioning, real-time delivery, and image upload all
| verified against the real Agora project before this was wired up —
| see app/api/agora/chat-token/route.ts for the token/identity side.)
|--------------------------------------------------------------------------
*/

export interface ChatSession {
  connection: any;
  username: string;
  peerUsername: string;
}

export interface ChatMintResponse {
  success: boolean;
  message?: string;
  appId?: string;
  username?: string;
  peerUsername?: string;
  token?: string;
}

export async function mintAgoraChatToken(
  audience: "user" | "astrologer",
  consultationId: number,
  callerToken: string
): Promise<ChatMintResponse> {
  const response = await fetch("/api/agora/chat-token", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: callerToken,
    },
    body: JSON.stringify({ audience, consultationId }),
  });

  return response.json();
}

let activeChatConnection: any = null;

export async function loginAgoraChat(
  appId: string,
  username: string,
  peerUsername: string,
  accessToken: string
): Promise<ChatSession> {
  const { default: AC } = await loadAgoraChat();

  if (activeChatConnection) {
    try {
      activeChatConnection.close();
    } catch {
      // Best-effort.
    }
  }

  const connection = new AC.connection({ appId });

  // Confirmed live: a login attempt right after a previous session's
  // disconnect (e.g. reconnecting shortly after a refresh) can fail
  // outright with a transient DNS/timeout error ("get DNS failed",
  // type 304) rather than a clean "already logged in" conflict like
  // RTM's -10027 — with no retry, that one failed attempt permanently
  // disabled attachments for the rest of the session. Retry a few
  // times with a short backoff, mirroring createAndLoginRtm's pattern.
  const attemptOpen = async (retriesLeft: number): Promise<void> => {
    try {
      await connection.open({ user: username, accessToken });
    } catch (error) {
      if (retriesLeft > 0) {
        await new Promise((resolve) => setTimeout(resolve, 2000));
        return attemptOpen(retriesLeft - 1);
      }
      throw error;
    }
  };

  await attemptOpen(3);

  activeChatConnection = connection;

  return { connection, username, peerUsername };
}

export function logoutAgoraChat(session: ChatSession | null) {
  if (!session) return;

  try {
    session.connection.close();
  } catch {
    // Best-effort — the session is ending either way.
  }

  if (activeChatConnection === session.connection) activeChatConnection = null;
}

export interface ChatAttachmentPayload {
  clientMessageId: string;
  kind: "image" | "audio";
  url: string;
  thumbUrl?: string;
  durationSeconds?: number;
  sentAt: string;
}

export function onAgoraChatAttachment(
  session: ChatSession,
  handler: (payload: ChatAttachmentPayload, from: string) => void
) {
  const key = `attachmentHandler-${session.username}`;

  session.connection.addEventHandler(key, {
    onImageMessage: (msg: any) => {
      handler(
        {
          clientMessageId: String(msg.id),
          kind: "image",
          url: msg.url,
          thumbUrl: msg.thumb,
          sentAt: new Date(msg.time || Date.now()).toISOString(),
        },
        msg.from
      );
    },
    onAudioMessage: (msg: any) => {
      handler(
        {
          clientMessageId: String(msg.id),
          kind: "audio",
          url: msg.url,
          durationSeconds: msg.length,
          sentAt: new Date(msg.time || Date.now()).toISOString(),
        },
        msg.from
      );
    },
  });

  return () => session.connection.removeEventHandler(key);
}

// Agora Chat durably stores every image/voice message server-side on
// its own (confirmed live — see the real "create user"/send-image
// tests this was built against), completely separate from the Go
// backend's messages/sync table RTM text relies on for the same
// "restore transcript after a refresh" behavior. Without this, a
// refresh mid-session only restored text — image/voice messages sent
// before the refresh silently vanished from the thread (RTM has no
// record of them at all, and a fresh conn.open() on reload starts
// with an empty local message list). Called once right after Chat
// login succeeds in joinAgora(), on both a fresh accept and a resume.
export async function loadAgoraChatAttachmentHistory(
  session: ChatSession,
  pageSize = 50
): Promise<Array<ChatAttachmentPayload & { from: string }>> {
  const result = await session.connection.getHistoryMessages({
    targetId: session.peerUsername,
    chatType: "singleChat",
    pageSize,
    searchDirection: "up",
  });

  const attachments: Array<ChatAttachmentPayload & { from: string }> = [];

  for (const msg of result?.messages || []) {
    if (msg.type === "img") {
      attachments.push({
        clientMessageId: String(msg.id),
        kind: "image",
        url: msg.url,
        thumbUrl: msg.thumb,
        sentAt: new Date(msg.time || Date.now()).toISOString(),
        from: msg.from,
      });
    } else if (msg.type === "audio") {
      attachments.push({
        clientMessageId: String(msg.id),
        kind: "audio",
        url: msg.url,
        durationSeconds: msg.length,
        sentAt: new Date(msg.time || Date.now()).toISOString(),
        from: msg.from,
      });
    }
  }

  return attachments;
}

export async function sendAgoraChatImage(
  session: ChatSession,
  file: File
): Promise<ChatAttachmentPayload> {
  const { default: AC } = await loadAgoraChat();

  const msg = AC.message.create({
    type: "img",
    chatType: "singleChat",
    to: session.peerUsername,
    // The SDK's own FileObj type declares url/filetype as required,
    // but its real runtime behavior (confirmed live) only needs
    // data/filename — it fills the rest in during upload.
    file: { data: file, filename: file.name } as any,
  });

  const result: any = await session.connection.send(msg);
  const sent = result.message;

  return {
    clientMessageId: String(sent.id),
    kind: "image",
    url: sent.url,
    thumbUrl: sent.thumb,
    sentAt: new Date(sent.time || Date.now()).toISOString(),
  };
}

export async function sendAgoraChatAudio(
  session: ChatSession,
  file: File,
  durationSeconds: number
): Promise<ChatAttachmentPayload> {
  const { default: AC } = await loadAgoraChat();

  const msg = AC.message.create({
    type: "audio",
    chatType: "singleChat",
    to: session.peerUsername,
    file: { data: file, filename: file.name } as any,
    filename: file.name,
    length: Math.round(durationSeconds),
  });

  const result: any = await session.connection.send(msg);
  const sent = result.message;

  return {
    clientMessageId: String(sent.id),
    kind: "audio",
    url: sent.url,
    durationSeconds,
    sentAt: new Date(sent.time || Date.now()).toISOString(),
  };
}
