import { Dictionary } from "@insite/client-framework/Common/Types";
import { checkIsWebCrawler, setIsWebCrawler } from "@insite/client-framework/Common/WebCrawler";
import {
    serverSiteMessageResolver,
    serverTranslationResolver,
    setDomain,
    setHeaders,
    setIsShareEntity,
    setUrl,
} from "@insite/client-framework/ServerSideRendering";
import { getPageByUrl, getPageUrlByType } from "@insite/client-framework/Services/ContentService";
import { setResolver } from "@insite/client-framework/SiteMessage";
import { setTranslationResolver } from "@insite/client-framework/Translate";
import diagnostics from "@insite/server-framework/diagnostics";
import getTemplate, { getTemplatePaths } from "@insite/server-framework/getTemplate";
import healthCheck from "@insite/server-framework/healthCheck";
import generateBlueprintTranslations from "@insite/server-framework/ImportTranslations";
import manifest from "@insite/server-framework/Manifest";
import { pageRenderer } from "@insite/server-framework/PageRenderer";
import { getRelayEndpoints, relayRequest } from "@insite/server-framework/Relay";
import robots from "@insite/server-framework/Robots";
import serviceWorker from "@insite/server-framework/ServiceWorker";
import { shellRenderer } from "@insite/server-framework/ShellRenderer";
import { getAutoUpdatedPageTypes } from "@insite/server-framework/SiteGeneration/PageCreators";
import { getFontContent } from "@insite/server-framework/Utilities/fontProcessing";
import { Request, Response } from "express";
import logger from "@insite/client-framework/Logger";
import llms from "@insite/server-framework/Llms";

setResolver(serverSiteMessageResolver);
setTranslationResolver(serverTranslationResolver);

export const shareEntityRoute = "/.spire/shareEntity";

const classicToSpirePageMapping: Dictionary<string> = {
    MyAccountAddressPage: "AddressesPage",
    BrandDetailPage: "BrandDetailsPage",
    BudgetPage: "BudgetManagementPage",
    ChangeAccountPasswordPage: "ChangePasswordPage",
    ReviewAndPayPage: "CheckoutReviewAndSubmitPage",
    CheckoutAddressPage: "CheckoutShippingPage",
    DealerPage: "DealerDetailsPage",
    InvoiceDetailPage: "InvoiceDetailsPage",
    InvoicesPage: "InvoiceHistoryPage",
    DealerLocatorPage: "LocationFinderPage",
    MyListDetailPage: "MyListsDetailsPage",
    OrderApprovalDetailPage: "OrderApprovalDetailsPage",
    OrderDetailPage: "OrderDetailsPage",
    OrdersPage: "OrderHistoryPage",
    ProductComparisonPage: "ProductComparePage",
    ProductDetailPage: "ProductDetailsPage",
    RequisitionPage: "RequisitionsPage",
    JobQuoteDetailsPage: "RfqJobQuoteDetailsPage",
    MyJobQuotesPage: "RfqJobQuotesPage",
    SavedOrderDetailPage: "SavedOrderDetailsPage",
    MySavedPaymentsPage: "SavedPaymentsPage",
    ErrorPage: "UnhandledErrorPage",
    VmiOrdersPage: "VmiOrderHistoryPage",
};

const redirectTo = async ({ originalUrl, path }: Request, response: Response) => {
    let pageType = path.substr("/RedirectTo/".length);

    const classicToSpirePageMappingKey = Object.keys(classicToSpirePageMapping).find(
        o => o.toLowerCase() === pageType.toLowerCase(),
    );
    if (classicToSpirePageMappingKey) {
        pageType = classicToSpirePageMapping[classicToSpirePageMappingKey];
    }

    const destination = (await getPageUrlByType(pageType)) || "/";
    response.redirect(destination + originalUrl.substring(path.length));
};

type RouteHandler = (request: Request, response: Response) => Promise<void>;

const routes: {
    path: string | RegExp;
    handler: RouteHandler;
    makeLowerCase: boolean;
    allowOnlyGetRequests: boolean;
}[] = [];

function addRoute(path: RegExp | string, handler: RouteHandler, makeLowerCase = true, allowOnlyGetRequests = true) {
    routes.push({
        path: typeof path === "string" ? path.toLowerCase() : path,
        handler,
        makeLowerCase,
        allowOnlyGetRequests,
    });
}

addRoute("/.spire/autoUpdatedPageTypes", getAutoUpdatedPageTypes);
addRoute("/.spire/health", healthCheck);
addRoute("/.spire/diagnostics", diagnostics);
addRoute("/robots.txt", robots, false);
addRoute("/llms.txt", llms, false);
addRoute("/manifest.json", manifest, false);
addRoute("/serviceworker.js", serviceWorker, false);
addRoute("/.spire/content/getTemplatePaths", getTemplatePaths);
addRoute("/.spire/content/getTemplate", getTemplate);
addRoute(/^\/sitemap.*\.xml/i, relayRequest);
addRoute(/^\/.*\.txt/i, relayRequest);
for (const endpoint of getRelayEndpoints()) {
    addRoute(new RegExp(`^/${endpoint}(\\/|$)`, "i"), relayRequest, true, false);
}
addRoute(/^\/redirectTo\/(?!mobileauth\/)/i, redirectTo);
addRoute(/^\/contentAdmin/i, shellRenderer, true, false);
addRoute(
    shareEntityRoute,
    async (request: Request, response: Response) => {
        const page = await getPageByUrl(request.body.urlPathToLoadForAttachmentHtml);
        if (
            (page.page?.type !== "OrderDetailsPage" &&
                page.page?.type !== "InvoiceDetailsPage" &&
                page.page?.type !== "OrderStatusPage") ||
            (page.page.type === "OrderStatusPage" &&
                (() => {
                    const url = new URL(request.body.urlPathToLoadForAttachmentHtml, "https://example.com");
                    return !url.searchParams.get("orderNumber") || !url.searchParams.get("stPostalCode");
                })())
        ) {
            response.status(400).send("Invalid page type for share entity");
            return;
        }

        const tempUrl = new URL(request.body.urlPathToLoadForAttachmentHtml, "https://example.com");
        request.url = tempUrl.pathname + tempUrl.search;
        return pageRenderer(request, response);
    },
    true,
    false,
);
addRoute("/.spire/fonts/getFont", getFontContent);
addRoute("/.spire/generateBlueprintTranslations", generateBlueprintTranslations);

export default function server(request: Request, response: Response, domain: Parameters<typeof setDomain>[0]) {
    setupSSR(request, domain);

    const loweredPath = request.path.toLowerCase();
    for (const route of routes) {
        if (
            (typeof route.path === "string" && route.path === (route.makeLowerCase ? loweredPath : request.path)) ||
            (typeof route.path !== "string" && loweredPath.match(route.path))
        ) {
            if (route.allowOnlyGetRequests && request.method !== "GET") {
                logger.info(
                    `${request.method} to ${loweredPath} was 405ed because it is configured to only allow GET requests.`,
                );
                return Promise.resolve(response.status(405).send());
            }
            return route.handler(request, response);
        }
    }
    return pageRenderer(request, response);
}

function setupSSR(request: Request, domain: Parameters<typeof setDomain>[0]) {
    setDomain(domain);

    const { headers } = request;
    const ip =
        (headers["x-forwarded-for"] || "").toString().split(",").pop()?.trim() ||
        request.connection.remoteAddress ||
        request.socket.remoteAddress;
    headers["x-forwarded-for"] = ip;
    if (request.originalUrl === shareEntityRoute) {
        setIsShareEntity(true);
        // This request is being "hijacked" and a different request (with a different body) is being sent instead.
        // Because of this, this header needs to be deleted so it's not inaccurate. If it's inaccurate, the web server
        // will throw an exception while trying to parse the body.
        delete headers["content-length"];
    }
    setHeaders(headers);

    setIsWebCrawler(checkIsWebCrawler(headers["user-agent"] as string));

    const protocol = headers["x-forwarded-proto"]?.toString().toLowerCase() === "https" ? "https" : request.protocol;
    setUrl(`${protocol}://${request.get("host")}${request.originalUrl}`);
}
