/**
 * Client-Side Backup Manager — encrypted backup push/pull to server.
 *
 * Reads already-encrypted blobs from IndexedDB (no double-encryption needed).
 * Compares local manifest vs server manifest, uploads only changed docs.
 * On restore: downloads all blobs, writes to IndexedDB.
 */

import type { DocumentId } from './document';
import type { EncryptedDocStore } from './storage';

export interface BackupResult {
	uploaded: number;
	skipped: number;
	errors: string[];
}

export interface RestoreResult {
	downloaded: number;
	errors: string[];
}

export interface BackupStatus {
	enabled: boolean;
	lastBackupAt: string | null;
	docCount: number;
	totalBytes: number;
}

interface ServerManifestEntry {
	docId: string;
	hash: string;
	size: number;
	updatedAt: string;
}

interface ServerManifest {
	spaceSlug: string;
	entries: ServerManifestEntry[];
	updatedAt: string;
}

export class BackupSyncManager {
	#spaceId: string;
	#store: EncryptedDocStore;
	#baseUrl: string;
	#autoBackupTimer: ReturnType<typeof setInterval> | null = null;

	constructor(spaceId: string, store: EncryptedDocStore, baseUrl?: string) {
		this.#spaceId = spaceId;
		this.#store = store;
		this.#baseUrl = baseUrl || '';
	}

	/**
	 * Push backup — upload changed docs to server.
	 * Reads encrypted blobs from IndexedDB and compares with server manifest.
	 */
	async pushBackup(): Promise<BackupResult> {
		const result: BackupResult = { uploaded: 0, skipped: 0, errors: [] };
		const token = this.#getAuthToken();
		if (!token) {
			result.errors.push('Not authenticated');
			return result;
		}

		try {
			// Get server manifest
			const serverManifest = await this.#fetchManifest(token);
			const serverHashes = new Map(
				serverManifest.entries.map((e) => [e.docId, e.hash]),
			);

			// List all local docs for this space
			const localDocs = await this.#store.listAll();
			const spaceDocs = localDocs.filter((id) =>
				id.startsWith(`${this.#spaceId}:`),
			);

			for (const docId of spaceDocs) {
				try {
					const blob = await this.#store.loadRaw(docId);
					if (!blob) continue;

					// Hash local blob and compare
					const localHash = await this.#hashBlob(blob);
					if (serverHashes.get(docId) === localHash) {
						result.skipped++;
						continue;
					}

					// Upload
					await this.#uploadBlob(token, docId, blob);
					result.uploaded++;
				} catch (e) {
					result.errors.push(`${docId}: ${e}`);
				}
			}

			// Update last backup time
			try {
				localStorage.setItem(
					`rspace:${this.#spaceId}:last_backup`,
					new Date().toISOString(),
				);
			} catch { /* SSR */ }
		} catch (e) {
			result.errors.push(`Manifest fetch failed: ${e}`);
		}

		return result;
	}

	/**
	 * Pull restore — download all blobs from server to IndexedDB.
	 * Used when setting up a new device or recovering data.
	 */
	async pullRestore(): Promise<RestoreResult> {
		const result: RestoreResult = { downloaded: 0, errors: [] };
		const token = this.#getAuthToken();
		if (!token) {
			result.errors.push('Not authenticated');
			return result;
		}

		try {
			const manifest = await this.#fetchManifest(token);

			for (const entry of manifest.entries) {
				try {
					const blob = await this.#downloadBlob(
						token,
						entry.docId,
					);
					if (blob) {
						await this.#store.saveImmediate(
							entry.docId as DocumentId,
							blob,
						);
						result.downloaded++;
					}
				} catch (e) {
					result.errors.push(`${entry.docId}: ${e}`);
				}
			}
		} catch (e) {
			result.errors.push(`Manifest fetch failed: ${e}`);
		}

		return result;
	}

	/**
	 * Get current backup status.
	 */
	async getStatus(): Promise<BackupStatus> {
		let lastBackupAt: string | null = null;
		let enabled = false;
		try {
			lastBackupAt = localStorage.getItem(
				`rspace:${this.#spaceId}:last_backup`,
			);
			enabled =
				localStorage.getItem('encryptid_backup_enabled') === 'true';
		} catch { /* SSR */ }

		const token = this.#getAuthToken();
		if (!token || !enabled) {
			return { enabled, lastBackupAt, docCount: 0, totalBytes: 0 };
		}

		try {
			const manifest = await this.#fetchManifest(token);
			const totalBytes = manifest.entries.reduce(
				(sum, e) => sum + e.size,
				0,
			);
			return {
				enabled,
				lastBackupAt,
				docCount: manifest.entries.length,
				totalBytes,
			};
		} catch {
			return { enabled, lastBackupAt, docCount: 0, totalBytes: 0 };
		}
	}

	/**
	 * Enable/disable periodic auto-backup.
	 */
	setAutoBackup(enabled: boolean, intervalMs = 5 * 60 * 1000): void {
		if (this.#autoBackupTimer) {
			clearInterval(this.#autoBackupTimer);
			this.#autoBackupTimer = null;
		}
		if (enabled) {
			this.#autoBackupTimer = setInterval(() => {
				this.pushBackup().catch((e) =>
					console.error('[BackupSync] Auto-backup failed:', e),
				);
			}, intervalMs);
		}
	}

	destroy(): void {
		this.setAutoBackup(false);
	}

	// ---- Private ----

	#getAuthToken(): string | null {
		try {
			const sess = JSON.parse(
				localStorage.getItem('encryptid_session') || '',
			);
			return sess?.accessToken || null;
		} catch {
			return null;
		}
	}

	async #fetchManifest(token: string): Promise<ServerManifest> {
		const resp = await fetch(
			`${this.#baseUrl}/api/backup/${encodeURIComponent(this.#spaceId)}`,
			{ headers: { Authorization: `Bearer ${token}` } },
		);
		if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
		return resp.json();
	}

	async #uploadBlob(
		token: string,
		docId: string,
		blob: Uint8Array,
	): Promise<void> {
		const body = new Uint8Array(blob).buffer as ArrayBuffer;
		const resp = await fetch(
			`${this.#baseUrl}/api/backup/${encodeURIComponent(this.#spaceId)}/${encodeURIComponent(docId)}`,
			{
				method: 'PUT',
				headers: {
					Authorization: `Bearer ${token}`,
					'Content-Type': 'application/octet-stream',
				},
				body,
			},
		);
		if (!resp.ok) throw new Error(`Upload failed: HTTP ${resp.status}`);
	}

	async #downloadBlob(
		token: string,
		docId: string,
	): Promise<Uint8Array | null> {
		const resp = await fetch(
			`${this.#baseUrl}/api/backup/${encodeURIComponent(this.#spaceId)}/${encodeURIComponent(docId)}`,
			{ headers: { Authorization: `Bearer ${token}` } },
		);
		if (resp.status === 404) return null;
		if (!resp.ok) throw new Error(`Download failed: HTTP ${resp.status}`);
		const buf = await resp.arrayBuffer();
		return new Uint8Array(buf);
	}

	async #hashBlob(blob: Uint8Array): Promise<string> {
		const buf = new Uint8Array(blob).buffer as ArrayBuffer;
		const hash = await crypto.subtle.digest('SHA-256', buf);
		return Array.from(new Uint8Array(hash))
			.map((b) => b.toString(16).padStart(2, '0'))
			.join('');
	}
}
