/**
 * ProseMirror plugin that intercepts user input in "suggesting" mode
 * and converts edits into track-changes marks instead of direct mutations.
 *
 * In suggesting mode:
 * - Typed text → inserted with `suggestionInsert` mark (green underline)
 * - Backspace/Delete → text NOT deleted, marked with `suggestionDelete` (red strikethrough)
 * - Select + type → old text gets `suggestionDelete`, new text gets `suggestionInsert`
 * - Paste → same as select + type
 *
 * Uses ProseMirror props (handleTextInput, handleKeyDown, handlePaste) rather
 * than filterTransaction for reliability.
 */

import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';
import type { EditorView } from '@tiptap/pm/view';
import type { Slice } from '@tiptap/pm/model';
import type { Editor } from '@tiptap/core';

const pluginKey = new PluginKey('suggestion-plugin');

function makeSuggestionId(): string {
	return `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
}

// ── Typing session tracker ──
// Reuses the same suggestionId while the user types consecutively,
// so an entire typed word/phrase becomes ONE suggestion in the sidebar.
let _sessionSuggestionId: string | null = null;
let _sessionNextPos: number = -1; // the position where the next char is expected

function getOrCreateSessionId(insertPos: number): string {
	if (_sessionSuggestionId && insertPos === _sessionNextPos) {
		return _sessionSuggestionId;
	}
	_sessionSuggestionId = makeSuggestionId();
	return _sessionSuggestionId;
}

function advanceSession(id: string, nextPos: number): void {
	_sessionSuggestionId = id;
	_sessionNextPos = nextPos;
}

function resetSession(): void {
	_sessionSuggestionId = null;
	_sessionNextPos = -1;
}

/**
 * Create the suggestion mode ProseMirror plugin.
 * @param getSuggesting - callback that returns current suggesting mode state
 * @param getAuthor - callback that returns { authorId, authorName }
 */
export function createSuggestionPlugin(
	getSuggesting: () => boolean,
	getAuthor: () => { authorId: string; authorName: string },
): Plugin {
	return new Plugin({
		key: pluginKey,

		props: {
			/** Intercept typed text — insert with suggestionInsert mark. */
			handleTextInput(view: EditorView, from: number, to: number, text: string): boolean {
				if (!getSuggesting()) return false;

				const { state } = view;
				const { authorId, authorName } = getAuthor();
				// Reuse session ID for consecutive typing at the same position
				const suggestionId = (from !== to)
					? makeSuggestionId()           // replacement → new suggestion
					: getOrCreateSessionId(from);  // plain insert → batch with session
				const tr = state.tr;

				// If there's a selection (replacement), mark the selected text as deleted
				if (from !== to) {
					// Check if selected text is all suggestionInsert from the same author
					// → if so, just replace it (editing your own suggestion)
					const ownInsert = isOwnSuggestionInsert(state, from, to, authorId);
					if (ownInsert) {
						tr.replaceWith(from, to, state.schema.text(text, [
							state.schema.marks.suggestionInsert.create({
								suggestionId: ownInsert, authorId, authorName, createdAt: Date.now(),
							}),
						]));
						tr.setMeta('suggestion-applied', true);
						view.dispatch(tr);
						return true;
					}

					const deleteMark = state.schema.marks.suggestionDelete.create({
						suggestionId, authorId, authorName, createdAt: Date.now(),
					});
					tr.addMark(from, to, deleteMark);
				}

				// Insert the new text with insert mark after the (marked-for-deletion) text
				const insertPos = to;
				const insertMark = state.schema.marks.suggestionInsert.create({
					suggestionId, authorId, authorName, createdAt: Date.now(),
				});
				tr.insert(insertPos, state.schema.text(text, [insertMark]));
				tr.setMeta('suggestion-applied', true);

				// Place cursor after the inserted text
				const newCursorPos = insertPos + text.length;
				tr.setSelection(TextSelection.create(tr.doc, newCursorPos));

				view.dispatch(tr);
				advanceSession(suggestionId, newCursorPos);
				return true;
			},

			/** Intercept Backspace/Delete — mark text as deleted instead of removing. */
			handleKeyDown(view: EditorView, event: KeyboardEvent): boolean {
				if (!getSuggesting()) return false;
				if (event.key !== 'Backspace' && event.key !== 'Delete') return false;
				resetSession(); // break typing session on delete actions

				const { state } = view;
				const { from, to, empty } = state.selection;
				const { authorId, authorName } = getAuthor();

				let deleteFrom = from;
				let deleteTo = to;

				if (empty) {
					if (event.key === 'Backspace') {
						if (from === 0) return true;
						deleteFrom = from - 1;
						deleteTo = from;
					} else {
						if (from >= state.doc.content.size) return true;
						deleteFrom = from;
						deleteTo = from + 1;
					}

					// Don't cross block boundaries
					const $from = state.doc.resolve(deleteFrom);
					const $to = state.doc.resolve(deleteTo);
					if ($from.parent !== $to.parent) return true;
				}

				// Backspace/delete on own suggestionInsert → actually remove it
				const ownInsert = isOwnSuggestionInsert(state, deleteFrom, deleteTo, authorId);
				if (ownInsert) {
					const tr = state.tr;
					tr.delete(deleteFrom, deleteTo);
					tr.setMeta('suggestion-applied', true);
					view.dispatch(tr);
					return true;
				}

				// Already marked as suggestionDelete → skip past it
				if (isAlreadySuggestionDelete(state, deleteFrom, deleteTo)) {
					const tr = state.tr;
					const newPos = event.key === 'Backspace' ? deleteFrom : deleteTo;
					tr.setSelection(TextSelection.create(state.doc, newPos));
					view.dispatch(tr);
					return true;
				}

				// Mark the text as deleted
				const suggestionId = makeSuggestionId();
				const tr = state.tr;
				const deleteMark = state.schema.marks.suggestionDelete.create({
					suggestionId, authorId, authorName, createdAt: Date.now(),
				});
				tr.addMark(deleteFrom, deleteTo, deleteMark);
				tr.setMeta('suggestion-applied', true);

				if (event.key === 'Backspace') {
					tr.setSelection(TextSelection.create(tr.doc, deleteFrom));
				} else {
					tr.setSelection(TextSelection.create(tr.doc, deleteTo));
				}

				view.dispatch(tr);
				return true;
			},

			/** Intercept paste — insert pasted text as a suggestion. */
			handlePaste(view: EditorView, _event: ClipboardEvent, slice: Slice): boolean {
				if (!getSuggesting()) return false;
				resetSession(); // paste is a discrete action, break typing session

				const { state } = view;
				const { from, to } = state.selection;
				const { authorId, authorName } = getAuthor();
				const suggestionId = makeSuggestionId();
				const tr = state.tr;

				// Mark selected text as deleted
				if (from !== to) {
					const deleteMark = state.schema.marks.suggestionDelete.create({
						suggestionId, authorId, authorName, createdAt: Date.now(),
					});
					tr.addMark(from, to, deleteMark);
				}

				// Extract text from slice and insert with mark
				let pastedText = '';
				slice.content.forEach((node: any) => {
					if (pastedText) pastedText += '\n';
					pastedText += node.textContent;
				});

				if (pastedText) {
					const insertPos = to;
					const insertMark = state.schema.marks.suggestionInsert.create({
						suggestionId, authorId, authorName, createdAt: Date.now(),
					});
					tr.insert(insertPos, state.schema.text(pastedText, [insertMark]));
					tr.setMeta('suggestion-applied', true);
					tr.setSelection(TextSelection.create(tr.doc, insertPos + pastedText.length));
				}

				view.dispatch(tr);
				return true;
			},
		},
	});
}

/** Check if the range is entirely covered by suggestionInsert marks from the same author. */
function isOwnSuggestionInsert(
	state: { doc: any; schema: any },
	from: number,
	to: number,
	authorId: string,
): string | null {
	let allOwn = true;
	let foundId: string | null = null;
	state.doc.nodesBetween(from, to, (node: any) => {
		if (!node.isText) return;
		const mark = node.marks.find(
			(m: any) => m.type.name === 'suggestionInsert' && m.attrs.authorId === authorId
		);
		if (!mark) {
			allOwn = false;
		} else if (!foundId) {
			foundId = mark.attrs.suggestionId;
		}
	});
	return allOwn && foundId ? foundId : null;
}

/** Check if the range is already entirely covered by suggestionDelete marks. */
function isAlreadySuggestionDelete(state: { doc: any }, from: number, to: number): boolean {
	let allDeleted = true;
	state.doc.nodesBetween(from, to, (node: any) => {
		if (!node.isText) return;
		if (!node.marks.find((m: any) => m.type.name === 'suggestionDelete')) allDeleted = false;
	});
	return allDeleted;
}


/**
 * Accept a suggestion: insertions stay, deletions are removed.
 */
export function acceptSuggestion(editor: Editor, suggestionId: string) {
	const { state } = editor;
	const { tr } = state;

	// Collect ranges first, apply from end→start to preserve positions
	const deleteRanges: [number, number][] = [];
	const insertRanges: [number, number, any][] = [];

	state.doc.descendants((node: any, pos: number) => {
		if (!node.isText) return;
		const deleteMark = node.marks.find(
			(m: any) => m.type.name === 'suggestionDelete' && m.attrs.suggestionId === suggestionId
		);
		if (deleteMark) {
			deleteRanges.push([pos, pos + node.nodeSize]);
			return;
		}
		const insertMark = node.marks.find(
			(m: any) => m.type.name === 'suggestionInsert' && m.attrs.suggestionId === suggestionId
		);
		if (insertMark) {
			insertRanges.push([pos, pos + node.nodeSize, insertMark]);
		}
	});

	for (const [from, to] of deleteRanges.sort((a, b) => b[0] - a[0])) {
		tr.delete(from, to);
	}
	for (const [from, to, mark] of insertRanges.sort((a, b) => b[0] - a[0])) {
		tr.removeMark(from, to, mark);
	}

	if (tr.docChanged) {
		tr.setMeta('suggestion-accept', true);
		editor.view.dispatch(tr);
	}
}

/**
 * Reject a suggestion: insertions are removed, deletions stay.
 */
export function rejectSuggestion(editor: Editor, suggestionId: string) {
	const { state } = editor;
	const { tr } = state;

	const insertRanges: [number, number][] = [];
	const deleteRanges: [number, number, any][] = [];

	state.doc.descendants((node: any, pos: number) => {
		if (!node.isText) return;
		const insertMark = node.marks.find(
			(m: any) => m.type.name === 'suggestionInsert' && m.attrs.suggestionId === suggestionId
		);
		if (insertMark) {
			insertRanges.push([pos, pos + node.nodeSize]);
			return;
		}
		const deleteMark = node.marks.find(
			(m: any) => m.type.name === 'suggestionDelete' && m.attrs.suggestionId === suggestionId
		);
		if (deleteMark) {
			deleteRanges.push([pos, pos + node.nodeSize, deleteMark]);
		}
	});

	for (const [from, to] of insertRanges.sort((a, b) => b[0] - a[0])) {
		tr.delete(from, to);
	}
	for (const [from, to, mark] of deleteRanges.sort((a, b) => b[0] - a[0])) {
		tr.removeMark(from, to, mark);
	}

	if (tr.docChanged) {
		tr.setMeta('suggestion-reject', true);
		editor.view.dispatch(tr);
	}
}

/** Accept all suggestions in the document. */
export function acceptAllSuggestions(editor: Editor) {
	const ids = new Set<string>();
	editor.state.doc.descendants((node: any) => {
		if (!node.isText) return;
		for (const mark of node.marks) {
			if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
				ids.add(mark.attrs.suggestionId);
			}
		}
	});
	for (const id of ids) acceptSuggestion(editor, id);
}

/** Reject all suggestions in the document. */
export function rejectAllSuggestions(editor: Editor) {
	const ids = new Set<string>();
	editor.state.doc.descendants((node: any) => {
		if (!node.isText) return;
		for (const mark of node.marks) {
			if (mark.type.name === 'suggestionInsert' || mark.type.name === 'suggestionDelete') {
				ids.add(mark.attrs.suggestionId);
			}
		}
	});
	for (const id of ids) rejectSuggestion(editor, id);
}
