import { Router, Request, Response } from 'express';
import { timingSafeEqual } from 'crypto';
import { prisma } from '../db/prisma';
import { requireAuth } from '../middleware/auth';
import {
  createIncomingInvoice,
  verifyIncomingPaymentPaid,
  getPublicPaymentStatus,
} from '../services/lnbits';

const router = Router();

const MAX_CONTENT = 300;
const MAX_NAME = 80;
const MESSAGE_PRICE_SATS = Math.max(1, parseInt(process.env.MESSAGE_PRICE_SATS || '1000', 10) || 1000);
const WEBHOOK_BASE = (process.env.WEBHOOK_BASE_URL || process.env.FRONTEND_URL || 'http://localhost:3000').replace(
  /\/$/,
  ''
);
const WEBHOOK_SECRET = process.env.LNBITS_WEBHOOK_SECRET || '';

function sanitizeText(s: string, max: number): string {
  const t = s
    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '')
    .replace(/<[^>]*>/g, '')
    .trim();
  return t.length > max ? t.slice(0, max) : t;
}

/** LNbits sends `json=payment.json()` where `.json()` returns a Pydantic JSON
 *  string — httpx double-encodes it, so Express may parse the body as a plain
 *  string instead of an object. Unwrap up to two layers of JSON encoding. */
function normalizeWebhookBody(raw: unknown): unknown {
  let v = raw;
  for (let i = 0; i < 2 && typeof v === 'string'; i++) {
    try { v = JSON.parse(v); } catch { break; }
  }
  return v;
}

const HEX64 = /^[a-f0-9]{64}$/i;

function extractPaymentHash(body: unknown): string | null {
  const o = normalizeWebhookBody(body);
  if (!o || typeof o !== 'object') return null;
  const r = o as Record<string, unknown>;
  for (const key of ['payment_hash', 'paymentHash', 'checking_id']) {
    const v = r[key];
    if (typeof v === 'string' && HEX64.test(v)) return v.toLowerCase();
  }
  const p = r.payment;
  if (p && typeof p === 'object') {
    const nested = p as Record<string, unknown>;
    for (const key of ['payment_hash', 'paymentHash', 'checking_id']) {
      const v = nested[key];
      if (typeof v === 'string' && HEX64.test(v)) return v.toLowerCase();
    }
  }
  return null;
}

function verifyWebhookSecret(req: Request): boolean {
  if (!WEBHOOK_SECRET) return true;
  const h = req.headers['x-bbe-webhook-secret'];
  if (typeof h === 'string' && safeEqualStr(h, WEBHOOK_SECRET)) return true;
  const auth = req.headers.authorization;
  if (auth?.startsWith('Bearer ')) {
    const t = auth.slice(7);
    if (safeEqualStr(t, WEBHOOK_SECRET)) return true;
  }
  const q = req.query.token;
  const token = typeof q === 'string' ? q : Array.isArray(q) ? q[0] : '';
  if (typeof token === 'string' && token.length > 0 && safeEqualStr(token, WEBHOOK_SECRET)) {
    return true;
  }
  return false;
}

function safeEqualStr(a: string, b: string): boolean {
  try {
    const ab = Buffer.from(a, 'utf8');
    const bb = Buffer.from(b, 'utf8');
    if (ab.length !== bb.length) return false;
    return timingSafeEqual(ab, bb);
  } catch {
    return false;
  }
}

/** Public: board config for the frontend */
router.get('/config', (_req: Request, res: Response) => {
  res.json({
    priceSats: MESSAGE_PRICE_SATS,
    bbeZapPubkey: process.env.BOARD_ZAP_PUBKEY || null,
    bbeZapAddress: process.env.BOARD_ZAP_LN_ADDRESS || null,
  });
});

/** Create Lightning invoice for a new message (no BoardMessage row yet). */
router.post('/invoice', async (req: Request, res: Response) => {
  try {
    const { content, name, pubkey, profilePic, postAsAnon } = req.body as Record<string, unknown>;

    if (typeof content !== 'string') {
      res.status(400).json({ error: 'content is required' });
      return;
    }

    const cleanContent = sanitizeText(content, MAX_CONTENT);
    if (!cleanContent) {
      res.status(400).json({ error: 'content is empty' });
      return;
    }

    let guestName: string | null = null;
    let authorPubkey: string | null = null;
    let pic: string | null = null;

    const pk = typeof pubkey === 'string' && /^[a-f0-9]{64}$/i.test(pubkey) ? pubkey.toLowerCase() : null;
    const asAnon = postAsAnon === true;

    if (pk && !asAnon) {
      authorPubkey = pk;
      if (typeof profilePic === 'string' && profilePic.length > 0 && profilePic.length < 2048) {
        pic = profilePic.slice(0, 2048);
      }
      const n = typeof name === 'string' ? sanitizeText(name, MAX_NAME) : '';
      guestName = n || null;
    } else {
      const n = typeof name === 'string' ? sanitizeText(name, MAX_NAME) : '';
      guestName = n || null;
    }

    const webhookPath = WEBHOOK_SECRET
      ? `/api/messages/webhook?token=${encodeURIComponent(WEBHOOK_SECRET)}`
      : '/api/messages/webhook';
    const webhookUrl = `${WEBHOOK_BASE}${webhookPath}`;

    const { payment_hash, payment_request, checking_id } = await createIncomingInvoice({
      amountSats: MESSAGE_PRICE_SATS,
      memo: 'BBE message board',
      webhookUrl,
    });

    await prisma.boardInvoicePending.upsert({
      where: { paymentHash: payment_hash },
      create: {
        paymentHash: payment_hash,
        content: cleanContent,
        guestName,
        pubkey: authorPubkey,
        profilePic: pic,
      },
      update: {
        content: cleanContent,
        guestName,
        pubkey: authorPubkey,
        profilePic: pic,
      },
    });

    res.json({
      payment_request,
      checking_id,
      payment_hash,
    });
  } catch (err) {
    console.error('Board invoice error:', err);
    const msg = err instanceof Error ? err.message : 'Internal server error';
    if (msg.includes('LNBITS_API_KEY')) {
      res.status(503).json({ error: 'Lightning payments are not configured' });
      return;
    }
    res.status(500).json({ error: msg });
  }
});

/**
 * Shared logic: verify payment on LNbits, look up pending row, create BoardMessage.
 * Returns { ok, duplicate?, ignored?, error? }.
 */
async function promotePendingToMessage(paymentHash: string): Promise<{
  ok: boolean;
  duplicate?: boolean;
  ignored?: string;
  error?: string;
}> {
  const existing = await prisma.boardMessage.findUnique({ where: { paymentHash } });
  if (existing) {
    await prisma.boardInvoicePending.deleteMany({ where: { paymentHash } });
    return { ok: true, duplicate: true };
  }

  const verified = await verifyIncomingPaymentPaid(paymentHash);
  if (!verified.paid) {
    return { ok: true, ignored: 'not_paid' };
  }

  const expectedMsat = MESSAGE_PRICE_SATS * 1000;
  if (verified.amountMsat != null && verified.amountMsat < expectedMsat) {
    console.warn('Board: amount mismatch', paymentHash, verified.amountMsat);
    return { ok: true, ignored: 'amount' };
  }

  const pending = await prisma.boardInvoicePending.findUnique({ where: { paymentHash } });
  if (!pending) {
    return { ok: true, ignored: 'no_pending' };
  }

  const authorName = pending.pubkey
    ? pending.guestName && pending.guestName.length > 0
      ? pending.guestName
      : 'Nostr user'
    : pending.guestName && pending.guestName.length > 0
      ? pending.guestName
      : 'anon';

  try {
    await prisma.$transaction([
      prisma.boardMessage.create({
        data: {
          paymentHash,
          content: pending.content,
          authorName,
          pubkey: pending.pubkey,
          profilePic: pending.profilePic,
          satsPaid: MESSAGE_PRICE_SATS,
          status: 'active',
        },
      }),
      prisma.boardInvoicePending.delete({ where: { paymentHash } }),
    ]);
  } catch (e: unknown) {
    const code = e && typeof e === 'object' && 'code' in e ? (e as { code: string }).code : '';
    if (code === 'P2002') {
      await prisma.boardInvoicePending.deleteMany({ where: { paymentHash } });
      return { ok: true, duplicate: true };
    }
    throw e;
  }

  return { ok: true };
}

/** LNbits calls this when an invoice is paid */
router.post('/webhook', async (req: Request, res: Response) => {
  try {
    if (!verifyWebhookSecret(req)) {
      res.status(401).json({ error: 'Unauthorized' });
      return;
    }

    const paymentHash = extractPaymentHash(req.body);
    if (!paymentHash) {
      const preview = typeof req.body === 'string' ? req.body.slice(0, 200) : JSON.stringify(req.body).slice(0, 200);
      console.warn('Board webhook: could not extract payment_hash', typeof req.body, preview);
      res.status(200).json({ ok: false, error: 'missing payment_hash' });
      return;
    }

    const result = await promotePendingToMessage(paymentHash);
    res.json(result);
  } catch (err) {
    console.error('Board webhook error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

/** Client-side reconciliation: verify payment and create message if webhook was missed. */
router.post('/confirm/:paymentHash', async (req: Request, res: Response) => {
  try {
    const raw = req.params.paymentHash;
    const paymentHash = Array.isArray(raw) ? raw[0] : raw;
    if (!paymentHash || !HEX64.test(paymentHash)) {
      res.status(400).json({ error: 'invalid payment_hash' });
      return;
    }
    const result = await promotePendingToMessage(paymentHash.toLowerCase());
    res.json(result);
  } catch (err) {
    console.error('Board confirm error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

/** Poll payment status (server-side LNbits public API — avoids CORS). */
router.get('/payment/:paymentHash/status', async (req: Request, res: Response) => {
  try {
    const raw = req.params.paymentHash;
    const paymentHash = Array.isArray(raw) ? raw[0] : raw;
    if (!paymentHash || !/^[a-f0-9]{64}$/i.test(paymentHash)) {
      res.status(400).json({ error: 'invalid payment_hash' });
      return;
    }
    const hash = paymentHash.toLowerCase();
    const paid = await getPublicPaymentStatus(hash);
    const message = await prisma.boardMessage.findUnique({
      where: { paymentHash: hash },
      select: { id: true },
    });
    res.json({ paid: paid || !!message, messageCreated: !!message });
  } catch (err) {
    console.error('Board payment status error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

/** Public list: active messages only */
router.get('/', async (_req: Request, res: Response) => {
  try {
    const messages = await prisma.boardMessage.findMany({
      where: { status: 'active' },
      orderBy: { createdAt: 'desc' },
      select: {
        id: true,
        paymentHash: true,
        content: true,
        authorName: true,
        pubkey: true,
        profilePic: true,
        satsPaid: true,
        likeCount: true,
        createdAt: true,
      },
    });
    res.json(messages);
  } catch (err) {
    console.error('List board messages error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

/** Like (Nostr-attributed messages only; logged-in users) */
router.post('/:id/like', requireAuth, async (req: Request, res: Response) => {
  try {
    const rawId = req.params.id;
    const id = Array.isArray(rawId) ? rawId[0] : rawId;
    const msg = await prisma.boardMessage.findUnique({ where: { id } });
    if (!msg || msg.status !== 'active') {
      res.status(404).json({ error: 'Message not found' });
      return;
    }
    if (!msg.pubkey) {
      res.status(400).json({ error: 'Likes are only for Nostr-attributed messages' });
      return;
    }
    const updated = await prisma.boardMessage.update({
      where: { id: msg.id },
      data: { likeCount: { increment: 1 } },
      select: { likeCount: true },
    });
    res.json({ likeCount: updated.likeCount });
  } catch (err) {
    console.error('Board like error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

export default router;
