/**
 * Logseq graph ↔ rNotes/rDocs converter.
 *
 * Import: ZIP of pages/ + journals/ dirs, property:: value syntax, bullet outliner blocks
 * Export: ZIP with Logseq-compatible page files + properties
 *
 * Shared across rDocs and rNotes — each module registers this converter in its own registry.
 */

import JSZip from 'jszip';
import { markdownToTiptap, tiptapToMarkdown, extractPlainTextFromTiptap } from '../markdown-tiptap';
import { hashContent } from './types';
import type { ConvertedNote, ImportInput, ImportResult, ExportOptions, ExportResult, NoteConverter, NoteItem } from './types';

/** Parse Logseq property:: value lines from the top of a page. */
function parseLogseqProperties(content: string): { properties: Record<string, string>; body: string } {
	const lines = content.split('\n');
	const properties: Record<string, string> = {};
	let bodyStart = 0;

	for (let i = 0; i < lines.length; i++) {
		const match = lines[i].match(/^([a-zA-Z_-]+)::\s*(.*)$/);
		if (match) {
			properties[match[1].toLowerCase()] = match[2].trim();
			bodyStart = i + 1;
		} else if (lines[i].trim() === '') {
			bodyStart = i + 1;
			continue;
		} else {
			break;
		}
	}

	return { properties, body: lines.slice(bodyStart).join('\n') };
}

/** Convert Logseq outliner bullet format to regular markdown. */
function convertOutlinerToMarkdown(content: string): string {
	const lines = content.split('\n');
	const result: string[] = [];

	for (const line of lines) {
		const match = line.match(/^(\s*)- (.*)$/);
		if (match) {
			const indent = match[1];
			const text = match[2];

			let level = 0;
			if (indent.length > 0) {
				const tabCount = (indent.match(/\t/g) || []).length;
				const spaceCount = indent.replace(/\t/g, '').length;
				level = tabCount + Math.floor(spaceCount / 2);
			}

			if (level === 0 && text.startsWith('# ')) {
				result.push(text);
			} else if (level === 0) {
				result.push(`- ${text}`);
			} else {
				const indentation = '  '.repeat(level);
				result.push(`${indentation}- ${text}`);
			}
		} else {
			result.push(line);
		}
	}

	return result.join('\n');
}

/** Convert [[page references]] to standard links. */
function convertPageRefs(md: string): string {
	return md.replace(/\[\[([^\]]+)\]\]/g, '[$1]($1)');
}

/** Convert Logseq tags (#tag or #[[multi word tag]]). */
function extractLogseqTags(content: string): string[] {
	const tags: string[] = [];
	const singleTags = content.match(/#([a-zA-Z0-9_-]+)/g);
	if (singleTags) tags.push(...singleTags.map(t => t.slice(1).toLowerCase()));
	const multiTags = content.match(/#\[\[([^\]]+)\]\]/g);
	if (multiTags) tags.push(...multiTags.map(t => t.slice(3, -2).toLowerCase().replace(/\s+/g, '-')));
	return [...new Set(tags)];
}

/** Parse Logseq journal filename to date. */
function parseJournalDate(filename: string): string | null {
	const match = filename.match(/(\d{4})[_-](\d{2})[_-](\d{2})\.md$/);
	if (match) return `${match[1]}-${match[2]}-${match[3]}`;
	return null;
}

/** Extract title from filename. */
function titleFromPath(filePath: string): string {
	const filename = filePath.split('/').pop() || 'Untitled';
	return filename.replace(/\.md$/i, '').replace(/%2F/g, '/').replace(/_/g, ' ');
}

export const logseqConverter: NoteConverter = {
	id: 'logseq',
	name: 'Logseq',
	requiresAuth: false,

	async import(input: ImportInput): Promise<ImportResult> {
		if (!input.fileData) {
			throw new Error('Logseq import requires a ZIP file');
		}

		const zip = await JSZip.loadAsync(input.fileData);
		const notes: ConvertedNote[] = [];
		const warnings: string[] = [];
		let graphName = 'Logseq Import';

		const mdFiles: { path: string; file: JSZip.JSZipObject; isJournal: boolean }[] = [];
		zip.forEach((path, file) => {
			if (file.dir) return;
			if (!path.endsWith('.md')) return;
			if (path.includes('logseq/') && !path.includes('pages/') && !path.includes('journals/')) return;
			if (path.includes('.recycle/')) return;

			const isJournal = path.includes('journals/');
			mdFiles.push({ path, file, isJournal });
		});

		if (mdFiles.length === 0) {
			warnings.push('No .md files found in pages/ or journals/ directories');
			return { notes, notebookTitle: graphName, warnings };
		}

		const firstPath = mdFiles[0].path;
		const rootFolder = firstPath.split('/')[0];
		if (rootFolder && mdFiles.every(f => f.path.startsWith(rootFolder + '/'))) {
			graphName = rootFolder;
			for (const f of mdFiles) {
				f.path = f.path.slice(rootFolder.length + 1);
			}
		}

		for (const { path, file, isJournal } of mdFiles) {
			try {
				const raw = await file.async('string');
				const { properties, body } = parseLogseqProperties(raw);

				let md = convertOutlinerToMarkdown(body);
				md = convertPageRefs(md);

				const filename = path.split('/').pop() || '';
				let title: string;

				if (isJournal) {
					const date = parseJournalDate(filename);
					title = date ? `Journal: ${date}` : titleFromPath(path);
				} else {
					title = properties.title || titleFromPath(path);
				}

				const tiptapJson = markdownToTiptap(md);
				const contentPlain = extractPlainTextFromTiptap(tiptapJson);

				const tags: string[] = [];
				if (properties.tags) {
					const tagStr = properties.tags.replace(/\[\[|\]\]/g, '');
					tags.push(...tagStr.split(',').map(t => t.trim().toLowerCase()).filter(Boolean));
				}
				tags.push(...extractLogseqTags(raw));
				if (isJournal) tags.push('journal');

				notes.push({
					title,
					content: tiptapJson,
					contentPlain,
					markdown: md,
					tags: [...new Set(tags)],
					sourceRef: {
						source: 'logseq',
						externalId: path,
						lastSyncedAt: Date.now(),
						contentHash: hashContent(raw),
					},
				});
			} catch (err) {
				warnings.push(`Failed to parse ${path}: ${(err as Error).message}`);
			}
		}

		return { notes, notebookTitle: graphName, warnings };
	},

	async export(notes: NoteItem[], opts: ExportOptions): Promise<ExportResult> {
		const zip = new JSZip();
		const graphName = opts.notebookTitle || 'rNotes Export';
		const pagesDir = zip.folder('pages')!;

		for (const note of notes) {
			let md: string;
			if (note.contentFormat === 'tiptap-json' && note.content) {
				md = tiptapToMarkdown(note.content);
			} else if (note.content) {
				md = note.content.replace(/<[^>]*>/g, '').trim();
			} else {
				md = '';
			}

			const props: string[] = [];
			if (note.tags.length > 0) {
				props.push(`tags:: ${note.tags.map(t => `[[${t}]]`).join(', ')}`);
			}
			if (note.type !== 'NOTE') {
				props.push(`type:: ${note.type.toLowerCase()}`);
			}
			props.push(`created:: ${new Date(note.createdAt).toISOString().split('T')[0]}`);

			const mdLines = md.split('\n');
			const outliner: string[] = [];
			for (const line of mdLines) {
				if (line.trim() === '') continue;
				if (line.startsWith('#')) {
					outliner.push(`- ${line}`);
				} else if (line.startsWith('- ') || line.startsWith('* ')) {
					outliner.push(`- ${line.slice(2)}`);
				} else if (line.match(/^\d+\.\s/)) {
					outliner.push(`- ${line.replace(/^\d+\.\s/, '')}`);
				} else {
					outliner.push(`- ${line}`);
				}
			}

			const propsBlock = props.length > 0 ? props.join('\n') + '\n\n' : '';
			const fileContent = `${propsBlock}${outliner.join('\n')}\n`;

			const filename = note.title
				.replace(/[<>:"/\\|?*]/g, '')
				.replace(/\//g, '%2F')
				.trim() || 'Untitled';

			pagesDir.file(`${filename}.md`, fileContent);
		}

		const data = await zip.generateAsync({ type: 'uint8array' });
		return {
			data,
			filename: `${graphName.replace(/\s+/g, '-').toLowerCase()}-logseq.zip`,
			mimeType: 'application/zip',
		};
	},
};
