/* eslint-disable @typescript-eslint/naming-convention */
/* eslint-disable @typescript-eslint/no-floating-promises */
import { MarkdownSerializerState } from 'prosemirror-markdown';
import MarkdownIt from 'markdown-it/lib';
import Token from 'markdown-it/lib/token';
import StateInline from 'markdown-it/lib/rules_inline/state_inline';
import { Node as ProsemirrorNode } from 'prosemirror-model';
import { EditorView, NodeView } from 'prosemirror-view';
import { Octokit } from '@octokit/rest';
import { GetResponseTypeFromEndpointMethod } from '@octokit/types';
import { ExternalEditorPlugin } from '@microsoft/stacks-editor';
import { AltTextValidationResult, getPosParam, images } from '../models/editor-model';
import { escapeHTML } from '../utils/escapeHtml';
import { isBoolean } from '../utils/typeHandlers';
import { getImageFromGitHubAndSetSrc } from '../api/github';
const token = localStorage.getItem('gitHubToken');
const octokit = new Octokit({
	auth: token
});
type GetRepoResponseType = GetResponseTypeFromEndpointMethod<typeof octokit.repos.get>;

const MARKER_STR = ':';
const TRIPLE_COLON = ':::';
const SOURCE_RE = /(?<=source=")(.*?)(?=")/i;
const TYPE_RE = /(?<=type=")(.*?)(?=")/i;
const ALT_TEXT_RE = /(?<=alt-text=")(.*?)(?=")/i;
const LIGHTBOX_RE = /(?<=lightbox=")(.*?)(?=")/i;
const BORDER_RE = /(?<=border=")(.*?)(?=")/i;
const LOC_SCOPE_RE = /(?<=loc-scope=")(.*?)(?=")/i;
const LINK_RE = /(?<=link=")(.*?)(?=")/i;

export interface IObjectKeys {
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	[key: string]: any;
}

interface DocsAttribute {
	value: boolean | string;
	regex: RegExp;
	attribute?: string;
	docsAttribute: string;
}

interface DocsImage extends IObjectKeys {
	source: DocsAttribute;
	content: DocsAttribute;
	'alt-text'?: DocsAttribute;
	lightbox?: DocsAttribute;
	link?: DocsAttribute;
	border?: DocsAttribute;
	type: DocsAttribute;
	'loc-scope'?: DocsAttribute;
	data?: DocsAttribute;
}

// keep track of all images so we can assign unique IDs for stacks popovers
let imageId = 0;
export class DocsTripleColonImagesView implements NodeView {
	dom?: Node | null;
	img: HTMLElement;
	popover: HTMLElement;
	form: HTMLFormElement;
	id: number;
	selectionActive: boolean;
	docsImg: DocsImage = {
		source: {
			value: '',
			regex: SOURCE_RE,
			attribute: 'src',
			docsAttribute: 'source'
		},
		content: { value: '', regex: null, docsAttribute: 'content' },
		'alt-text': {
			value: '',
			regex: ALT_TEXT_RE,
			attribute: 'alt',
			docsAttribute: 'alt-text'
		},
		lightbox: {
			value: '',
			regex: LIGHTBOX_RE,
			docsAttribute: 'lightbox'
		},
		link: { value: '', regex: LINK_RE, docsAttribute: 'link' },
		border: { value: false, regex: BORDER_RE, docsAttribute: 'border' },
		type: { value: '', regex: TYPE_RE, docsAttribute: 'type' },
		'loc-scope': {
			value: '',
			regex: LOC_SCOPE_RE,
			docsAttribute: 'loc-scope'
		},
		data: {
			value: '',
			regex: null,
			docsAttribute: null
		}
	};

	constructor(node: ProsemirrorNode, view: EditorView, getPos: getPosParam) {
		this.id = imageId++;

		this.img = this.createImage(node, view, getPos);
		this.form = this.createForm();
		this.form.addEventListener('submit', event =>
			this.handleChangedImageAttributes(event, getPos, view)
		);

		this.popover = this.createPopover();

		this.dom = document.createElement('div');
		this.dom.appendChild(this.img);
		this.dom.appendChild(this.popover);
		this.dom.addEventListener('s-popover:hide', (event: Event) => this.preventClose(event));
	}
	/**
	 * We want to trigger Stacks' showing and hiding of popovers whenever an image is considered
	 * "selected" by prosemirror. This can happen by mouse-clicking or arrowing through the editor
	 */
	selectNode(): void {
		const img = this.getImgElement();
		img.classList.add('bs-ring');
		this.selectionActive = true;
		// tell Stacks to hide the popover
		img.dispatchEvent(new Event('image-popover-show'));

		const inputFields = this.form.querySelectorAll('input');
		if (inputFields.length > 0) {
			inputFields[0].focus({
				preventScroll: true
			});
		}
	}

	deselectNode(): void {
		const img = this.getImgElement();
		img.classList.remove('bs-ring');
		this.selectionActive = false;
		// tell Stacks to hide the popover
		img.dispatchEvent(new Event('image-popover-hide'));
	}

	stopEvent(event: Event): boolean {
		// prevent elements from within the tooltip to bubble up to the outer editor
		// as this would make the editor view close the tooltip view right away
		return this.popover.contains(event.target as Element);
	}

	ignoreMutation(): boolean {
		return true;
	}

	destroy(): void {
		this.img.remove();
		this.popover.remove();
		this.dom = null;
		this.form.remove();
	}

	private createImage(node: ProsemirrorNode, view: EditorView, getPos: getPosParam): HTMLElement {
		const src = node.attrs.src as string;
		// is this a relative path?
		// pull that image from github
		if (src && !images.get(src)) {
			if (!/^http(s)?:\/\//.test(src) && !/^data:image\/(gif|png|jpeg|webp);/.test(src)) {
				const updateNode = this.updateNode;
				const id = this.id;
				getImageFromGitHubAndSetSrc(view, getPos, node, updateNode, id);
			}
		} else {
			const found = images.get(src);
			this.docsImg.data.value = found.data;
		}

		let element: HTMLElement;
		const img = document.createElement('img');
		img.setAttribute('aria-controls', `img-popover-${this.id}`);
		img.setAttribute('data-controller', 's-popover');
		img.setAttribute(
			'data-action',
			'image-popover-show->s-popover#show image-popover-hide->s-popover#hide'
		);

		img.src = this.docsImg.data ? (this.docsImg.data.value as string) : (node.attrs.src as string);
		element = img;
		this.docsImg.source.value = node.attrs.src as string;
		if (node.attrs.alt) {
			img.alt = node.attrs.alt as string;
			this.docsImg['alt-text'].value = node.attrs.alt as string;
			element = img;
		}
		if (node.attrs.type) this.docsImg.type.value = node.attrs.type as string;
		if (node.attrs.lightbox) {
			this.docsImg.lightbox.value = node.attrs.lightbox as string;
		}
		if (node.attrs.border) {
			this.docsImg.border.value = node.attrs.border as boolean;
			if (this.docsImg.border.value) {
				const div = document.createElement('div');
				div.setAttribute('class', 'mx-imgBorder');
				div.appendChild(img);
				element = div;
			}
		}
		if (node.attrs['loc-scope'])
			this.docsImg['loc-scope'].value = node.attrs['loc-scope'] as string;
		if (node.attrs.link) {
			this.docsImg.link.value = node.attrs.link as string;
		}
		if (node.attrs.content) this.docsImg.content.value = node.attrs.content as string;
		return element;
	}

	private createForm(): HTMLFormElement {
		const form = document.createElement('form');
		const border = this.docsImg.border?.value;
		form.className = 'd-flex fd-column';
		form.innerHTML = escapeHTML`
            <label class="flex--item s-label mb4" for="img-src-${this.id}">Image source</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="src" id="img-src-${
									this.id
								}" value="${
			this.docsImg.source.value
		}" placeholder="https://example.com/image.png"/>
            </div>
            <label class="flex--item s-label mb4" for="img-alt-${this.id}">Image description</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="alt" id="img-alt-${
									this.id
								}" value="${
			this.docsImg['alt-text'].value
		}" placeholder="A description for the image"/>
            </div>
            <div class="d-flex gs4 gsy fd-column">
            <label class="flex--item s-label" for="img-type-${this.id}">
            Image type
            </label>
                <div class="flex--item s-select">
                    <select id="img-type-${this.id}">
                        <option value="" ${
													this.docsImg.type.value ? '' : 'selected'
												}>Please select image type</option>
                        <option value="content" ${
													this.docsImg.type.value === 'content' ? 'selected' : ''
												}>Content</option>
                        <option value="icon" ${
													this.docsImg.type.value === 'icon' ? 'selected' : ''
												}>Icon</option>
                        <option value="complex" ${
													this.docsImg.type.value === 'complex' ? 'selected' : ''
												}>Complex</option>
                    </select>
                </div>
            </div>
            <label class="flex--item s-label mb4" for="img-link-${this.id}">Image link</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="link" id="img-link-${
									this.id
								}" value="${this.docsImg.link.value}" placeholder="Image link"/>
            </div>
            <label class="flex--item s-label mb4" for="img-border-${this.id}">Image border</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-checkbox" type="checkbox" name="border" id="img-border-${
									this.id
								}" ${(border as boolean) ? `checked` : ``} value="${
			this.docsImg.border.value
		}" placeholder="Image has border"/>
            </div>
            <label class="flex--item s-label mb4" for="img-lightbox-${
							this.id
						}">Image lightbox</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="lightbox" id="img-lightbox-${
									this.id
								}" value="${this.docsImg.lightbox.value}" placeholder="Image has a lightbox"/>
            </div>
            <label class="flex--item s-label mb4" for="img-loc-scope-${
							this.id
						}">Image loc-scope</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="loc-scope" id="img-loc-scope-${
									this.id
								}" value="${this.docsImg['loc-scope'].value}" placeholder="Image loc-scope"/>
            </div>
            <aside class="s-notice s-notice__warning d-none m8 js-validation-message" role="status" aria-hidden="true"></aside>
            <button class="s-btn s-btn__primary" type="submit" aria-pressed="false">Apply</button>
        `;
		return form;
	}

	private createPopover(): HTMLDivElement {
		const popover = document.createElement('div');
		popover.className = 's-popover ws-normal wb-normal js-img-popover';
		popover.id = `img-popover-${this.id}`;

		// TODO added `ws-normal` to fix FF only bug. Will file bug against Stacks and revisit
		popover.innerHTML = `<div class="s-popover--arrow ws-normal"></div>`;
		popover.append(this.form);

		return popover;
	}

	private handleChangedImageAttributes(event: Event, getPos: getPosParam, view: EditorView) {
		event.preventDefault();

		if (typeof getPos !== 'function') return;

		this.hideValidationError();

		const findInput = (selector: string): HTMLInputElement => this.form.querySelector(selector);
		const alt = findInput(`#img-alt-${this.id}`);
		const validationResult = this.validateImageAltText(alt.value);
		switch (validationResult) {
			case AltTextValidationResult.AltTextDoesntEndWithPeriod:
				this.showValidationError('Alt text should end with a period.');
				return;
			case AltTextValidationResult.AltTextStartsWithLowerCase:
				this.showValidationError('Alt text should start with an uppercase letter.');
				return;
			case AltTextValidationResult.AltTextTooLong:
				this.showValidationError('Alt text too long. The max is 150 characters, including spaces.');
				return;
		}

		Object.keys(this.docsImg).forEach(key => {
			const docsAttribute: DocsAttribute = this.docsImg[key] as DocsAttribute | DocsAttribute;
			if (docsAttribute.regex) {
				const input = findInput(
					`#img-${docsAttribute.attribute ?? docsAttribute.docsAttribute}-${this.id}`
				);
				let value;
				switch (input.type) {
					case 'checkbox':
						value = input.checked;
						this.updateContentIfAttributeChangedBoolean(this.docsImg.content, docsAttribute, value);
						break;
					default:
						value = input.value as string;
						this.updateContentIfAttributeChanged(this.docsImg.content, docsAttribute, value);
						break;
				}
			}
		});

		//fix any issues where there are double
		//spaces from removing or adding attributes
		this.docsImg.content.value = (this.docsImg.content.value as string)
			.replace(/[  ]/g, ' ')
			.replace(/[ ](?=:::)/, '');
		const docsImg = this.docsImg;
		this.updateNode(view, getPos, docsImg);
		view.focus();
	}

	private updateNode(view: EditorView, getPos: getPosParam, docsImg: DocsImage): void {
		if (typeof getPos !== 'function') return;

		view.dispatch(
			view.state.tr.setNodeMarkup(getPos(), null, {
				src: docsImg.source.value,
				alt: docsImg['alt-text'].value,
				type: docsImg.type.value,
				lightbox: docsImg.lightbox.value,
				border: docsImg.border,
				'loc-scope': docsImg['loc-scope'].value,
				link: docsImg.link.value,
				content: docsImg.content.value,
				data: docsImg.data.value
			})
		);

		view.focus();
	}

	private updateContentIfAttributeChanged(
		content: DocsAttribute,
		attribute: DocsAttribute,
		value: string
	): void {
		let { value: output } = <{ value: string }>content;
		const attributeFoundAndThisIsTheValue = getAttribute(attribute.regex, output);
		if (attributeFoundAndThisIsTheValue === value) {
			//no change
		} else if (value && !attributeFoundAndThisIsTheValue) {
			//couldnt find attribute
			//so add the attribute
			output = addAttribute(output, value, attribute.docsAttribute);
		} else if (value !== attributeFoundAndThisIsTheValue) {
			output = updateContentValues(output, attribute, value);
		} else if (!value) {
			//new value is empty
			//so remove the attribute
			output = removeAttribute(output, attribute.docsAttribute);
		}
		attribute.value = value;
		content.value = output;
	}

	private updateContentIfAttributeChangedBoolean(
		content: DocsAttribute,
		attribute: DocsAttribute,
		value: boolean
	): void {
		let { value: output } = <{ value: string }>content;
		const attributeFoundAndThisIsTheValue = getAttribute(attribute.regex, output);
		if (value) {
			if (!attributeFoundAndThisIsTheValue && value) {
				//couldnt find attribute
				//so add the attribute
				output = addAttribute(output, value.toString(), attribute.docsAttribute);
			}
		} else if (attributeFoundAndThisIsTheValue && isBoolean(attributeFoundAndThisIsTheValue)) {
			const attributeValue: boolean = Boolean(attributeFoundAndThisIsTheValue);
			// attributeValue is now boolean
			if (attributeValue && !value) {
				output = removeAttribute(output, attribute.docsAttribute, attributeValue.toString());
			}
		}

		attribute.value = value;
		content.value = output;
	}

	private showValidationError(errorMessage: string, level = 'warning'): void {
		const validationElement = (this.form as HTMLElement).querySelector('.js-validation-message');

		if (level === 'warning') {
			validationElement.classList.remove('s-notice__danger');
			validationElement.classList.add('s-notice__warning');
		} else {
			validationElement.classList.remove('s-notice__warning');
			validationElement.classList.add('s-notice__danger');
		}

		validationElement.classList.remove('d-none');
		validationElement.textContent = errorMessage;
	}

	private hideValidationError(): void {
		const validationElement = (this.form as HTMLElement).querySelector('.js-validation-message');
		validationElement.classList.add('d-none');
		validationElement.classList.remove('s-notice__warning');
		validationElement.classList.remove('s-notice__danger');
		validationElement.innerHTML = '';
	}

	private validateImageAltText(alt: string): AltTextValidationResult {
		if (!alt.endsWith('.')) {
			return AltTextValidationResult.AltTextDoesntEndWithPeriod;
		}
		if (alt.charAt(0) === alt.charAt(0).toLowerCase()) {
			return AltTextValidationResult.AltTextStartsWithLowerCase;
		}
		if (alt.length > 150) {
			return AltTextValidationResult.AltTextTooLong;
		}

		return AltTextValidationResult.Ok;
	}

	// don't let Stacks close the popover if the image
	// element is still selected in the editor
	private preventClose(event: Event) {
		if (this.selectionActive) {
			event.preventDefault();
		}
	}

	private getImgElement(): HTMLImageElement {
		return (this.img.tagName !== 'IMG' ? this.img.firstChild : this.img) as HTMLImageElement;
	}
}

function getAttribute(regex: RegExp, content: string) {
	const match = regex?.exec(content);
	if (match && match.length > 0) {
		return match[1];
	}
	return '';
}

/**
 * Parser rule for docs_images
 * @param state
 * @param silent
 */
function docsImagesMarkdownPluginImpl(state: StateInline, silent: boolean) {
	let pos = state.pos;
	if (state.src.charAt(pos) !== MARKER_STR) {
		return false;
	}
	pos++;
	if (state.src.charAt(pos) !== MARKER_STR) {
		return false;
	}
	pos++;
	if (state.src.charAt(pos) !== MARKER_STR) {
		return false;
	}
	pos++;
	const markup = state.src.slice(state.pos, pos);

	const endOfContainer = state.src.indexOf(TRIPLE_COLON, pos);
	if (endOfContainer !== -1) {
		const params = state.src.slice(state.pos, endOfContainer + 3);

		if (!params.startsWith(`:::image`)) {
			return false;
		}

		if (!silent) {
			const token = state.push(`docs_triple_colon_image`, 'docs_triple_colon_image', 0);
			token.attrPush(['src', getAttribute(SOURCE_RE, params)]);
			token.attrPush(['type', getAttribute(TYPE_RE, params)]);
			token.attrPush(['alt', getAttribute(ALT_TEXT_RE, params)]);
			token.attrPush(['lightbox', getAttribute(LIGHTBOX_RE, params)]);
			token.attrPush(['border', getAttribute(BORDER_RE, params)]);
			token.attrPush(['loc-scope', getAttribute(LOC_SCOPE_RE, params)]);
			token.attrPush(['link', getAttribute(LINK_RE, params)]);
			token.attrPush(['content', params]);
			token.info = params;
		}
		state.pos = endOfContainer + 3;
		return true;
	}

	if (!silent) {
		state.pending += markup;
	}
	state.pos = pos;
	return true;
}

/**
 * Parses out `stack_snippet`s
 * @param md
 */
function docsImagesMarkdownPlugin(md: MarkdownIt): void {
	md.inline.ruler.push(`docs_images`, docsImagesMarkdownPluginImpl, {
		alt: []
	});
	// md.renderer.rules[`${name}_container`] = render;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const DocsTripleColonImagesPlugin: ExternalEditorPlugin = {
	markdownParser: {
		tokens: {
			docs_image: {
				block: 'docs_image',
				getAttrs: (tok: Token): Record<string, unknown> => ({
					src: tok.attrGet('src'),
					alt: tok.attrGet('alt') || tok.children?.[0]?.content || null,
					type: tok.attrGet('type'),
					lightbox: tok.attrGet('lightbox'),
					border: tok.attrGet('border'),
					'loc-scope': tok.attrGet('loc-scope'),
					link: tok.attrGet('link'),
					content: tok.attrGet('content')
				}),
				noCloseToken: true
			}
		},
		plugins: [docsImagesMarkdownPlugin]
	},
	markdownSerializers: {
		docs_image(state: MarkdownSerializerState, node: ProsemirrorNode): void {
			state.write(node.attrs.content as string);
		}
	},
	menuEntries: [],
	nodeViews: {
		docs_image(node, view, getPos): NodeView {
			return new DocsTripleColonImagesView(node, view, getPos);
		}
	},
	plugins: [],
	schema: {
		nodes: {
			docs_image: {
				attrs: {
					src: { default: '' },
					alt: { default: '' },
					type: { default: '' },
					lightbox: { default: '' },
					border: { default: '' },
					'loc-scope': { default: '' },
					link: { default: '' },
					content: { default: '' }
				},
				marks: '',
				group: 'inline',
				inline: true,
				selectable: true,
				noCloseToken: true
			}
		}
	}
};

export function addAttribute(content: string, value: string, attribute: string): string {
	const attr = `${attribute}="${value}"`;
	return `${content.substring(0, content.length - 3)} ${attr}:::`;
}

export function removeAttribute(
	content: string,
	attribute: string,
	attributeValue: string = null
): string {
	if (attributeValue) {
		const attr = new RegExp(`${attribute}="${attributeValue}"`);
		return content.replace(attr, '');
	} else {
		const attr = new RegExp(`${attribute}="(\\s*)?"`);
		return content.replace(attr, '');
	}
}

export function updateContentValues(
	content: string,
	docsAttribute: DocsAttribute | DocsAttribute,
	replacement: string
): string {
	// need to check if values have been added
	// to content. If so then add the attribute to the
	// markdown content that will be rendered in commonmark
	// update attribute values if they exist
	return content.replace(docsAttribute.regex, replacement);
}

export function getWebEditorAttributes(): {
	repo: string;
	org: string;
	path: string;
} {
	const docsMarkdownEditor = document.getElementsByTagName('docs-markdown-editor');
	if (docsMarkdownEditor && docsMarkdownEditor.length > 0) {
		const editorElement = docsMarkdownEditor[0];
		// const mode = editorElement.getAttribute("mode");
		const org = editorElement.getAttribute('org');
		const repo = editorElement.getAttribute('repo');
		const path = editorElement.getAttribute('path');

		return { repo, org, path };
	} else {
		//TESTING REMOVE BEFORE RELEASE!!
		return {
			repo: 'azure-docs-pr',
			org: 'MicrosoftDocs',
			path: 'articles/azure-functions/create-first-function-vs-code-csharp.md'
		};
		// return null;
	}
}

export async function fetchImageFromGitHub(
	org: string,
	repo: string,
	branch: string,
	fileName: string
): Promise<string> {
	const { data } = await octokit.rest.repos.getContent({
		owner: org,
		repo,
		path: fileName,
		ref: branch
	});

	if (data) {
		const { name, content } = data as Content;
		const extension = name.split('.').pop();
		// data:image/jpeg;base64,<data>
		const image = `data:image/${extension};base64,${content}`;
		return image.replace(/\n/g, '');
	}
	return null;
}

export async function getRepo(org: string, repo: string): Promise<{ default_branch: string }> {
	const { data } = (await octokit.request(`GET /repos/${org}/${repo}`, {
		owner: org,
		repo
	})) as GetRepoResponseType;
	if (data) {
		return data;
	}
	return null;
}

export function relativeToAbsolute(base: string, relative: string): string {
	const stack = base.split('/');
	const parts = relative.split('/');
	stack.pop();
	for (let i = 0; i < parts.length; i++) {
		if (parts[i] === '.') continue;
		if (parts[i] === '..') stack.pop();
		else stack.push(parts[i]);
	}
	return stack.join('/');
}

export interface Content {
	type: string;
	size: number;
	encoding: string;
	content: string;
	name: string;
	path: string;
	sha: string;
	url: string;
	git_url: string;
	html_url: string;
	download_url: string;
}
