// Fastify app builder — separated from src/index.ts so tests can build and
// `.inject()` against an app instance without binding a real port.
// See docs/superpowers/plans/2026-07-31-local-app.md, Tasks 1-2.

import { existsSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import Fastify, { type FastifyInstance } from "fastify";
import fastifyStatic from "@fastify/static";
import fastifyMultipart from "@fastify/multipart";
import { createDb, type Db } from "./db.js";
import { companyContextPlugin } from "./companyContext.js";
import { registerCompanyRoutes } from "./companies.js";
import { registerReportRoutes } from "./reports.js";
import { registerSetupRoutes } from "./setup.js";
import { registerImportRoutes, MAX_UPLOAD_BYTES } from "./imports.js";
import { registerUniversalImportRoutes } from "./universalImport.js";
import { registerEntryRoutes } from "./entries.js";
import { registerAccountRoutes } from "./accounts.js";
import { registerCloseRoutes } from "./close.js";
import { registerBackupRoutes } from "./backupRoutes.js";
import { registerUpdateRoutes } from "./updateRoutes.js";
import { registerCustomerRoutes } from "./customers.js";
import { registerInvoiceRoutes } from "./invoices.js";
import { registerPaymentRoutes } from "./payments.js";
import { registerSettingsRoutes } from "./settings.js";
import { registerSalesTaxRoutes } from "./salesTax.js";
import { registerVendorRoutes } from "./vendors.js";
import { registerBillRoutes } from "./bills.js";
import { registerItemRoutes } from "./items.js";
import { registerPurchaseOrderRoutes } from "./purchaseOrders.js";
import { registerBuildRoutes } from "./builds.js";
import { registerInventoryAdjustmentRoutes } from "./inventoryAdjustments.js";
import { registerBillPaymentRoutes } from "./billPayments.js";
import { registerCheckRoutes } from "./checks.js";
import { registerBankRoutes } from "./bank.js";
import { registerRegisterRoutes } from "./register.js";
import { registerSampleRoutes } from "./sample/sampleRoutes.js";

const serverDir = path.dirname(fileURLToPath(import.meta.url));
const webDist = path.resolve(serverDir, "../../web/dist");

export interface AppOptions {
  /** Overrides APP_DATABASE_URL / the fallback — used by tests to point at a throwaway DB. */
  databaseUrl?: string;
  /** Overrides DATABASE_URL / the fallback — used by tests to point at a throwaway DB. */
  adminDatabaseUrl?: string;
  /**
   * Overrides the register CSV export's row cap (default 100k) — used by
   * tests to exercise the cap without seeding 100k rows.
   */
  registerCsvMaxRows?: number;
}

interface HealthResult {
  ok: boolean;
  db: boolean;
  migrations: string | null;
}

async function checkHealth(db: Db): Promise<HealthResult> {
  // Preferred: latest applied migration filename. The `app` role is not
  // granted SELECT on schema_migrations (it's a superuser-owned bookkeeping
  // table, see db/migrate.ts) so this commonly fails on permissions alone —
  // that's expected, not an error condition worth surfacing loudly.
  try {
    const res = await db.appPool.query<{ filename: string }>(
      "SELECT filename FROM schema_migrations ORDER BY applied_at DESC LIMIT 1",
    );
    return { ok: true, db: true, migrations: res.rows[0]?.filename ?? null };
  } catch {
    // Fall back to a bare reachability check; migrations state is simply
    // unknown to the app role in that case.
    try {
      await db.appPool.query("SELECT 1");
      return { ok: true, db: true, migrations: null };
    } catch {
      return { ok: false, db: false, migrations: null };
    }
  }
}

export async function buildApp(opts: AppOptions = {}): Promise<FastifyInstance> {
  const fastify = Fastify({ logger: false });

  // Awaited because createDb() may have to read this machine's instance.json to
  // find the bundled cluster (server/src/db.ts). It opens no connection and
  // cannot reject, so this stays a plain await rather than a startup gate.
  const db = await createDb({ appDatabaseUrl: opts.databaseUrl, adminDatabaseUrl: opts.adminDatabaseUrl });

  fastify.decorate("db", db);
  fastify.addHook("onClose", async () => {
    await db.close();
  });

  // Called directly and awaited (NOT via fastify.register()): register()
  // creates a new encapsulation context, and a preHandler hook added inside
  // one only applies to routes registered within that SAME context — it
  // would silently not apply to sibling routes registered directly on this
  // parent instance, which is exactly how registerCompanyRoutes (and every
  // future /api/c/* route module) registers its routes. Calling the plugin
  // as a plain function attaches the hook straight onto `fastify`, so it
  // applies globally no matter how many separate route modules register
  // handlers under that prefix over time.
  // @fastify/multipart wraps itself in fastify-plugin, so — like
  // companyContextPlugin above — this decorates `fastify` directly rather
  // than a new encapsulated child context, making request.file() available
  // to every sibling route module below (registerImportRoutes), not just
  // routes registered inside this call.
  await fastify.register(fastifyMultipart, {
    limits: { fileSize: MAX_UPLOAD_BYTES, files: 1 },
  });

  await companyContextPlugin(fastify);
  registerCompanyRoutes(fastify, db);
  registerReportRoutes(fastify, db);
  registerSetupRoutes(fastify, db);
  registerImportRoutes(fastify, db);
  registerUniversalImportRoutes(fastify, db);
  registerEntryRoutes(fastify, db);
  registerAccountRoutes(fastify, db);
  registerCloseRoutes(fastify, db);
  registerBackupRoutes(fastify);
  registerUpdateRoutes(fastify);
  registerCustomerRoutes(fastify, db);
  registerInvoiceRoutes(fastify, db);
  registerPaymentRoutes(fastify, db);
  registerSettingsRoutes(fastify, db);
  registerSalesTaxRoutes(fastify, db);
  registerVendorRoutes(fastify, db);
  registerBillRoutes(fastify, db);
  registerItemRoutes(fastify, db);
  registerPurchaseOrderRoutes(fastify, db);
  registerBuildRoutes(fastify, db);
  registerInventoryAdjustmentRoutes(fastify, db);
  registerBillPaymentRoutes(fastify, db);
  registerCheckRoutes(fastify, db);
  registerBankRoutes(fastify, db);
  registerRegisterRoutes(fastify, db, { csvMaxRows: opts.registerCsvMaxRows });
  registerSampleRoutes(fastify, db);

  fastify.get("/api/health", async () => checkHealth(db));

  if (existsSync(webDist)) {
    void fastify.register(fastifyStatic, {
      root: webDist,
      wildcard: false,
    });

    fastify.setNotFoundHandler((request, reply) => {
      if (request.method === "GET" && !request.url.startsWith("/api")) {
        void reply.sendFile("index.html");
        return;
      }
      void reply.code(404).send({ error: "not found" });
    });
  }

  return fastify;
}
