/**
 * Obsidian vault ↔ rNotes/rDocs converter.
 *
 * Import: ZIP of .md files with YAML frontmatter, [[wikilinks]], callouts, nested folders → tags
 * Export: ZIP of .md files with YAML frontmatter, organized by notebook
 *
 * Shared across rDocs and rNotes — each module registers this converter in its own registry.
 */

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

/** Parse YAML frontmatter from an Obsidian markdown file. */
function parseFrontmatter(content: string): { frontmatter: Record<string, any>; body: string } {
	const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
	if (!match) return { frontmatter: {}, body: content };

	try {
		const frontmatter = parseYaml(match[1]) || {};
		return { frontmatter, body: match[2] };
	} catch {
		return { frontmatter: {}, body: content };
	}
}

/** Convert Obsidian [[wikilinks]] to standard markdown links. */
function convertWikilinks(md: string): string {
	return md.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '[$2]($1)')
		.replace(/\[\[([^\]]+)\]\]/g, '[$1]($1)');
}

/** Convert Obsidian callouts to blockquotes. */
function convertCallouts(md: string): string {
	return md.replace(/^> \[!(\w+)\]\s*(.*)/gm, (_, type, title) => {
		const label = type.charAt(0).toUpperCase() + type.slice(1);
		return title ? `> **${label}:** ${title}` : `> **${label}**`;
	});
}

/** Extract folder path as tag prefix from file path. */
function pathToTags(filePath: string): string[] {
	const parts = filePath.split('/').slice(0, -1);
	const filtered = parts.filter(p => !['attachments', 'assets', 'templates', '.obsidian'].includes(p.toLowerCase()));
	if (filtered.length === 0) return [];
	return filtered.map(p => p.toLowerCase().replace(/\s+/g, '-'));
}

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

export const obsidianConverter: NoteConverter = {
	id: 'obsidian',
	name: 'Obsidian',
	requiresAuth: false,

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

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

		// Build image map from non-.md files in the ZIP for embedded image resolution
		const imageMap = new Map<string, { file: JSZip.JSZipObject; mimeType: string }>();
		const imageExts: Record<string, string> = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml', '.bmp': 'image/bmp' };
		zip.forEach((path, file) => {
			if (file.dir) return;
			const ext = path.substring(path.lastIndexOf('.')).toLowerCase();
			if (imageExts[ext]) {
				const basename = path.split('/').pop()!;
				imageMap.set(basename, { file, mimeType: imageExts[ext] });
				imageMap.set(path, { file, mimeType: imageExts[ext] });
			}
		});

		// Find markdown files in the ZIP
		const mdFiles: { path: string; file: JSZip.JSZipObject }[] = [];
		zip.forEach((path, file) => {
			if (file.dir) return;
			if (!path.endsWith('.md')) return;
			if (path.includes('.obsidian/') || path.includes('.trash/')) return;
			mdFiles.push({ path, file });
		});

		if (mdFiles.length === 0) {
			warnings.push('No .md files found in the ZIP archive');
			return { notes, notebookTitle: vaultName, warnings };
		}

		// Try to detect vault name from common root folder
		const firstPath = mdFiles[0].path;
		const rootFolder = firstPath.split('/')[0];
		if (rootFolder && mdFiles.every(f => f.path.startsWith(rootFolder + '/'))) {
			vaultName = rootFolder;
			for (const f of mdFiles) {
				f.path = f.path.slice(rootFolder.length + 1);
			}
		}

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

				let md = convertWikilinks(body);
				md = convertCallouts(md);

				// Resolve embedded images
				const attachments: { filename: string; data: Uint8Array; mimeType: string }[] = [];
				const imageRefPattern = /!\[([^\]]*)\]\(([^)]+)\)/g;
				const resolvedImages = new Set<string>();

				let match: RegExpExecArray | null;
				while ((match = imageRefPattern.exec(md)) !== null) {
					const ref = match[2];
					const basename = ref.split('/').pop()!;
					const imgEntry = imageMap.get(basename) || imageMap.get(ref);
					if (imgEntry && !resolvedImages.has(basename)) {
						resolvedImages.add(basename);
						const data = await imgEntry.file.async('uint8array');
						attachments.push({ filename: basename, data, mimeType: imgEntry.mimeType });
					}
				}

				if (attachments.length > 0) {
					md = md.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (full, alt, ref) => {
						const basename = ref.split('/').pop()!;
						if (imageMap.has(basename) || imageMap.has(ref)) {
							return `![${alt}](/data/files/uploads/${basename})`;
						}
						return full;
					});
				}

				const title = frontmatter.title || titleFromPath(path);
				const tiptapJson = markdownToTiptap(md);
				const contentPlain = extractPlainTextFromTiptap(tiptapJson);

				const tags: string[] = [];
				if (frontmatter.tags) {
					const fmTags = Array.isArray(frontmatter.tags) ? frontmatter.tags : [frontmatter.tags];
					tags.push(...fmTags.map((t: string) => String(t).toLowerCase().replace(/^#/, '')));
				}
				tags.push(...pathToTags(path));

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

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

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

		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 frontmatter: Record<string, any> = {};
			if (note.tags.length > 0) frontmatter.tags = note.tags;
			frontmatter.created = new Date(note.createdAt).toISOString();
			frontmatter.updated = new Date(note.updatedAt).toISOString();
			if (note.type !== 'NOTE') frontmatter.type = note.type.toLowerCase();
			if (note.sourceRef) {
				frontmatter['rnotes-id'] = note.id;
			}

			const yamlStr = stringifyYaml(frontmatter).trim();
			const fileContent = `---\n${yamlStr}\n---\n\n${md}\n`;

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

			zip.file(`${notebookTitle}/${filename}.md`, fileContent);
		}

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