import { serveFile } from "@std/http/file-server";
import { STATUS_TEXT } from "@std/http/status";
import { type App, toValue } from "vue";
import { type Router } from "vue-router";
import { renderToString } from "vue/server-renderer";
import transpileResponse from "./transpile.ts";
import { DOMParser } from "@b-fuze/deno-dom";
import { join } from "@std/path/join";
import { exists } from "@std/fs";
import { type DjSSRContext } from "@/useDjSSRContext.ts";
import { type DjAPIResult, type DjAPIResultMap } from "@/api.ts";

const utf8Decoder = new TextDecoder("utf-8");

const parser = new DOMParser();

function appHeaderScript(params: { ssrContext: DjSSRContext, entryPath: string }) {
    return `
    <title>${ toValue(params.ssrContext.head.title) }</title>
    ${ toValue(params.ssrContext.head.metatags).map(_ => `<meta name="${ _.name }" content="${ _.content }">`).join('\n\t') }
    <script type="importmap">
        {
            "imports": {
                "vue": "/deps/vue/dist/vue.esm-browser.prod.js",
                "vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js",
                "vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs",
                "@vue/devtools-api": "/app/devtools-shim.ts",
                "@/": "/app/"
            }
        }
    </script>
    <style>
        ${ Object.values(params.ssrContext.styles).join('\n') }
    </style>
    <script type="module"> 
        window.appstate = ${JSON.stringify(params.ssrContext.registry)}; 
        import('${params.entryPath}');
    </script>`;
}

async function* siteEntries(path: string): AsyncGenerator<string> {
    for await (const dirEnt of Deno.readDir(path)) {
        if (dirEnt.isDirectory) {
            yield* siteEntries(join(path, dirEnt.name));
        } else if (dirEnt.name === "index_template.html") {
            yield path.split("/")[1] ?? "";
        }
    }
}

const publicFiles = siteEntries("public");
const sites: string[] = [];
for await (const path of publicFiles) {
    sites.push(path);
}

async function getAPIResponse(apiReq: Request): Promise<Response> {
    let jsonResponse: DjAPIResult | { error: string } | null = null;
    let status = 200;

    const pathname = URL.parse(apiReq.url)?.pathname;

    if (!pathname) {
        jsonResponse = { error: "Invalid Route" };
        status = 400;
    }

    if (!jsonResponse && pathname) {
        const apiPath = pathname.split("/api")[1];

        if (apiPath === "/rp-articles") {
            const paths: string[] = [];
            const contentDir = './public/generative-energy/content/';
            for await (const dirEnt of Deno.readDir(contentDir)) {
                if (dirEnt.isFile && dirEnt.name.endsWith('.html')) {
                    paths.push(`${contentDir}${dirEnt.name}`);
                }
            }
            const result: DjAPIResultMap['/rp-articles'] = [];
            for (const filePath of paths) {
                const content = await Deno.readTextFile(filePath);
                const dom = parser.parseFromString(content, 'text/html');
                const metadata = { title: '', author: 'Ray Peat, übersetzt von Daniel Ledda', titleEn: '', titleDe: '', tags: [] as string[], slug: '' };
                const metaTags = dom.querySelectorAll('meta') as unknown as NodeListOf<HTMLMetaElement>;
                for (const metaTag of metaTags) {
                    const name = metaTag.attributes.getNamedItem('name')?.value ?? '';
                    const content = metaTag.attributes.getNamedItem('content')?.value ?? '';
                    if (name === 'title-de') {
                        metadata.titleDe = content;
                        metadata.title = content;
                    } else if (name === 'title-en') {
                        metadata.titleEn = content;
                    } else if (name === 'tags') {
                        metadata.tags = content ? content.split(",") : [];
                    } else if (name === 'slug') {
                        metadata.slug = content;
                    }
                }
                result.push(metadata);
            }
            result.sort((a, b) => a.titleDe.localeCompare(b.titleDe));
            jsonResponse = result;
        } else if (apiPath === "/blog-entries") {
            const paths: string[] = [];
            const contentDir = './public/blog/content/';
            for await (const dirEnt of Deno.readDir(contentDir)) {
                if (dirEnt.isFile && dirEnt.name.endsWith('.html')) {
                    paths.push(`${contentDir}${dirEnt.name}`);
                }
            }
            const result: DjAPIResultMap['/blog-entries'] = [];
            for (const filePath of paths) {
                const [ stat, content ] = await Promise.all([Deno.stat(filePath), Deno.readTextFile(filePath)]);
                const dom = parser.parseFromString(content, 'text/html');
                const metadata = {
                    slug: '',
                    tags: [] as string[],
                    title: '',
                    createdAt: '',
                    updatedAt: '',
                };
                const metaTags = dom.querySelectorAll('meta') as unknown as NodeListOf<HTMLMetaElement>;
                for (const metaTag of metaTags) {
                    const name = metaTag.attributes.getNamedItem('name')?.value ?? '';
                    const content = metaTag.attributes.getNamedItem('content')?.value ?? '';
                    if (name === 'title') {
                        metadata.title = content;
                    } else if (name === 'tags') {
                        metadata.tags = content ? content.split(",") : [];
                    } else if (name === 'slug') {
                        metadata.slug = content;
                    } else if (name === 'updatedAt') {
                        metadata.createdAt = content;
                    } else if (name === 'createdAt') {
                        metadata.updatedAt = content;
                    }
                }
                result.push(metadata);
            }
            result.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
            jsonResponse = result;
        }

        if (!jsonResponse) {
            jsonResponse = { error: `API route ${ apiPath } not found.` };
            status = 404;
        }
    }

    const headers = new Headers();
    headers.set("Content-Type", "application/json");
    return new Response(JSON.stringify(jsonResponse), {
        status,
        headers,
    });
}

const redirects = {
    '/generative-energy/rp-deutsch': {
        target: '/generative-energy/raypeat-deutsch',
        code: 301,
    },
} as const;

const redirectPaths = Object.keys(redirects) as (keyof typeof redirects)[];

Deno.serve({
    port: 8080,
    hostname: "0.0.0.0",
    onListen({ port, hostname }) {
        console.log(`Listening on port http://${hostname}:${port}/`);
    },
}, async (req, _conn) => {
    const timeStart = new Date().getTime();

    let response: Response | null = null;

    const url = URL.parse(req.url);

    if (req.method === "GET") {
        const pathname = url?.pathname ?? "/";

        // Redirects
        const redirect = redirectPaths.find(_ => pathname.startsWith(_))
        if (response === null && redirect) {
            const entry = redirects[redirect];
            const headers = new Headers();
            headers.set('Location', entry.target);
            response = new Response(STATUS_TEXT[entry.code], { headers, status: entry.code });
        }

        // API
        if (response === null && pathname.startsWith("/api/")) {
            response = await getAPIResponse(req);
        }

        // Public/static files
        if (response === null) {
            let filepath = join(".", "public", pathname);
            if (filepath.endsWith("/")) {
                filepath = join(filepath, "index.html");
            }
            if (await exists(filepath, { isFile: true })) {
                response = await serveFile(req, filepath);
            }
        }

        // NPM Vendor deps
        if (response === null && pathname.startsWith("/deps")) {
            response = await serveFile(req, `node_modules/${pathname.split("/deps")[1]}`);
        }

        // Transpile Code
        if (
            response === null &&
            pathname.startsWith("/app") &&
            (pathname.endsWith(".ts") || pathname.endsWith(".tsx")) &&
            !pathname.endsWith("server.ts")
        ) {
            response = await serveFile(req, "./" + pathname);
            response = await transpileResponse(response, req.url, pathname);
        }

        // SSR
        if (response === null) {
            const baseDirectoryName = pathname.split("/")[1] ?? "";
            if (sites.includes(baseDirectoryName)) {
                const appLocation = baseDirectoryName === "" ? "home" : baseDirectoryName;
                const siteTemplate = join("public", baseDirectoryName, "index_template.html");
                const siteEntry = join("app", appLocation, "server.ts");
                const clientEntry = join("@", appLocation, "client.ts");
                const { app, router } = (await import("./" + siteEntry)).default() as {
                    app: App;
                    router: Router | null;
                };
                app.provide("dom-parse", (innerHTML: string) => {
                    return parser.parseFromString(innerHTML, "text/html").documentElement;
                });
                const ssrContext: DjSSRContext = { styles: {}, registry: {}, head: { title: "", metatags: [] } };
                if (router) {
                    await router.replace(pathname.split('/' + baseDirectoryName)[1]);
                    await router.isReady();
                }
                const rendered = await renderToString(app, ssrContext);
                const content = utf8Decoder.decode(await Deno.readFile(siteTemplate))
                    .replace(`<!-- SSR OUTLET -->`, rendered)
                    .replace(
                        `<!-- SSR HEAD OUTLET -->`,
                        appHeaderScript({ ssrContext, entryPath: clientEntry }),
                    );
                response = new Response(content, { headers: { "Content-Type": "text/html" } });
            }
        }
    } else {
        response = new Response("Only GET allowed.", { status: 500 });
    }

    if (response === null) {
        response = new Response("Not found.", { status: 404 })
    }

    const timeEnd = new Date().getTime();

    console.log(`Request ${ url?.pathname ?? 'malformed' }\tStatus ${ response.status }, Duration ${ timeEnd - timeStart }ms`);

    return response;
});

Deno.addSignalListener("SIGINT", () => {
    console.info("Shutting down (received SIGINT)");
    Deno.exit();
});
