import {
  getCustomHeaders,
  getStaticRegenerationResponse,
  getThrottledStaticRegenerationCachePolicy,
  handleFallback,
  Route,
  RoutesManifest,
  StaticRoute
} from "@sls-next/core";
import { CloudFrontRequest, CloudFrontResultResponse } from "aws-lambda";
import { OriginRequestDefaultHandlerManifest } from "../types";
import { Readable } from "stream";
import { triggerStaticRegeneration } from "../lib/triggerStaticRegeneration";
import { s3StorePage } from "../s3/s3StorePage";
import { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from "http";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import getStream from "get-stream";

/**
 * This function is experimental to allow rendering static pages fully from the handler.
 * It uses a client such as S3 client to retrieve the page.
 */
export const renderStaticPage = async ({
  route,
  request,
  req,
  res,
  responsePromise,
  manifest,
  routesManifest,
  bucketName,
  s3Key,
  s3Uri,
  basePath
}: {
  route: Route;
  request: CloudFrontRequest; // eventually we want to make this agnostic to CloudFront
  req: IncomingMessage;
  res: ServerResponse;
  responsePromise: Promise<CloudFrontResultResponse>;
  manifest: OriginRequestDefaultHandlerManifest;
  routesManifest: RoutesManifest;
  bucketName: string;
  s3Key: string;
  s3Uri: string;
  basePath: string;
}): Promise<CloudFrontResultResponse> => {
  const staticRoute = route.isStatic ? (route as StaticRoute) : undefined;
  const statusCode = route?.statusCode ?? 200;

  // For PUT, DELETE, PATCH, POST just return the page as this is a static page, so HTTP method doesn't really do anything.
  // For OPTIONS, we should not return the content but instead return allowed methods.
  if (req.method === "OPTIONS") {
    res.writeHead(204, {
      Allow: "OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE"
    });
    res.end();
    return await responsePromise;
  }

  // Render response from S3
  const s3 = new S3Client({
    region: request.origin?.s3?.region,
    maxAttempts: 3
  });
  const s3BasePath = basePath ? `${basePath.replace(/^\//, "")}/` : "";
  const s3Params = {
    Bucket: bucketName,
    Key: s3Key
  };

  let s3StatusCode;
  let bodyString;
  let s3Response;

  try {
    s3Response = await s3.send(new GetObjectCommand(s3Params));
    // S3 Body is stream per: https://github.com/aws/aws-sdk-js-v3/issues/1096
    bodyString = await getStream(s3Response.Body as Readable);
    s3StatusCode = s3Response.$metadata.httpStatusCode;
  } catch (e: any) {
    s3StatusCode = e.$metadata.httpStatusCode;
  }

  // These statuses are returned when S3 does not have access to the page.
  // 404 will also be returned if CloudFront has permissions to list objects.
  if (s3Response && s3StatusCode !== 403 && s3StatusCode !== 404) {
    let cacheControl = s3Response.CacheControl;

    // If these are error pages, then just return them
    if (statusCode === 404 || statusCode === 500) {
      cacheControl =
        statusCode === 500
          ? "public, max-age=0, s-maxage=0, must-revalidate"
          : cacheControl;
    } else {
      // Otherwise we may need to do static regeneration
      const staticRegenerationResponse = getStaticRegenerationResponse({
        expiresHeader: s3Response.Expires?.toString() ?? "",
        lastModifiedHeader: s3Response.LastModified?.toString() ?? "",
        initialRevalidateSeconds: staticRoute?.revalidate
      });

      if (staticRegenerationResponse) {
        cacheControl = staticRegenerationResponse.cacheControl;

        if (
          staticRoute?.page &&
          staticRegenerationResponse.secondsRemainingUntilRevalidation === 0
        ) {
          const regenerationQueueName =
            manifest.regenerationQueueName ?? `${bucketName}.fifo`; // if queue name not specified, we used [bucketName].fifo as used in deployment

          if (!regenerationQueueName) {
            throw new Error("Regeneration queue name is undefined.");
          }

          const { throttle } = await triggerStaticRegeneration({
            basePath,
            request,
            pageS3Path: s3Key,
            eTag: s3Response.ETag,
            lastModified: s3Response.LastModified?.getTime().toString(),
            pagePath: staticRoute.page,
            queueName: regenerationQueueName
          });

          // Occasionally we will get rate-limited by the Queue (in the event we
          // send it too many messages) and so we we use the cache to reduce
          // requests to the queue for a short period.
          if (throttle) {
            cacheControl =
              getThrottledStaticRegenerationCachePolicy(1).cacheControl;
          }
        }
      }
    }

    // Get custom headers and convert it into a plain object
    const customHeaders = getCustomHeaders(request.uri, routesManifest);
    const convertedCustomHeaders: { [key: string]: string } = {};
    for (const key in customHeaders) {
      convertedCustomHeaders[key] = customHeaders[key][0].value;
    }

    const headers: OutgoingHttpHeaders = {
      "Cache-Control": cacheControl,
      "Content-Disposition": s3Response.ContentDisposition,
      "Content-Type": s3Response.ContentType,
      "Content-Language": s3Response.ContentLanguage,
      "Content-Length": s3Response.ContentLength,
      "Content-Encoding": s3Response.ContentEncoding,
      "Content-Range": s3Response.ContentRange,
      ETag: s3Response.ETag,
      LastModified: s3Response.LastModified?.getTime().toString(),
      "Accept-Ranges": s3Response.AcceptRanges,
      ...convertedCustomHeaders
    };

    res.writeHead(statusCode, headers);
    res.end(bodyString);
    return await responsePromise;
  }

  const getPage = (pagePath: string) => {
    return require(`./${pagePath}`);
  };

  const fallbackRoute = await handleFallback(
    { req, res, responsePromise },
    route,
    manifest,
    routesManifest,
    getPage
  );

  // Already handled dynamic error path
  if (!fallbackRoute) {
    return await responsePromise;
  }

  // Either a fallback: true page or a static error page
  if (fallbackRoute.isStatic) {
    const file = fallbackRoute.file.slice("pages".length);
    const s3Key = `${s3BasePath}static-pages/${manifest.buildId}${file}`;

    const s3Params = {
      Bucket: bucketName,
      Key: s3Key
    };

    const s3Response = await s3.send(new GetObjectCommand(s3Params));
    const bodyString = await getStream(s3Response.Body as Readable);

    const statusCode = fallbackRoute.statusCode || 200;
    const is500 = statusCode === 500;

    const cacheControl = is500
      ? "public, max-age=0, s-maxage=0, must-revalidate" // static 500 page should never be cached
      : s3Response.CacheControl ??
        (fallbackRoute.fallback // Use cache-control from S3 response if possible, otherwise use defaults
          ? "public, max-age=0, s-maxage=0, must-revalidate" // fallback should never be cached
          : "public, max-age=0, s-maxage=2678400, must-revalidate");

    res.writeHead(statusCode, {
      "Cache-Control": cacheControl,
      "Content-Type": "text/html"
    });
    res.end(bodyString);
    return await responsePromise;
  }

  // This is a fallback route that should be stored in S3 before returning it
  const { renderOpts, html } = fallbackRoute;
  const { expires } = await s3StorePage({
    html,
    uri: s3Uri,
    basePath,
    bucketName: bucketName || "",
    buildId: manifest.buildId,
    pageData: renderOpts.pageData,
    region: request.origin?.s3?.region || "",
    revalidate: renderOpts.revalidate
  });

  const isrResponse = expires
    ? getStaticRegenerationResponse({
        expiresHeader: expires.toJSON(),
        lastModifiedHeader: undefined,
        initialRevalidateSeconds: staticRoute?.revalidate
      })
    : null;

  const cacheControl =
    (isrResponse && isrResponse.cacheControl) ||
    "public, max-age=0, s-maxage=2678400, must-revalidate";

  res.setHeader("Cache-Control", cacheControl);

  if (fallbackRoute.route.isData) {
    res.setHeader("Content-Type", "application/json");
    res.end(JSON.stringify(renderOpts.pageData));
  } else {
    res.setHeader("Content-Type", "text/html");
    res.end(html);
  }

  return await responsePromise;
};
