/**
 * Holon Service — Local-first data layer for H3 geospatial holons.
 *
 * Replaces the dead HoloSphereService (GunDB stub) with Automerge-backed
 * CRDT storage via window.__rspaceOfflineRuntime.
 *
 * Data model:
 *   Registry doc: `{space}:holons:registry:{holonId}` — HolonData
 *   Lens doc:     `{space}:holons:lenses:{holonId}`   — all lens data
 *
 * H3 hierarchy methods use pure h3-js (no external service).
 * Designed so data could bridge to AD4M Perspectives later.
 */

import * as h3 from 'h3-js';
import type { DocSchema, DocumentId } from '../shared/local-first/document';
import type { RSpaceOfflineRuntime } from '../shared/local-first/runtime';

// ── Types ──

export interface HolonData {
	id: string;
	name: string;
	description: string;
	latitude: number;
	longitude: number;
	resolution: number;
	timestamp: number;
}

export interface HolonLens {
	name: string;
	data: Record<string, any>;
}

export interface HolonConnection {
	id: string;
	name: string;
	type: 'federation' | 'reference';
	targetSpace: string;
	status: 'connected' | 'disconnected' | 'error';
}

// ── Standard lenses ──

export const STANDARD_LENSES = [
	'active_users', 'users', 'rankings', 'stats', 'tasks', 'progress',
	'events', 'activities', 'items', 'shopping', 'active_items',
	'proposals', 'offers', 'requests', 'checklists', 'roles',
] as const;

export type LensName = (typeof STANDARD_LENSES)[number] | string;

// ── Lens icons ──

export const LENS_ICONS: Record<string, string> = {
	active_users: '👥', users: '👤', rankings: '🏆', stats: '📊',
	tasks: '✅', progress: '📈', events: '📅', activities: '🔔',
	items: '📦', shopping: '🛒', active_items: '⚡', proposals: '💡',
	offers: '🤝', requests: '📋', checklists: '☑️', roles: '🎭',
};

// ── Resolution names ──

const RESOLUTION_NAMES = [
	'Country', 'State/Province', 'Metropolitan Area', 'City', 'District',
	'Neighborhood', 'Block', 'Building', 'Room', 'Desk', 'Chair', 'Point',
];

const RESOLUTION_DESCRIPTIONS = [
	'Country level — covers entire countries',
	'State/Province level — covers states and provinces',
	'Metropolitan area level — covers large urban areas',
	'City level — covers individual cities',
	'District level — covers city districts',
	'Neighborhood level — covers neighborhoods',
	'Block level — covers city blocks',
	'Building level — covers individual buildings',
	'Room level — covers individual rooms',
	'Desk level — covers individual desks',
	'Chair level — covers individual chairs',
	'Point level — covers individual points',
];

export function getResolutionName(resolution: number): string {
	if (resolution < 0) return 'Workspace / Group';
	return RESOLUTION_NAMES[resolution] ?? `Level ${resolution}`;
}

export function getResolutionDescription(resolution: number): string {
	if (resolution < 0) return 'Non-geospatial workspace or group';
	return RESOLUTION_DESCRIPTIONS[resolution] ?? `Geographic level ${resolution}`;
}

// ── Automerge schemas ──

interface HolonRegistryDoc {
	holon: HolonData;
}

interface HolonLensesDoc {
	lenses: Record<string, Record<string, any>>;
}

const registrySchema: DocSchema<HolonRegistryDoc> = {
	module: 'holons',
	collection: 'registry',
	version: 1,
	init: () => ({
		holon: { id: '', name: '', description: '', latitude: 0, longitude: 0, resolution: 0, timestamp: 0 },
	}),
};

const lensesSchema: DocSchema<HolonLensesDoc> = {
	module: 'holons',
	collection: 'lenses',
	version: 1,
	init: () => ({ lenses: {} }),
};

// ── Helper: get runtime ──

function getRuntime(): RSpaceOfflineRuntime | null {
	return (window as any).__rspaceOfflineRuntime ?? null;
}

function getSpace(): string {
	return getRuntime()?.space ?? 'demo';
}

// ── H3 validation ──

export function isValidHolonId(id: string): boolean {
	if (!id || !id.trim()) return false;
	const trimmed = id.trim();
	try { if (h3.isValidCell(trimmed)) return true; } catch { /* not h3 */ }
	if (/^\d{6,20}$/.test(trimmed)) return true;
	if (/^[a-zA-Z0-9_-]{3,50}$/.test(trimmed)) return true;
	return false;
}

export function isH3CellId(id: string): boolean {
	if (!id || !id.trim()) return false;
	try { return h3.isValidCell(id.trim()); } catch { return false; }
}

// ── H3 hierarchy (pure h3-js) ──

export function getHolonHierarchy(holonId: string): { parent?: string; children: string[] } {
	try {
		const resolution = h3.getResolution(holonId);
		const parent = resolution > 0 ? h3.cellToParent(holonId, resolution - 1) : undefined;
		const children = resolution < 15 ? h3.cellToChildren(holonId, resolution + 1) : [];
		return { parent, children };
	} catch {
		return { children: [] };
	}
}

export function getHolonScalespace(holonId: string): string[] {
	try {
		const resolution = h3.getResolution(holonId);
		const scales: string[] = [holonId];
		let current = holonId;
		for (let r = resolution - 1; r >= 0; r--) {
			current = h3.cellToParent(current, r);
			scales.unshift(current);
		}
		return scales;
	} catch {
		return [];
	}
}

// ── CRDT-backed data operations ──

/** Get or create the registry doc for a holon. */
async function ensureRegistryDoc(holonId: string): Promise<DocumentId> {
	const runtime = getRuntime();
	if (!runtime) throw new Error('Offline runtime not available');
	const docId = `${getSpace()}:holons:registry:${holonId}` as DocumentId;
	await runtime.subscribe(docId, registrySchema);
	return docId;
}

/** Get or create the lenses doc for a holon. */
async function ensureLensesDoc(holonId: string): Promise<DocumentId> {
	const runtime = getRuntime();
	if (!runtime) throw new Error('Offline runtime not available');
	const docId = `${getSpace()}:holons:lenses:${holonId}` as DocumentId;
	await runtime.subscribe(docId, lensesSchema);
	return docId;
}

/** Save holon metadata to Automerge. */
export async function saveHolon(data: HolonData): Promise<void> {
	const runtime = getRuntime();
	if (!runtime) return;
	const docId = await ensureRegistryDoc(data.id);
	runtime.change<HolonRegistryDoc>(docId, 'Update holon metadata', (doc) => {
		doc.holon.id = data.id;
		doc.holon.name = data.name;
		doc.holon.description = data.description;
		doc.holon.latitude = data.latitude;
		doc.holon.longitude = data.longitude;
		doc.holon.resolution = data.resolution;
		doc.holon.timestamp = data.timestamp;
	});
}

/** Load holon metadata from Automerge. */
export async function loadHolon(holonId: string): Promise<HolonData | null> {
	const runtime = getRuntime();
	if (!runtime) return null;
	const docId = await ensureRegistryDoc(holonId);
	const doc = runtime.get<HolonRegistryDoc>(docId);
	if (!doc || !doc.holon.id) return null;
	return { ...doc.holon };
}

/** Put data into a lens. */
export async function putLensData(holonId: string, lens: string, data: Record<string, any>): Promise<void> {
	const runtime = getRuntime();
	if (!runtime) return;
	const docId = await ensureLensesDoc(holonId);
	runtime.change<HolonLensesDoc>(docId, `Update lens: ${lens}`, (doc) => {
		if (!doc.lenses[lens]) doc.lenses[lens] = {};
		Object.assign(doc.lenses[lens], data);
	});
}

/** Get data from a lens. */
export async function getLensData(holonId: string, lens: string): Promise<Record<string, any> | null> {
	const runtime = getRuntime();
	if (!runtime) return null;
	const docId = await ensureLensesDoc(holonId);
	const doc = runtime.get<HolonLensesDoc>(docId);
	if (!doc?.lenses?.[lens]) return null;
	return { ...doc.lenses[lens] };
}

/** Get all lenses with data for a holon. */
export async function getAvailableLenses(holonId: string): Promise<string[]> {
	const runtime = getRuntime();
	if (!runtime) return [];
	const docId = await ensureLensesDoc(holonId);
	const doc = runtime.get<HolonLensesDoc>(docId);
	if (!doc?.lenses) return [];
	return Object.keys(doc.lenses).filter((k) => {
		const v = doc.lenses[k];
		return v && Object.keys(v).length > 0;
	});
}

/** Subscribe to changes on a holon's lens data. Returns unsubscribe function. */
export function subscribeLenses(holonId: string, cb: (lenses: Record<string, Record<string, any>>) => void): () => void {
	const runtime = getRuntime();
	if (!runtime) return () => {};
	const docId = `${getSpace()}:holons:lenses:${holonId}` as DocumentId;
	return runtime.onChange<HolonLensesDoc>(docId, (doc) => {
		cb(doc.lenses ?? {});
	});
}
