/**
 * GroupManager — named shape clusters on the canvas.
 * Manages CRUD, collapse/expand, group movement, and template instantiation.
 * All state persists via CommunitySync's Automerge doc.
 */

import type { CommunitySync, CommunityDoc, ShapeData } from "./community-sync";
import * as Automerge from "@automerge/automerge";

export interface CanvasGroup {
	id: string;
	name: string;
	color: string;
	icon: string;
	memberIds: string[];
	collapsed: boolean;
	isTemplate: boolean;
	templateName?: string;
	createdAt: number;
	updatedAt: number;
}

export interface GroupTemplateMember extends Omit<ShapeData, "id"> {
	relX: number;
	relY: number;
}

export interface GroupTemplate {
	name: string;
	icon: string;
	color: string;
	/** Shapes with positions relative to group origin (0,0 = top-left of bounding box) */
	members: GroupTemplateMember[];
}

const GROUP_COLORS = [
	"#14b8a6", "#8b5cf6", "#f59e0b", "#ef4444",
	"#3b82f6", "#ec4899", "#10b981", "#f97316",
];

let colorIndex = 0;
function nextColor(): string {
	return GROUP_COLORS[colorIndex++ % GROUP_COLORS.length];
}

export class GroupManager extends EventTarget {
	#sync: CommunitySync;

	constructor(sync: CommunitySync) {
		super();
		this.#sync = sync;
	}

	/** Access the groups map from the doc. */
	#getGroups(): Record<string, CanvasGroup> {
		return (this.#sync.doc as any).groups || {};
	}

	/** Batch-mutate the Automerge doc. */
	#change(msg: string, fn: (doc: any) => void): void {
		// Use changeDoc via the public accessor pattern
		const oldDoc = this.#sync.doc;
		const newDoc = Automerge.change(oldDoc, msg, (d: any) => {
			if (!d.groups) d.groups = {};
			fn(d);
		});
		// Apply via internal doc setter — we need to go through sync's methods
		// Since CommunitySync doesn't expose a raw setter, we use addShapeData pattern
		// Actually we'll use the changeDoc method we'll add
		(this.#sync as any)._applyDocChange(newDoc);
	}

	// ── CRUD ──

	createGroup(name: string, shapeIds: string[], opts?: { color?: string; icon?: string }): string {
		const id = `group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
		const now = Date.now();
		const group: CanvasGroup = {
			id,
			name,
			color: opts?.color || nextColor(),
			icon: opts?.icon || "📦",
			memberIds: [...shapeIds],
			collapsed: false,
			isTemplate: false,
			createdAt: now,
			updatedAt: now,
		};

		this.#change(`Create group "${name}"`, (d) => {
			d.groups[id] = group;
			// Tag each member shape with groupId
			for (const sid of shapeIds) {
				if (d.shapes?.[sid]) {
					d.shapes[sid].groupId = id;
				}
			}
		});

		this.dispatchEvent(new CustomEvent("group-created", { detail: group }));
		return id;
	}

	dissolveGroup(groupId: string): void {
		const group = this.#getGroups()[groupId];
		if (!group) return;

		this.#change(`Dissolve group "${group.name}"`, (d) => {
			// Clear groupId from members
			for (const sid of (group.memberIds || [])) {
				if (d.shapes?.[sid]) {
					delete d.shapes[sid].groupId;
				}
			}
			delete d.groups[groupId];
		});

		this.dispatchEvent(new CustomEvent("group-dissolved", { detail: { groupId } }));
	}

	addToGroup(groupId: string, shapeId: string): void {
		this.#change(`Add shape to group`, (d) => {
			const g = d.groups[groupId];
			if (!g) return;
			if (!g.memberIds.includes(shapeId)) {
				g.memberIds.push(shapeId);
			}
			if (d.shapes?.[shapeId]) {
				d.shapes[shapeId].groupId = groupId;
			}
			g.updatedAt = Date.now();
		});
	}

	removeFromGroup(groupId: string, shapeId: string): void {
		this.#change(`Remove shape from group`, (d) => {
			const g = d.groups[groupId];
			if (!g) return;
			const idx = g.memberIds.indexOf(shapeId);
			if (idx >= 0) g.memberIds.splice(idx, 1);
			if (d.shapes?.[shapeId]) {
				delete d.shapes[shapeId].groupId;
			}
			g.updatedAt = Date.now();
			// Auto-dissolve if empty
			if (g.memberIds.length === 0) {
				delete d.groups[groupId];
			}
		});
	}

	collapseGroup(groupId: string): void {
		const group = this.#getGroups()[groupId];
		if (!group || group.collapsed) return;

		this.#change(`Collapse group "${group.name}"`, (d) => {
			d.groups[groupId].collapsed = true;
			d.groups[groupId].updatedAt = Date.now();
			// Minimize member shapes
			for (const sid of (group.memberIds || [])) {
				if (d.shapes?.[sid]) {
					d.shapes[sid].isMinimized = true;
				}
			}
		});

		this.dispatchEvent(new CustomEvent("group-collapsed", { detail: { groupId } }));
	}

	expandGroup(groupId: string): void {
		const group = this.#getGroups()[groupId];
		if (!group || !group.collapsed) return;

		this.#change(`Expand group "${group.name}"`, (d) => {
			d.groups[groupId].collapsed = false;
			d.groups[groupId].updatedAt = Date.now();
			// Restore member shapes
			for (const sid of (group.memberIds || [])) {
				if (d.shapes?.[sid]) {
					d.shapes[sid].isMinimized = false;
				}
			}
		});

		this.dispatchEvent(new CustomEvent("group-expanded", { detail: { groupId } }));
	}

	moveGroup(groupId: string, dx: number, dy: number): void {
		const group = this.#getGroups()[groupId];
		if (!group) return;

		this.#change(`Move group "${group.name}"`, (d) => {
			for (const sid of (group.memberIds || [])) {
				if (d.shapes?.[sid]) {
					d.shapes[sid].x = (d.shapes[sid].x || 0) + dx;
					d.shapes[sid].y = (d.shapes[sid].y || 0) + dy;
				}
			}
			d.groups[groupId].updatedAt = Date.now();
		});

		this.dispatchEvent(new CustomEvent("group-moved", { detail: { groupId, dx, dy } }));
	}

	// ── Templates ──

	saveAsTemplate(groupId: string, templateName: string): GroupTemplate | null {
		const group = this.#getGroups()[groupId];
		if (!group) return null;

		const shapes = this.#sync.doc.shapes || {};
		const bounds = this.getGroupBounds(groupId);
		if (!bounds) return null;

		const members: GroupTemplateMember[] = [];
		for (const sid of group.memberIds) {
			const s = shapes[sid];
			if (!s) continue;
			const clone = JSON.parse(JSON.stringify(s)) as ShapeData & { relX: number; relY: number };
			delete (clone as any).id;
			clone.relX = s.x - bounds.x;
			clone.relY = s.y - bounds.y;
			members.push(clone as GroupTemplateMember);
		}

		return {
			name: templateName,
			icon: group.icon,
			color: group.color,
			members,
		};
	}

	instantiateTemplate(template: GroupTemplate, x: number, y: number): string {
		const shapeIds: string[] = [];

		// Create shapes at offset position
		for (const member of template.members) {
			const id = `shape-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
			const { relX, relY, ...rest } = member;
			const shapeData = {
				...rest,
				id,
				x: x + relX,
				y: y + relY,
			} as ShapeData;
			this.#sync.addShapeData(shapeData);
			shapeIds.push(id);
		}

		return this.createGroup(template.name, shapeIds, {
			color: template.color,
			icon: template.icon,
		});
	}

	// ── Queries ──

	getGroup(groupId: string): CanvasGroup | undefined {
		return this.#getGroups()[groupId];
	}

	getAllGroups(): CanvasGroup[] {
		return Object.values(this.#getGroups());
	}

	getGroupForShape(shapeId: string): CanvasGroup | undefined {
		const shapes = this.#sync.doc.shapes || {};
		const gid = (shapes[shapeId] as any)?.groupId;
		if (!gid) return undefined;
		return this.#getGroups()[gid];
	}

	getGroupBounds(groupId: string): { x: number; y: number; width: number; height: number } | null {
		const group = this.#getGroups()[groupId];
		if (!group || group.memberIds.length === 0) return null;

		const shapes = this.#sync.doc.shapes || {};
		let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;

		for (const sid of group.memberIds) {
			const s = shapes[sid];
			if (!s) continue;
			minX = Math.min(minX, s.x);
			minY = Math.min(minY, s.y);
			maxX = Math.max(maxX, s.x + s.width);
			maxY = Math.max(maxY, s.y + s.height);
		}

		if (!isFinite(minX)) return null;
		return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
	}
}
