/**
 * ClickUp sync service — import, outbound watcher, inbound webhook.
 *
 * Outbound: registerWatcher on ':tasks:boards:' docs, queue + batch push.
 * Inbound:  webhook handler validates HMAC, maps fields, applies via changeDoc.
 */

import type { SyncServer } from '../../../server/local-first/sync-server';
import { ClickUpClient, ClickUpApiError } from './clickup-client';
import {
	mapClickUpTaskToRTasks,
	mapRTasksToClickUpBody,
	contentHash,
	buildStatusMaps,
	detectConflicts,
} from './clickup-mapping';
import {
	boardDocId,
	clickupConnectionDocId,
	createTaskItem,
} from '../schemas';
import type {
	BoardDoc,
	TaskItem,
	ClickUpConnectionDoc,
	ClickUpBoardMeta,
} from '../schemas';

// ── Outbound push queue ──

interface PushItem {
	docId: string;
	taskId: string;
	retries: number;
}

const pushQueue: PushItem[] = [];
let pushTimerId: ReturnType<typeof setInterval> | null = null;

// Track last-pushed hashes to avoid re-pushing unchanged tasks
const lastPushedHash = new Map<string, string>();

// ── Init: register watcher + start queue processor ──

export function initClickUpSync(syncServer: SyncServer) {
	syncServer.registerWatcher(':tasks:boards:', (docId, doc) => {
		const boardDoc = doc as BoardDoc;
		if (!boardDoc?.board?.clickup?.syncEnabled) return;

		const cu = boardDoc.board.clickup;
		for (const [taskId, task] of Object.entries(boardDoc.tasks)) {
			if (!task.clickup) continue;
			if (task.clickup.syncStatus === 'synced') {
				// Check if content actually changed since last push
				const key = `${docId}:${taskId}`;
				const currentFields = `${task.title}\x00${task.description}\x00${task.status}\x00${task.priority ?? ''}`;
				if (lastPushedHash.get(key) === currentFields) continue;

				// Content may have changed — enqueue for push
				if (!pushQueue.some(p => p.docId === docId && p.taskId === taskId)) {
					pushQueue.push({ docId, taskId, retries: 0 });
				}
			}
		}
	});

	// Start queue processor (runs every 5s)
	if (!pushTimerId) {
		pushTimerId = setInterval(() => processPushQueue(syncServer), 5_000);
	}

	console.log('[ClickUp] Sync watcher registered');
}

async function processPushQueue(syncServer: SyncServer) {
	if (pushQueue.length === 0) return;

	const batch = pushQueue.splice(0, 10); // drain ≤10 items

	for (const item of batch) {
		try {
			const doc = syncServer.getDoc<BoardDoc>(item.docId);
			if (!doc) continue;

			const task = doc.tasks[item.taskId];
			if (!task?.clickup) continue;

			const cu = doc.board.clickup;
			if (!cu?.syncEnabled) continue;

			// Get connection token
			const space = doc.meta.spaceSlug;
			const connDoc = syncServer.getDoc<ClickUpConnectionDoc>(clickupConnectionDocId(space));
			if (!connDoc?.clickup?.accessToken) continue;

			const client = new ClickUpClient(connDoc.clickup.accessToken);

			// Compute current hash
			const hash = await contentHash({
				title: task.title,
				description: task.description,
				status: task.status,
				priority: task.priority,
			});

			// Skip if hash matches last synced
			if (hash === task.clickup.contentHash) {
				const key = `${item.docId}:${item.taskId}`;
				lastPushedHash.set(key, `${task.title}\x00${task.description}\x00${task.status}\x00${task.priority ?? ''}`);
				continue;
			}

			// Push to ClickUp
			const body = mapRTasksToClickUpBody(
				{ title: task.title, description: task.description, status: task.status, priority: task.priority, labels: [...task.labels] },
				cu.statusMap,
			);

			await client.updateTask(task.clickup.taskId, body);

			// Update sync status
			syncServer.changeDoc<BoardDoc>(item.docId, `Sync task ${item.taskId} to ClickUp`, (d) => {
				const t = d.tasks[item.taskId];
				if (!t?.clickup) return;
				t.clickup.syncStatus = 'synced';
				t.clickup.contentHash = hash;
				t.clickup.lastSyncedAt = Date.now();
			});

			const key = `${item.docId}:${item.taskId}`;
			lastPushedHash.set(key, `${task.title}\x00${task.description}\x00${task.status}\x00${task.priority ?? ''}`);

		} catch (err) {
			item.retries++;
			if (item.retries < 3) {
				pushQueue.push(item); // re-enqueue
			} else {
				// Mark as failed
				try {
					syncServer.changeDoc<BoardDoc>(item.docId, `Mark push failed ${item.taskId}`, (d) => {
						const t = d.tasks[item.taskId];
						if (t?.clickup) t.clickup.syncStatus = 'push-failed';
					});
				} catch {}
				console.error(`[ClickUp] Push failed after 3 retries for task ${item.taskId}:`, err);
			}
		}
	}
}

// ── Import a ClickUp list into an rTasks board ──

export async function importClickUpList(
	syncServer: SyncServer,
	space: string,
	boardSlug: string,
	listId: string,
	accessToken: string,
	opts: { enableSync?: boolean; createNew?: boolean } = {},
): Promise<{ boardId: string; taskCount: number }> {
	const client = new ClickUpClient(accessToken);

	// Fetch list details for status mapping
	const listInfo = await client.getList(listId);
	const listStatuses = listInfo.statuses || [];
	const { statusMap, reverseStatusMap } = buildStatusMaps(listStatuses);

	// Fetch all tasks (paginate)
	let allTasks: any[] = [];
	let page = 0;
	while (true) {
		const batch = await client.getTasks(listId, page);
		allTasks = allTasks.concat(batch);
		if (batch.length < 100) break;
		page++;
	}

	const docId = boardDocId(space, boardSlug);
	let doc = syncServer.getDoc<BoardDoc>(docId);

	if (!doc || opts.createNew) {
		// Create new board
		const Automerge = await import('@automerge/automerge');
		const now = Date.now();
		doc = Automerge.change(Automerge.init<BoardDoc>(), 'import ClickUp list', (d: BoardDoc) => {
			d.meta = { module: 'tasks', collection: 'boards', version: 1, spaceSlug: space, createdAt: now };
			d.board = {
				id: boardSlug,
				name: listInfo.name || 'Imported Board',
				slug: boardSlug,
				description: `Imported from ClickUp list: ${listInfo.name || listId}`,
				icon: null,
				ownerDid: null,
				statuses: ['TODO', 'IN_PROGRESS', 'REVIEW', 'DONE'],
				labels: [],
				createdAt: now,
				updatedAt: now,
			};
			d.tasks = {};

			// Set ClickUp board meta
			if (opts.enableSync) {
				d.board.clickup = {
					listId,
					listName: listInfo.name || '',
					workspaceId: listInfo.folder?.space?.id || '',
					syncEnabled: true,
					statusMap,
					reverseStatusMap,
				};
			}
		});
		syncServer.setDoc(docId, doc!);
	} else if (opts.enableSync) {
		// Add ClickUp meta to existing board
		syncServer.changeDoc<BoardDoc>(docId, 'Enable ClickUp sync', (d) => {
			d.board.clickup = {
				listId,
				listName: listInfo.name || '',
				workspaceId: listInfo.folder?.space?.id || '',
				syncEnabled: true,
				statusMap,
				reverseStatusMap,
			};
		});
	}

	// Import tasks
	let taskCount = 0;
	for (const cuTask of allTasks) {
		const mapped = mapClickUpTaskToRTasks(cuTask, reverseStatusMap);
		const taskId = crypto.randomUUID();
		const hash = await contentHash({
			title: mapped.title,
			description: mapped.description,
			status: mapped.status,
			priority: mapped.priority,
		});

		syncServer.changeDoc<BoardDoc>(docId, `Import task ${cuTask.id}`, (d) => {
			d.tasks[taskId] = createTaskItem(taskId, space, mapped.title, {
				description: mapped.description,
				status: mapped.status,
				priority: mapped.priority,
				labels: mapped.labels,
				sortOrder: taskCount * 1000,
				createdBy: 'clickup-import',
			});
			d.tasks[taskId].clickup = {
				taskId: mapped.clickUpMeta.taskId,
				listId: mapped.clickUpMeta.listId,
				url: mapped.clickUpMeta.url,
				lastSyncedAt: Date.now(),
				syncStatus: 'synced',
				contentHash: hash,
			};
		});
		taskCount++;
	}

	console.log(`[ClickUp] Imported ${taskCount} tasks from list ${listId} into ${boardSlug}`);
	return { boardId: boardSlug, taskCount };
}

// ── Export rTasks board → ClickUp list ──

export async function pushBoardToClickUp(
	syncServer: SyncServer,
	space: string,
	boardSlug: string,
	listId: string,
	accessToken: string,
	statusMap?: Record<string, string>,
): Promise<{ pushed: number }> {
	const client = new ClickUpClient(accessToken);
	const docId = boardDocId(space, boardSlug);
	const doc = syncServer.getDoc<BoardDoc>(docId);
	if (!doc) throw new Error('Board not found');

	let pushed = 0;
	for (const [taskId, task] of Object.entries(doc.tasks)) {
		if (task.clickup?.taskId) continue; // already linked

		const body = mapRTasksToClickUpBody(
			{ title: task.title, description: task.description, status: task.status, priority: task.priority, labels: [...task.labels] },
			statusMap,
		);

		const created = await client.createTask(listId, body);
		const hash = await contentHash({
			title: task.title,
			description: task.description,
			status: task.status,
			priority: task.priority,
		});

		syncServer.changeDoc<BoardDoc>(docId, `Link task ${taskId} to ClickUp`, (d) => {
			const t = d.tasks[taskId];
			if (!t) return;
			t.clickup = {
				taskId: created.id,
				listId,
				url: created.url || `https://app.clickup.com/t/${created.id}`,
				lastSyncedAt: Date.now(),
				syncStatus: 'synced',
				contentHash: hash,
			};
		});
		pushed++;
	}

	console.log(`[ClickUp] Pushed ${pushed} tasks from ${boardSlug} to ClickUp list ${listId}`);
	return { pushed };
}

// ── Inbound webhook handler ──

export async function handleClickUpWebhook(
	syncServer: SyncServer,
	space: string,
	body: any,
	signature: string | null,
): Promise<{ ok: boolean; message: string }> {
	// Get connection for HMAC validation
	const connDoc = syncServer.getDoc<ClickUpConnectionDoc>(clickupConnectionDocId(space));
	if (!connDoc?.clickup) {
		return { ok: false, message: 'No ClickUp connection for this space' };
	}

	// Validate HMAC-SHA256 signature
	if (signature && connDoc.clickup.webhookSecret) {
		const encoder = new TextEncoder();
		const key = await crypto.subtle.importKey(
			'raw',
			encoder.encode(connDoc.clickup.webhookSecret),
			{ name: 'HMAC', hash: 'SHA-256' },
			false,
			['sign'],
		);
		const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(JSON.stringify(body)));
		const expected = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
		if (signature !== expected) {
			return { ok: false, message: 'Invalid webhook signature' };
		}
	}

	const event = body.event;
	const taskId = body.task_id;

	if (!taskId || !['taskUpdated', 'taskStatusUpdated', 'taskCreated'].includes(event)) {
		return { ok: true, message: 'Event ignored' };
	}

	// Find which board doc has this ClickUp task
	const boardDocIds = syncServer.getDocIds().filter(id => id.startsWith(`${space}:tasks:boards:`));
	let targetDocId: string | null = null;
	let targetTaskId: string | null = null;

	for (const docId of boardDocIds) {
		const doc = syncServer.getDoc<BoardDoc>(docId);
		if (!doc?.board?.clickup?.syncEnabled) continue;

		for (const [tid, task] of Object.entries(doc.tasks)) {
			if (task.clickup?.taskId === taskId) {
				targetDocId = docId;
				targetTaskId = tid;
				break;
			}
		}
		if (targetDocId) break;
	}

	if (!targetDocId || !targetTaskId) {
		// New task from ClickUp — only import if a board is watching this list
		if (event === 'taskCreated' && body.list_id) {
			for (const docId of boardDocIds) {
				const doc = syncServer.getDoc<BoardDoc>(docId);
				if (doc?.board?.clickup?.syncEnabled && doc.board.clickup.listId === body.list_id) {
					// Fetch full task and import
					const client = new ClickUpClient(connDoc.clickup.accessToken);
					try {
						const cuTask = await client.getTask(taskId);
						const mapped = mapClickUpTaskToRTasks(cuTask, doc.board.clickup.reverseStatusMap);
						const newTaskId = crypto.randomUUID();
						const hash = await contentHash({
							title: mapped.title,
							description: mapped.description,
							status: mapped.status,
							priority: mapped.priority,
						});

						syncServer.changeDoc<BoardDoc>(docId, `Webhook: import new task ${taskId}`, (d) => {
							d.tasks[newTaskId] = createTaskItem(newTaskId, space, mapped.title, {
								description: mapped.description,
								status: mapped.status,
								priority: mapped.priority,
								labels: mapped.labels,
								sortOrder: Object.keys(d.tasks).length * 1000,
								createdBy: 'clickup-webhook',
							});
							d.tasks[newTaskId].clickup = {
								taskId: mapped.clickUpMeta.taskId,
								listId: mapped.clickUpMeta.listId,
								url: mapped.clickUpMeta.url,
								lastSyncedAt: Date.now(),
								syncStatus: 'synced',
								contentHash: hash,
							};
						});
						return { ok: true, message: `Imported new task ${taskId}` };
					} catch (err) {
						console.error(`[ClickUp] Webhook import failed for ${taskId}:`, err);
						return { ok: false, message: 'Failed to import task' };
					}
				}
			}
		}
		return { ok: true, message: 'Task not tracked' };
	}

	// Fetch latest task from ClickUp
	const client = new ClickUpClient(connDoc.clickup.accessToken);
	try {
		const doc = syncServer.getDoc<BoardDoc>(targetDocId)!;
		const cuTask = await client.getTask(taskId);
		const mapped = mapClickUpTaskToRTasks(cuTask, doc.board.clickup?.reverseStatusMap);
		const remoteHash = await contentHash({
			title: mapped.title,
			description: mapped.description,
			status: mapped.status,
			priority: mapped.priority,
		});

		const localTask = doc.tasks[targetTaskId];
		if (!localTask) return { ok: true, message: 'Task not found locally' };

		// Skip if remote hasn't changed
		if (localTask.clickup?.remoteHash === remoteHash) {
			return { ok: true, message: 'No changes detected' };
		}

		// Check for conflicts
		const localHash = await contentHash({
			title: localTask.title,
			description: localTask.description,
			status: localTask.status,
			priority: localTask.priority,
		});

		const localChanged = localTask.clickup?.contentHash !== localHash;
		const remoteChanged = localTask.clickup?.remoteHash !== remoteHash;

		if (localChanged && remoteChanged) {
			// Both sides changed — detect field-level conflicts
			// rTasks wins on conflicts, apply remote-only changes
			const baseFields = {
				title: localTask.title,
				description: localTask.description,
				status: localTask.status,
				priority: localTask.priority,
			};
			const { remoteOnly, conflicts } = detectConflicts(
				{ title: localTask.title, description: localTask.description, status: localTask.status, priority: localTask.priority },
				{ title: mapped.title, description: mapped.description, status: mapped.status, priority: mapped.priority },
				baseFields,
			);

			syncServer.changeDoc<BoardDoc>(targetDocId, `Webhook: merge task ${taskId}`, (d) => {
				const t = d.tasks[targetTaskId!];
				if (!t) return;
				// Apply remote-only field changes
				for (const field of remoteOnly) {
					if (field === 'title') t.title = mapped.title;
					if (field === 'description') t.description = mapped.description;
					if (field === 'status') t.status = mapped.status;
					if (field === 'priority') t.priority = mapped.priority;
				}
				if (t.clickup) {
					t.clickup.remoteHash = remoteHash;
					t.clickup.lastSyncedAt = Date.now();
					t.clickup.syncStatus = conflicts.length > 0 ? 'conflict' : 'synced';
				}
				t.updatedAt = Date.now();
			});
		} else {
			// Only remote changed — apply all remote fields
			syncServer.changeDoc<BoardDoc>(targetDocId, `Webhook: update task ${taskId}`, (d) => {
				const t = d.tasks[targetTaskId!];
				if (!t) return;
				t.title = mapped.title;
				t.description = mapped.description;
				t.status = mapped.status;
				t.priority = mapped.priority;
				if (mapped.labels.length > 0) t.labels = mapped.labels;
				if (t.clickup) {
					t.clickup.remoteHash = remoteHash;
					t.clickup.contentHash = remoteHash;
					t.clickup.lastSyncedAt = Date.now();
					t.clickup.syncStatus = 'synced';
				}
				t.updatedAt = Date.now();
			});
		}

		return { ok: true, message: `Updated task ${taskId}` };
	} catch (err) {
		console.error(`[ClickUp] Webhook update failed for ${taskId}:`, err);
		return { ok: false, message: 'Failed to update task' };
	}
}
