import { Err, Ok, Result } from "@/types/result";
import * as mfm from "mfm-js";
import { types } from "magnetar-common";

export type MagnetarParseError =
    | "XMLParseError"
    | "XMLParseException"
    | "InvalidRootNode";

export function parseMagnetarMarkdownXml(
    xml: types.MmXml,
): Result<XMLDocument, MagnetarParseError> {
    const parser = new DOMParser();
    const document = parser.parseFromString(
        xml,
        "application/xml",
    ) as XMLDocument;
    const error = document.querySelector("parsererror");

    if (error) {
        return Err("XMLParseError");
    }

    if (document.documentElement.tagName !== "mmm") {
        return Err("InvalidRootNode");
    }

    return Ok(document);
}

export declare type MmmMatrixMention = {
    type: "matrixMention";
    props: {
        username: string;
        host: string;
    };
    children?: [];
};

type MagnetarChildren<N extends { children?: any[] }> = {
    [T in keyof N]: T extends "children"
        ? N[T] extends []
            ? []
            : MagNode[]
        : N[T];
};

export type MagNodeInline = mfm.MfmNode | MmmMatrixMention;
export type MagNode = MagnetarChildren<mfm.MfmNode> | MmmMatrixMention;

const mkBase = <T>(type: T): { type: T; props: Record<string, unknown> } => ({
    type,
    props: {},
});

const mk = <T, P extends Record<string, unknown>, C>(
    type: T,
    props: P,
    children?: C,
): {
    type: T;
    props: P;
    children: C extends (infer I & {})[] ? I[] : [];
} => {
    return {
        ...mkBase(type),
        props,
        ...(typeof children === "undefined" ? children : { children }),
    } as {
        type: T;
        props: P;
        children: C extends (infer I & {})[] ? I[] : [];
    };
};

export function mapMmXmlNodeListToMfm(nodes: NodeListOf<ChildNode>): MagNode[] {
    return Array.from(nodes).map(mapMmXmlNodeToMfm);
}

export function mapMmXmlNodeToMfm(node: Node): MagNode {
    switch (node.nodeType) {
        case Node.TEXT_NODE:
            return mfm.TEXT(node.textContent ?? "");
        case Node.ELEMENT_NODE: {
            const el = node as Element;
            switch (el.tagName) {
                case "quote":
                case "small":
                case "center":
                    return mk(
                        el.tagName,
                        {},
                        mapMmXmlNodeListToMfm(el.childNodes),
                    );
                case "b":
                    return mk("bold", {}, mapMmXmlNodeListToMfm(el.childNodes));
                case "i":
                    return mk(
                        "italic",
                        {},
                        mapMmXmlNodeListToMfm(el.childNodes),
                    );
                case "s":
                    return mk(
                        "strike",
                        {},
                        mapMmXmlNodeListToMfm(el.childNodes),
                    );
                case "inline-code":
                    return mk("inlineCode", {
                        code: el.textContent ?? "",
                    });
                case "inline-math":
                    return mk("mathInline", {
                        formula: el.textContent ?? "",
                    });
                case "a":
                    const url = el.getAttribute("href");

                    if (!url) break;

                    return mk(
                        "link",
                        {
                            url,
                            silent: el.getAttribute("embed") !== "true",
                        },
                        mapMmXmlNodeListToMfm(el.childNodes),
                    );
                case "code":
                    return mk("blockCode", {
                        code: el.textContent ?? "",
                        lang: el.getAttribute("lang"),
                    });
                case "math":
                    return mk("mathBlock", {
                        formula: el.textContent ?? "",
                    });
                case "hashtag":
                    if (!el.textContent) break;

                    return mk("hashtag", {
                        hashtag: el.textContent,
                    });
                case "function":
                    return mk("mathBlock", {
                        formula: el.textContent ?? "",
                    });
                case "ue":
                    if (!el.textContent) break;
                    return mk("unicodeEmoji", {
                        emoji: el.textContent,
                    });
                case "ee":
                    if (!el.textContent) break;
                    return mk("emojiCode", {
                        name: el.textContent,
                    });
                case "mention":
                    const username = el.getAttribute("name");
                    const host = el.getAttribute("host");
                    const type = el.getAttribute("type");
                    if (!username || (type === "matrix_user" && !host)) break;

                    switch (type) {
                        case "matrix_user":
                            return mk("matrixMention", {
                                username,
                                host: host!,
                            });
                        default:
                            return mk("mention", {
                                username,
                                host,
                                acct: host
                                    ? `@${username}@${host}`
                                    : `@${username}`,
                            });
                    }
                case "fn":
                    const name = el.getAttribute("name") ?? "";
                    const args = el
                        .getAttributeNames()
                        .filter((v) => v.startsWith("arg-"))
                        .map((v) => [
                            v.substring("arg-".length),
                            el.getAttribute(v) || true,
                        ])
                        .reduce<Record<string, string | true>>(
                            (acc, [k, v]) => ({
                                ...acc,
                                [k as string]: v as string | true,
                            }),
                            {},
                        );

                    return mk(
                        "fn",
                        {
                            name,
                            args,
                        },
                        mapMmXmlNodeListToMfm(el.childNodes),
                    );
            }
        }
    }

    return mfm.TEXT(node.textContent ?? "");
}

export function magnetarMarkdownToMfm(
    doc: XMLDocument,
): Result<MagNode[], MagnetarParseError> {
    const el = doc.documentElement;

    if (el.tagName !== "mmm") {
        return Err("InvalidRootNode");
    }

    return Ok(mapMmXmlNodeListToMfm(el.childNodes));
}
