import path from "node:path";
import fs, { Stats } from "node:fs";
import type { BunFile, Serve } from "bun";
import * as Sentry from "@sentry/node";
import prom from "bun-prometheus-client";

import readConfig from "./config";

Sentry.init({
  release: `homestead@${Bun.env.FLY_MACHINE_VERSION}`,
  tracesSampleRate: 1.0,
});

const publicDir = "public" + path.sep;

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

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

const metrics = {
  requests: new prom.Counter({
    name: "homestead_requests",
    help: "Number of requests by path, status code, and method",
    labelNames: ["path", "status_code", "method"],
  }),
  requestDuration: new prom.Histogram({
    name: "homestead_request_duration_seconds",
    help: "Request duration in seconds",
    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);
  }
  const handle = Bun.file(filename);
  files.set(pathname, {
    filename,
    relPath: "/" + path,
    handle: handle,
    type: pathname.startsWith("/feed-styles.xsl") ? "text/xsl" : handle.type,
    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> {
  return new Response(await file.handle.arrayBuffer(), {
    headers: {
      "last-modified": file.mtime.toUTCString(),
      ...extraHeaders,
      ...(file.headers || defaultHeaders),
    },
    status: statusCode,
  });
}

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

export const metricsServer = {
  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 });
    }
  },
} satisfies Serve;

export const server = {
  fetch: async function (request) {
    const pathname = new URL(request.url).pathname.replace(/\/\/+/g, "/");
    const endTimer = metrics.requestDuration.startTimer({ path: pathname });
    const transaction = Sentry.startTransaction({
      name: pathname,
      op: "http.server",
      description: `${request.method} ${pathname}`,
      tags: {
        url: request.url,
        "http.method": request.method,
        "http.user_agent": request.headers.get("user-agent"),
      },
    });
    try {
      const file = files.get(pathname);
      metrics.requests.inc({ path: pathname });
      if (file && (await file.handle.exists())) {
        if (
          parseIfModifiedSinceHeader(
            request.headers.get("if-modified-since"),
          ) >= file?.mtime.getTime()
        ) {
          metrics.requests.inc({
            method: request.method,
            path: pathname,
            status_code: 304,
          });
          transaction.setHttpStatus(304);
          return new Response("", { status: 304, headers: defaultHeaders });
        }
        metrics.requests.inc({
          method: request.method,
          path: pathname,
          status_code: 200,
        });
        const encodings = (request.headers.get("accept-encoding") || "")
          .split(",")
          .map((x) => x.trim().toLowerCase());
        if (encodings.includes("br") && files.has(file.relPath + ".br")) {
          transaction.setHttpStatus(200);
          transaction.setTag("http.content-encoding", "br");
          return serveFile(files.get(file.relPath + ".br"), 200, {
            "content-encoding": "br",
            "content-type": file.type,
          });
        } else if (
          encodings.includes("zstd") &&
          files.has(file.relPath + ".zst")
        ) {
          transaction.setHttpStatus(200);
          transaction.setTag("http.content-encoding", "zstd");
          return serveFile(files.get(file.relPath + ".zst"), 200, {
            "content-encoding": "zstd",
            "content-type": file.type,
          });
        } else if (
          encodings.includes("gzip") &&
          files.has(file.relPath + ".gz")
        ) {
          transaction.setHttpStatus(200);
          transaction.setTag("http.content-encoding", "gzip");
          return serveFile(files.get(file.relPath + ".gz"), 200, {
            "content-encoding": "gzip",
            "content-type": file.type,
          });
        }
        transaction.setHttpStatus(200);
        transaction.setTag("http.content-encoding", "identity");
        return serveFile(file);
      } else {
        metrics.requests.inc({
          method: request.method,
          path: pathname,
          status_code: 404,
        });
        transaction.setHttpStatus(404);
        transaction.setTag("http.content-encoding", "identity");
        return serveFile(files.get("/404.html"), 404);
      }
    } catch (error) {
      transaction.setTag("http.content-encoding", "identity");
      transaction.setHttpStatus(503);
      metrics.requests.inc({
        method: request.method,
        path: pathname,
        status_code: 503,
      });
      Sentry.captureException(error);
      return new Response("Something went wrong", { status: 503 });
    } finally {
      const seconds = endTimer();
      metrics.requestDuration.observe(seconds);
      transaction.finish();
    }
  },
} satisfies Serve;