import { createServer } from 'node:http';
import { timingSafeEqual } from 'node:crypto';

import { config } from './env.js';
import { broadcast, discoverChatIds, getBotIdentity } from './telegram.js';

const MAX_BODY_BYTES = 16 * 1024;

function json(res, status, payload) {
  const body = JSON.stringify(payload);
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'content-length': Buffer.byteLength(body) });
  res.end(body);
}

function readJsonBody(req) {
  return new Promise((resolve, reject) => {
    let size = 0;
    const chunks = [];

    req.on('data', (chunk) => {
      size += chunk.length;
      if (size > MAX_BODY_BYTES) {
        reject(new Error('request body too large'));
        req.destroy();
        return;
      }
      chunks.push(chunk);
    });

    req.on('error', reject);
    req.on('end', () => {
      const raw = Buffer.concat(chunks).toString('utf8').trim();
      if (!raw) return resolve({});
      try {
        resolve(JSON.parse(raw));
      } catch {
        reject(new Error('request body is not valid JSON'));
      }
    });
  });
}

/** Constant-time `X-Api-Key` comparison. The key is always required — `config` guarantees one exists. */
function isAuthorized(req) {
  const supplied = req.headers['x-api-key'];
  if (typeof supplied !== 'string') return false;

  const a = Buffer.from(supplied);
  const b = Buffer.from(config.apiKey);
  return a.length === b.length && timingSafeEqual(a, b);
}

function otpMessage(phone, code) {
  return ['🔐 Balinyaar — login code', '', `phone: ${phone}`, `code:  ${code}`, '', new Date().toISOString()].join('\n');
}

function plainMessage(phone, message) {
  return ['📩 Balinyaar', '', `phone: ${phone}`, '', message].join('\n');
}

async function deliver(res, text, logLine) {
  if (config.chatIds.length === 0) {
    console.error('refused: TELEGRAM_CHAT_IDS is empty — nowhere to deliver');
    return json(res, 503, { ok: false, error: 'no recipients configured (TELEGRAM_CHAT_IDS is empty)' });
  }

  const { delivered, failed } = await broadcast(text);
  console.log(`${logLine} → delivered ${delivered.length}/${config.chatIds.length}`);
  for (const f of failed) console.error(`  ✗ ${f.chatId}: ${f.error}`);

  // Partial delivery still counts as sent — one reachable recipient is enough to read the code.
  // Only a total failure is reported upstream, so the API's OTP command fails loudly instead of
  // pretending an undeliverable code was sent.
  const status = delivered.length > 0 ? 200 : 502;
  return json(res, status, { ok: delivered.length > 0, delivered, failed });
}

const routes = {
  // The only unauthenticated route, and it deliberately echoes no configuration — it exists so a
  // caller can answer "is the relay up?" without holding the key.
  'GET /health': async (_req, res) => json(res, 200, { ok: true }),

  'GET /chat_ids': async (_req, res) => {
    const chats = await discoverChatIds();
    return json(res, 200, {
      ok: true,
      chats,
      hint: chats.length
        ? 'Copy the chat_id values into TELEGRAM_CHAT_IDS (comma-separated) and restart.'
        : 'No recent messages. Open the bot in Telegram, press Start / send it any message, then call this again.',
    });
  },

  'POST /send_otp': async (req, res) => {
    const { phone, code } = await readJsonBody(req);
    if (!phone || !code) return json(res, 400, { ok: false, error: '`phone` and `code` are required' });

    const shown = config.redactCodeInLogs ? '******' : code;
    console.log(`sending OTP ${shown} for ${phone} → ${config.chatIds.length} recipients`);
    return deliver(res, otpMessage(phone, code), `otp ${shown} for ${phone}`);
  },

  'POST /send': async (req, res) => {
    const { phone, message } = await readJsonBody(req);
    if (!phone || !message) return json(res, 400, { ok: false, error: '`phone` and `message` are required' });

    return deliver(res, plainMessage(phone, message), `message for ${phone}`);
  },
};

const server = createServer(async (req, res) => {
  const path = new URL(req.url, 'http://localhost').pathname.replace(/\/+$/, '') || '/';
  const key = `${req.method} ${path}`;
  const handler = routes[key];

  if (!handler) {
    return json(res, 404, { ok: false, error: `no route for ${key}`, routes: Object.keys(routes) });
  }

  if (key !== 'GET /health' && !isAuthorized(req)) {
    console.error(`401 ${key} from ${req.socket.remoteAddress} — missing or invalid X-Api-Key`);
    return json(res, 401, { ok: false, error: 'missing or invalid X-Api-Key' });
  }

  try {
    await handler(req, res);
  } catch (error) {
    console.error(`${key} failed:`, error.message);
    if (!res.headersSent) json(res, 500, { ok: false, error: error.message });
  }
});

server.listen(config.port, config.host, async () => {
  console.log(`balinyaar telegram-otp-bot listening on http://${config.host}:${config.port}`);
  console.log(`  recipients: ${config.chatIds.length ? config.chatIds.join(', ') : '(none — call GET /chat_ids to discover)'}`);
  console.log('  auth:       X-Api-Key required on every route except GET /health');
  console.log(`  proxy:      ${config.proxy ? config.proxy.label : '(none — direct to api.telegram.org)'}`);

  try {
    const me = await getBotIdentity();
    console.log(`  bot:        @${me.username} (${me.id})`);
  } catch (error) {
    console.error(`  bot:        token check FAILED — ${error.message}`);
  }
});

for (const signal of ['SIGINT', 'SIGTERM']) {
  process.on(signal, () => server.close(() => process.exit(0)));
}
