import https from 'node:https';
import tls from 'node:tls';
import { Buffer } from 'node:buffer';
import { config } from './env.js';
import { connectThroughProxy } from './proxy.js';

const API_HOST = 'api.telegram.org';

async function callApi(method, payload, { timeoutMs = 10_000 } = {}) {
  const { status, text } = await postJson(`/bot${config.botToken}/${method}`, payload, timeoutMs);

  let body = {};
  try {
    body = JSON.parse(text);
  } catch {
    // keep the empty body; the status check below reports it
  }

  if (status < 200 || status >= 300 || body.ok !== true) {
    // Telegram carries the real failure in `description` (e.g. "chat not found" when the
    // recipient never messaged the bot first) — surface it verbatim so the cause is obvious.
    throw new Error(body.description ?? `HTTP ${status}`);
  }

  return body.result;
}

/**
 * One HTTPS POST to Telegram. Built on `node:https` rather than `fetch` so the optional proxy applies
 * identically on every supported Node version: with a proxy configured the request rides a tunnelled socket
 * (see `proxy.js`), without one it takes the default direct route.
 */
async function postJson(path, payload, timeoutMs) {
  const body = JSON.stringify(payload);
  const tunnel = config.proxy
    ? await connectThroughProxy(config.proxy, API_HOST, 443, timeoutMs)
    : null;

  return new Promise((resolve, reject) => {
    const request = https.request({
      host: API_HOST,
      port: 443,
      path,
      method: 'POST',
      headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) },
      // Only set when tunnelling: passing createConnection is what makes node skip the default agent.
      ...(tunnel ? { createConnection: () => tls.connect({ socket: tunnel, servername: API_HOST }) } : {}),
    }, (response) => {
      let text = '';
      response.setEncoding('utf8');
      response.on('data', (chunk) => { text += chunk; });
      response.on('end', () => resolve({ status: response.statusCode, text }));
    });

    request.setTimeout(timeoutMs, () => request.destroy(new Error(`no response within ${timeoutMs}ms`)));
    request.on('error', (error) => {
      if (tunnel) tunnel.destroy();
      reject(error);
    });
    request.end(body);
  });
}

/** Sends one message to every configured chat id. Never rejects — per-recipient outcomes are returned. */
export async function broadcast(text) {
  const results = await Promise.all(
    config.chatIds.map(async (chatId) => {
      try {
        await callApi('sendMessage', { chat_id: chatId, text, disable_notification: false });
        return { chatId, ok: true };
      } catch (error) {
        return { chatId, ok: false, error: error.message };
      }
    }),
  );

  return {
    delivered: results.filter((r) => r.ok).map((r) => r.chatId),
    failed: results.filter((r) => !r.ok).map(({ chatId, error }) => ({ chatId, error })),
  };
}

/**
 * Lists the chat ids that have messaged the bot recently, so a new recipient can be discovered
 * without hunting through the Telegram API by hand. getUpdates only sees messages newer than the
 * last 24h and returns nothing while a webhook is set.
 */
export async function discoverChatIds() {
  const updates = await callApi('getUpdates', { limit: 100, timeout: 0 });
  const seen = new Map();

  for (const update of updates) {
    const chat = update.message?.chat ?? update.edited_message?.chat ?? update.channel_post?.chat;
    if (!chat) continue;

    seen.set(String(chat.id), {
      chat_id: String(chat.id),
      type: chat.type,
      username: chat.username ?? null,
      title: chat.title ?? ([chat.first_name, chat.last_name].filter(Boolean).join(' ') || null),
    });
  }

  return [...seen.values()];
}

export async function getBotIdentity() {
  return callApi('getMe', {});
}
