/**
 * Telegram channel E2E helpers.
 *
 * Wraps `callOpenhumanRpc` (core RPC) and the Telegram mock admin endpoints so
 * specs can drive the full Telegram channel lifecycle without knowing the raw
 * RPC method names or admin path strings.
 *
 * Design principles:
 *  - All helpers are pure async functions — no hidden state.
 *  - Admin HTTP helpers call mock server endpoints that are already wired by
 *    WS-A (see `app/test/e2e/mock-server.ts` and `scripts/mock-api/routes/telegram.mjs`).
 *  - RPC helpers forward to `callOpenhumanRpc` using the exact field names from
 *    `src/openhuman/channels/controllers/schemas.rs` (camelCase for the wire
 *    format; the Rust serde layer translates).
 *
 * Key RPC shapes (verified from schemas.rs / ops.rs):
 *   channels_connect  -> { channel, authMode, credentials: { bot_token, allowed_users?, mention_only? } }
 *   channels_disconnect -> { channel, authMode }
 *   channels_status   -> { channel? }   -> entries: ChannelStatusEntry[]
 */
import {
  getTelegramSentMessages as adminGetSentMessages,
  injectTelegramUpdate as adminInjectUpdate,
  resetTelegramMock as adminReset,
} from '../mock-server';
import { callOpenhumanRpc } from './core-rpc';

const LOG_PREFIX = '[TelegramChannel]';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface TelegramConnectOptions {
  /** Bot token issued by BotFather. */
  botToken: string;
  /**
   * Optional allowlist of Telegram usernames (without `@`) or numeric user
   * IDs. When provided only those identities can trigger the bot. When
   * omitted the bot uses the pairing-code flow.
   */
  allowedUsers?: string[];
  /**
   * When true the bot only responds to messages that mention it by
   * `@username` in group chats.
   */
  mentionOnly?: boolean;
}

export interface TelegramConnectResult {
  ok: boolean;
  status?: string;
  restartRequired?: boolean;
  message?: string;
  error?: string;
}

export interface TelegramStatusEntry {
  channelId: string;
  authMode: string;
  connected: boolean;
  hasCredentials: boolean;
}

export interface TelegramUpdate {
  update_id: number;
  message: {
    message_id: number;
    from: { id: number; is_bot: boolean; first_name: string; username?: string };
    chat: {
      id: number;
      type: 'private' | 'group' | 'supergroup' | 'channel';
      title?: string;
      username?: string;
      first_name?: string;
    };
    date: number;
    text: string;
  };
}

export interface SentMessage {
  method: string;
  chat_id: string | number;
  text?: string;
  [key: string]: unknown;
}

// ---------------------------------------------------------------------------
// RPC wrappers
// ---------------------------------------------------------------------------

/**
 * Connect a Telegram bot via the `channels_connect` RPC.
 *
 * Maps to `openhuman.channels_connect` with `authMode: "bot_token"`.
 * The connect call writes TOML config and sets `restart_required: true` —
 * it does NOT start the live polling loop immediately.
 *
 * Returns the raw RPC result so callers can assert on specific fields.
 */
export async function connectTelegramBot(
  opts: TelegramConnectOptions
): Promise<TelegramConnectResult> {
  const credentials: Record<string, unknown> = { bot_token: opts.botToken };
  if (opts.allowedUsers !== undefined) {
    credentials.allowed_users = opts.allowedUsers;
  }
  if (opts.mentionOnly !== undefined) {
    credentials.mention_only = opts.mentionOnly;
  }

  console.log(
    `${LOG_PREFIX} connectTelegramBot: token=***${opts.botToken.slice(-4)} ` +
      `allowedUsers=${JSON.stringify(opts.allowedUsers ?? [])} ` +
      `mentionOnly=${opts.mentionOnly ?? false}`
  );

  const out = await callOpenhumanRpc('openhuman.channels_connect', {
    channel: 'telegram',
    authMode: 'bot_token',
    credentials,
  });

  if (!out.ok) {
    console.warn(`${LOG_PREFIX} connectTelegramBot: RPC failed — ${JSON.stringify(out)}`);
    return { ok: false, error: String(out.error ?? 'unknown error') };
  }

  // The result shape from ops.rs is { status, restart_required, message? }.
  // It is wrapped by RpcOutcome which the Node RPC client unwraps one level.
  const result = (out.result as Record<string, unknown> | null) ?? {};
  const inner =
    typeof result.result === 'object' && result.result !== null
      ? (result.result as Record<string, unknown>)
      : result;

  console.log(`${LOG_PREFIX} connectTelegramBot: ok — ${JSON.stringify(inner)}`);
  return {
    ok: true,
    status: inner.status as string | undefined,
    restartRequired: inner.restart_required as boolean | undefined,
    message: inner.message as string | undefined,
  };
}

/**
 * Disconnect the Telegram bot via `channels_disconnect` RPC.
 *
 * Removes stored credentials and clears TOML config. Returns true on success.
 */
export async function disconnectTelegramBot(): Promise<boolean> {
  console.log(`${LOG_PREFIX} disconnectTelegramBot: calling channels_disconnect`);

  const out = await callOpenhumanRpc('openhuman.channels_disconnect', {
    channel: 'telegram',
    authMode: 'bot_token',
  });

  if (!out.ok) {
    console.warn(`${LOG_PREFIX} disconnectTelegramBot: RPC failed — ${JSON.stringify(out)}`);
    return false;
  }

  console.log(`${LOG_PREFIX} disconnectTelegramBot: ok`);
  return true;
}

/**
 * Fetch the channel status for Telegram.
 *
 * Calls `channels_status` with `channel: "telegram"` and returns the first
 * matching entry (the `bot_token` mode entry). Returns `null` if no entry is
 * found or the RPC fails.
 */
export async function getTelegramChannelStatus(): Promise<TelegramStatusEntry | null> {
  console.log(`${LOG_PREFIX} getTelegramChannelStatus: calling channels_status`);

  const out = await callOpenhumanRpc('openhuman.channels_status', { channel: 'telegram' });

  if (!out.ok) {
    console.warn(`${LOG_PREFIX} getTelegramChannelStatus: RPC failed — ${JSON.stringify(out)}`);
    return null;
  }

  // channels_status returns entries: ChannelStatusEntry[].
  // The core wraps with RpcOutcome so the Node client may unwrap one level.
  const result = (out.result as Record<string, unknown> | null) ?? {};
  const entries: TelegramStatusEntry[] = Array.isArray(result)
    ? result
    : Array.isArray((result as Record<string, unknown>).entries)
      ? ((result as Record<string, unknown>).entries as TelegramStatusEntry[])
      : Array.isArray((result as Record<string, unknown>).result)
        ? ((result as Record<string, unknown>).result as TelegramStatusEntry[])
        : [];

  const raw = entries.find(
    (e: TelegramStatusEntry) =>
      (e.channelId === 'telegram' || (e as Record<string, unknown>).channel_id === 'telegram') &&
      (e.authMode === 'bot_token' || (e as Record<string, unknown>).auth_mode === 'bot_token')
  ) as (TelegramStatusEntry & Record<string, unknown>) | undefined;

  // Normalise snake_case fields that the Rust core serialises.
  const match: TelegramStatusEntry | undefined = raw
    ? {
        channelId: (raw.channelId ?? raw.channel_id) as string,
        authMode: (raw.authMode ?? raw.auth_mode) as string,
        connected: raw.connected,
        hasCredentials: (raw.hasCredentials ?? raw.has_credentials ?? false) as boolean,
      }
    : undefined;

  console.log(`${LOG_PREFIX} getTelegramChannelStatus: ${JSON.stringify(match ?? null)}`);
  return match ?? null;
}

// ---------------------------------------------------------------------------
// Mock admin helpers (relay to WS-A admin endpoints)
// ---------------------------------------------------------------------------

/**
 * Build a realistic Telegram Update JSON for a private or group message.
 *
 * @param opts.updateId    — Telegram update_id (must increase monotonically).
 * @param opts.chatId      — Chat numeric ID.
 * @param opts.userId      — Sender numeric user ID.
 * @param opts.username    — Sender Telegram username (without `@`).
 * @param opts.text        — Message text.
 * @param opts.isGroup     — When true, emits a group chat type.
 * @param opts.botUsername — When provided AND isGroup, includes the mention
 *                           in the text so mention-only filtering fires.
 */
export function buildTelegramUpdate(opts: {
  updateId: number;
  chatId: number;
  userId: number;
  username: string;
  text: string;
  isGroup?: boolean;
  botUsername?: string;
}): TelegramUpdate {
  const chatType = opts.isGroup ? 'group' : 'private';

  return {
    update_id: opts.updateId,
    message: {
      message_id: opts.updateId * 10, // stable across retries
      from: { id: opts.userId, is_bot: false, first_name: opts.username, username: opts.username },
      chat: {
        id: opts.chatId,
        type: chatType,
        ...(opts.isGroup ? { title: `e2e-group-${opts.chatId}` } : { first_name: opts.username }),
      },
      date: Math.floor(Date.now() / 1000),
      text: opts.text,
    },
  };
}

/**
 * Inject a Telegram Update into the mock server's pending queue.
 * The Telegram provider's `getUpdates` poll will drain this on the next call.
 */
export async function injectTelegramUpdate(update: TelegramUpdate): Promise<void> {
  console.log(
    `${LOG_PREFIX} injectTelegramUpdate: update_id=${update.update_id} ` +
      `chat_id=${update.message.chat.id} text="${update.message.text.slice(0, 60)}"`
  );
  await adminInjectUpdate(update);
}

/**
 * Poll the mock's sent-messages log until a `sendMessage` entry appears that
 * matches the given `chatId` and optional `contains` predicate.
 *
 * Returns the matching entry, or throws after `timeoutMs`.
 */
export async function waitForTelegramReply(opts: {
  chatId: number;
  contains?: string;
  timeoutMs?: number;
}): Promise<SentMessage> {
  const { chatId, contains, timeoutMs = 20_000 } = opts;

  console.log(
    `${LOG_PREFIX} waitForTelegramReply: chatId=${chatId} ` +
      `contains="${contains ?? '*'}" timeout=${timeoutMs}ms`
  );

  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const sent = (await adminGetSentMessages()) as SentMessage[];
    const match = sent.find(entry => {
      // method might be 'sendMessage', 'sendText', etc.; filter by chat_id first.
      const matchesChat =
        String(entry.chat_id) === String(chatId) ||
        // Some mock implementations nest the chat_id inside a request body JSON.
        String((entry as Record<string, unknown>).body_chat_id ?? '') === String(chatId);

      if (!matchesChat) return false;
      if (!contains) return true;

      const text = String(entry.text ?? entry.message ?? '');
      return text.includes(contains);
    });

    if (match) {
      console.log(`${LOG_PREFIX} waitForTelegramReply: found match — ${JSON.stringify(match)}`);
      return match;
    }

    await browser.pause(300);
  }

  const allSent = (await adminGetSentMessages()) as SentMessage[];
  throw new Error(
    `${LOG_PREFIX} waitForTelegramReply: TIMEOUT — no reply to chatId=${chatId}` +
      (contains ? ` containing "${contains}"` : '') +
      ` after ${timeoutMs}ms. Sent log (${allSent.length} entries): ` +
      JSON.stringify(allSent.slice(-5))
  );
}

/**
 * Poll the mock's sent-messages log and assert that NO reply to `chatId`
 * appears within `timeoutMs`. Returns `true` if the window passes cleanly,
 * `false` if a matching message is observed.
 */
export async function assertNoTelegramReply(opts: {
  chatId: number;
  contains?: string;
  timeoutMs?: number;
}): Promise<boolean> {
  const { chatId, contains, timeoutMs = 5_000 } = opts;

  console.log(
    `${LOG_PREFIX} assertNoTelegramReply: chatId=${chatId} ` +
      `contains="${contains ?? '*'}" window=${timeoutMs}ms`
  );

  const deadline = Date.now() + timeoutMs;

  while (Date.now() < deadline) {
    const sent = (await adminGetSentMessages()) as SentMessage[];
    const match = sent.find(entry => {
      const matchesChat = String(entry.chat_id) === String(chatId);
      if (!matchesChat) return false;
      if (!contains) return true;
      const text = String(entry.text ?? entry.message ?? '');
      return text.includes(contains);
    });

    if (match) {
      console.warn(
        `${LOG_PREFIX} assertNoTelegramReply: UNEXPECTED reply to chatId=${chatId} — ` +
          JSON.stringify(match)
      );
      return false;
    }

    await browser.pause(300);
  }

  console.log(`${LOG_PREFIX} assertNoTelegramReply: clean — no reply in ${timeoutMs}ms window`);
  return true;
}

/**
 * Reset the Telegram mock state (pending update queue + sent log + counter).
 * Delegates to the WS-A admin endpoint via `mock-server.ts`.
 */
export async function resetTelegramMock(): Promise<void> {
  await adminReset();
  console.log(`${LOG_PREFIX} resetTelegramMock: done`);
}
