import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { config, ROOT, validateRuntimeConfig } from './config.js';
import { db, statements, transaction } from './db.js';
import { createPayPalOrder, capturePayPalOrder, getPayPalOrder, verifyCapturedProduct, verifyWebhookSignature } from './paypal.js';

validateRuntimeConfig();
if (!fs.existsSync(config.product.filePath)) throw new Error(`Protected product PDF not found: ${config.product.filePath}`);

const PUBLIC_DIR = path.join(ROOT, 'public');
const nowIso = () => new Date().toISOString();
const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
const randomToken = () => crypto.randomBytes(32).toString('base64url');
const cleanOrderId = (value) => typeof value === 'string' && /^[A-Za-z0-9-]{8,80}$/.test(value) ? value : null;

const MIME = {
  '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8',
  '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg',
  '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.xml': 'application/xml; charset=utf-8', '.txt': 'text/plain; charset=utf-8'
};

const rateBuckets = new Map();
function clientIp(req) {
  if (config.trustProxy) return String(req.headers['x-forwarded-for'] || '').split(',')[0].trim() || req.socket.remoteAddress || 'unknown';
  return req.socket.remoteAddress || 'unknown';
}
function allowed(req, key, limit, windowMs) {
  const bucketKey = `${key}:${clientIp(req)}`;
  const now = Date.now();
  const current = rateBuckets.get(bucketKey);
  if (!current || current.reset <= now) {
    rateBuckets.set(bucketKey, { count: 1, reset: now + windowMs });
    return true;
  }
  current.count += 1;
  return current.count <= limit;
}
setInterval(() => {
  const now = Date.now();
  for (const [key, value] of rateBuckets) if (value.reset <= now) rateBuckets.delete(key);
}, 60_000).unref();

function baseHeaders(contentType = 'text/html; charset=utf-8') {
  return {
    'Content-Type': contentType,
    'X-Content-Type-Options': 'nosniff',
    'X-Frame-Options': 'DENY',
    'Referrer-Policy': 'no-referrer',
    'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), payment=(self "https://www.paypal.com")',
    'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
    'Content-Security-Policy': "default-src 'self'; script-src 'self' https://www.paypal.com https://www.paypalobjects.com; connect-src 'self' https://www.paypal.com https://www.sandbox.paypal.com https://api-m.paypal.com https://api-m.sandbox.paypal.com; frame-src https://www.paypal.com https://www.sandbox.paypal.com; img-src 'self' data: https://www.paypalobjects.com; style-src 'self' 'unsafe-inline'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self' https://www.paypal.com https://www.sandbox.paypal.com; frame-ancestors 'none';",
    'Strict-Transport-Security': config.baseUrl.startsWith('https://') ? 'max-age=31536000; includeSubDomains' : 'max-age=0'
  };
}
function send(res, status, body, contentType = 'text/html; charset=utf-8', extra = {}) {
  const data = Buffer.isBuffer(body) ? body : Buffer.from(String(body));
  res.writeHead(status, { ...baseHeaders(contentType), 'Content-Length': data.length, ...extra });
  res.end(data);
}
function sendJson(res, status, payload) { send(res, status, JSON.stringify(payload), 'application/json; charset=utf-8', { 'Cache-Control': 'no-store' }); }
function sendFile(res, filePath, status = 200, cache = true) {
  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) return false;
  const ext = path.extname(filePath).toLowerCase();
  const size = fs.statSync(filePath).size;
  res.writeHead(status, { ...baseHeaders(MIME[ext] || 'application/octet-stream'), 'Content-Length': size, 'Cache-Control': cache && ext !== '.html' ? 'public, max-age=604800, immutable' : 'no-cache' });
  fs.createReadStream(filePath).pipe(res);
  return true;
}
async function readBody(req, maxBytes = 64 * 1024) {
  return new Promise((resolve, reject) => {
    const chunks = []; let size = 0;
    req.on('data', (chunk) => {
      size += chunk.length;
      if (size > maxBytes) { reject(new Error('Request body too large.')); req.destroy(); return; }
      chunks.push(chunk);
    });
    req.on('end', () => resolve(Buffer.concat(chunks)));
    req.on('error', reject);
  });
}
async function readJson(req, maxBytes) {
  const body = await readBody(req, maxBytes);
  if (!body.length) return {};
  return JSON.parse(body.toString('utf8'));
}

function entitlementForOrder(paypalOrderId, captureData) {
  const token = randomToken();
  const tokenHash = sha256(token);
  const expiresAt = new Date(Date.now() + config.downloadTokenMinutes * 60_000).toISOString();
  const updatedAt = nowIso();
  transaction(() => {
    const existing = statements.getOrder.get(paypalOrderId);
    if (!existing) throw new Error('Local order record was not found.');
    if (existing.download_used_at) {
      const error = new Error('This purchase has already delivered its one permitted download.');
      error.code = 'DOWNLOAD_USED';
      throw error;
    }
    if (existing.status === 'COMPLETED' && existing.paypal_capture_id) {
      statements.refreshUnusedToken.run(tokenHash, expiresAt, updatedAt, paypalOrderId);
    } else {
      statements.completeOrder.run(captureData.captureId, captureData.payerEmail, captureData.amount, captureData.currency, tokenHash, expiresAt, updatedAt, paypalOrderId);
    }
  });
  return { token, expiresAt };
}

function renderClaimPage(token) {
  const safeToken = encodeURIComponent(token);
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>Your companion is ready | Marloth Park Central</title><link rel="stylesheet" href="/styles.css"></head><body class="claim-body"><main class="claim-card"><img src="/assets/mpc-banner.webp" alt="Marloth Park Central" class="claim-logo"><p class="eyebrow">PAYMENT APPROVED</p><h1>Your companion is ready.</h1><p>Select the button once and allow the secure PDF download to complete.</p><form method="post" action="/download/${safeToken}"><button class="primary-button download-button" type="submit">Download companion now</button></form><p class="microcopy">This entitlement permits exactly one download and expires shortly.</p><a href="/" class="text-link">Return to the product page</a></main></body></html>`;
}
function renderUnavailable(heading, message) {
  return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${heading} | Marloth Park Central</title><link rel="stylesheet" href="/styles.css"></head><body class="claim-body"><main class="claim-card"><img src="/assets/mpc-banner.webp" alt="Marloth Park Central" class="claim-logo"><h1>${heading}</h1><p>${message}</p><a href="/" class="primary-button inline-button">Return to secure checkout</a><p class="microcopy">Support: <a href="mailto:${config.supportEmail}">${config.supportEmail}</a></p></main></body></html>`;
}

async function handleCreateOrder(req, res) {
  if (!allowed(req, 'create', 12, 10 * 60_000)) return sendJson(res, 429, { ok: false, error: 'Too many checkout attempts. Please wait and try again.' });
  try {
    const invoiceId = `MPC-${Date.now()}-${crypto.randomBytes(5).toString('hex').toUpperCase()}`;
    const order = await createPayPalOrder({ invoiceId });
    const timestamp = nowIso();
    statements.insertOrder.run(order.id, invoiceId, order.status || 'CREATED', config.product.price, config.product.currency, config.product.sku, timestamp, timestamp);
    sendJson(res, 200, { ok: true, id: order.id });
  } catch (error) {
    console.error('create-order', error);
    sendJson(res, 502, { ok: false, error: 'Secure checkout could not start. Please try again or contact support.' });
  }
}

async function handleCaptureOrder(req, res) {
  if (!allowed(req, 'capture', 12, 10 * 60_000)) return sendJson(res, 429, { ok: false, error: 'Too many checkout attempts. Please wait and try again.' });
  try {
    const payload = await readJson(req);
    const orderId = cleanOrderId(payload.orderID);
    if (!orderId) return sendJson(res, 400, { ok: false, error: 'Invalid PayPal order identifier.' });
    const localOrder = statements.getOrder.get(orderId);
    if (!localOrder) return sendJson(res, 400, { ok: false, error: 'This checkout session was not created by this store.' });
    let orderData = await capturePayPalOrder(orderId);
    if (orderData?.alreadyCaptured) orderData = await getPayPalOrder(orderId);
    const captureData = verifyCapturedProduct(orderData);
    const entitlement = entitlementForOrder(orderId, captureData);
    sendJson(res, 200, { ok: true, status: 'COMPLETED', downloadToken: entitlement.token, expiresAt: entitlement.expiresAt });
  } catch (error) {
    console.error('capture-order', error);
    if (error.code === 'DOWNLOAD_USED') return sendJson(res, 410, { ok: false, error: error.message });
    sendJson(res, 422, { ok: false, error: 'Payment could not be verified as completed. No download was issued.' });
  }
}

async function handleWebhook(req, res) {
  try {
    const body = await readBody(req, 256 * 1024);
    const event = JSON.parse(body.toString('utf8'));
    const valid = await verifyWebhookSignature(req.headers, event);
    if (!valid) return sendJson(res, 400, { ok: false, error: 'Invalid webhook signature.' });
    const inserted = statements.insertWebhookEvent.run(event.id, event.event_type, nowIso());
    if (!inserted.changes) return sendJson(res, 200, { ok: true, duplicate: true });
    const resource = event.resource || {};
    const orderId = resource?.supplementary_data?.related_ids?.order_id || resource?.invoice_id || '';
    if (orderId) {
      const statusMap = { 'PAYMENT.CAPTURE.COMPLETED': 'COMPLETED', 'PAYMENT.CAPTURE.DENIED': 'DENIED', 'PAYMENT.CAPTURE.REFUNDED': 'REFUNDED', 'PAYMENT.CAPTURE.REVERSED': 'REVERSED' };
      const nextStatus = statusMap[event.event_type];
      if (nextStatus) statements.updateStatus.run(nextStatus, nowIso(), nextStatus, orderId);
    }
    sendJson(res, 200, { ok: true });
  } catch (error) {
    console.error('webhook', error);
    sendJson(res, 400, { ok: false, error: 'Webhook could not be processed.' });
  }
}

function handleClaim(res, token) {
  if (!/^[A-Za-z0-9_-]{30,100}$/.test(token)) return send(res, 200, renderUnavailable('Invalid download link', 'Return to the secure checkout page to purchase the companion.'));
  const order = statements.getOrderByToken.get(sha256(token));
  if (!order || order.status !== 'COMPLETED' || order.download_used_at || !order.download_expires_at || order.download_expires_at <= nowIso()) {
    return send(res, 200, renderUnavailable('Download unavailable', 'This entitlement has expired or has already been used. A new purchase is required for another download.'));
  }
  send(res, 200, renderClaimPage(token));
}

function handleDownload(req, res, token) {
  if (!allowed(req, 'download', 8, 60_000)) return send(res, 429, renderUnavailable('Please wait', 'Too many download requests were received. Wait one minute and use the secure button once.'));
  if (!/^[A-Za-z0-9_-]{30,100}$/.test(token)) return send(res, 400, renderUnavailable('Invalid download request', 'The secure download token is invalid.'));
  const tokenHash = sha256(token);
  const order = statements.getOrderByToken.get(tokenHash);
  const usedAt = nowIso();
  if (!order || order.status !== 'COMPLETED' || order.download_used_at || !order.download_expires_at || order.download_expires_at <= usedAt) {
    return send(res, 410, renderUnavailable('Download already used or expired', 'This purchase no longer has an available download. A new purchase is required for another copy.'));
  }
  const result = transaction(() => statements.consumeToken.run(usedAt, usedAt, order.id, usedAt, tokenHash));
  if (result.changes !== 1) return send(res, 410, renderUnavailable('Download already used', 'This entitlement was consumed by another request. A new purchase is required for another copy.'));
  const stat = fs.statSync(config.product.filePath);
  res.writeHead(200, {
    ...baseHeaders('application/pdf'),
    'Content-Disposition': `attachment; filename="${config.product.downloadName}"`,
    'Content-Length': stat.size,
    'Cache-Control': 'private, no-store, max-age=0',
    Pragma: 'no-cache'
  });
  fs.createReadStream(config.product.filePath).pipe(res);
}

function servePublic(res, pathname) {
  const clean = pathname === '/' ? '/index.html' : pathname;
  const decoded = decodeURIComponent(clean);
  const candidate = path.resolve(PUBLIC_DIR, `.${decoded}`);
  if (!candidate.startsWith(PUBLIC_DIR + path.sep)) return false;
  let filePath = candidate;
  if (!path.extname(filePath)) filePath += '.html';
  return sendFile(res, filePath, 200, true);
}

const server = http.createServer(async (req, res) => {
  try {
    const url = new URL(req.url, config.baseUrl);
    const pathname = url.pathname;

    if (req.method === 'GET' && pathname === '/health') return sendJson(res, 200, { ok: true, service: 'mpc-companion-sales', version: '1.0.0', paypal: config.paypalEnv });
    if (req.method === 'GET' && pathname === '/api/config') {
      return sendJson(res, 200, { ok: true, paypalClientId: config.paypalClientId, paypalEnv: config.paypalEnv, product: { name: config.product.name, price: config.product.price, currency: config.product.currency, compareAt: config.product.compareAt } });
    }
    if (req.method === 'POST' && pathname === '/api/paypal/create-order') return await handleCreateOrder(req, res);
    if (req.method === 'POST' && pathname === '/api/paypal/capture-order') return await handleCaptureOrder(req, res);
    if (req.method === 'POST' && pathname === '/api/paypal/webhook') return await handleWebhook(req, res);
    if (req.method === 'GET' && pathname.startsWith('/claim/')) return handleClaim(res, decodeURIComponent(pathname.slice('/claim/'.length)));
    if (req.method === 'POST' && pathname.startsWith('/download/')) return handleDownload(req, res, decodeURIComponent(pathname.slice('/download/'.length)));
    if (req.method === 'GET' && pathname.startsWith('/api/admin/order/')) {
      if (!config.adminApiKey || req.headers['x-admin-key'] !== config.adminApiKey) return sendJson(res, 401, { ok: false, error: 'Admin authentication required.' });
      const id = decodeURIComponent(pathname.slice('/api/admin/order/'.length));
      const row = statements.getOrderForAdmin.get(id, id, id);
      return sendJson(res, 200, { ok: true, order: row || null });
    }
    if (req.method === 'GET' && pathname === '/robots.txt') return send(res, 200, 'User-agent: *\nAllow: /\nDisallow: /api/\nDisallow: /claim/\nDisallow: /download/\n', 'text/plain; charset=utf-8');
    if (req.method === 'GET' && pathname === '/sitemap.xml') return send(res, 200, `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>${config.baseUrl}/</loc></url><url><loc>${config.baseUrl}/privacy</loc></url><url><loc>${config.baseUrl}/terms</loc></url><url><loc>${config.baseUrl}/refunds</loc></url></urlset>`, 'application/xml; charset=utf-8');
    if (pathname.startsWith('/api/')) return sendJson(res, 200, { ok: false, error: 'Unknown API route.' });
    if (req.method === 'GET' && servePublic(res, pathname)) return;
    return sendFile(res, path.join(PUBLIC_DIR, 'not-found.html'), 200, false);
  } catch (error) {
    console.error('request-error', error);
    if (!res.headersSent) sendJson(res, 500, { ok: false, error: 'A secure server error occurred. No payment or download was completed.' });
    else res.destroy(error);
  }
});

server.listen(config.port, '0.0.0.0', () => console.log(`MPC companion sales server running on ${config.baseUrl} (port ${config.port}, PayPal ${config.paypalEnv})`));
function shutdown(signal) {
  console.log(`${signal}: shutting down`);
  server.close(() => { db.close(); process.exit(0); });
  setTimeout(() => process.exit(1), 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
