import { seedAdmin } from '@bmm/auth';
import cookie from '@fastify/cookie';
import cors from '@fastify/cors';
import websocket from '@fastify/websocket';
import Fastify from 'fastify';
import { config } from './config.js';
import { ensureActiveKey } from './lib/crypto.js';
import { validateStripePriceConfig } from './lib/stripe.js';
import { accountRoutes } from './routes/account.js';
import { adminRoutes } from './routes/admin.js';
import { authRoutes } from './routes/auth.js';
import { billingRoutes } from './routes/billing.js';
import { oauthRoutes } from './routes/oauth.js';
import { serverRoutes } from './routes/servers.js';
import { settingsRoutes } from './routes/settings.js';
import { supportRoutes } from './routes/support.js';
import { templateRoutes } from './routes/templates.js';

// Stripe webhook signature verification requires the raw request body, so we
// stash a copy on req.rawBody during JSON parsing. Merges into Fastify's
// FastifyRequest interface declaration alongside `user` from plugins/session.
declare module 'fastify' {
  interface FastifyRequest {
    rawBody?: Buffer;
  }
}

const app = Fastify({
  logger: {
    level: config.NODE_ENV === 'production' ? 'info' : 'debug',
  },
  // We run behind nginx + Cloudflare — both prepend the real client IP into
  // X-Forwarded-For. Without trustProxy=true Fastify reports the nginx peer
  // (always 127.0.0.1 / docker-bridge) for req.ip, which silently collapses
  // every per-IP rate-limit into a single global counter. (See Z3-001.)
  trustProxy: true,
});

// Replace the default JSON parser with one that keeps the raw buffer for the
// Stripe-webhook signature check. Must run BEFORE any route registration.
app.addContentTypeParser(
  'application/json',
  { parseAs: 'buffer' },
  (req, body, done) => {
    const buf = body as Buffer;
    req.rawBody = buf;
    if (buf.length === 0) return done(null, undefined);
    try {
      done(null, JSON.parse(buf.toString('utf8')));
    } catch (err) {
      done(err as Error, undefined);
    }
  },
);

// RFC 6749 §3.2 makes application/x-www-form-urlencoded the mandatory wire
// format for the OAuth token endpoint, and most DCR-emitting clients
// (Claude Desktop included) post it that way without negotiating. Fastify
// has no built-in parser for it, so without this every POST /oauth/token
// hit 415 before reaching our handler. Parsed into a plain object so the
// existing zod schemas don't need to change.
app.addContentTypeParser(
  'application/x-www-form-urlencoded',
  { parseAs: 'string' },
  (_req, body, done) => {
    const text = body as string;
    if (!text) return done(null, {});
    try {
      const params = new URLSearchParams(text);
      const out: Record<string, string> = {};
      for (const [k, v] of params) out[k] = v;
      done(null, out);
    } catch (err) {
      done(err as Error, undefined);
    }
  },
);

await app.register(cors, {
  origin: [config.NEXT_PUBLIC_APP_URL],
  credentials: true,
});
await app.register(cookie);
await app.register(websocket, { options: { maxPayload: 1024 * 1024 } });

app.get('/health', async () => ({ ok: true, ts: Date.now() }));

// Fail-closed: initialize envelope encryption before serving any request that
// could write a secret. If the encryption subsystem can't come up, don't run.
try {
  await ensureActiveKey();
  app.log.info('[crypto] envelope encryption ready');
} catch (err) {
  app.log.error({ err }, '[crypto] failed to initialize encryption — refusing to start');
  process.exit(1);
}

await app.register(authRoutes);
await app.register(serverRoutes);
await app.register(oauthRoutes);
await app.register(settingsRoutes);
await app.register(adminRoutes);
await app.register(templateRoutes);
await app.register(billingRoutes);
await app.register(supportRoutes);
await app.register(accountRoutes);

// Loud warning if STRIPE_PRICE_* env vars are set to product ids (prod_…)
// instead of price ids (price_…). Stripe Checkout would silently 400 — easier
// to find at boot.
validateStripePriceConfig({ warn: (msg) => app.log.warn(msg) });

// Bootstrap admin user from env (idempotent)
if (config.ADMIN_EMAIL && config.ADMIN_PASSWORD) {
  try {
    const result = await seedAdmin({
      email: config.ADMIN_EMAIL,
      password: config.ADMIN_PASSWORD,
      name: config.ADMIN_NAME,
    });
    app.log.info(
      { email: config.ADMIN_EMAIL, created: result.created },
      `[admin-seed] ${result.created ? 'created' : 'updated'} admin user`,
    );
  } catch (err) {
    app.log.error({ err }, '[admin-seed] failed to seed admin user');
  }
}

app.setErrorHandler((err, _req, reply) => {
  app.log.error(err);
  if (!reply.sent) {
    reply.code(err.statusCode ?? 500).send({ error: err.message ?? 'internal_error' });
  }
});

try {
  await app.listen({ port: config.PORT, host: '0.0.0.0' });
  app.log.info(`api listening on http://localhost:${config.PORT}`);
} catch (err) {
  app.log.error(err);
  process.exit(1);
}
