import path from "node:path";
import fs, { Stats } from "node:fs";
import type { BunFile, Serve } from "bun";
import prom from "bun-prometheus-client";

import readConfig from "./config";

const base = "./website/";
const publicDir = path.resolve(base, "public") + path.sep;

const config = readConfig(base);
const defaultHeaders = {
  ...config.extra.headers,
  vary: "Accept-Encoding",
};

type File = {
  filename: string;
  handle: BunFile;
  relPath: string;
  headers?: Record<string, string>;
  size: number;
  mtime: Date;
};

const collectDefaultMetrics = prom.collectDefaultMetrics;
collectDefaultMetrics({
  labels: {
    FLY_APP_NAME: Bun.env.FLY_APP_NAME,
    FLY_ALLOC_ID: Bun.env.FLY_ALLOC_ID,
    FLY_REGION: Bun.env.FLY_REGION,
  },
});
const counters = {
  requestsByStatus: new prom.Counter({
    name: "requests_by_status",
    help: "Number of requests by status code",
    labelNames: ["status_code"],
  }),
  requestsByPath: new prom.Counter({
    name: "requests_by_path",
    help: "Number of requests by path",
    labelNames: ["path"],
  }),
};

let files = new Map<string, File>();

function registerFile(
  path: string,
  pathname: string,
  filename: string,
  stat: Stats,
): void {
  pathname = "/" + (pathname === "." || pathname === "./" ? "" : pathname);

  if (files.get(pathname) !== undefined) {
    console.warn("File already registered:", pathname);
  }
  files.set(pathname, {
    filename,
    relPath: "/" + path,
    handle: Bun.file(filename),
    headers:
      pathname === "/404.html"
        ? Object.assign({}, defaultHeaders, { "cache-control": "no-cache" })
        : undefined,
    size: stat.size,
    mtime: stat.mtime,
  });
}

function walkDirectory(root: string, dir: string) {
  const absDir = path.join(root, dir);
  for (let pathname of fs.readdirSync(absDir)) {
    const relPath = path.join(dir, pathname);
    const absPath = path.join(absDir, pathname);
    const stat = fs.statSync(absPath);
    if (stat.isDirectory()) {
      walkDirectory(root, relPath + path.sep);
    } else if (stat.isFile()) {
      if (pathname.startsWith("index.html")) {
        const dir = relPath.replace("index.html", "");
        registerFile(relPath, dir, absPath, stat);
        if (dir !== ".") {
          registerFile(relPath, dir + path.sep, absPath, stat);
        }
      }
      registerFile(relPath, relPath, absPath, stat);
    }
  }
}

walkDirectory(publicDir, "");

async function serveFile(
  file: File | undefined,
  statusCode: number = 200,
  extraHeaders: Record<string, string> = {},
): Promise<Response> {
  if (file && (await file.handle.exists())) {
    counters.requestsByStatus.inc({ status_code: statusCode });
    return new Response(file.handle, {
      headers: {
        "last-modified": file.mtime.toUTCString(),
        ...extraHeaders,
        ...(file.headers || defaultHeaders),
      },
      status: statusCode,
    });
  } else {
    counters.requestsByStatus.inc({ status_code: 404 });
    // TODO return encoded
    return serveFile(files.get("/404.html"), 404);
  }
}

async function serveEncodedFile(
  file: File | undefined,
  statusCode: number = 200,
  extraHeaders: Record<string, string> = {},
): Promise<Response> {
  const res = await serveFile(file, statusCode, extraHeaders);
  res.headers.delete("content-disposition");
  return res;
}

function parseIfModifiedSinceHeader(header: string | null): number {
  return header ? new Date(header).getTime() + 999 : 0;
}

const metricsServer = Bun.serve({
  port: 9091,
  fetch: async function (request) {
    const pathname = new URL(request.url).pathname;
    switch (pathname) {
      case "/metrics":
        return new Response(await prom.register.metrics());
      default:
        return new Response("", { status: 404 });
    }
  },
});

console.info(
  `Serving metrics on http://${metricsServer.hostname}:${metricsServer.port}/metrics`,
);

const server = Bun.serve({
  fetch: async function (request) {
    const pathname = new URL(request.url).pathname;
    const file = files.get(pathname);
    counters.requestsByPath.inc({ path: pathname });
    if (file) {
      if (
        parseIfModifiedSinceHeader(request.headers.get("if-modified-since")) >=
        file?.mtime.getTime()
      ) {
        counters.requestsByStatus.inc({ status_code: 304 });
        return new Response("", { status: 304, headers: defaultHeaders });
      }
      const encodings = (request.headers.get("accept-encoding") || "")
        .split(",")
        .map((x) => x.trim().toLowerCase());
      if (encodings.includes("br") && files.has(file.relPath + ".br")) {
        return serveEncodedFile(files.get(file.relPath + ".br"), 200, {
          "content-encoding": "br",
          "content-type": file.handle.type,
        });
      } else if (
        encodings.includes("zstd") &&
        files.has(file.relPath + ".zst")
      ) {
        return serveEncodedFile(files.get(file.relPath + ".zst"), 200, {
          "content-encoding": "zstd",
          "content-type": file.handle.type,
        });
      } else if (
        encodings.includes("gzip") &&
        files.has(file.relPath + ".gz")
      ) {
        return serveEncodedFile(files.get(file.relPath + ".gz"), 200, {
          "content-encoding": "gzip",
          "content-type": file.handle.type,
        });
      }
    }
    return serveFile(file);
  },
});

console.info(`Serving website on http://${server.hostname}:${server.port}/`);