/**
 * The PIG server.
 *
 * Serves the API and, in production, the built front end from the same origin.
 * Same-origin matters more than it might appear: the browser holds its auth
 * session per origin, so splitting the app across two hostnames turns sign-in
 * into a loop that looks like a broken deployment.
 */
import { serve } from '@hono/node-server';
import { serveStatic } from '@hono/node-server/serve-static';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { createDatabase } from '@pig/db';
import { createApp } from './app';
import { loadConfig } from './lib/config';
import { startPrimeSync } from './services/sync';
import { reconcileInviteCode } from './services/bootstrap';
import { CapacityService } from './services/capacity';
import { effectivePrimeSyncConfig } from './routes/admin-settings';
import { NotificationOutbox, NotificationWorker } from './services/notification-outbox';
import { SlackNotifier } from './services/slack';
import { BuzzNotifier } from './services/buzz';

const config = loadConfig();
const db = createDatabase({ url: config.DATABASE_URL });
let stopSync = () => {};
async function reloadPrimeSync(): Promise<void> {
  stopSync();
  stopSync = startPrimeSync(await effectivePrimeSyncConfig(config, db), db);
}
const app = createApp(config, db, undefined, { onPlatformSettingsChanged: reloadPrimeSync });

// Serve the built SPA when it exists. Absent in development, where Vite serves
// it on its own port with hot reload.
const webDist = join(process.cwd(), 'apps/web/dist');
if (existsSync(webDist)) {
  /*
   * Serve the ENTIRE dist directory, not just /assets.
   *
   * Serving only /assets and letting everything else fall through to the SPA
   * shell quietly breaks every top-level file: og.png, the icons, the manifest
   * and robots.txt each returned `200 text/html` containing index.html.
   *
   * That failure is close to invisible. The app works, the browser tab shows
   * an icon (from the SVG, which browsers cache aggressively), and nothing
   * errors — but a link shared to iMessage, Slack or X fetches og.png, gets
   * HTML, and renders a card with no image. Verified with a real request from
   * an outside host rather than assumed, because that is the only way this
   * class of bug shows up.
   *
   * serveStatic calls next() when there is no matching file, so unmatched
   * paths still reach the SPA fallback below.
   */
  app.use('*', async (c, next) => {
    // API routes must never be answered from disk.
    if (new URL(c.req.url).pathname.startsWith('/api/')) return next();
    return serveStatic({ root: './apps/web/dist' })(c, next);
  });

  /*
   * Client-side routes (/margin, /capacity, …) have no file on disk and must
   * receive the shell so the router can take over.
   *
   * The `/api/` guard is repeated here deliberately. Without it an unknown API
   * path — a typo, a renamed endpoint, an older client — falls through to this
   * fallback and returns **HTTP 200 with the SPA's HTML**. That is close to the
   * worst possible failure for an API consumer: `response.ok` is true, so
   * nothing treats it as an error, and the caller then fails on `JSON.parse`
   * with "Unexpected token '<'" a long way from the actual cause. The MCP
   * server, the CLI and Piggy all consume this API and would all have hit it.
   *
   * Confirmed against production before fixing: an authenticated GET to
   * /api/keys (the real path is /api/api-keys) returned 200 text/html.
   */
  app.get('*', async (c, next) => {
    if (new URL(c.req.url).pathname.startsWith('/api/')) return next();
    return serveStatic({ path: './apps/web/dist/index.html' })(c, next);
  });
  console.log('[pig] serving front end from', webDist);
}

// Reconcile configuration into the database before accepting traffic, so a
// freshly set invite code works on the first request rather than the second.
await reconcileInviteCode(config, db).catch((error) =>
  console.error('[pig] could not reconcile the invite code:', error),
);

const server = serve({ fetch: app.fetch, port: config.PIG_PORT }, (info) => {
  console.log(`[pig] listening on http://localhost:${info.port}`);
  console.log(`[pig] environment: ${config.NODE_ENV}`);
});

// Background work. Both are optional and the application is fully usable with
// neither running.
await reloadPrimeSync();

const capacity = new CapacityService(db);
const notificationOutbox = new NotificationOutbox(db);
const slackNotificationsEnabled = Boolean(config.SLACK_BOT_TOKEN);
const buzzNotificationsEnabled = Boolean(config.BUZZ_RELAY_URL && config.BUZZ_PRIVATE_KEY);
const stopSlackNotifications = config.SLACK_BOT_TOKEN
  ? new NotificationWorker(db, new SlackNotifier({ botToken: config.SLACK_BOT_TOKEN })).start()
  : () => {};
const stopBuzzNotifications = config.BUZZ_RELAY_URL && config.BUZZ_PRIVATE_KEY
  ? new NotificationWorker(
      db,
      new BuzzNotifier({
        relayUrl: config.BUZZ_RELAY_URL,
        privateKey: config.BUZZ_PRIVATE_KEY,
        authTag: config.BUZZ_AUTH_TAG,
      }),
    ).start()
  : () => {};
let checkingIdleCapacity = false;
const checkIdleCapacity = () => {
  if (checkingIdleCapacity || (!slackNotificationsEnabled && !buzzNotificationsEnabled)) return;
  checkingIdleCapacity = true;
  void capacity
    .idleCapacity({ thresholdPct: 0.25, withinDays: 30 })
    .then((rows) => notificationOutbox.enqueueIdleCapacity(rows))
    .catch((error) => console.error('[pig] idle-capacity notification queue failed', error))
    .finally(() => {
      checkingIdleCapacity = false;
    });
};
const idleNotificationTimer = setInterval(checkIdleCapacity, 6 * 60 * 60 * 1_000);
idleNotificationTimer.unref();
checkIdleCapacity();
const holdSweeper = setInterval(
  () => {
    void capacity
      .sweepExpiredHolds()
      .then((n) => n > 0 && console.log(`[pig] released ${n} expired capacity hold(s)`))
      .catch((error) => console.error('[pig] hold sweep failed', error));
  },
  5 * 60 * 1000,
);

/**
 * Shut down cleanly so that in-flight requests finish and Postgres connections
 * are returned rather than left for the server to reap.
 */
function shutdown(signal: string) {
  console.log(`[pig] ${signal} received, shutting down`);
  clearInterval(holdSweeper);
  clearInterval(idleNotificationTimer);
  stopSlackNotifications();
  stopBuzzNotifications();
  stopSync();
  server.close(() => process.exit(0));
  // Do not hang forever if a connection refuses to drain.
  setTimeout(() => process.exit(1), 10_000).unref();
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
