import { randomBytes, randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

import {
  BootstrapResponseSchema,
  CorrelationIdSchema,
  ErrorDetailsSchema,
  ErrorEnvelopeSchema,
  GenerationErrorCategorySchema,
  ModelConfigSseEventSchema,
  ModelRuntimeSseEventSchema,
  SseEventSchema,
  StableEngineeringErrorCodeSchema,
  StateSseEventSchema,
  createErrorEnvelope,
  isCorrelationId,
  type BootstrapResponse,
} from "@dada/shared-contracts";
import swagger from "@fastify/swagger";
import Fastify, { type FastifyReply } from "fastify";

import {
  BrowserSupportRequestSchema,
  BrowserSupportSuccessSchema,
  BrowserUnsupportedReasonSchema,
  browserSupportCookieMaxAgeSeconds,
  browserSupportCookieName,
  checkBrowserSupport,
  createBrowserSupportCookie,
  supportedBrowserSummary,
  verifyBrowserSupportCookie,
  type BrowserSupportRelease,
  type BrowserUnsupportedReason,
} from "./browser-support.js";
import { EventHub } from "./event-hub.js";
import type { PublicAssetResolver } from "./local-data-root.js";
import { isAllowedNetworkRequest, type NetworkBoundaryOptions } from "./network-boundary.js";

const defaultBootstrap: BootstrapResponse = {
  app_version: "0.0.0",
  dependencies: [],
  model_summary: {
    config_set_version: null,
    configured_default_model_id: null,
    recommended_model_id: null,
    runtime_availability_version: null,
  },
  public_features: [],
};

export interface CreateAppOptions {
  bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
  browserGate?: boolean;
  browserSupportRelease?: BrowserSupportRelease;
  browserSupportSecret?: Buffer;
  eventHub?: EventHub;
  networkBoundary?: NetworkBoundaryOptions;
  publicAssets?: PublicAssetResolver;
}

const supportGateDirectory = resolve("apps/web/support-gate");
const supportGateHtml = readFileSync(resolve(supportGateDirectory, "index.html"), "utf8");
const supportGateCss = readFileSync(resolve(supportGateDirectory, "support-gate.css"), "utf8");
const supportGateJavaScript = readFileSync(resolve(supportGateDirectory, "support-gate.js"), "utf8");
const clientHints = "Sec-CH-UA, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Platform";
const contentSecurityPolicy = [
  "default-src 'self'",
  "script-src 'self'",
  "style-src 'self'",
  "img-src 'self' blob:",
  "font-src 'self' blob:",
  "connect-src 'self'",
  "object-src 'none'",
  "base-uri 'none'",
  "frame-ancestors 'none'",
].join("; ");

function requestCorrelationId(headers: Record<string, string | string[] | undefined>) {
  const header = headers["x-correlation-id"];
  const candidate = Array.isArray(header) ? header[0] : header;
  return isCorrelationId(candidate) ? candidate : randomUUID();
}

function headerValue(value: string | string[] | undefined) {
  return Array.isArray(value) ? value[0] : value;
}

function isSupportGateRequest(method: string, path: string) {
  if (method === "POST" && path === "/api/v1/support/check") return true;
  if (method !== "GET" && method !== "HEAD") return false;
  return (
    path === "/" ||
    path === "/app" ||
    path.startsWith("/app/") ||
    path === "/admin" ||
    path.startsWith("/admin/") ||
    path === "/support-gate.css" ||
    path === "/support-gate.js" ||
    path === "/RELEASE.json" ||
    path === "/healthz"
  );
}

function sendBrowserUnsupported(
  reply: FastifyReply,
  correlationId: string,
  reason: BrowserUnsupportedReason,
  release: BrowserSupportRelease | undefined,
) {
  return reply.code(426).send(
    createErrorEnvelope({
      code: "BROWSER_UNSUPPORTED",
      correlationId,
      details: {
        reason,
        supported_browsers: supportedBrowserSummary(release),
      },
    }),
  );
}

export async function createApp(options: CreateAppOptions = {}) {
  const eventHub = options.eventHub ?? new EventHub();
  const bootstrap = options.bootstrap ?? (() => defaultBootstrap);
  const browserGate = options.browserGate ?? true;
  const browserSupportSecret = options.browserSupportSecret ?? randomBytes(32);
  const browserSupportRelease = options.browserSupportRelease;
  const app = Fastify({
    genReqId: (request) => requestCorrelationId(request.headers),
    logger: false,
  });

  await app.register(swagger, {
    openapi: {
      info: {
        title: "Dada P0-A",
        version: "0.0.0",
      },
      openapi: "3.1.0",
    },
    refResolver: {
      buildLocalReference: (schema, _baseUri, _fragment, index) =>
        typeof schema.$id === "string" ? schema.$id : `schema-${index}`,
    },
  });

  for (const schema of [
    CorrelationIdSchema,
    GenerationErrorCategorySchema,
    StableEngineeringErrorCodeSchema,
    ErrorDetailsSchema,
    ErrorEnvelopeSchema,
    BrowserUnsupportedReasonSchema,
    BrowserSupportRequestSchema,
    BrowserSupportSuccessSchema,
    BootstrapResponseSchema,
    StateSseEventSchema,
    ModelConfigSseEventSchema,
    ModelRuntimeSseEventSchema,
    SseEventSchema,
  ]) {
    app.addSchema(schema);
  }

  app.addHook("onRequest", async (request, reply) => {
    reply.header("X-Correlation-Id", request.id);
    reply.header("Accept-CH", clientHints);
    reply.header("Cache-Control", "no-store");
    reply.header("Content-Security-Policy", contentSecurityPolicy);
    reply.header("X-Content-Type-Options", "nosniff");

    const host = headerValue(request.headers.host);
    const origin = headerValue(request.headers.origin);
    if (!isAllowedNetworkRequest({ host, method: request.method, origin }, options.networkBoundary)) {
      return sendBrowserUnsupported(reply, request.id, "identity_unavailable", browserSupportRelease);
    }

    const path = request.url.split("?", 1)[0] ?? "/";
    if (!browserGate || isSupportGateRequest(request.method, path)) return;
    const verified = verifyBrowserSupportCookie({
      cookieHeader: headerValue(request.headers.cookie),
      release: browserSupportRelease,
      secChUa: headerValue(request.headers["sec-ch-ua"]),
      secret: browserSupportSecret,
    });
    if (!verified.supported) {
      return sendBrowserUnsupported(reply, request.id, verified.reason, browserSupportRelease);
    }
  });

  for (const route of ["/", "/app", "/app/*", "/admin", "/admin/*"]) {
    app.get(route, { schema: { hide: true } }, async (_request, reply) => {
      reply.type("text/html; charset=utf-8");
      return supportGateHtml;
    });
  }
  app.get("/support-gate.css", { schema: { hide: true } }, async (_request, reply) => {
    reply.type("text/css; charset=utf-8");
    return supportGateCss;
  });
  app.get("/support-gate.js", { schema: { hide: true } }, async (_request, reply) => {
    reply.type("text/javascript; charset=utf-8");
    return supportGateJavaScript;
  });
  app.get("/RELEASE.json", { schema: { hide: true } }, async () => ({
    app_version: browserSupportRelease?.appVersion ?? null,
    browsers: supportedBrowserSummary(browserSupportRelease),
  }));
  app.get("/healthz", { schema: { hide: true } }, async () => ({
    bind_scope: "loopback",
    port: 43121,
    status: "ready",
  }));

  app.get(
    "/api/v1/assets/public/:resourceVersion/:assetId",
    { schema: { hide: true } },
    async (request, reply) => {
      const { assetId, resourceVersion } = request.params as { assetId?: string; resourceVersion?: string };
      const resource = assetId && resourceVersion
        ? options.publicAssets?.read(resourceVersion, assetId)
        : undefined;
      if (!resource) return reply.code(404).send();
      reply.type(resource.mimeType);
      reply.header("Content-Disposition", "inline");
      return resource.bytes;
    },
  );

  app.post(
    "/api/v1/support/check",
    {
      attachValidation: true,
      schema: {
        body: BrowserSupportRequestSchema,
        operationId: "checkBrowserSupport",
        response: {
          200: BrowserSupportSuccessSchema,
          426: ErrorEnvelopeSchema,
        },
        tags: ["Browser support"],
      },
    },
    async (request, reply) => {
      const checked = checkBrowserSupport(
        request.validationError ? undefined : request.body,
        {
          secChUa: headerValue(request.headers["sec-ch-ua"]),
          secChUaFullVersionList: headerValue(request.headers["sec-ch-ua-full-version-list"]),
          secChUaPlatform: headerValue(request.headers["sec-ch-ua-platform"]),
        },
        browserSupportRelease,
      );
      if (!checked.supported || !browserSupportRelease) {
        return sendBrowserUnsupported(
          reply,
          request.id,
          checked.supported ? "version_unsupported" : checked.reason,
          browserSupportRelease,
        );
      }

      const cookie = createBrowserSupportCookie(
        browserSupportSecret,
        browserSupportRelease,
        checked.identity,
      );
      reply.header(
        "Set-Cookie",
        `${browserSupportCookieName}=${cookie}; Max-Age=${browserSupportCookieMaxAgeSeconds}; Path=/; HttpOnly; SameSite=Strict`,
      );
      return {
        app_version: browserSupportRelease.appVersion,
        browser: checked.identity,
        status: "supported" as const,
        supported_browsers: supportedBrowserSummary(browserSupportRelease),
      };
    },
  );

  app.get(
    "/api/v1/bootstrap",
    {
      schema: {
        operationId: "getBootstrap",
        response: {
          200: BootstrapResponseSchema,
          426: ErrorEnvelopeSchema,
        },
        tags: ["Bootstrap"],
      },
    },
    async () => bootstrap(),
  );

  app.get(
    "/api/v1/events",
    {
      schema: {
        operationId: "getEvents",
        response: {
          200: {
            content: {
              "text/event-stream": {
                schema: SseEventSchema,
              },
            },
            description: "Non-sensitive state change hints. REST remains authoritative.",
          },
          426: ErrorEnvelopeSchema,
        },
        tags: ["State events"],
      },
    },
    (request, reply) => {
      reply.hijack();
      reply.raw.setHeader("Cache-Control", "no-store");
      reply.raw.setHeader("Connection", "keep-alive");
      reply.raw.setHeader("Content-Type", "text/event-stream; charset=utf-8");
      reply.raw.setHeader("X-Correlation-Id", request.id);
      reply.raw.writeHead(200);
      reply.raw.write(": connected\n\n");

      const unsubscribe = eventHub.connect(
        (event) => {
          reply.raw.write(`id: ${event.event_id}\n`);
          reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
        },
        () => reply.raw.end(),
      );
      request.raw.once("close", unsubscribe);
    },
  );

  return app;
}
