/**
 * rBnb module — community hospitality rApp.
 *
 * Trust-based space sharing and couch surfing within community networks.
 * No platform extraction — leverages rNetwork trust graph, supports gift economy
 * as a first-class option, keeps all data local-first via Automerge CRDTs.
 *
 * All persistence uses Automerge documents via SyncServer —
 * no PostgreSQL dependency.
 */

import { Hono } from "hono";
import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { bnbSchema, bnbDocId } from './schemas';
import type {
	BnbDoc, Listing, AvailabilityWindow, StayRequest, StayMessage,
	Endorsement, SpaceConfig, EconomyModel, ListingType, StayStatus,
} from './schemas';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── Local-first helpers ──

function ensureDoc(space: string): BnbDoc {
	const docId = bnbDocId(space);
	let doc = _syncServer!.getDoc<BnbDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<BnbDoc>(), 'init bnb', (d) => {
			const init = bnbSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.config = init.config;
			d.listings = {};
			d.availability = {};
			d.stays = {};
			d.endorsements = {};
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function daysFromNow(days: number): number {
	const d = new Date();
	d.setDate(d.getDate() + days);
	d.setHours(0, 0, 0, 0);
	return d.getTime();
}

// ── JSON response helpers ──

function listingToRow(l: Listing) {
	return {
		id: l.id,
		host_did: l.hostDid,
		host_name: l.hostName,
		title: l.title,
		description: l.description,
		type: l.type,
		economy: l.economy,
		suggested_amount: l.suggestedAmount,
		currency: l.currency,
		sliding_min: l.slidingMin,
		sliding_max: l.slidingMax,
		exchange_description: l.exchangeDescription,
		location_name: l.locationName,
		location_lat: l.locationLat,
		location_lng: l.locationLng,
		location_granularity: l.locationGranularity,
		guest_capacity: l.guestCapacity,
		bedroom_count: l.bedroomCount,
		bed_count: l.bedCount,
		bathroom_count: l.bathroomCount,
		amenities: l.amenities,
		house_rules: l.houseRules,
		photos: l.photos,
		cover_photo: l.coverPhoto,
		trust_threshold: l.trustThreshold,
		instant_accept: l.instantAccept,
		is_active: l.isActive,
		created_at: l.createdAt ? new Date(l.createdAt).toISOString() : null,
		updated_at: l.updatedAt ? new Date(l.updatedAt).toISOString() : null,
	};
}

function availabilityToRow(a: AvailabilityWindow) {
	return {
		id: a.id,
		listing_id: a.listingId,
		start_date: new Date(a.startDate).toISOString().split('T')[0],
		end_date: new Date(a.endDate).toISOString().split('T')[0],
		status: a.status,
		notes: a.notes,
		created_at: a.createdAt ? new Date(a.createdAt).toISOString() : null,
	};
}

function stayToRow(s: StayRequest) {
	return {
		id: s.id,
		listing_id: s.listingId,
		guest_did: s.guestDid,
		guest_name: s.guestName,
		host_did: s.hostDid,
		check_in: new Date(s.checkIn).toISOString().split('T')[0],
		check_out: new Date(s.checkOut).toISOString().split('T')[0],
		guest_count: s.guestCount,
		status: s.status,
		messages: s.messages.map(m => ({
			id: m.id,
			sender_did: m.senderDid,
			sender_name: m.senderName,
			body: m.body,
			sent_at: new Date(m.sentAt).toISOString(),
		})),
		offered_amount: s.offeredAmount,
		offered_currency: s.offeredCurrency,
		offered_exchange: s.offeredExchange,
		requested_at: s.requestedAt ? new Date(s.requestedAt).toISOString() : null,
		responded_at: s.respondedAt ? new Date(s.respondedAt).toISOString() : null,
		completed_at: s.completedAt ? new Date(s.completedAt).toISOString() : null,
		cancelled_at: s.cancelledAt ? new Date(s.cancelledAt).toISOString() : null,
	};
}

function endorsementToRow(e: Endorsement) {
	return {
		id: e.id,
		stay_id: e.stayId,
		listing_id: e.listingId,
		author_did: e.authorDid,
		author_name: e.authorName,
		subject_did: e.subjectDid,
		subject_name: e.subjectName,
		direction: e.direction,
		body: e.body,
		rating: e.rating,
		tags: e.tags,
		visibility: e.visibility,
		trust_weight: e.trustWeight,
		created_at: e.createdAt ? new Date(e.createdAt).toISOString() : null,
	};
}

// ── Seed demo data ──

function seedDemoIfEmpty(space: string) {
	const docId = bnbDocId(space);
	const doc = ensureDoc(space);
	if ((doc.meta as any)?.seeded || Object.keys(doc.listings).length > 0) return;

	_syncServer!.changeDoc<BnbDoc>(docId, 'seed demo data', (d) => {
		const now = Date.now();

		// ── 6 Demo Listings ──

		const listing1 = crypto.randomUUID();
		d.listings[listing1] = {
			id: listing1,
			hostDid: 'did:key:z6MkDemoHost1',
			hostName: 'Marta K.',
			title: 'Cozy Couch in Kreuzberg Co-op',
			description: 'A comfortable pull-out couch in our community living room. Shared kitchen, rooftop garden access. We host weekly dinners on Thursdays — guests welcome to join!',
			type: 'couch',
			economy: 'gift',
			suggestedAmount: null,
			currency: null,
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			locationName: 'Kreuzberg, Berlin',
			locationLat: 52.4934,
			locationLng: 13.4014,
			locationGranularity: 'neighborhood',
			guestCapacity: 1,
			bedroomCount: null,
			bedCount: 1,
			bathroomCount: 1,
			amenities: ['wifi', 'kitchen', 'laundry', 'garden', 'linens'],
			houseRules: ['shoes_off', 'quiet_hours', 'no_smoking'],
			photos: [],
			coverPhoto: null,
			trustThreshold: 20,
			instantAccept: true,
			isActive: true,
			createdAt: now - 86400000 * 45,
			updatedAt: now - 86400000 * 3,
		};

		const listing2 = crypto.randomUUID();
		d.listings[listing2] = {
			id: listing2,
			hostDid: 'did:key:z6MkDemoHost2',
			hostName: 'Tomás R.',
			title: 'Permaculture Farm Stay — Help & Learn',
			description: 'Private room in the farmhouse. Join morning chores in exchange for your stay: feeding chickens, harvesting vegetables, composting. Meals included from the farm kitchen.',
			type: 'room',
			economy: 'exchange',
			suggestedAmount: null,
			currency: null,
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: '3-4 hours of farm work per day (morning shift). All meals from the farm kitchen included.',
			locationName: 'Alentejo, Portugal',
			locationLat: 38.5667,
			locationLng: -7.9,
			locationGranularity: 'region',
			guestCapacity: 2,
			bedroomCount: 1,
			bedCount: 1,
			bathroomCount: 1,
			amenities: ['kitchen', 'garden', 'parking', 'linens', 'towels', 'hot_water'],
			houseRules: ['no_smoking', 'clean_up_after', 'no_parties'],
			photos: [],
			coverPhoto: null,
			trustThreshold: 15,
			instantAccept: false,
			isActive: true,
			createdAt: now - 86400000 * 60,
			updatedAt: now - 86400000 * 5,
		};

		const listing3 = crypto.randomUUID();
		d.listings[listing3] = {
			id: listing3,
			hostDid: 'did:key:z6MkDemoHost3',
			hostName: 'Anika S.',
			title: 'Lakeside Tent Site in Mecklenburg',
			description: 'Pitch your tent by the lake on our community land. Composting toilet, outdoor shower (solar-heated), fire pit, and kayak available. Pure quiet — no cars, no lights, just stars.',
			type: 'tent_site',
			economy: 'suggested',
			suggestedAmount: 10,
			currency: 'EUR',
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			locationName: 'Mecklenburg Lake District, Germany',
			locationLat: 53.45,
			locationLng: 12.7,
			locationGranularity: 'region',
			guestCapacity: 4,
			bedroomCount: null,
			bedCount: null,
			bathroomCount: 1,
			amenities: ['parking', 'garden', 'pets_welcome'],
			houseRules: ['no_parties', 'clean_up_after'],
			photos: [],
			coverPhoto: null,
			trustThreshold: 10,
			instantAccept: true,
			isActive: true,
			createdAt: now - 86400000 * 30,
			updatedAt: now - 86400000 * 2,
		};

		const listing4 = crypto.randomUUID();
		d.listings[listing4] = {
			id: listing4,
			hostDid: 'did:key:z6MkDemoHost4',
			hostName: 'Lucia V.',
			title: 'Artist Loft in Neukölln — Sliding Scale',
			description: 'Bright, spacious loft in a converted factory. Art supplies and a shared studio space available. Great for creative retreats. Pay what feels right.',
			type: 'loft',
			economy: 'sliding_scale',
			suggestedAmount: 35,
			currency: 'EUR',
			slidingMin: 15,
			slidingMax: 60,
			exchangeDescription: null,
			locationName: 'Neukölln, Berlin',
			locationLat: 52.4811,
			locationLng: 13.4346,
			locationGranularity: 'neighborhood',
			guestCapacity: 2,
			bedroomCount: 1,
			bedCount: 1,
			bathroomCount: 1,
			amenities: ['wifi', 'kitchen', 'workspace', 'heating', 'hot_water', 'linens', 'towels'],
			houseRules: ['shoes_off', 'no_smoking', 'quiet_hours'],
			photos: [],
			coverPhoto: null,
			trustThreshold: 25,
			instantAccept: false,
			isActive: true,
			createdAt: now - 86400000 * 20,
			updatedAt: now - 86400000 * 1,
		};

		const listing5 = crypto.randomUUID();
		d.listings[listing5] = {
			id: listing5,
			hostDid: 'did:key:z6MkDemoHost5',
			hostName: 'Commons Hub Berlin',
			title: 'Commons Hub Guest Room — Gift Economy',
			description: 'A dedicated guest room in our co-working and community hub. Access to shared kitchen, meeting rooms, library, and rooftop terrace. Visitors are part of the community for the duration of their stay.',
			type: 'room',
			economy: 'gift',
			suggestedAmount: null,
			currency: null,
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			locationName: 'Mitte, Berlin',
			locationLat: 52.52,
			locationLng: 13.405,
			locationGranularity: 'neighborhood',
			guestCapacity: 2,
			bedroomCount: 1,
			bedCount: 2,
			bathroomCount: 1,
			amenities: ['wifi', 'kitchen', 'workspace', 'laundry', 'heating', 'hot_water', 'linens', 'towels', 'wheelchair_accessible'],
			houseRules: ['no_smoking', 'quiet_hours', 'clean_up_after', 'shoes_off'],
			photos: [],
			coverPhoto: null,
			trustThreshold: 30,
			instantAccept: true,
			isActive: true,
			createdAt: now - 86400000 * 90,
			updatedAt: now - 86400000 * 7,
		};

		const listing6 = crypto.randomUUID();
		d.listings[listing6] = {
			id: listing6,
			hostDid: 'did:key:z6MkDemoHost6',
			hostName: 'Jakob W.',
			title: 'Off-Grid Cabin in the Black Forest',
			description: 'A small wooden cabin heated by wood stove. No electricity, no internet — just forest, silence, and a creek. Solar lantern and firewood provided. Ideal for digital detox.',
			type: 'cabin',
			economy: 'fixed',
			suggestedAmount: 45,
			currency: 'EUR',
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			locationName: 'Black Forest, Germany',
			locationLat: 48.0,
			locationLng: 8.2,
			locationGranularity: 'region',
			guestCapacity: 2,
			bedroomCount: 1,
			bedCount: 1,
			bathroomCount: null,
			amenities: ['parking', 'pets_welcome', 'heating'],
			houseRules: ['no_smoking', 'clean_up_after', 'check_in_by_10pm'],
			photos: [],
			coverPhoto: null,
			trustThreshold: 40,
			instantAccept: false,
			isActive: true,
			createdAt: now - 86400000 * 15,
			updatedAt: now - 86400000 * 1,
		};

		// ── Availability windows ──

		const avail = (listingId: string, startDays: number, endDays: number, status: 'available' | 'blocked' = 'available') => {
			const id = crypto.randomUUID();
			d.availability[id] = {
				id,
				listingId,
				startDate: daysFromNow(startDays),
				endDate: daysFromNow(endDays),
				status,
				notes: null,
				createdAt: now,
			};
		};

		// Listing 1 (Kreuzberg couch) — available next 2 months
		avail(listing1, 0, 60);
		// Listing 2 (Farm) — available next month, then blocked
		avail(listing2, 0, 30);
		avail(listing2, 31, 45, 'blocked');
		avail(listing2, 46, 90);
		// Listing 3 (Tent site) — seasonal, available May-September feel
		avail(listing3, 0, 120);
		// Listing 4 (Artist loft) — some gaps
		avail(listing4, 3, 20);
		avail(listing4, 25, 50);
		// Listing 5 (Commons hub) — available year-round
		avail(listing5, 0, 180);
		// Listing 6 (Cabin) — weekends only for next month, then open
		avail(listing6, 5, 7);
		avail(listing6, 12, 14);
		avail(listing6, 19, 21);
		avail(listing6, 26, 28);
		avail(listing6, 30, 90);

		// ── Sample stay requests ──

		const stay1 = crypto.randomUUID();
		d.stays[stay1] = {
			id: stay1,
			listingId: listing1,
			guestDid: 'did:key:z6MkDemoGuest1',
			guestName: 'Ravi P.',
			hostDid: 'did:key:z6MkDemoHost1',
			checkIn: daysFromNow(5),
			checkOut: daysFromNow(8),
			guestCount: 1,
			status: 'accepted',
			messages: [
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkDemoGuest1',
					senderName: 'Ravi P.',
					body: 'Hi Marta! I\'m visiting Berlin for a commons conference and would love to crash at your place. I\'m quiet, clean, and happy to cook!',
					sentAt: now - 86400000 * 3,
				},
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkDemoHost1',
					senderName: 'Marta K.',
					body: 'Welcome Ravi! You\'re auto-accepted (trust score 72). Thursday dinner is lasagna night — you\'re invited. Key code will be sent day-of.',
					sentAt: now - 86400000 * 3 + 3600000,
				},
			],
			offeredAmount: null,
			offeredCurrency: null,
			offeredExchange: null,
			requestedAt: now - 86400000 * 3,
			respondedAt: now - 86400000 * 3 + 3600000,
			completedAt: null,
			cancelledAt: null,
		};

		const stay2 = crypto.randomUUID();
		d.stays[stay2] = {
			id: stay2,
			listingId: listing2,
			guestDid: 'did:key:z6MkDemoGuest2',
			guestName: 'Elena M.',
			hostDid: 'did:key:z6MkDemoHost2',
			checkIn: daysFromNow(-14),
			checkOut: daysFromNow(-7),
			guestCount: 1,
			status: 'completed',
			messages: [
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkDemoGuest2',
					senderName: 'Elena M.',
					body: 'I\'d love to learn permaculture! I have experience with composting and seed saving.',
					sentAt: now - 86400000 * 21,
				},
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkDemoHost2',
					senderName: 'Tomás R.',
					body: 'Perfect! We\'re starting a new compost system this week. Come join us.',
					sentAt: now - 86400000 * 20,
				},
			],
			offeredAmount: null,
			offeredCurrency: null,
			offeredExchange: 'Help with composting and seed saving',
			requestedAt: now - 86400000 * 21,
			respondedAt: now - 86400000 * 20,
			completedAt: now - 86400000 * 7,
			cancelledAt: null,
		};

		const stay3 = crypto.randomUUID();
		d.stays[stay3] = {
			id: stay3,
			listingId: listing4,
			guestDid: 'did:key:z6MkDemoGuest3',
			guestName: 'Kai L.',
			hostDid: 'did:key:z6MkDemoHost4',
			checkIn: daysFromNow(10),
			checkOut: daysFromNow(14),
			guestCount: 1,
			status: 'pending',
			messages: [
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkDemoGuest3',
					senderName: 'Kai L.',
					body: 'Hi Lucia, I\'m a muralist and would love to stay in your loft for a few days while working on a commission nearby. Happy to pay sliding scale.',
					sentAt: now - 86400000 * 1,
				},
			],
			offeredAmount: 30,
			offeredCurrency: 'EUR',
			offeredExchange: null,
			requestedAt: now - 86400000 * 1,
			respondedAt: null,
			completedAt: null,
			cancelledAt: null,
		};

		// ── Sample endorsements ──

		const endorsement1 = crypto.randomUUID();
		d.endorsements[endorsement1] = {
			id: endorsement1,
			stayId: stay2,
			listingId: listing2,
			authorDid: 'did:key:z6MkDemoGuest2',
			authorName: 'Elena M.',
			subjectDid: 'did:key:z6MkDemoHost2',
			subjectName: 'Tomás R.',
			direction: 'guest_to_host',
			body: 'An incredible experience. Tomás is a generous teacher and the farm is a paradise. I learned so much about permaculture in one week. The food was amazing — all from the garden.',
			rating: 5,
			tags: ['welcoming', 'generous', 'great_conversation', 'helpful'],
			visibility: 'public',
			trustWeight: 0.85,
			createdAt: now - 86400000 * 6,
		};

		const endorsement2 = crypto.randomUUID();
		d.endorsements[endorsement2] = {
			id: endorsement2,
			stayId: stay2,
			listingId: listing2,
			authorDid: 'did:key:z6MkDemoHost2',
			authorName: 'Tomás R.',
			subjectDid: 'did:key:z6MkDemoGuest2',
			subjectName: 'Elena M.',
			direction: 'host_to_guest',
			body: 'Elena was a wonderful helper. She arrived with energy and curiosity, respected the land, and left the compost system in better shape than she found it. Welcome back anytime.',
			rating: 5,
			tags: ['respectful', 'helpful', 'clean', 'great_conversation'],
			visibility: 'public',
			trustWeight: 0.8,
			createdAt: now - 86400000 * 6,
		};
	});

	_syncServer!.changeDoc<BnbDoc>(docId, 'mark seeded', (d) => {
		if (d.meta) (d.meta as any).seeded = true;
	});
	console.log("[rBnb] Demo data seeded: 6 listings, 3 stay requests, 2 endorsements");
}

// ── API: Listings ──

routes.get("/api/listings", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const { type, economy, active, search } = c.req.query();

	const doc = ensureDoc(dataSpace);
	let listings = Object.values(doc.listings);

	if (type) listings = listings.filter(l => l.type === type);
	if (economy) listings = listings.filter(l => l.economy === economy);
	if (active !== undefined) listings = listings.filter(l => l.isActive === (active === 'true'));
	if (search) {
		const term = search.toLowerCase();
		listings = listings.filter(l =>
			l.title.toLowerCase().includes(term) ||
			l.description.toLowerCase().includes(term) ||
			l.locationName.toLowerCase().includes(term)
		);
	}

	listings.sort((a, b) => b.createdAt - a.createdAt);
	const rows = listings.map(listingToRow);
	return c.json({ count: rows.length, results: rows });
});

routes.post("/api/listings", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const body = await c.req.json();

	if (!body.title?.trim()) return c.json({ error: "Title required" }, 400);

	const docId = bnbDocId(dataSpace);
	ensureDoc(dataSpace);
	const listingId = crypto.randomUUID();
	const now = Date.now();

	_syncServer!.changeDoc<BnbDoc>(docId, `create listing ${listingId}`, (d) => {
		d.listings[listingId] = {
			id: listingId,
			hostDid: body.host_did || '',
			hostName: body.host_name || 'Anonymous',
			title: body.title.trim(),
			description: body.description || '',
			type: body.type || 'room',
			economy: body.economy || d.config.defaultEconomy,
			suggestedAmount: body.suggested_amount ?? null,
			currency: body.currency ?? null,
			slidingMin: body.sliding_min ?? null,
			slidingMax: body.sliding_max ?? null,
			exchangeDescription: body.exchange_description ?? null,
			locationName: body.location_name || '',
			locationLat: body.location_lat ?? null,
			locationLng: body.location_lng ?? null,
			locationGranularity: body.location_granularity ?? null,
			guestCapacity: body.guest_capacity || 1,
			bedroomCount: body.bedroom_count ?? null,
			bedCount: body.bed_count ?? null,
			bathroomCount: body.bathroom_count ?? null,
			amenities: body.amenities || [],
			houseRules: body.house_rules || [],
			photos: body.photos || [],
			coverPhoto: body.cover_photo ?? null,
			trustThreshold: body.trust_threshold ?? d.config.defaultTrustThreshold,
			instantAccept: body.instant_accept ?? false,
			isActive: true,
			createdAt: now,
			updatedAt: now,
		};
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(listingToRow(updated.listings[listingId]), 201);
});

routes.get("/api/listings/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");
	const doc = ensureDoc(dataSpace);
	const listing = doc.listings[id];
	if (!listing) return c.json({ error: "Listing not found" }, 404);
	return c.json(listingToRow(listing));
});

routes.patch("/api/listings/:id", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");
	const body = await c.req.json();

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.listings[id]) return c.json({ error: "Not found" }, 404);

	const fieldMap: Record<string, keyof Listing> = {
		title: 'title', description: 'description', type: 'type', economy: 'economy',
		suggested_amount: 'suggestedAmount', currency: 'currency',
		sliding_min: 'slidingMin', sliding_max: 'slidingMax',
		exchange_description: 'exchangeDescription',
		location_name: 'locationName', location_lat: 'locationLat', location_lng: 'locationLng',
		guest_capacity: 'guestCapacity', bedroom_count: 'bedroomCount',
		bed_count: 'bedCount', bathroom_count: 'bathroomCount',
		amenities: 'amenities', house_rules: 'houseRules',
		photos: 'photos', cover_photo: 'coverPhoto',
		trust_threshold: 'trustThreshold', instant_accept: 'instantAccept',
		is_active: 'isActive',
	};

	const updates: Array<{ field: keyof Listing; value: any }> = [];
	for (const [bodyKey, docField] of Object.entries(fieldMap)) {
		if (body[bodyKey] !== undefined) {
			updates.push({ field: docField, value: body[bodyKey] });
		}
	}
	if (updates.length === 0) return c.json({ error: "No fields" }, 400);

	_syncServer!.changeDoc<BnbDoc>(docId, `update listing ${id}`, (d) => {
		const l = d.listings[id];
		for (const { field, value } of updates) {
			(l as any)[field] = value;
		}
		l.updatedAt = Date.now();
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(listingToRow(updated.listings[id]));
});

routes.delete("/api/listings/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.listings[id]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<BnbDoc>(docId, `delete listing ${id}`, (d) => {
		delete d.listings[id];
		// Also clean up availability windows for this listing
		for (const [aid, aw] of Object.entries(d.availability)) {
			if (aw.listingId === id) delete d.availability[aid];
		}
	});
	return c.json({ ok: true });
});

// ── API: Availability ──

routes.get("/api/listings/:id/availability", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const listingId = c.req.param("id");
	const { status } = c.req.query();

	const doc = ensureDoc(dataSpace);
	let windows = Object.values(doc.availability).filter(a => a.listingId === listingId);

	if (status) windows = windows.filter(a => a.status === status);
	windows.sort((a, b) => a.startDate - b.startDate);

	return c.json({ count: windows.length, results: windows.map(availabilityToRow) });
});

routes.post("/api/listings/:id/availability", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const listingId = c.req.param("id");
	const body = await c.req.json();

	if (!body.start_date || !body.end_date) return c.json({ error: "start_date and end_date required" }, 400);

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.listings[listingId]) return c.json({ error: "Listing not found" }, 404);

	const awId = crypto.randomUUID();

	_syncServer!.changeDoc<BnbDoc>(docId, `add availability ${awId}`, (d) => {
		d.availability[awId] = {
			id: awId,
			listingId,
			startDate: new Date(body.start_date).getTime(),
			endDate: new Date(body.end_date).getTime(),
			status: body.status || 'available',
			notes: body.notes ?? null,
			createdAt: Date.now(),
		};
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(availabilityToRow(updated.availability[awId]), 201);
});

routes.patch("/api/availability/:id", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");
	const body = await c.req.json();

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.availability[id]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<BnbDoc>(docId, `update availability ${id}`, (d) => {
		const aw = d.availability[id];
		if (body.start_date) aw.startDate = new Date(body.start_date).getTime();
		if (body.end_date) aw.endDate = new Date(body.end_date).getTime();
		if (body.status) aw.status = body.status;
		if (body.notes !== undefined) aw.notes = body.notes;
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(availabilityToRow(updated.availability[id]));
});

routes.delete("/api/availability/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.availability[id]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<BnbDoc>(docId, `delete availability ${id}`, (d) => {
		delete d.availability[id];
	});
	return c.json({ ok: true });
});

// ── API: Stay Requests ──

routes.get("/api/stays", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const { listing_id, status, guest_did, host_did } = c.req.query();

	const doc = ensureDoc(dataSpace);
	let stays = Object.values(doc.stays);

	if (listing_id) stays = stays.filter(s => s.listingId === listing_id);
	if (status) stays = stays.filter(s => s.status === status);
	if (guest_did) stays = stays.filter(s => s.guestDid === guest_did);
	if (host_did) stays = stays.filter(s => s.hostDid === host_did);

	stays.sort((a, b) => b.requestedAt - a.requestedAt);
	return c.json({ count: stays.length, results: stays.map(stayToRow) });
});

routes.post("/api/stays", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const body = await c.req.json();

	if (!body.listing_id || !body.check_in || !body.check_out) {
		return c.json({ error: "listing_id, check_in, and check_out required" }, 400);
	}

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	const listing = doc.listings[body.listing_id];
	if (!listing) return c.json({ error: "Listing not found" }, 404);

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

	// Check trust threshold for auto-accept
	const guestTrustScore = body.guest_trust_score ?? 0;
	const autoAccept = listing.instantAccept && listing.trustThreshold !== null
		&& guestTrustScore >= listing.trustThreshold;

	const initialMessage: StayMessage | null = body.message ? {
		id: crypto.randomUUID(),
		senderDid: body.guest_did || '',
		senderName: body.guest_name || 'Guest',
		body: body.message,
		sentAt: now,
	} : null;

	_syncServer!.changeDoc<BnbDoc>(docId, `create stay ${stayId}`, (d) => {
		d.stays[stayId] = {
			id: stayId,
			listingId: body.listing_id,
			guestDid: body.guest_did || '',
			guestName: body.guest_name || 'Guest',
			hostDid: listing.hostDid,
			checkIn: new Date(body.check_in).getTime(),
			checkOut: new Date(body.check_out).getTime(),
			guestCount: body.guest_count || 1,
			status: autoAccept ? 'accepted' : 'pending',
			messages: initialMessage ? [initialMessage] : [],
			offeredAmount: body.offered_amount ?? null,
			offeredCurrency: body.offered_currency ?? null,
			offeredExchange: body.offered_exchange ?? null,
			requestedAt: now,
			respondedAt: autoAccept ? now : null,
			completedAt: null,
			cancelledAt: null,
		};
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(stayToRow(updated.stays[stayId]), 201);
});

routes.get("/api/stays/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");
	const doc = ensureDoc(dataSpace);
	const stay = doc.stays[id];
	if (!stay) return c.json({ error: "Stay not found" }, 404);
	return c.json(stayToRow(stay));
});

// ── Stay status transitions ──

function stayTransition(statusTarget: StayStatus, timestampField: 'respondedAt' | 'completedAt' | 'cancelledAt') {
	return async (c: any) => {
		const token = extractToken(c.req.raw.headers);
		if (!token) return c.json({ error: "Authentication required" }, 401);
		try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

		const space = c.req.param("space") || "demo";
		const dataSpace = c.get("effectiveSpace") || space;
		const id = c.req.param("id");

		const docId = bnbDocId(dataSpace);
		const doc = ensureDoc(dataSpace);
		if (!doc.stays[id]) return c.json({ error: "Not found" }, 404);

		_syncServer!.changeDoc<BnbDoc>(docId, `${statusTarget} stay ${id}`, (d) => {
			d.stays[id].status = statusTarget;
			(d.stays[id] as any)[timestampField] = Date.now();
		});

		const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
		return c.json(stayToRow(updated.stays[id]));
	};
}

routes.post("/api/stays/:id/accept", stayTransition('accepted', 'respondedAt'));
routes.post("/api/stays/:id/decline", stayTransition('declined', 'respondedAt'));
routes.post("/api/stays/:id/cancel", stayTransition('cancelled', 'cancelledAt'));
routes.post("/api/stays/:id/complete", stayTransition('completed', 'completedAt'));

// ── Stay messages ──

routes.post("/api/stays/:id/messages", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const id = c.req.param("id");
	const body = await c.req.json();

	if (!body.body?.trim()) return c.json({ error: "Message body required" }, 400);

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.stays[id]) return c.json({ error: "Stay not found" }, 404);

	const msgId = crypto.randomUUID();

	_syncServer!.changeDoc<BnbDoc>(docId, `add message to stay ${id}`, (d) => {
		d.stays[id].messages.push({
			id: msgId,
			senderDid: body.sender_did || '',
			senderName: body.sender_name || 'Anonymous',
			body: body.body.trim(),
			sentAt: Date.now(),
		});
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(stayToRow(updated.stays[id]));
});

// ── API: Endorsements ──

routes.get("/api/endorsements", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const { listing_id, subject_did, direction } = c.req.query();

	const doc = ensureDoc(dataSpace);
	let endorsements = Object.values(doc.endorsements);

	if (listing_id) endorsements = endorsements.filter(e => e.listingId === listing_id);
	if (subject_did) endorsements = endorsements.filter(e => e.subjectDid === subject_did);
	if (direction) endorsements = endorsements.filter(e => e.direction === direction);

	endorsements.sort((a, b) => b.createdAt - a.createdAt);
	return c.json({ count: endorsements.length, results: endorsements.map(endorsementToRow) });
});

routes.post("/api/endorsements", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const body = await c.req.json();

	if (!body.stay_id || !body.body?.trim()) {
		return c.json({ error: "stay_id and body required" }, 400);
	}

	const docId = bnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	const stay = doc.stays[body.stay_id];
	if (!stay) return c.json({ error: "Stay not found" }, 404);
	if (stay.status !== 'completed' && stay.status !== 'endorsed') {
		return c.json({ error: "Can only endorse completed stays" }, 400);
	}

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

	_syncServer!.changeDoc<BnbDoc>(docId, `create endorsement ${endorsementId}`, (d) => {
		d.endorsements[endorsementId] = {
			id: endorsementId,
			stayId: body.stay_id,
			listingId: stay.listingId,
			authorDid: body.author_did || '',
			authorName: body.author_name || 'Anonymous',
			subjectDid: body.subject_did || '',
			subjectName: body.subject_name || '',
			direction: body.direction || 'guest_to_host',
			body: body.body.trim(),
			rating: body.rating ?? null,
			tags: body.tags || [],
			visibility: body.visibility || 'public',
			trustWeight: body.trust_weight ?? 0.5,
			createdAt: now,
		};

		// Update stay status to endorsed
		if (d.stays[body.stay_id].status === 'completed') {
			d.stays[body.stay_id].status = 'endorsed';
		}
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(endorsementToRow(updated.endorsements[endorsementId]), 201);
});

routes.get("/api/endorsements/summary/:did", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const did = c.req.param("did");

	const doc = ensureDoc(dataSpace);
	const endorsements = Object.values(doc.endorsements).filter(e => e.subjectDid === did);
	const asHost = endorsements.filter(e => e.direction === 'guest_to_host');
	const asGuest = endorsements.filter(e => e.direction === 'host_to_guest');

	const avgRating = (list: Endorsement[]) => {
		const rated = list.filter(e => e.rating !== null);
		return rated.length ? rated.reduce((sum, e) => sum + e.rating!, 0) / rated.length : null;
	};

	const tagCounts = (list: Endorsement[]) => {
		const counts: Record<string, number> = {};
		for (const e of list) {
			for (const t of e.tags) counts[t] = (counts[t] || 0) + 1;
		}
		return counts;
	};

	return c.json({
		did,
		total_endorsements: endorsements.length,
		as_host: {
			count: asHost.length,
			avg_rating: avgRating(asHost),
			tags: tagCounts(asHost),
			avg_trust_weight: asHost.length ? asHost.reduce((s, e) => s + e.trustWeight, 0) / asHost.length : 0,
		},
		as_guest: {
			count: asGuest.length,
			avg_rating: avgRating(asGuest),
			tags: tagCounts(asGuest),
			avg_trust_weight: asGuest.length ? asGuest.reduce((s, e) => s + e.trustWeight, 0) / asGuest.length : 0,
		},
	});
});

// ── API: Search ──

routes.get("/api/search", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const { q, type, economy, guests, lat, lng, radius, check_in, check_out } = c.req.query();

	const doc = ensureDoc(dataSpace);
	let listings = Object.values(doc.listings).filter(l => l.isActive);

	// Text search
	if (q) {
		const term = q.toLowerCase();
		listings = listings.filter(l =>
			l.title.toLowerCase().includes(term) ||
			l.description.toLowerCase().includes(term) ||
			l.locationName.toLowerCase().includes(term) ||
			l.hostName.toLowerCase().includes(term)
		);
	}

	// Type filter
	if (type) listings = listings.filter(l => l.type === type);

	// Economy filter
	if (economy) listings = listings.filter(l => l.economy === economy);

	// Guest count
	if (guests) {
		const g = parseInt(guests);
		listings = listings.filter(l => l.guestCapacity >= g);
	}

	// Location proximity (simple distance filter)
	if (lat && lng && radius) {
		const cLat = parseFloat(lat);
		const cLng = parseFloat(lng);
		const r = parseFloat(radius); // km
		listings = listings.filter(l => {
			if (!l.locationLat || !l.locationLng) return false;
			// Haversine approximation
			const dLat = (l.locationLat - cLat) * Math.PI / 180;
			const dLng = (l.locationLng - cLng) * Math.PI / 180;
			const a = Math.sin(dLat / 2) ** 2 +
				Math.cos(cLat * Math.PI / 180) * Math.cos(l.locationLat * Math.PI / 180) *
				Math.sin(dLng / 2) ** 2;
			const dist = 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
			return dist <= r;
		});
	}

	// Date availability check
	if (check_in && check_out) {
		const ciMs = new Date(check_in).getTime();
		const coMs = new Date(check_out).getTime();
		const availability = Object.values(doc.availability);
		listings = listings.filter(l => {
			const windows = availability.filter(a =>
				a.listingId === l.id && a.status === 'available'
			);
			// At least one available window must cover the requested range
			return windows.some(w => w.startDate <= ciMs && w.endDate >= coMs);
		});
	}

	const rows = listings.map(listingToRow);
	return c.json({ count: rows.length, results: rows });
});

// ── API: Stats ──

routes.get("/api/stats", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const doc = ensureDoc(dataSpace);

	const listings = Object.values(doc.listings);
	const stays = Object.values(doc.stays);
	const endorsements = Object.values(doc.endorsements);

	return c.json({
		listings: listings.length,
		active_listings: listings.filter(l => l.isActive).length,
		stays: stays.length,
		pending_stays: stays.filter(s => s.status === 'pending').length,
		completed_stays: stays.filter(s => s.status === 'completed' || s.status === 'endorsed').length,
		endorsements: endorsements.length,
		economy_breakdown: {
			gift: listings.filter(l => l.economy === 'gift').length,
			suggested: listings.filter(l => l.economy === 'suggested').length,
			fixed: listings.filter(l => l.economy === 'fixed').length,
			sliding_scale: listings.filter(l => l.economy === 'sliding_scale').length,
			exchange: listings.filter(l => l.economy === 'exchange').length,
		},
	});
});

// ── API: Config ──

routes.get("/api/config", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const doc = ensureDoc(dataSpace);
	return c.json(doc.config);
});

routes.patch("/api/config", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const body = await c.req.json();

	const docId = bnbDocId(dataSpace);
	ensureDoc(dataSpace);

	_syncServer!.changeDoc<BnbDoc>(docId, 'update config', (d) => {
		if (body.default_economy) d.config.defaultEconomy = body.default_economy;
		if (body.default_trust_threshold !== undefined) d.config.defaultTrustThreshold = body.default_trust_threshold;
		if (body.amenity_catalog) d.config.amenityCatalog = body.amenity_catalog;
		if (body.house_rule_catalog) d.config.houseRuleCatalog = body.house_rule_catalog;
		if (body.endorsement_tag_catalog) d.config.endorsementTagCatalog = body.endorsement_tag_catalog;
		if (body.require_endorsement !== undefined) d.config.requireEndorsement = body.require_endorsement;
		if (body.max_stay_days !== undefined) d.config.maxStayDays = body.max_stay_days;
	});

	const updated = _syncServer!.getDoc<BnbDoc>(docId)!;
	return c.json(updated.config);
});

// ── Page route ──

routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${space} — Hospitality | rSpace`,
		moduleId: "rbnb",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-bnb-view space="${space}"></folk-bnb-view>`,
		scripts: `<script type="module" src="/modules/rbnb/folk-bnb-view.js?v=2"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rbnb/bnb.css">
			<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">`,
	}));
});

export function getActiveListingsForMI(space: string, limit = 5): { id: string; title: string; type: string; locationName: string; economy: string; createdAt: number }[] {
	if (!_syncServer) return [];
	const docId = bnbDocId(space);
	const doc = _syncServer.getDoc<BnbDoc>(docId);
	if (!doc) return [];
	return Object.values(doc.listings)
		.filter((l) => l.isActive)
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, limit)
		.map((l) => ({ id: l.id, title: l.title, type: l.type, locationName: l.locationName || "", economy: l.economy, createdAt: l.createdAt }));
}

// ── Module export ──

export const bnbModule: RSpaceModule = {
	id: "rbnb",
	name: "rBnb",
	icon: "\u{1F3E0}",
	description: "Community hospitality — trust-based space sharing and couch surfing",
	scoping: { defaultScope: 'space', userConfigurable: true },
	docSchemas: [{ pattern: '{space}:bnb:listings', description: 'Listings, stays, endorsements', init: bnbSchema.init }],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedDemoIfEmpty,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		seedDemoIfEmpty("demo");
	},
	feeds: [
		{
			id: "listings",
			name: "Listings",
			kind: "data",
			description: "Hospitality listings with location, capacity, and economy model",
			filterable: true,
		},
		{
			id: "stays",
			name: "Stays",
			kind: "data",
			description: "Stay requests and their status (pending, accepted, completed)",
		},
		{
			id: "endorsements",
			name: "Endorsements",
			kind: "trust",
			description: "Trust-based endorsements from completed stays, feeds rNetwork",
		},
		{
			id: "hospitality-value",
			name: "Hospitality Value",
			kind: "economic",
			description: "Economic value of stays (contributions, exchanges, gifts)",
		},
	],
	acceptsFeeds: ["data", "trust", "economic"],
	outputPaths: [
		{ path: "listings", name: "Listings", icon: "\u{1F3E0}", description: "Community hospitality listings" },
		{ path: "stays", name: "Stays", icon: "\u{1F6CC}", description: "Stay requests and history" },
		{ path: "endorsements", name: "Endorsements", icon: "\u{2B50}", description: "Trust endorsements from stays" },
	],
	onboardingActions: [
		{ label: "Create a Listing", icon: "🏠", description: "List a space for community stays", type: 'create', href: '/{space}/rbnb' },
	],
};
