import { alert } from "@mdit/plugin-alert";
import { footnote } from "@mdit/plugin-footnote";
import { tasklist } from "@mdit/plugin-tasklist";
import MarkdownIt from "markdown-it";
import anchor from "markdown-it-anchor";
import { full as emoji } from "markdown-it-emoji";
import { sanitizeMarkup } from "./sanitize";
import type { MarkupContext } from "./paths";

// past this a document renders as plain text, a readme this big is pathological
export const SOURCE_LIMIT = 512 * 1024;

// github's heading slugs, that is what fragment links in a readme are written
// against
const slugify = (text: string): string =>
	text
		.trim()
		.toLowerCase()
		.replace(/[^\p{L}\p{N}\p{M}\s_-]+/gu, "")
		.replace(/\s+/g, "-");

// the dotted dns handle appview/pages/markup/extension/atlink.go matches
const MENTION = /^@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z][a-zA-Z0-9-]*\b/;

const mentions = (md: MarkdownIt): void => {
	md.inline.ruler.before("link", "mention", (state, silent) => {
		// inside a link label a mention would nest an anchor in an anchor. markdown-it
		// tracks this but its published types leave the field out
		if ((state as unknown as { linkLevel: number }).linkLevel > 0) return false;
		if (state.src.charCodeAt(state.pos) !== 0x40) return false;
		const before = state.pos === 0 ? " " : state.src[state.pos - 1];
		if (before !== " " && before !== "\n" && before !== "(") return false;
		const match = MENTION.exec(state.src.slice(state.pos, state.posMax));
		if (!match) return false;

		if (!silent) {
			const open = state.push("link_open", "a", 1);
			open.attrs = [
				["href", `/${match[0].slice(1)}`],
				["class", "mention"]
			];
			state.push("text", "", 0).content = match[0];
			state.push("link_close", "a", -1);
		}
		state.pos += match[0].length;
		return true;
	});
};

const SHA = /^[0-9a-f]{7,40}$/;

// `https://host/owner/repo/commit/<sha>` is a mouthful to read inline
const shortSha = (href: string, host: string): string | null => {
	let url: URL;
	try {
		url = new URL(href);
	} catch {
		return null;
	}
	if (url.host !== host) return null;
	const parts = url.pathname.replace(/^\/+|\/+$/g, "").split("/");
	if (parts.length !== 4 || parts[2] !== "commit" || !SHA.test(parts[3])) return null;
	return parts[3].slice(0, 8);
};

const commitLinks =
	(host: string) =>
	(md: MarkdownIt): void => {
		md.core.ruler.push("commit_link", (state) => {
			for (const token of state.tokens) {
				if (token.type !== "inline" || !token.children) continue;
				for (let i = 0; i < token.children.length; i++) {
					const open = token.children[i];
					// only bare urls, a link someone gave a label to keeps it
					if (open.type !== "link_open" || open.info !== "auto") continue;
					const text = token.children[i + 1];
					if (text?.type !== "text" || token.children[i + 2]?.type !== "link_close") continue;
					const sha = shortSha(open.attrGet("href") ?? "", host);
					if (!sha) continue;
					const code = new state.Token("code_inline", "code", 0);
					code.content = sha;
					token.children[i + 1] = code;
				}
			}
		});
	};

// todo: no syntax highlighting, mermaid or math yet. the first two want a client
// side renderer, math wants mathjax loaded on demand the way the appview does it
const build = (host: string): MarkdownIt => {
	const md = new MarkdownIt({
		html: true,
		linkify: true,
		// the appview only rewrites dashes in the blog, readmes keep their text
		typographer: false
	})
		.use(anchor, {
			slugify,
			permalink: anchor.permalink.linkInsideHeader({ class: "anchor", symbol: "#" })
		})
		.use(footnote)
		.use(tasklist, { disabled: true, label: true })
		.use(alert)
		.use(emoji)
		.use(mentions);

	if (host) md.use(commitLinks(host));
	return md;
};

const renderers = new Map<string, MarkdownIt>();

// building an instance means running every plugin, so keep one per host
const rendererFor = (host: string): MarkdownIt => {
	let md = renderers.get(host);
	if (!md) {
		md = build(host);
		renderers.set(host, md);
	}
	return md;
};

export const renderMarkdown = (source: string, ctx: MarkupContext): string =>
	sanitizeMarkup(rendererFor(ctx.host ?? "").render(source), ctx);
