import sanitizeHtml from "sanitize-html";
import { isRepoRelative, mediaSrcset, mediaUrl, treeUrl } from "./paths";
import type { MarkupContext } from "./paths";

const HEADINGS = ["h1", "h2", "h3", "h4", "h5", "h6"];

// mirrors appview/pages/markup/sanitizer, which is bluemonday's UGC policy plus
// the elements our own extensions emit. markdown renders with raw html enabled,
// so anything hand written in a readme lands here too
const ALLOWED_TAGS = [
	...HEADINGS,
	"p",
	"br",
	"hr",
	"div",
	"span",
	"section",
	"blockquote",
	"pre",
	"code",
	"kbd",
	"samp",
	"var",
	"tt",
	"b",
	"strong",
	"i",
	"em",
	"u",
	"s",
	"strike",
	"del",
	"ins",
	"sub",
	"sup",
	"small",
	"mark",
	"a",
	"img",
	"picture",
	"source",
	"video",
	"ul",
	"ol",
	"li",
	"dl",
	"dt",
	"dd",
	"table",
	"thead",
	"tbody",
	"tfoot",
	"tr",
	"th",
	"td",
	"caption",
	"colgroup",
	"col",
	"details",
	"summary",
	"figure",
	"figcaption",
	"abbr",
	"bdo",
	"cite",
	"dfn",
	"q",
	"ruby",
	"rt",
	"rp",
	"time",
	"wbr",
	"center",
	"input",
	"label"
];

// bluemonday's standard attributes
const GLOBAL_ATTRIBUTES = ["id", "title", "dir", "lang", "align"];

const ALLOWED_ATTRIBUTES: sanitizeHtml.IOptions["allowedAttributes"] = {
	"*": GLOBAL_ATTRIBUTES,
	a: ["href", "name", "rel", "aria-hidden"],
	img: ["src", "srcset", "alt", "width", "height", "loading"],
	source: ["src", "srcset", "type", "media"],
	video: ["src", "poster", "controls", "autoplay", "loop", "muted", "width", "height"],
	// the tasklist plugin renders disabled checkboxes tied to their labels
	input: ["type", "checked", "disabled"],
	label: ["for"],
	th: ["colspan", "rowspan", "scope"],
	td: ["colspan", "rowspan"],
	col: ["span", "width"],
	colgroup: ["span"],
	ol: ["start", "type", "reversed"],
	details: ["open"],
	time: ["datetime"],
	abbr: ["title"]
};

// classes are allowlisted per tag, so a readme cannot reach the app's own styles
const ALLOWED_CLASSES: sanitizeHtml.IOptions["allowedClasses"] = {
	a: ["anchor", "mention", "footnote-ref", "footnote-backref", "footnote-anchor"],
	sup: ["footnote-ref"],
	hr: ["footnotes-sep"],
	section: ["footnotes"],
	ol: ["footnotes-list", "task-list-container"],
	ul: ["task-list-container"],
	li: ["footnote-item", "task-list-item"],
	input: ["task-list-item-checkbox"],
	label: ["task-list-item-label"],
	div: ["markdown-alert", "markdown-alert-*"],
	p: ["markdown-alert-title"],
	code: ["language-*"]
};

const externalRel = (href: string): string | undefined =>
	isRepoRelative(href) || href.startsWith("#") ? undefined : "nofollow noopener noreferrer";

const optionsFor = (ctx: MarkupContext): sanitizeHtml.IOptions => ({
	allowedTags: ALLOWED_TAGS,
	allowedAttributes: ALLOWED_ATTRIBUTES,
	allowedClasses: ALLOWED_CLASSES,
	allowedSchemes: ["http", "https", "mailto"],
	allowedSchemesAppliedToAttributes: ["href", "src", "srcset", "poster"],
	// resolving urls here rather than in a renderer rule catches the ones
	// written as raw html too
	transformTags: {
		a: (tagName, attribs) => {
			const href = attribs.href ?? "";
			// a mention already points at a profile
			const rewritten =
				isRepoRelative(href) && attribs.class !== "mention" ? treeUrl(href, ctx) : href;
			const rel = externalRel(rewritten);
			return { tagName, attribs: { ...attribs, href: rewritten, ...(rel ? { rel } : {}) } };
		},
		img: (tagName, attribs) => ({ tagName, attribs: resolveMedia(attribs, ctx) }),
		source: (tagName, attribs) => ({ tagName, attribs: resolveMedia(attribs, ctx) }),
		video: (tagName, attribs) => ({ tagName, attribs: resolveMedia(attribs, ctx) })
	},
	// the tasklist checkboxes are the only inputs we render
	exclusiveFilter: (frame) => frame.tag === "input" && frame.attribs.type !== "checkbox"
});

const resolveMedia = (
	attribs: Record<string, string>,
	ctx: MarkupContext
): Record<string, string> => {
	const resolved = { ...attribs };
	for (const key of ["src", "poster"]) {
		const value = resolved[key];
		if (value) resolved[key] = mediaUrl(value, ctx);
	}
	if (resolved.srcset) resolved.srcset = mediaSrcset(resolved.srcset, ctx);
	return resolved;
};

export const sanitizeMarkup = (html: string, ctx: MarkupContext): string =>
	sanitizeHtml(html, optionsFor(ctx));
