import type Core from 'markdown-it/lib/parser_core';
import type Token from 'markdown-it/lib/token';
import type StateCore from 'markdown-it/lib/rules_core/state_core';
import type {MarkdownItPluginCb} from '../typings';

import {bold} from 'chalk';
import yaml from 'js-yaml';

import {getSrcTokenAttr} from '../../utils';

interface Options {
    extractChangelogs?: boolean;
}

const CHANGELOG_OPEN_RE = /^\{% changelog %}/;
const CHANGELOG_CLOSE_RE = /^\{% endchangelog %}/;

function isOpenToken(tokens: Token[], i: number) {
    return (
        tokens[i].type === 'paragraph_open' &&
        tokens[i + 1].type === 'inline' &&
        tokens[i + 2].type === 'paragraph_close' &&
        CHANGELOG_OPEN_RE.test(tokens[i + 1].content)
    );
}

function isCloseToken(tokens: Token[], i: number) {
    return (
        tokens[i]?.type === 'paragraph_open' &&
        tokens[i + 1].type === 'inline' &&
        tokens[i + 2].type === 'paragraph_close' &&
        CHANGELOG_CLOSE_RE.test(tokens[i + 1].content)
    );
}

function isTitle(tokens: Token[], i = 0) {
    return (
        tokens[i].type === 'heading_open' &&
        tokens[i + 1].type === 'inline' &&
        tokens[i + 2].type === 'heading_close'
    );
}

function isImageParagraph(tokens: Token[], i = 0) {
    return (
        tokens[i].type === 'paragraph_open' &&
        tokens[i + 1].type === 'inline' &&
        tokens[i + 2].type === 'paragraph_close' &&
        tokens[i + 1].children?.some((t) => t.type === 'image')
    );
}

function parseBody(tokens: Token[], state: StateCore) {
    const {md, env} = state;

    const metadataToken = tokens.shift();
    if (metadataToken?.type !== 'fence') {
        throw new Error('Metadata tag not found');
    }

    let metadata: Record<string, unknown> = {};
    const rawMetadata = yaml.load(metadataToken.content, {
        schema: yaml.JSON_SCHEMA,
    }) as Record<string, unknown>;
    if (rawMetadata && typeof rawMetadata === 'object') {
        metadata = rawMetadata;
    }

    if (!isTitle(tokens)) {
        throw new Error('Title tag not found');
    }
    const title = tokens.splice(0, 3)[1].content;

    let image;
    if (isImageParagraph(tokens)) {
        const paragraphTokens = tokens.splice(0, 3);
        const imageToken = paragraphTokens[1]?.children?.find((token) => token.type === 'image');
        if (imageToken) {
            const width = Number(imageToken.attrGet('width'));
            const height = Number(imageToken.attrGet('height'));
            let ratio;
            if (Number.isFinite(width) && Number.isFinite(height)) {
                ratio = height / width;
            }
            let alt = imageToken.attrGet('title') || '';
            if (!alt && imageToken.children) {
                alt = md.renderer.renderInlineAsText(imageToken.children, md.options, env);
            }
            image = {
                src: getSrcTokenAttr(imageToken),
                alt,
                ratio,
            };
        }
    }

    const description = md.renderer.render(tokens, md.options, env);

    if (typeof metadata.storyId === 'number') {
        metadata.storyId = String(metadata.storyId);
    }

    return {
        ...metadata,
        title,
        image,
        description,
    };
}

const changelog: MarkdownItPluginCb<Options> = function (md, {extractChangelogs, log, path}) {
    const plugin: Core.RuleCore = (state) => {
        const {tokens, env} = state;

        for (let i = 0, len = tokens.length; i < len; i++) {
            const isOpen = isOpenToken(tokens, i);
            if (!isOpen) continue;

            const openAt = i;
            let isCloseFound = false;
            while (i < len) {
                i++;
                if (isCloseToken(tokens, i)) {
                    isCloseFound = true;
                    break;
                }
            }

            if (!isCloseFound) {
                log.error(`Changelog close tag in not found: ${bold(path)}`);
                break;
            }

            const closeAt = i + 2;

            if (env && extractChangelogs) {
                const content = tokens.slice(openAt, closeAt + 1);

                // cut open
                content.splice(0, 3);
                // cut close
                content.splice(-3);

                try {
                    const changelogLocal = parseBody(content, state);

                    if (!env.changelogs) {
                        env.changelogs = [];
                    }

                    env.changelogs.push(changelogLocal);
                } catch (err) {
                    log.error(`Changelog error: ${(err as Error).message} in ${bold(path)}`);
                    continue;
                }
            }

            tokens.splice(openAt, closeAt + 1 - openAt);
            len = tokens.length;
            i = openAt - 1;
        }
    };

    try {
        md.core.ruler.before('curly_attributes', 'changelog', plugin);
    } catch (e) {
        md.core.ruler.push('changelog', plugin);
    }
};

export = changelog;
