import {
    DOMParser,
} from "https://deno.land/x/deno_dom@v0.1.46/deno-dom-wasm.ts";
import * as css from "https://deno.land/x/css@0.3.0/mod.ts";

// so, so brittle
function is_valid_color(color: string): boolean {
    try {
        const { stylesheet } = css.parse(`*{color:${color};}`);
        return stylesheet.rules.length == 1 &&
            stylesheet.rules[0].declarations.length == 1;
    } catch {
        return false;
    }
}

// maybe a bit high, maybe a bit low
const MAX_AGE_MS = 10 * 60 * 1000;

// > "light-dark(oklab(000, 000, 000), oklab(255, 255, 255))".length
// 54
const MAX_COLOR_LEN = 512;

const colors: Record<
    string,
    { color: string | undefined; last_checked: Date }
> = {};
const dom_parser = new DOMParser();

function parse_color(str: string): string | undefined {
    const text = str.replace(/^#/, "");
    if (text.startsWith("name-color:")) {
        const line = text.split("\n", 1)[0];
        const color = line.split("name-color:")[1]
            .trim()
            .slice(0, MAX_COLOR_LEN)
            .replaceAll(/[;{}"']|\/\*|\*\//g, "");
        if (is_valid_color(color)) {
            return color;
        } else {
            console.log(`invalid color!!!! ${color}`);
        }
    }
}

async function get_name_color(name: string): Promise<string | undefined> {
    console.log(`fetching ${name}...`);
    const resp = await fetch(`https://cohost.org/${name}`);
    if (!resp.ok) {
        console.log(`fetching https://cohost.org/${name} failed!`);
        return;
    }
    const body = await resp.text();
    const dom = dom_parser.parseFromString(body, "text/html");
    if (!dom) {
        console.log(`parsing https://cohost.org/${name} to the dom failed!`);
        return;
    }

    const a = dom.querySelector("a[href*='name-color:']");
    if (a) {
        const href = a.getAttribute("href");
        if (href) {
            const color = parse_color(href);
            if (color) return color;
        }
    }

    for (const p of dom.querySelectorAll("p")) {
        if (!p.textContent.includes("name-color:")) continue;

        for (const node of p.childNodes) {
            const color = parse_color(node.textContent);
            if (color) return color;
        }
    }
}

async function update_name_color(name: string) {
    const now = new Date();
    if (
        colors[name] &&
        now.getTime() - colors[name].last_checked.getTime() <= MAX_AGE_MS
    ) {
        return;
    }
    const color = await get_name_color(name);
    colors[name] = { color, last_checked: now };
}

function cssify({ requester }: { requester?: string }): string {
    const preamble = `/* customized: ${
        Object.keys(colors)
            .filter((name) => colors[name].color)
            .sort()
            .join(", ")
    } */\n/* look, i don't like !important either, but i think it's a better choice than some lame .spec.ific.ity.hack.ing */\n\n`;

    let vars = `:root {\n`;
    if (requester && colors[requester]) {
        vars += `--name-color: var(--name-color-${requester.toLowerCase()});\n`;
    }
    let classes = "";
    for (const name of Object.keys(colors).sort()) {
        const { color } = colors[name];
        if (!color) continue;
        vars += `--name-color-${name.toLowerCase()}: ${color};\n`;
        classes +=
            `a[href$='/${name}' i]{color: var(--name-color-${name.toLowerCase()}) !important;}\n`;
    }
    vars += "}\n\n";
    return preamble + vars + classes;
}

Deno.serve({ port: 61266 }, async (req) => {
    const url = new URL(req.url);
    const names = url.searchParams.getAll("name").filter((name) =>
        !name.match(/[\/\\?"';{}]/)
    ).map((name) => name.toLowerCase());
    await Promise.all(names.map(update_name_color));
    const css = cssify({
        requester: url.searchParams.get("requester") ?? undefined,
    });
    return new Response(css, { headers: { "content-type": "text/css" } });
});
