import { Prisma } from "@prisma/client";
import cors from "cors";
import express, { type NextFunction, type Request, type Response } from "express";
import helmet from "helmet";
import path from "path";
import { ZodError } from "zod";
import { ensureDataDir } from "./db";
import { HttpError } from "./errors";
import { rootDir } from "./env";
import { checkoutOrigins } from "./ipay";
import { renderMessage } from "./pages";
import { loadProducts } from "./products";
import { adminRouter } from "./routes/admin";
import { apiRouter } from "./routes/api";
import { callbackRouter } from "./routes/callback";
import { publicRouter } from "./routes/public";
import "./types";

function rememberRawBody(req: Request, _res: Response, buf: Buffer): void {
  req.rawBody = buf.toString("utf8");
}

export function createApp(): express.Express {
  ensureDataDir();
  loadProducts();

  const app = express();
  app.disable("x-powered-by");
  app.set("trust proxy", 1);
  app.use(helmet({ contentSecurityPolicy: false, referrerPolicy: { policy: "no-referrer" } }));
  app.use((_req, res, next) => {
    const origins = checkoutOrigins().join(" ");
    res.setHeader(
      "Content-Security-Policy",
      `default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; form-action 'self' ${origins}; base-uri 'self'; object-src 'none'; frame-ancestors 'none'`
    );
    next();
  });
  app.use(
    cors({
      origin: true,
      methods: ["GET", "POST"],
      allowedHeaders: ["Content-Type", "X-API-Key", "X-Admin-Key"],
    })
  );
  app.use(
    express.json({
      limit: "100kb",
      type: ["application/json", "application/*+json"],
      verify: rememberRawBody,
    })
  );
  app.use(express.urlencoded({ extended: true, limit: "100kb", verify: rememberRawBody }));
  app.use(express.static(path.join(rootDir, "public"), { index: false, dotfiles: "ignore" }));

  app.use("/api/v1/ipay", callbackRouter);
  app.use("/api/v1", apiRouter);
  app.use("/admin", adminRouter);
  app.use(publicRouter);

  app.use((_req, res) => {
    if (_req.path.startsWith("/api/")) {
      res.status(404).json({ error: "Not found" });
      return;
    }
    res.status(404).type("html").send(renderMessage({ title: "Not found", message: "This page does not exist." }));
  });

  app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
    const parsed = normalizeError(err);
    if (!(err instanceof HttpError) && parsed.status >= 500) console.error(err);
    if (req.path === "/api/v1/ipay/callback") {
      res.status(parsed.status).json({ ok: false, error: parsed.message });
      return;
    }
    if (req.path.startsWith("/api/")) {
      const body: { error: string; details?: { path: string; message: string }[] } = { error: parsed.message };
      if (parsed.details) body.details = parsed.details;
      res.status(parsed.status).json(body);
      return;
    }
    res
      .status(parsed.status)
      .type("html")
      .send(renderMessage({ title: parsed.status === 404 ? "Not found" : "Something went wrong", message: parsed.message }));
  });

  return app;
}

function normalizeError(err: unknown): {
  status: number;
  message: string;
  details?: { path: string; message: string }[];
} {
  if (err instanceof HttpError) return { status: err.status, message: err.message };
  if (err instanceof ZodError) {
    return {
      status: 400,
      message: "Invalid request",
      details: err.issues.map((issue) => ({ path: issue.path.join("."), message: issue.message })),
    };
  }
  if (err instanceof Prisma.PrismaClientInitializationError) {
    return { status: 503, message: "Database is not ready. Run npx prisma migrate deploy." };
  }
  if (err instanceof Prisma.PrismaClientKnownRequestError && (err.code === "P2021" || err.code === "P1001" || err.code === "P1003")) {
    return { status: 503, message: "Database is not ready. Run npx prisma migrate deploy." };
  }
  if (typeof err === "object" && err !== null && "type" in err && (err as { type?: string }).type === "entity.parse.failed") {
    return { status: 400, message: "Invalid JSON" };
  }
  return { status: 500, message: "Internal error" };
}
