/// <reference types="bun" />
import { isDev } from '@qwik.dev/core/build';
import { setServerPlatform } from '@qwik.dev/core/server';
import type {
  ClientConn,
  ServerRenderOptions,
  ServerRequestEvent,
} from '@qwik.dev/router/middleware/request-handler';
import {
  _TextEncoderStream_polyfill,
  isStaticPath,
  mergeHeadersCookies,
  requestHandler,
} from '@qwik.dev/router/middleware/request-handler';
import { extname, join } from 'node:path';
import { MIME_TYPES } from '../request-handler/mime-types';
import { normalizeRequestUrl } from '../shared/url';

/** @public */
export interface QwikRouterBunMiddleware {
  router: (request: Request) => Promise<Response | null>;
  /** @deprecated `router` handles 404 responses. Will be removed in V3. */
  notFound: (request: Request) => Promise<Response>;
  staticFile: (request: Request) => Promise<Response | null>;
}

function getRequestUrl(request: Request, opts: QwikCityBunOptions) {
  const url = new URL(request.url);
  const origin = opts.getOrigin?.(request) ?? Bun.env.ORIGIN;
  if (!origin) {
    return url;
  }
  return normalizeRequestUrl(`${url.pathname}${url.search}${url.hash}`, origin);
}

/** @public */
export function createQwikRouter(opts: QwikRouterBunOptions): QwikRouterBunMiddleware {
  // @qwik.dev/router/middleware/bun
  // still missing from bun: last check was bun version 1.1.8
  globalThis.TextEncoderStream ||= _TextEncoderStream_polyfill as any;

  if (opts.manifest) {
    setServerPlatform(opts.manifest);
  }

  const staticFolder =
    opts.static?.root ?? join(Bun.fileURLToPath(import.meta.url), '..', '..', 'dist');

  async function router(request: Request) {
    try {
      const url = getRequestUrl(request, opts);

      const serverRequestEv: ServerRequestEvent<Response> = {
        mode: 'server',
        locale: undefined,
        url,
        env: {
          get(key) {
            return Bun.env[key];
          },
        },
        request,
        getWritableStream: (status, headers, cookies, resolve) => {
          const { readable, writable } = new TransformStream<Uint8Array>();
          const response = new Response(readable, {
            status,
            headers: mergeHeadersCookies(headers, cookies),
          });
          resolve(response);
          return writable;
        },
        platform: {
          ssr: true,
        },
        getClientConn: () => {
          return opts.getClientConn ? opts.getClientConn(request) : {};
        },
      };

      // send request to qwik router request handler
      const handledResponse = await requestHandler(serverRequestEv, opts);
      if (handledResponse) {
        handledResponse.completion.then((completion) => {
          if (completion) {
            console.error(completion);
          }
        });
        const response = await handledResponse.response;
        if (response) {
          // bun fails to redirect if there is a body.
          // remove the body if there a redirect.
          const status = response.status;
          const location = response.headers.get('Location');
          const isRedirect = status >= 301 && status <= 308 && location;
          if (isRedirect) {
            return new Response(null, response);
          }
          return response;
        }
      }

      // qwik router did not have a route for this request
      return null;
    } catch (e: any) {
      console.error(e);
      return new Response(isDev ? String(e || 'Error') : 'Internal Server Error', {
        status: 500,
        headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Error': 'bun-server' },
      });
    }
  }

  /** @deprecated `router` handles 404 responses. Will be removed in V3. */
  const notFound = async (_request: Request) => {
    return null as never;
  };

  const openStaticFile = async (url: URL) => {
    const pathname = url.pathname;
    const fileName = pathname.slice(url.pathname.lastIndexOf('/'));
    let filePath: string;
    if (fileName.includes('.')) {
      filePath = join(staticFolder, pathname);
    } else if (!globalThis.__NO_TRAILING_SLASH__) {
      filePath = join(staticFolder, pathname + 'index.html');
    } else {
      filePath = join(staticFolder, pathname, 'index.html');
    }
    return {
      filePath,
      content: Bun.file(filePath),
    };
  };

  const staticFile = async (request: Request) => {
    try {
      const url = getRequestUrl(request, opts);

      if (isStaticPath(request.method || 'GET', url)) {
        const { filePath, content } = await openStaticFile(url);
        // We know that it's in the static folder, but it could still be missing
        // If we start the stream with a missing file, it will throw a 500 error during the stream
        if (!(await content.exists())) {
          return new Response('Not Found', {
            status: 404,
            headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Not-Found': url.pathname },
          });
        }

        const ext = extname(filePath).replace(/^\./, '');

        return new Response(await content.stream(), {
          status: 200,
          headers: {
            'content-type': MIME_TYPES[ext] || 'text/plain; charset=utf-8',
            'Cache-Control': opts.static?.cacheControl || 'max-age=3600',
          },
        });
      }

      return null;
    } catch (e) {
      console.error(e);
      return new Response(isDev ? String(e || 'Error') : 'Internal Server Error', {
        status: 500,
        headers: { 'Content-Type': 'text/plain; charset=utf-8', 'X-Error': 'bun-server' },
      });
    }
  };

  return {
    router,
    notFound,
    staticFile,
  };
}

/**
 * @deprecated Use `createQwikRouter` instead. Will be removed in V3
 * @public
 */
export const createQwikCity = createQwikRouter;

/** @public */
export interface QwikRouterBunOptions extends ServerRenderOptions {
  /** Options for serving static files */
  static?: {
    /** The root folder for statics files. Defaults to /dist */
    root?: string;
    /** Set the Cache-Control header for all static files */
    cacheControl?: string;
  };

  /**
   * Provide a function that computes the origin of the server, used to resolve relative URLs and
   * validate the request origin against CSRF attacks.
   *
   * When not specified, it defaults to the `ORIGIN` environment variable (if set).
   *
   * If `ORIGIN` is not set, it's derived from the incoming request, which is not recommended for
   * production use.
   */
  getOrigin?: (request: Request) => string | null;

  /** Provide a function that returns a `ClientConn` for the given request. */
  getClientConn?: (request: Request) => ClientConn;
}

/**
 * @deprecated Use `QwikRouterBunOptions` instead. Will be removed in V3
 * @public
 */
export type QwikCityBunOptions = QwikRouterBunOptions;
