/**
 * Cart module — cosmolocal print-on-demand shop.
 *
 * Ported from /opt/apps/rcart/ (Express → Hono).
 * Handles catalog (artifact listings), orders, fulfillment resolution.
 * Integrates with provider-registry for provider matching and flow-service for revenue splits.
 *
 * Storage: Automerge documents via SyncServer (no PostgreSQL).
 */

import * as Automerge from "@automerge/automerge";
import { Hono } from "hono";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import { depositOrderRevenue } from "./flow";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import {
	catalogSchema, orderSchema, shoppingCartSchema, shoppingCartIndexSchema,
	catalogDocId, orderDocId, shoppingCartDocId, shoppingCartIndexDocId,
	type CatalogDoc, type CatalogEntry,
	type OrderDoc, type OrderMeta,
	type ShoppingCartDoc, type ShoppingCartIndexDoc,
	type CartItem, type CartStatus,
} from './schemas';
import { extractProductFromUrl } from './extract';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// Provider registry URL (for fulfillment resolution)
const PROVIDER_REGISTRY_URL = process.env.PROVIDER_REGISTRY_URL || "";

function getProviderUrl(): string {
	// In unified mode, providers module is co-located — call its routes directly via internal URL
	// In standalone mode, use PROVIDER_REGISTRY_URL env
	return PROVIDER_REGISTRY_URL || "http://localhost:3000/demo/providers";
}

// ── Automerge helpers ──

/** Lazily create (or retrieve) the catalog doc for a space. */
function ensureCatalogDoc(space: string): Automerge.Doc<CatalogDoc> {
	const docId = catalogDocId(space);
	let doc = _syncServer!.getDoc<CatalogDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<CatalogDoc>(), 'init catalog', (d) => {
			const init = catalogSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

/** Get all order docs for a space by scanning known doc IDs. */
function getSpaceOrderDocs(space: string): Array<{ docId: string; doc: Automerge.Doc<OrderDoc> }> {
	const prefix = `${space}:cart:orders:`;
	const results: Array<{ docId: string; doc: Automerge.Doc<OrderDoc> }> = [];
	for (const id of _syncServer!.listDocs()) {
		if (id.startsWith(prefix)) {
			const doc = _syncServer!.getDoc<OrderDoc>(id);
			if (doc) results.push({ docId: id, doc });
		}
	}
	return results;
}

// ── CATALOG ROUTES ──

// POST /api/catalog/ingest — Add artifact to catalog
routes.post("/api/catalog/ingest", async (c) => {
	const space = c.req.param("space") || "demo";
	const artifact = await c.req.json();

	if (!artifact.id || !artifact.schema_version || !artifact.type) {
		return c.json({ error: "Invalid artifact envelope. Required: id, schema_version, type" }, 400);
	}
	if (artifact.type !== "print-ready") {
		return c.json({ error: `Only 'print-ready' artifacts can be listed. Got: '${artifact.type}'` }, 400);
	}
	if (!artifact.render_targets || Object.keys(artifact.render_targets).length === 0) {
		return c.json({ error: "print-ready artifacts must have at least one render_target" }, 400);
	}

	const doc = ensureCatalogDoc(space);

	// Check for duplicate artifact_id
	for (const [, entry] of Object.entries(doc.items)) {
		if (entry.artifactId === artifact.id) {
			return c.json({ error: "Artifact already listed", catalog_entry_id: entry.id }, 409);
		}
	}

	const entryId = crypto.randomUUID();
	const now = Date.now();

	const docId = catalogDocId(space);
	_syncServer!.changeDoc<CatalogDoc>(docId, 'ingest catalog entry', (d) => {
		d.items[entryId] = {
			id: entryId,
			artifactId: artifact.id,
			artifact: artifact,
			title: artifact.payload?.title || "Untitled",
			productType: artifact.spec?.product_type || null,
			requiredCapabilities: artifact.spec?.required_capabilities || [],
			substrates: artifact.spec?.substrates || [],
			creatorId: artifact.creator?.id || null,
			sourceSpace: artifact.source_space || space,
			tags: artifact.payload?.tags || [],
			status: "active",
			createdAt: now,
			updatedAt: now,
		};
	});

	return c.json({
		id: entryId,
		artifact_id: artifact.id,
		title: artifact.payload?.title || "Untitled",
		product_type: artifact.spec?.product_type || null,
		status: "active",
		created_at: new Date(now).toISOString(),
	}, 201);
});

// GET /api/catalog — Browse catalog
routes.get("/api/catalog", async (c) => {
	const space = c.req.param("space") || "demo";
	const { product_type, capability, tag, source_space, q, limit = "50", offset = "0" } = c.req.query();

	const doc = ensureCatalogDoc(space);
	let entries = Object.values(doc.items);

	// Apply filters
	entries = entries.filter((e) => e.status === "active");
	if (product_type) entries = entries.filter((e) => e.productType === product_type);
	if (capability) {
		const caps = capability.split(",");
		entries = entries.filter((e) => caps.some((cap) => e.requiredCapabilities.includes(cap)));
	}
	if (tag) entries = entries.filter((e) => e.tags.includes(tag));
	if (source_space) entries = entries.filter((e) => e.sourceSpace === source_space);
	if (q) {
		const lower = q.toLowerCase();
		entries = entries.filter((e) => e.title.toLowerCase().includes(lower));
	}

	// Sort by createdAt descending
	entries.sort((a, b) => b.createdAt - a.createdAt);

	const limitNum = Math.min(parseInt(limit) || 50, 100);
	const offsetNum = parseInt(offset) || 0;
	const total = entries.length;
	const paged = entries.slice(offsetNum, offsetNum + limitNum);

	// Map to response shape matching the original SQL response
	const result = paged.map((e) => {
		const art = e.artifact as Record<string, any> | undefined;
		return {
			id: e.id,
			artifact_id: e.artifactId,
			title: e.title,
			product_type: e.productType,
			required_capabilities: e.requiredCapabilities,
			tags: e.tags,
			source_space: e.sourceSpace,
			description: art?.payload?.description || null,
			pricing: art?.pricing || null,
			dimensions: art?.spec?.dimensions || null,
			status: e.status,
			created_at: new Date(e.createdAt).toISOString(),
		};
	});

	return c.json({ entries: result, total, limit: limitNum, offset: offsetNum });
});

// GET /api/catalog/:id — Single catalog entry
routes.get("/api/catalog/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");
	const doc = ensureCatalogDoc(space);

	// Look up by entry id or artifact id
	let entry: CatalogEntry | undefined;
	if (doc.items[id]) {
		entry = doc.items[id];
	} else {
		entry = Object.values(doc.items).find((e) => e.artifactId === id);
	}

	if (!entry) return c.json({ error: "Catalog entry not found" }, 404);

	return c.json({
		id: entry.id,
		artifact: entry.artifact,
		status: entry.status,
		created_at: new Date(entry.createdAt).toISOString(),
		updated_at: new Date(entry.updatedAt).toISOString(),
	});
});

// PATCH /api/catalog/:id — Update listing status
routes.patch("/api/catalog/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const { status } = await c.req.json();
	const valid = ["active", "paused", "sold_out", "removed"];
	if (!valid.includes(status)) return c.json({ error: `status must be one of: ${valid.join(", ")}` }, 400);

	const doc = ensureCatalogDoc(space);
	const entryId = c.req.param("id");

	if (!doc.items[entryId]) return c.json({ error: "Catalog entry not found" }, 404);

	const docId = catalogDocId(space);
	_syncServer!.changeDoc<CatalogDoc>(docId, `update catalog status → ${status}`, (d) => {
		d.items[entryId].status = status;
		d.items[entryId].updatedAt = Date.now();
	});

	return c.json({ id: entryId, status });
});

// ── ORDER ROUTES ──

// POST /api/orders — Create an order
routes.post("/api/orders", async (c) => {
	const space = c.req.param("space") || "demo";

	// Optional auth — set buyer_did from claims if authenticated
	const token = extractToken(c.req.raw.headers);
	let buyerDid: string | null = null;
	if (token) {
		try { const claims = await verifyEncryptIDToken(token); buyerDid = claims.sub; } catch {}
	}

	const body = await c.req.json();
	const {
		catalog_entry_id, artifact_id, buyer_id, buyer_location, buyer_contact,
		provider_id, provider_name, provider_distance_km,
		quantity = 1, production_cost, creator_payout, community_payout,
		total_price, currency = "USD", payment_method = "manual",
		payment_tx, payment_network,
	} = body;

	if (!catalog_entry_id && !artifact_id) return c.json({ error: "Required: catalog_entry_id or artifact_id" }, 400);
	if (!provider_id || !total_price) return c.json({ error: "Required: provider_id, total_price" }, 400);

	// Look up catalog entry
	const catalogDoc = ensureCatalogDoc(space);
	const lookupId = catalog_entry_id || artifact_id;
	let entry: CatalogEntry | undefined;
	if (catalogDoc.items[lookupId]) {
		entry = catalogDoc.items[lookupId];
	} else {
		entry = Object.values(catalogDoc.items).find((e) => e.artifactId === lookupId || e.id === lookupId);
	}
	if (!entry) return c.json({ error: "Catalog entry not found" }, 404);

	// x402 detection
	const x402Header = c.req.header("x-payment");
	const effectiveMethod = x402Header ? "x402" : payment_method;
	const initialStatus = x402Header ? "paid" : "pending";

	const orderId = crypto.randomUUID();
	const now = Date.now();

	// Create order doc
	const oDocId = orderDocId(space, orderId);
	let orderDoc = Automerge.change(Automerge.init<OrderDoc>(), 'create order', (d) => {
		const init = orderSchema.init();
		Object.assign(d, init);
		d.meta.spaceSlug = space;
		d.order.id = orderId;
		d.order.catalogEntryId = entry!.id;
		d.order.artifactId = entry!.artifactId;
		d.order.buyerId = buyerDid || buyer_id || null;
		d.order.buyerLocation = buyer_location ? JSON.stringify(buyer_location) : null;
		d.order.buyerContact = buyer_contact ? JSON.stringify(buyer_contact) : null;
		d.order.providerId = provider_id;
		d.order.providerName = provider_name || null;
		d.order.providerDistanceKm = provider_distance_km || null;
		d.order.quantity = quantity;
		d.order.productionCost = production_cost || null;
		d.order.creatorPayout = creator_payout || null;
		d.order.communityPayout = community_payout || null;
		d.order.totalPrice = total_price;
		d.order.currency = currency;
		d.order.status = initialStatus;
		d.order.paymentMethod = effectiveMethod;
		d.order.paymentTx = payment_tx || null;
		d.order.paymentNetwork = payment_network || null;
		d.order.createdAt = now;
		d.order.updatedAt = now;
		if (initialStatus === "paid") d.order.paidAt = now;
	});
	_syncServer!.setDoc(oDocId, orderDoc);

	const order = orderDoc.order;

	if (initialStatus === "paid") {
		depositOrderRevenue(total_price, orderId);
	}

	// Return response matching original shape
	return c.json(orderToResponse(order, entry), 201);
});

// GET /api/orders — List orders
routes.get("/api/orders", async (c) => {
	const space = c.req.param("space") || "demo";

	// Optional auth — filter by buyer if authenticated
	const token = extractToken(c.req.raw.headers);
	let authedBuyer: string | null = null;
	if (token) {
		try { const claims = await verifyEncryptIDToken(token); authedBuyer = claims.sub; } catch {}
	}

	const { status, provider_id, buyer_id, limit = "50", offset = "0" } = c.req.query();

	const orderDocs = getSpaceOrderDocs(space);

	// Build enriched order list with catalog info
	const catalogDoc = ensureCatalogDoc(space);

	let orders = orderDocs.map(({ doc }) => {
		const o = doc.order;
		const catEntry = catalogDoc.items[o.catalogEntryId];
		const resp = orderToResponse(o);
		resp.artifact_title = catEntry?.title || null;
		resp.product_type = catEntry?.productType || null;
		return resp;
	});

	// Apply filters
	if (status) orders = orders.filter((o) => o.status === status);
	if (provider_id) orders = orders.filter((o) => o.provider_id === provider_id);
	const effectiveBuyerId = buyer_id || (authedBuyer && !status && !provider_id ? authedBuyer : null);
	if (effectiveBuyerId) orders = orders.filter((o) => o.buyer_id === effectiveBuyerId);

	// Sort by created_at descending
	orders.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());

	const limitNum = Math.min(parseInt(limit) || 50, 100);
	const offsetNum = parseInt(offset) || 0;
	const paged = orders.slice(offsetNum, offsetNum + limitNum);

	return c.json({ orders: paged });
});

// GET /api/orders/:id — Single order
routes.get("/api/orders/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const orderId = c.req.param("id");
	const oDocId = orderDocId(space, orderId);
	const doc = _syncServer!.getDoc<OrderDoc>(oDocId);
	if (!doc) return c.json({ error: "Order not found" }, 404);

	const catalogDoc = ensureCatalogDoc(space);
	const catEntry = catalogDoc.items[doc.order.catalogEntryId];

	const resp = orderToResponse(doc.order);
	resp.artifact_envelope = catEntry?.artifact || null;
	resp.artifact_title = catEntry?.title || null;
	return c.json(resp);
});

// PATCH /api/orders/:id/status — Update order status
routes.patch("/api/orders/:id/status", async (c) => {
	const space = c.req.param("space") || "demo";
	const body = await c.req.json();
	const { status, payment_tx, payment_network } = body;
	const valid = ["pending", "paid", "accepted", "in_production", "ready", "shipped", "completed", "cancelled"];
	if (!valid.includes(status)) return c.json({ error: `status must be one of: ${valid.join(", ")}` }, 400);

	const orderId = c.req.param("id");
	const oDocId = orderDocId(space, orderId);
	const doc = _syncServer!.getDoc<OrderDoc>(oDocId);
	if (!doc) return c.json({ error: "Order not found" }, 404);

	const now = Date.now();

	const updated = _syncServer!.changeDoc<OrderDoc>(oDocId, `order status → ${status}`, (d) => {
		d.order.status = status;
		d.order.updatedAt = now;
		if (status === "paid") d.order.paidAt = now;
		if (status === "accepted") d.order.acceptedAt = now;
		if (status === "completed") d.order.completedAt = now;
		if (status === "paid" && payment_tx) {
			d.order.paymentTx = payment_tx;
			d.order.paymentNetwork = payment_network || null;
		}
	});

	if (!updated) return c.json({ error: "Order not found" }, 404);

	if (status === "paid" && updated.order.totalPrice) {
		depositOrderRevenue(updated.order.totalPrice, orderId);
	}

	return c.json(orderToResponse(updated.order));
});

// ── Response helpers ──

interface OrderResponse {
	id: string;
	catalog_entry_id: string;
	artifact_id: string;
	buyer_id: string | null;
	buyer_location: unknown;
	buyer_contact: unknown;
	provider_id: string | null;
	provider_name: string | null;
	provider_distance_km: number | null;
	quantity: number;
	production_cost: number | null;
	creator_payout: number | null;
	community_payout: number | null;
	total_price: number | null;
	currency: string;
	status: string;
	payment_method: string | null;
	payment_tx: string | null;
	payment_network: string | null;
	created_at: string;
	paid_at: string | null;
	accepted_at: string | null;
	completed_at: string | null;
	updated_at: string;
	artifact_title?: string | null;
	product_type?: string | null;
	artifact_envelope?: unknown;
}

/** Convert an OrderMeta to the flat response shape matching the original SQL rows. */
function orderToResponse(o: OrderMeta, catEntry?: CatalogEntry): OrderResponse {
	return {
		id: o.id,
		catalog_entry_id: o.catalogEntryId,
		artifact_id: o.artifactId,
		buyer_id: o.buyerId,
		buyer_location: o.buyerLocation ? tryParse(o.buyerLocation) : null,
		buyer_contact: o.buyerContact ? tryParse(o.buyerContact) : null,
		provider_id: o.providerId,
		provider_name: o.providerName,
		provider_distance_km: o.providerDistanceKm,
		quantity: o.quantity,
		production_cost: o.productionCost,
		creator_payout: o.creatorPayout,
		community_payout: o.communityPayout,
		total_price: o.totalPrice,
		currency: o.currency,
		status: o.status,
		payment_method: o.paymentMethod,
		payment_tx: o.paymentTx,
		payment_network: o.paymentNetwork,
		created_at: new Date(o.createdAt).toISOString(),
		paid_at: o.paidAt ? new Date(o.paidAt).toISOString() : null,
		accepted_at: o.acceptedAt ? new Date(o.acceptedAt).toISOString() : null,
		completed_at: o.completedAt ? new Date(o.completedAt).toISOString() : null,
		updated_at: new Date(o.updatedAt).toISOString(),
		...(catEntry ? { artifact_title: catEntry.title, product_type: catEntry.productType } : {}),
	};
}

function tryParse(s: string): unknown {
	try { return JSON.parse(s); } catch { return s; }
}

// ── FULFILLMENT ROUTES ──

function round2(n: number): number {
	return Math.round(n * 100) / 100;
}

interface ProviderMatch {
	id: string; name: string; distance_km: number;
	location: { city: string; region: string; country: string };
	turnaround: { standard_days: number; rush_days: number; rush_surcharge_pct: number };
	pricing: Record<string, { base_price: number; per_unit: number; per_unit_label: string; currency: string; volume_breaks?: { min_qty: number; per_unit: number }[] }>;
	wallet: string;
}

function composeCost(artifact: Record<string, unknown>, provider: ProviderMatch, quantity: number) {
	const spec = artifact.spec as Record<string, unknown> | undefined;
	const capabilities = (spec?.required_capabilities as string[]) || [];
	const pages = (spec?.pages as number) || 1;
	const breakdown: { label: string; amount: number }[] = [];

	for (const cap of capabilities) {
		const capPricing = provider.pricing?.[cap];
		if (!capPricing) continue;

		const basePrice = capPricing.base_price || 0;
		let perUnit = capPricing.per_unit || 0;
		const unitLabel = capPricing.per_unit_label || "per unit";

		if (capPricing.volume_breaks) {
			for (const vb of capPricing.volume_breaks) {
				if (quantity >= vb.min_qty) perUnit = vb.per_unit;
			}
		}

		let units = quantity;
		if (unitLabel.includes("page")) units = pages * quantity;

		if (basePrice > 0) breakdown.push({ label: `${cap} setup`, amount: round2(basePrice) });
		breakdown.push({ label: `${cap} (${units} x $${perUnit} ${unitLabel})`, amount: round2(perUnit * units) });
	}

	if (breakdown.length === 0) {
		breakdown.push({ label: "Production (estimated)", amount: round2(2.0 * quantity) });
	}

	return breakdown;
}

// POST /api/fulfill/resolve — Find fulfillment options
routes.post("/api/fulfill/resolve", async (c) => {
	const space = c.req.param("space") || "demo";
	const body = await c.req.json();
	const { artifact_id, catalog_entry_id, buyer_location, quantity = 1 } = body;

	if (!buyer_location?.lat || !buyer_location?.lng) {
		return c.json({ error: "Required: buyer_location.lat, buyer_location.lng" }, 400);
	}
	if (!artifact_id && !catalog_entry_id) {
		return c.json({ error: "Required: artifact_id or catalog_entry_id" }, 400);
	}

	const catalogDoc = ensureCatalogDoc(space);
	const lookupId = artifact_id || catalog_entry_id;

	// Find entry by id or artifact_id, must be active
	let entry: CatalogEntry | undefined;
	if (catalogDoc.items[lookupId] && catalogDoc.items[lookupId].status === "active") {
		entry = catalogDoc.items[lookupId];
	} else {
		entry = Object.values(catalogDoc.items).find(
			(e) => (e.artifactId === lookupId || e.id === lookupId) && e.status === "active"
		);
	}
	if (!entry) return c.json({ error: "Artifact not found in catalog" }, 404);

	const artifact = entry.artifact as Record<string, any>;
	const capabilities = artifact.spec?.required_capabilities || [];
	const substrates = artifact.spec?.substrates || [];

	if (capabilities.length === 0) {
		return c.json({ error: "Artifact has no required_capabilities" }, 400);
	}

	// Query provider registry (internal module or external service)
	const providerUrl = getProviderUrl();
	const params = new URLSearchParams({
		capabilities: capabilities.join(","),
		lat: String(buyer_location.lat),
		lng: String(buyer_location.lng),
	});
	if (substrates.length > 0) params.set("substrates", substrates.join(","));

	let providers: ProviderMatch[];
	try {
		const resp = await fetch(`${providerUrl}/api/providers/match?${params}`);
		if (!resp.ok) throw new Error(`Provider registry returned ${resp.status}`);
		const data = await resp.json() as { matches?: ProviderMatch[] };
		providers = data.matches || [];
	} catch (err) {
		return c.json({ error: "Failed to query provider registry", detail: err instanceof Error ? err.message : String(err) }, 502);
	}

	if (providers.length === 0) {
		return c.json({ options: [], message: "No local providers found", artifact_id: artifact.id });
	}

	const options = providers.map((provider) => {
		const costBreakdown = composeCost(artifact, provider, quantity);
		const productionCost = costBreakdown.reduce((sum, item) => sum + item.amount, 0);
		const pricing = artifact.pricing || {};
		const creatorPct = (pricing.creator_share_pct || 30) / 100;
		const communityPct = (pricing.community_share_pct || 0) / 100;
		const markupMultiplier = 1 / (1 - creatorPct - communityPct);
		const totalPrice = productionCost * markupMultiplier;
		const creatorPayout = totalPrice * creatorPct;
		const communityPayout = totalPrice * communityPct;

		return {
			provider: { id: provider.id, name: provider.name, distance_km: provider.distance_km, city: provider.location?.city || "Unknown" },
			production_cost: round2(productionCost),
			creator_payout: round2(creatorPayout),
			community_payout: round2(communityPayout),
			total_price: round2(totalPrice),
			currency: "USD",
			turnaround_days: provider.turnaround?.standard_days || 5,
			cost_breakdown: costBreakdown,
		};
	});

	options.sort((a, b) => a.total_price - b.total_price);

	return c.json({ artifact_id: artifact.id, artifact_title: artifact.payload?.title, buyer_location, quantity, options });
});

// ── SHOPPING CART helpers ──

/** Lazily create (or retrieve) the shopping cart index doc for a space. */
function ensureShoppingCartIndex(space: string): Automerge.Doc<ShoppingCartIndexDoc> {
	const docId = shoppingCartIndexDocId(space);
	let doc = _syncServer!.getDoc<ShoppingCartIndexDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ShoppingCartIndexDoc>(), 'init shopping cart index', (d) => {
			const init = shoppingCartIndexSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

/** Recompute index entry from a shopping cart doc. */
function reindexCart(space: string, cartId: string) {
	const cartDocId = shoppingCartDocId(space, cartId);
	const cartDoc = _syncServer!.getDoc<ShoppingCartDoc>(cartDocId);
	if (!cartDoc) return;

	const indexDocId = shoppingCartIndexDocId(space);
	ensureShoppingCartIndex(space);

	const items = cartDoc.items ? Object.values(cartDoc.items) : [];
	const totalAmount = items.reduce((sum, item) => sum + (item.price || 0) * item.quantity, 0);

	_syncServer!.changeDoc<ShoppingCartIndexDoc>(indexDocId, 'reindex cart', (d) => {
		d.carts[cartId] = {
			name: cartDoc.cart.name,
			status: cartDoc.cart.status as CartStatus,
			itemCount: items.length,
			totalAmount: Math.round(totalAmount * 100) / 100,
			fundedAmount: cartDoc.cart.fundedAmount,
			currency: cartDoc.cart.currency,
			createdAt: cartDoc.cart.createdAt,
			updatedAt: Date.now(),
		};
	});
}

// ── SHOPPING CART ROUTES ──

// POST /api/extract — Extract product from URL
routes.post("/api/extract", async (c) => {
	const { url } = await c.req.json();
	if (!url) return c.json({ error: "Required: url" }, 400);

	try {
		const product = await extractProductFromUrl(url);
		return c.json(product);
	} catch (err) {
		return c.json({
			error: "Failed to extract product",
			detail: err instanceof Error ? err.message : String(err),
		}, 502);
	}
});

// GET /api/shopping-carts — List carts from index
routes.get("/api/shopping-carts", async (c) => {
	const space = c.req.param("space") || "demo";
	const indexDoc = ensureShoppingCartIndex(space);
	const carts = Object.entries(indexDoc.carts || {}).map(([id, entry]) => ({
		id,
		...entry,
		createdAt: new Date(entry.createdAt).toISOString(),
		updatedAt: new Date(entry.updatedAt).toISOString(),
	}));
	carts.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
	return c.json({ carts });
});

// POST /api/shopping-carts — Create cart
routes.post("/api/shopping-carts", async (c) => {
	const space = c.req.param("space") || "demo";
	const { name, description = "", targetAmount = 0, currency = "USD" } = await c.req.json();
	if (!name) return c.json({ error: "Required: name" }, 400);

	const cartId = crypto.randomUUID();
	const now = Date.now();

	const docId = shoppingCartDocId(space, cartId);
	const cartDoc = Automerge.change(Automerge.init<ShoppingCartDoc>(), 'create shopping cart', (d) => {
		const init = shoppingCartSchema.init();
		Object.assign(d, init);
		d.meta.spaceSlug = space;
		d.cart.id = cartId;
		d.cart.name = name;
		d.cart.description = description;
		d.cart.status = 'OPEN';
		d.cart.targetAmount = targetAmount;
		d.cart.fundedAmount = 0;
		d.cart.currency = currency;
		d.cart.createdAt = now;
		d.cart.updatedAt = now;
	});
	_syncServer!.setDoc(docId, cartDoc);

	reindexCart(space, cartId);

	return c.json({ id: cartId, name, status: 'OPEN', created_at: new Date(now).toISOString() }, 201);
});

// GET /api/shopping-carts/:cartId — Full cart with items + contributions
routes.get("/api/shopping-carts/:cartId", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);

	const items = Object.entries(doc.items || {}).map(([id, item]) => ({
		id, ...item, addedAt: new Date(item.addedAt).toISOString(),
	}));
	const contributions = Object.entries(doc.contributions || {}).map(([id, contrib]) => ({
		id, ...contrib,
		createdAt: new Date(contrib.createdAt).toISOString(),
		updatedAt: new Date(contrib.updatedAt).toISOString(),
	}));

	return c.json({
		id: doc.cart.id,
		name: doc.cart.name,
		description: doc.cart.description,
		status: doc.cart.status,
		targetAmount: doc.cart.targetAmount,
		fundedAmount: doc.cart.fundedAmount,
		currency: doc.cart.currency,
		createdAt: new Date(doc.cart.createdAt).toISOString(),
		updatedAt: new Date(doc.cart.updatedAt).toISOString(),
		items,
		contributions,
		events: doc.events || [],
	});
});

// PUT /api/shopping-carts/:cartId — Update cart
routes.put("/api/shopping-carts/:cartId", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);

	const body = await c.req.json();
	const { name, description, status } = body;

	const validStatuses: CartStatus[] = ['OPEN', 'FUNDING', 'FUNDED', 'CHECKING_OUT', 'ORDERED', 'CLOSED'];
	if (status && !validStatuses.includes(status)) {
		return c.json({ error: `status must be one of: ${validStatuses.join(", ")}` }, 400);
	}

	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'update cart', (d) => {
		if (name !== undefined) d.cart.name = name;
		if (description !== undefined) d.cart.description = description;
		if (status) d.cart.status = status;
		d.cart.updatedAt = Date.now();
	});

	reindexCart(space, cartId);
	return c.json({ id: cartId, status: status || doc.cart.status });
});

// DELETE /api/shopping-carts/:cartId — Delete cart + remove from index
routes.delete("/api/shopping-carts/:cartId", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);

	// Remove from index
	const indexDocId = shoppingCartIndexDocId(space);
	ensureShoppingCartIndex(space);
	_syncServer!.changeDoc<ShoppingCartIndexDoc>(indexDocId, 'remove cart from index', (d) => {
		delete d.carts[cartId];
	});

	// Note: Automerge docs aren't truly deleted, but removing from index effectively hides it
	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'close cart', (d) => {
		d.cart.status = 'CLOSED';
		d.cart.updatedAt = Date.now();
	});

	return c.json({ deleted: true, id: cartId });
});

// POST /api/shopping-carts/:cartId/items — Add item
routes.post("/api/shopping-carts/:cartId/items", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);

	const body = await c.req.json();
	const { url, product: preExtracted } = body;

	let productData: any;
	if (preExtracted) {
		// Extension sent pre-extracted product data
		productData = preExtracted;
		productData.sourceUrl = url || preExtracted.sourceUrl;
	} else if (url) {
		// Extract from URL server-side
		try {
			productData = await extractProductFromUrl(url);
		} catch (err) {
			return c.json({ error: "Failed to extract product", detail: err instanceof Error ? err.message : String(err) }, 502);
		}
	} else {
		return c.json({ error: "Required: url or product" }, 400);
	}

	const itemId = crypto.randomUUID();
	const now = Date.now();
	const domain = productData.vendor?.domain || productData.sourceUrl ? (() => {
		try { return new URL(productData.sourceUrl).hostname.replace(/^www\./, ''); } catch { return 'unknown'; }
	})() : 'unknown';

	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'add item to cart', (d) => {
		d.items[itemId] = {
			name: productData.name || 'Unknown Product',
			price: productData.price ?? null,
			currency: productData.currency || 'USD',
			quantity: body.quantity || 1,
			sourceUrl: productData.sourceUrl || url || '',
			imageUrl: productData.imageUrl || null,
			description: productData.description || null,
			vendor: {
				name: productData.vendor?.name || domain,
				domain: productData.vendor?.domain || domain,
				platform: productData.vendor?.platform || null,
			},
			addedBy: null,
			addedAt: now,
			sku: productData.sku || null,
		};
		d.cart.updatedAt = now;
		d.events.push({
			type: 'item_added',
			actor: 'anonymous',
			detail: `Added ${productData.name || 'item'}`,
			timestamp: now,
		});
	});

	reindexCart(space, cartId);

	return c.json({
		id: itemId,
		name: productData.name,
		price: productData.price,
		sourceUrl: productData.sourceUrl || url,
		imageUrl: productData.imageUrl,
	}, 201);
});

// PUT /api/shopping-carts/:cartId/items/:itemId — Update item quantity
routes.put("/api/shopping-carts/:cartId/items/:itemId", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const itemId = c.req.param("itemId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);
	if (!doc.items?.[itemId]) return c.json({ error: "Item not found" }, 404);

	const { quantity } = await c.req.json();
	if (typeof quantity !== 'number' || quantity < 1) return c.json({ error: "quantity must be >= 1" }, 400);

	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'update item quantity', (d) => {
		d.items[itemId].quantity = quantity;
		d.cart.updatedAt = Date.now();
	});

	reindexCart(space, cartId);
	return c.json({ id: itemId, quantity });
});

// DELETE /api/shopping-carts/:cartId/items/:itemId — Remove item
routes.delete("/api/shopping-carts/:cartId/items/:itemId", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const itemId = c.req.param("itemId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);
	if (!doc.items?.[itemId]) return c.json({ error: "Item not found" }, 404);

	const itemName = doc.items[itemId].name;

	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'remove item from cart', (d) => {
		delete d.items[itemId];
		d.cart.updatedAt = Date.now();
		d.events.push({
			type: 'item_removed',
			actor: 'anonymous',
			detail: `Removed ${itemName}`,
			timestamp: Date.now(),
		});
	});

	reindexCart(space, cartId);
	return c.json({ deleted: true, id: itemId });
});

// POST /api/shopping-carts/:cartId/contribute — Add contribution
routes.post("/api/shopping-carts/:cartId/contribute", async (c) => {
	const space = c.req.param("space") || "demo";
	const cartId = c.req.param("cartId");
	const docId = shoppingCartDocId(space, cartId);
	const doc = _syncServer!.getDoc<ShoppingCartDoc>(docId);
	if (!doc) return c.json({ error: "Cart not found" }, 404);

	const { amount, username = "Anonymous", paymentMethod = "MANUAL" } = await c.req.json();
	if (typeof amount !== 'number' || amount <= 0) return c.json({ error: "amount must be > 0" }, 400);

	const contribId = crypto.randomUUID();
	const now = Date.now();

	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'add contribution', (d) => {
		d.contributions[contribId] = {
			userId: null,
			username,
			amount,
			currency: d.cart.currency,
			paymentMethod,
			status: 'confirmed',
			txHash: null,
			createdAt: now,
			updatedAt: now,
		};
		d.cart.fundedAmount = Math.round((d.cart.fundedAmount + amount) * 100) / 100;
		d.cart.updatedAt = now;
		d.events.push({
			type: 'contribution',
			actor: username,
			detail: `Contributed $${amount.toFixed(2)}`,
			timestamp: now,
		});
	});

	reindexCart(space, cartId);
	return c.json({ id: contribId, amount, fundedAmount: doc.cart.fundedAmount + amount }, 201);
});

// ── Extension shortcut routes ──

// POST /api/cart/quick-add — Simplified endpoint for extension
routes.post("/api/cart/quick-add", async (c) => {
	const space = c.req.param("space") || "demo";
	const { url, product, space: targetSpace } = await c.req.json();
	if (!url) return c.json({ error: "Required: url" }, 400);

	const effectiveSpace = targetSpace || space;

	// Find or create a default OPEN cart
	const indexDoc = ensureShoppingCartIndex(effectiveSpace);
	let activeCartId: string | null = null;

	for (const [id, entry] of Object.entries(indexDoc.carts || {})) {
		if (entry.status === 'OPEN') {
			activeCartId = id;
			break;
		}
	}

	if (!activeCartId) {
		// Create a default cart
		activeCartId = crypto.randomUUID();
		const now = Date.now();
		const docId = shoppingCartDocId(effectiveSpace, activeCartId);
		const cartDoc = Automerge.change(Automerge.init<ShoppingCartDoc>(), 'create default cart', (d) => {
			const init = shoppingCartSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = effectiveSpace;
			d.cart.id = activeCartId!;
			d.cart.name = 'My Cart';
			d.cart.description = 'Default shopping cart';
			d.cart.status = 'OPEN';
			d.cart.createdAt = now;
			d.cart.updatedAt = now;
		});
		_syncServer!.setDoc(docId, cartDoc);
		reindexCart(effectiveSpace, activeCartId);
	}

	// Extract product data
	let productData: any;
	if (product) {
		productData = product;
		productData.sourceUrl = url;
	} else {
		try {
			productData = await extractProductFromUrl(url);
		} catch {
			productData = { name: url, sourceUrl: url };
		}
	}

	const itemId = crypto.randomUUID();
	const now = Date.now();
	const docId = shoppingCartDocId(effectiveSpace, activeCartId);
	const domain = (() => { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return 'unknown'; } })();

	_syncServer!.changeDoc<ShoppingCartDoc>(docId, 'quick-add item', (d) => {
		d.items[itemId] = {
			name: productData.name || 'Unknown Product',
			price: productData.price ?? null,
			currency: productData.currency || 'USD',
			quantity: 1,
			sourceUrl: url,
			imageUrl: productData.imageUrl || null,
			description: productData.description || null,
			vendor: {
				name: productData.vendor?.name || domain,
				domain: productData.vendor?.domain || domain,
				platform: productData.vendor?.platform || null,
			},
			addedBy: null,
			addedAt: now,
			sku: productData.sku || null,
		};
		d.cart.updatedAt = now;
	});

	reindexCart(effectiveSpace, activeCartId);

	return c.json({
		success: true,
		data: {
			name: productData.name || url,
			cartId: activeCartId,
			itemId,
		},
	}, 201);
});

// GET /api/cart/summary — Badge count for extension popup
routes.get("/api/cart/summary", async (c) => {
	const space = c.req.param("space") || "demo";
	const indexDoc = ensureShoppingCartIndex(space);

	let totalItems = 0;
	let totalAmount = 0;
	const vendorGroups: Array<{ vendor: { name: string; domain: string }; items: Array<{ name: string; price: number; quantity: number }>; subtotal: number }> = [];

	for (const [cartId, entry] of Object.entries(indexDoc.carts || {})) {
		if (entry.status === 'OPEN' || entry.status === 'FUNDING') {
			totalItems += entry.itemCount;
			totalAmount += entry.totalAmount;

			// Get full cart doc for vendor grouping
			const cartDocId = shoppingCartDocId(space, cartId);
			const cartDoc = _syncServer!.getDoc<ShoppingCartDoc>(cartDocId);
			if (cartDoc) {
				const byVendor: Record<string, typeof vendorGroups[0]> = {};
				for (const item of Object.values(cartDoc.items || {})) {
					const key = item.vendor.domain;
					if (!byVendor[key]) {
						byVendor[key] = { vendor: { name: item.vendor.name, domain: item.vendor.domain }, items: [], subtotal: 0 };
					}
					byVendor[key].items.push({ name: item.name, price: item.price || 0, quantity: item.quantity });
					byVendor[key].subtotal += (item.price || 0) * item.quantity;
				}
				vendorGroups.push(...Object.values(byVendor));
			}
		}
	}

	return c.json({
		success: true,
		data: {
			totalItems,
			totalAmount: Math.round(totalAmount * 100) / 100,
			currency: 'USD',
			vendorGroups,
		},
	});
});

// ── Page route: shop ──
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${space} — Shop | rSpace`,
		moduleId: "rcart",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-cart-shop space="${space}"></folk-cart-shop>`,
		scripts: `<script type="module" src="/modules/rcart/folk-cart-shop.js"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rcart/cart.css">`,
	}));
});

// ── Seed template data ──

function seedTemplateCart(space: string) {
	if (!_syncServer) return;
	const doc = ensureCatalogDoc(space);
	if (Object.keys(doc.items).length > 0) return;

	const docId = catalogDocId(space);
	const now = Date.now();

	const items: Array<{ title: string; type: string; caps: string[]; subs: string[]; tags: string[] }> = [
		{
			title: 'Commons Community Sticker Pack', type: 'sticker',
			caps: ['laser-print'], subs: ['vinyl-matte'],
			tags: ['merch', 'stickers'],
		},
		{
			title: 'Cosmolocal Network Poster (A2)', type: 'poster',
			caps: ['risograph'], subs: ['paper-heavyweight'],
			tags: ['merch', 'poster', 'cosmolocal'],
		},
		{
			title: 'rSpace Contributor Tee', type: 'apparel',
			caps: ['screen-print'], subs: ['cotton-organic'],
			tags: ['merch', 'apparel'],
		},
	];

	_syncServer.changeDoc<CatalogDoc>(docId, 'seed template catalog', (d) => {
		for (const item of items) {
			const id = crypto.randomUUID();
			d.items[id] = {
				id, artifactId: crypto.randomUUID(), artifact: null,
				title: item.title, productType: item.type,
				requiredCapabilities: item.caps, substrates: item.subs,
				creatorId: 'did:demo:seed', sourceSpace: space,
				tags: item.tags, status: 'active',
				createdAt: now, updatedAt: now,
			};
		}
	});

	console.log(`[Cart] Template seeded for "${space}": 3 catalog entries`);
}

export const cartModule: RSpaceModule = {
	id: "rcart",
	name: "rCart",
	icon: "🛒",
	description: "Group shopping & cosmolocal print-on-demand shop",
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [
		{ pattern: '{space}:cart:catalog', description: 'Product catalog', init: catalogSchema.init },
		{ pattern: '{space}:cart:orders:{orderId}', description: 'Order document', init: orderSchema.init },
		{ pattern: '{space}:cart:shopping:{cartId}', description: 'Shopping cart', init: shoppingCartSchema.init },
		{ pattern: '{space}:cart:shopping-index', description: 'Shopping cart index', init: shoppingCartIndexSchema.init },
	],
	routes,
	standaloneDomain: "rcart.online",
	landingPage: renderLanding,
	seedTemplate: seedTemplateCart,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
	},
	feeds: [
		{
			id: "orders",
			name: "Orders",
			kind: "economic",
			description: "Order stream with pricing, fulfillment status, and revenue splits",
			filterable: true,
		},
		{
			id: "catalog",
			name: "Catalog",
			kind: "data",
			description: "Active catalog listings with product details and pricing",
			filterable: true,
		},
		{
			id: "shopping",
			name: "Shopping Carts",
			kind: "data",
			description: "Group shopping carts with pooled items and contributions",
			filterable: true,
		},
	],
	acceptsFeeds: ["economic", "data"],
	outputPaths: [
		{ path: "carts", name: "Carts", icon: "🛒", description: "Group shopping carts" },
		{ path: "products", name: "Products", icon: "🛍️", description: "Print-on-demand product catalog" },
		{ path: "orders", name: "Orders", icon: "📦", description: "Order history and fulfillment tracking" },
	],
};
