// Invoicing settings — AR milestone closeout, decision 2
// (db/migrations/0008_ar_closeout.sql's set_next_invoice_no()). Same
// route-module pattern as customers.ts/invoices.ts: every route lives under
// /api/c/ (company scope from X-Company-Id, enforced by companyContextPlugin)
// and every query/insert runs inside db.withCompany() so RLS is the real
// guard (CLAUDE.md §4/§12), never just the WHERE clause written here.
// created_by/p_user is always the company's owner user (see resolveOwner
// below) — same pattern as entries.ts/invoices.ts/payments.ts: there's no
// login in v1.
//
// invoice_counters.next_invoice_no is app-SELECT-only (0005_ar.sql) —
// set_next_invoice_no() is the only write path, mirroring every other AR
// posting function. It refuses (English, mapped in errors.ts) to set a
// number that would not exceed the highest invoice_no this company has ever
// actually issued — a fast-forward, never a rewind.

import type { FastifyInstance } from "fastify";
import { z } from "zod";
import type { Db } from "./db.js";
import { mapPgError } from "./errors.js";
import { findOwnerId } from "../../tools/lib/journal-load.js";

const nextInvoiceNoSchema = z.object({
  nextInvoiceNo: z.coerce
    .number({ error: "Enter a whole invoice number." })
    .int("Enter a whole number, with no decimal point.")
    .min(1, "Enter a number of 1 or more.")
    .max(999_999_999_999_999, "That number is too large."),
});

function zodMessage(issues: Array<{ message: string }>): string {
  return issues.map((i) => i.message).join("; ");
}

const NO_OWNER_ERROR = {
  status: 500,
  error: "This company doesn't have an owner user on file, so this can't be attributed. This shouldn't happen.",
};

/** Mirrors entries.ts/invoices.ts/payments.ts's resolveOwner — every write
 *  here is attributed to the company's owner user (no login in v1). */
async function resolveOwner(
  db: Db, companyId: string, log: { error: (obj: unknown, msg: string) => void },
): Promise<string | null> {
  try {
    return await findOwnerId(db.adminPool, companyId);
  } catch (err) {
    log.error({ err, companyId }, "resolveOwner: findOwnerId failed");
    return null;
  }
}

interface InvoicingSettings { nextInvoiceNo: string; highestUsed: string | null }

async function readInvoicingSettings(
  client: import("pg").PoolClient, companyId: string,
): Promise<InvoicingSettings> {
  const counterRes = await client.query<{ next_invoice_no: string }>(
    "SELECT next_invoice_no FROM invoice_counters WHERE company_id = $1",
    [companyId],
  );
  const highestRes = await client.query<{ max: string | null }>(
    "SELECT max(invoice_no)::text AS max FROM invoices WHERE company_id = $1",
    [companyId],
  );
  return {
    // No invoice_counters row yet (never issued) means the next number is
    // still the table's own default of 1 — same default issue_invoice()
    // relies on via its own `ON CONFLICT (company_id) DO NOTHING` insert.
    nextInvoiceNo: counterRes.rows[0]?.next_invoice_no ?? "1",
    highestUsed: highestRes.rows[0]?.max ?? null,
  };
}

export function registerSettingsRoutes(fastify: FastifyInstance, db: Db): void {
  // -------------------------------------------------------------------------
  // GET /api/c/settings/invoicing
  // -------------------------------------------------------------------------
  fastify.get("/api/c/settings/invoicing", async (request, reply) => {
    const companyId = request.companyId!;
    try {
      const result = await db.withCompany(companyId, (client) => readInvoicingSettings(client, companyId));
      return reply.send(result);
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "get invoicing settings failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });

  // -------------------------------------------------------------------------
  // PUT /api/c/settings/invoicing  { nextInvoiceNo }
  // -------------------------------------------------------------------------
  fastify.put("/api/c/settings/invoicing", async (request, reply) => {
    const companyId = request.companyId!;
    const parsed = nextInvoiceNoSchema.safeParse(request.body);
    if (!parsed.success) {
      return reply.code(400).send({ error: zodMessage(parsed.error.issues) });
    }

    const ownerId = await resolveOwner(db, companyId, request.log);
    if (ownerId === null) {
      return reply.code(NO_OWNER_ERROR.status).send({ error: NO_OWNER_ERROR.error });
    }

    try {
      const result = await db.withCompany(companyId, async (client) => {
        // Deliberately let set_next_invoice_no()'s own "must be greater than
        // the highest used" RAISE propagate and flow through mapPgError,
        // rather than pre-checking the max ourselves — same pattern as
        // invoices.ts's issue()/void() letting the DB function's own checks
        // speak (the DB is the real guard, per CLAUDE.md §12).
        await client.query(
          "SELECT set_next_invoice_no($1, $2, $3)",
          [companyId, parsed.data.nextInvoiceNo, ownerId],
        );
        return readInvoicingSettings(client, companyId);
      });
      return reply.send(result);
    } catch (err) {
      const mapped = mapPgError(err);
      if (mapped.status === 500) request.log.error({ err }, "set invoicing settings failed");
      return reply.code(mapped.status).send({ error: mapped.message });
    }
  });
}
