/**
 * rVnb module — community RV & camper rentals rApp.
 *
 * Peer-to-peer RV and camper rentals 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 { vnbSchema, vnbDocId } from './schemas';
import type {
	VnbDoc, Vehicle, TripWindow, RentalRequest, RentalMessage,
	Endorsement, SpaceConfig, EconomyModel, VehicleType, RentalStatus,
	MileagePolicy,
} from './schemas';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── Local-first helpers ──

function ensureDoc(space: string): VnbDoc {
	const docId = vnbDocId(space);
	let doc = _syncServer!.getDoc<VnbDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<VnbDoc>(), 'init vnb', (d) => {
			const init = vnbSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.config = init.config;
			d.vehicles = {};
			d.availability = {};
			d.rentals = {};
			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 vehicleToRow(v: Vehicle) {
	return {
		id: v.id,
		owner_did: v.ownerDid,
		owner_name: v.ownerName,
		title: v.title,
		description: v.description,
		type: v.type,
		economy: v.economy,
		year: v.year,
		make: v.make,
		model: v.model,
		length_feet: v.lengthFeet,
		sleeps: v.sleeps,
		fuel_type: v.fuelType,
		has_generator: v.hasGenerator,
		has_solar: v.hasSolar,
		has_ac: v.hasAC,
		has_heating: v.hasHeating,
		has_shower: v.hasShower,
		has_toilet: v.hasToilet,
		has_kitchen: v.hasKitchen,
		pet_friendly: v.petFriendly,
		tow_required: v.towRequired,
		mileage_policy: v.mileagePolicy,
		included_miles: v.includedMiles,
		per_mile_rate: v.perMileRate,
		suggested_amount: v.suggestedAmount,
		currency: v.currency,
		sliding_min: v.slidingMin,
		sliding_max: v.slidingMax,
		exchange_description: v.exchangeDescription,
		pickup_location_name: v.pickupLocationName,
		pickup_location_lat: v.pickupLocationLat,
		pickup_location_lng: v.pickupLocationLng,
		dropoff_same_as_pickup: v.dropoffSameAsPickup,
		dropoff_location_name: v.dropoffLocationName,
		dropoff_location_lat: v.dropoffLocationLat,
		dropoff_location_lng: v.dropoffLocationLng,
		photos: v.photos,
		cover_photo: v.coverPhoto,
		trust_threshold: v.trustThreshold,
		instant_accept: v.instantAccept,
		is_active: v.isActive,
		created_at: v.createdAt ? new Date(v.createdAt).toISOString() : null,
		updated_at: v.updatedAt ? new Date(v.updatedAt).toISOString() : null,
	};
}

function availabilityToRow(a: TripWindow) {
	return {
		id: a.id,
		vehicle_id: a.vehicleId,
		start_date: new Date(a.startDate).toISOString().split('T')[0],
		end_date: new Date(a.endDate).toISOString().split('T')[0],
		status: a.status,
		pickup_location_name: a.pickupLocationName,
		pickup_lat: a.pickupLat,
		pickup_lng: a.pickupLng,
		dropoff_location_name: a.dropoffLocationName,
		dropoff_lat: a.dropoffLat,
		dropoff_lng: a.dropoffLng,
		notes: a.notes,
		created_at: a.createdAt ? new Date(a.createdAt).toISOString() : null,
	};
}

function rentalToRow(r: RentalRequest) {
	return {
		id: r.id,
		vehicle_id: r.vehicleId,
		renter_did: r.renterDid,
		renter_name: r.renterName,
		owner_did: r.ownerDid,
		pickup_date: new Date(r.pickupDate).toISOString().split('T')[0],
		dropoff_date: new Date(r.dropoffDate).toISOString().split('T')[0],
		estimated_miles: r.estimatedMiles,
		requested_pickup_location: r.requestedPickupLocation,
		requested_pickup_lat: r.requestedPickupLat,
		requested_pickup_lng: r.requestedPickupLng,
		requested_dropoff_location: r.requestedDropoffLocation,
		requested_dropoff_lat: r.requestedDropoffLat,
		requested_dropoff_lng: r.requestedDropoffLng,
		status: r.status,
		messages: r.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: r.offeredAmount,
		offered_currency: r.offeredCurrency,
		offered_exchange: r.offeredExchange,
		requested_at: r.requestedAt ? new Date(r.requestedAt).toISOString() : null,
		responded_at: r.respondedAt ? new Date(r.respondedAt).toISOString() : null,
		completed_at: r.completedAt ? new Date(r.completedAt).toISOString() : null,
		cancelled_at: r.cancelledAt ? new Date(r.cancelledAt).toISOString() : null,
	};
}

function endorsementToRow(e: Endorsement) {
	return {
		id: e.id,
		rental_id: e.rentalId,
		vehicle_id: e.vehicleId,
		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 = vnbDocId(space);
	const doc = ensureDoc(space);
	if ((doc.meta as any)?.seeded || Object.keys(doc.vehicles).length > 0) return;

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

		// ── 4 Demo Vehicles ──

		const v1 = crypto.randomUUID();
		d.vehicles[v1] = {
			id: v1,
			ownerDid: 'did:key:z6MkVnbOwner1',
			ownerName: 'Dave S.',
			title: 'The Shaggin\' Wagon',
			description: 'A 2019 Sprinter camper van with a full kitchen, queen bed, and solar setup. We promise it\'s been cleaned since the festival.',
			type: 'camper_van',
			economy: 'suggested',
			year: 2019,
			make: 'Mercedes-Benz',
			model: 'Sprinter 144',
			lengthFeet: 19,
			sleeps: 2,
			fuelType: 'diesel',
			hasGenerator: false,
			hasSolar: true,
			hasAC: false,
			hasHeating: true,
			hasShower: true,
			hasToilet: false,
			hasKitchen: true,
			petFriendly: true,
			towRequired: false,
			mileagePolicy: 'included_miles',
			includedMiles: 150,
			perMileRate: 0.35,
			suggestedAmount: 120,
			currency: 'USD',
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			pickupLocationName: 'Portland, OR',
			pickupLocationLat: 45.5152,
			pickupLocationLng: -122.6784,
			dropoffSameAsPickup: true,
			dropoffLocationName: null,
			dropoffLocationLat: null,
			dropoffLocationLng: null,
			photos: [],
			coverPhoto: null,
			trustThreshold: 25,
			instantAccept: true,
			isActive: true,
			createdAt: now - 86400000 * 30,
			updatedAt: now - 86400000 * 2,
		};

		const v2 = crypto.randomUUID();
		d.vehicles[v2] = {
			id: v2,
			ownerDid: 'did:key:z6MkVnbOwner2',
			ownerName: 'Rosa M.',
			title: 'Dolly',
			description: 'A 1987 Airstream travel trailer. She\'s older than your marriage and twice as reliable. Recently restored interior, original exterior. Tow vehicle not included.',
			type: 'travel_trailer',
			economy: 'sliding_scale',
			year: 1987,
			make: 'Airstream',
			model: 'Excella 31',
			lengthFeet: 31,
			sleeps: 4,
			fuelType: null,
			hasGenerator: false,
			hasSolar: false,
			hasAC: true,
			hasHeating: true,
			hasShower: true,
			hasToilet: true,
			hasKitchen: true,
			petFriendly: false,
			towRequired: true,
			mileagePolicy: 'unlimited',
			includedMiles: null,
			perMileRate: null,
			suggestedAmount: 80,
			currency: 'USD',
			slidingMin: 40,
			slidingMax: 120,
			exchangeDescription: null,
			pickupLocationName: 'Asheville, NC',
			pickupLocationLat: 35.5951,
			pickupLocationLng: -82.5515,
			dropoffSameAsPickup: true,
			dropoffLocationName: null,
			dropoffLocationLat: null,
			dropoffLocationLng: null,
			photos: [],
			coverPhoto: null,
			trustThreshold: 35,
			instantAccept: false,
			isActive: true,
			createdAt: now - 86400000 * 60,
			updatedAt: now - 86400000 * 5,
		};

		const v3 = crypto.randomUUID();
		d.vehicles[v3] = {
			id: v3,
			ownerDid: 'did:key:z6MkVnbOwner3',
			ownerName: 'Mike & Jen T.',
			title: 'Big Bertha',
			description: 'A 2021 Class A motorhome with every amenity you can imagine. Technically a house. With wheels. And a generator. Sleeps 6 if you\'re friendly.',
			type: 'motorhome',
			economy: 'fixed',
			year: 2021,
			make: 'Thor',
			model: 'Palazzo 33.5',
			lengthFeet: 34,
			sleeps: 6,
			fuelType: 'diesel',
			hasGenerator: true,
			hasSolar: true,
			hasAC: true,
			hasHeating: true,
			hasShower: true,
			hasToilet: true,
			hasKitchen: true,
			petFriendly: true,
			towRequired: false,
			mileagePolicy: 'per_mile',
			includedMiles: null,
			perMileRate: 0.45,
			suggestedAmount: 200,
			currency: 'USD',
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			pickupLocationName: 'Austin, TX',
			pickupLocationLat: 30.2672,
			pickupLocationLng: -97.7431,
			dropoffSameAsPickup: false,
			dropoffLocationName: 'San Antonio, TX',
			dropoffLocationLat: 29.4241,
			dropoffLocationLng: -98.4936,
			photos: [],
			coverPhoto: null,
			trustThreshold: 50,
			instantAccept: false,
			isActive: true,
			createdAt: now - 86400000 * 15,
			updatedAt: now - 86400000 * 1,
		};

		const v4 = crypto.randomUUID();
		d.vehicles[v4] = {
			id: v4,
			ownerDid: 'did:key:z6MkVnbOwner4',
			ownerName: 'Sunny L.',
			title: 'The Skool Bus',
			description: 'A 2003 converted school bus with handmade everything. Compost toilet, rain catchment, woodburning stove. Still smells faintly of crayons. This is a feature.',
			type: 'skoolie',
			economy: 'gift',
			year: 2003,
			make: 'Blue Bird',
			model: 'All American RE',
			lengthFeet: 40,
			sleeps: 4,
			fuelType: 'diesel',
			hasGenerator: false,
			hasSolar: true,
			hasAC: false,
			hasHeating: true,
			hasShower: true,
			hasToilet: true,
			hasKitchen: true,
			petFriendly: true,
			towRequired: false,
			mileagePolicy: 'unlimited',
			includedMiles: null,
			perMileRate: null,
			suggestedAmount: null,
			currency: null,
			slidingMin: null,
			slidingMax: null,
			exchangeDescription: null,
			pickupLocationName: 'Taos, NM',
			pickupLocationLat: 36.4072,
			pickupLocationLng: -105.5731,
			dropoffSameAsPickup: true,
			dropoffLocationName: null,
			dropoffLocationLat: null,
			dropoffLocationLng: null,
			photos: [],
			coverPhoto: null,
			trustThreshold: 15,
			instantAccept: true,
			isActive: true,
			createdAt: now - 86400000 * 45,
			updatedAt: now - 86400000 * 3,
		};

		// ── Availability windows ──

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

		// Shaggin' Wagon — available next 2 months
		avail(v1, 0, 60);
		// Dolly — available next month, blocked for a wedding, then open
		avail(v2, 0, 25);
		avail(v2, 26, 33, 'blocked');
		avail(v2, 34, 90);
		// Big Bertha — weekends + some weeks
		avail(v3, 0, 120);
		// The Skool Bus — perpetually available (Sunny is generous)
		avail(v4, 0, 180);

		// ── Sample rental requests ──

		const r1 = crypto.randomUUID();
		d.rentals[r1] = {
			id: r1,
			vehicleId: v1,
			renterDid: 'did:key:z6MkVnbRenter1',
			renterName: 'Alex K.',
			ownerDid: 'did:key:z6MkVnbOwner1',
			pickupDate: daysFromNow(7),
			dropoffDate: daysFromNow(12),
			estimatedMiles: 600,
			requestedPickupLocation: 'Portland, OR',
			requestedPickupLat: 45.5152,
			requestedPickupLng: -122.6784,
			requestedDropoffLocation: null,
			requestedDropoffLat: null,
			requestedDropoffLng: null,
			status: 'accepted',
			messages: [
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkVnbRenter1',
					senderName: 'Alex K.',
					body: 'Hey Dave! Planning a trip down the Oregon coast. The Shaggin\' Wagon looks perfect. Promise to return it festival-free.',
					sentAt: now - 86400000 * 4,
				},
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkVnbOwner1',
					senderName: 'Dave S.',
					body: 'Auto-accepted! (Trust score 78.) She\'s all yours. Pro tip: the hot water takes 10 minutes to warm up and the passenger seat only reclines if you jiggle the lever.',
					sentAt: now - 86400000 * 4 + 3600000,
				},
			],
			offeredAmount: 120,
			offeredCurrency: 'USD',
			offeredExchange: null,
			requestedAt: now - 86400000 * 4,
			respondedAt: now - 86400000 * 4 + 3600000,
			completedAt: null,
			cancelledAt: null,
		};

		const r2 = crypto.randomUUID();
		d.rentals[r2] = {
			id: r2,
			vehicleId: v4,
			renterDid: 'did:key:z6MkVnbRenter2',
			renterName: 'Jordan W.',
			ownerDid: 'did:key:z6MkVnbOwner4',
			pickupDate: daysFromNow(-10),
			dropoffDate: daysFromNow(-3),
			estimatedMiles: 400,
			requestedPickupLocation: 'Taos, NM',
			requestedPickupLat: 36.4072,
			requestedPickupLng: -105.5731,
			requestedDropoffLocation: null,
			requestedDropoffLat: null,
			requestedDropoffLng: null,
			status: 'completed',
			messages: [
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkVnbRenter2',
					senderName: 'Jordan W.',
					body: 'Sunny! Can I take The Skool Bus to a gathering in the Gila? I\'ll bring it back with a full tank and good vibes.',
					sentAt: now - 86400000 * 14,
				},
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkVnbOwner4',
					senderName: 'Sunny L.',
					body: 'The bus is yours, friend. Fair warning: she doesn\'t go above 55 mph and the left blinker has a mind of its own. The crayons smell is free of charge.',
					sentAt: now - 86400000 * 13,
				},
			],
			offeredAmount: null,
			offeredCurrency: null,
			offeredExchange: null,
			requestedAt: now - 86400000 * 14,
			respondedAt: now - 86400000 * 13,
			completedAt: now - 86400000 * 3,
			cancelledAt: null,
		};

		const r3 = crypto.randomUUID();
		d.rentals[r3] = {
			id: r3,
			vehicleId: v3,
			renterDid: 'did:key:z6MkVnbRenter3',
			renterName: 'Sam C.',
			ownerDid: 'did:key:z6MkVnbOwner3',
			pickupDate: daysFromNow(14),
			dropoffDate: daysFromNow(21),
			estimatedMiles: 1200,
			requestedPickupLocation: 'Austin, TX',
			requestedPickupLat: 30.2672,
			requestedPickupLng: -97.7431,
			requestedDropoffLocation: 'San Antonio, TX',
			requestedDropoffLat: 29.4241,
			requestedDropoffLng: -98.4936,
			status: 'pending',
			messages: [
				{
					id: crypto.randomUUID(),
					senderDid: 'did:key:z6MkVnbRenter3',
					senderName: 'Sam C.',
					body: 'Hey Mike & Jen! Big Bertha looks like the perfect family road trip rig. We\'re taking the kids to Big Bend. Happy to pay the fixed rate + mileage.',
					sentAt: now - 86400000 * 1,
				},
			],
			offeredAmount: 200,
			offeredCurrency: 'USD',
			offeredExchange: null,
			requestedAt: now - 86400000 * 1,
			respondedAt: null,
			completedAt: null,
			cancelledAt: null,
		};

		// ── Sample endorsements ──

		const e1 = crypto.randomUUID();
		d.endorsements[e1] = {
			id: e1,
			rentalId: r2,
			vehicleId: v4,
			authorDid: 'did:key:z6MkVnbRenter2',
			authorName: 'Jordan W.',
			subjectDid: 'did:key:z6MkVnbOwner4',
			subjectName: 'Sunny L.',
			direction: 'renter_to_owner',
			body: 'The Skool Bus is a vibe. Sunny gave us the full tour, shared their secret hot springs map, and the woodburning stove kept us warm at 9,000 feet. Yes, it smells like crayons. Yes, that\'s a feature. 10/10 would crayon again.',
			rating: 5,
			tags: ['smells_like_adventure', 'felt_like_home', 'good_communication', 'smooth_handoff'],
			visibility: 'public',
			trustWeight: 0.85,
			createdAt: now - 86400000 * 2,
		};

		const e2 = crypto.randomUUID();
		d.endorsements[e2] = {
			id: e2,
			rentalId: r2,
			vehicleId: v4,
			authorDid: 'did:key:z6MkVnbOwner4',
			authorName: 'Sunny L.',
			subjectDid: 'did:key:z6MkVnbRenter2',
			subjectName: 'Jordan W.',
			direction: 'owner_to_renter',
			body: 'Jordan brought the bus back with a full tank, clean interior, and a jar of homemade salsa. The left blinker still has a mind of its own but that\'s not Jordan\'s fault. Welcome back anytime.',
			rating: 5,
			tags: ['reliable', 'suspiciously_clean', 'smooth_handoff'],
			visibility: 'public',
			trustWeight: 0.8,
			createdAt: now - 86400000 * 2,
		};
	});

	_syncServer!.changeDoc<VnbDoc>(docId, 'mark seeded', (d) => {
		if (d.meta) (d.meta as any).seeded = true;
	});
	console.log("[rVnb] Demo data seeded: 4 vehicles, 3 rental requests, 2 endorsements");
}

// ── API: Vehicles ──

routes.get("/api/vehicles", 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 vehicles = Object.values(doc.vehicles);

	if (type) vehicles = vehicles.filter(v => v.type === type);
	if (economy) vehicles = vehicles.filter(v => v.economy === economy);
	if (active !== undefined) vehicles = vehicles.filter(v => v.isActive === (active === 'true'));
	if (search) {
		const term = search.toLowerCase();
		vehicles = vehicles.filter(v =>
			v.title.toLowerCase().includes(term) ||
			v.description.toLowerCase().includes(term) ||
			v.pickupLocationName.toLowerCase().includes(term) ||
			(v.make || '').toLowerCase().includes(term) ||
			(v.model || '').toLowerCase().includes(term)
		);
	}

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

routes.post("/api/vehicles", 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 = vnbDocId(dataSpace);
	ensureDoc(dataSpace);
	const vehicleId = crypto.randomUUID();
	const now = Date.now();

	_syncServer!.changeDoc<VnbDoc>(docId, `create vehicle ${vehicleId}`, (d) => {
		d.vehicles[vehicleId] = {
			id: vehicleId,
			ownerDid: body.owner_did || '',
			ownerName: body.owner_name || 'Anonymous',
			title: body.title.trim(),
			description: body.description || '',
			type: body.type || 'camper_van',
			economy: body.economy || d.config.defaultEconomy,
			year: body.year ?? null,
			make: body.make ?? null,
			model: body.model ?? null,
			lengthFeet: body.length_feet ?? null,
			sleeps: body.sleeps || 2,
			fuelType: body.fuel_type ?? null,
			hasGenerator: body.has_generator ?? false,
			hasSolar: body.has_solar ?? false,
			hasAC: body.has_ac ?? false,
			hasHeating: body.has_heating ?? false,
			hasShower: body.has_shower ?? false,
			hasToilet: body.has_toilet ?? false,
			hasKitchen: body.has_kitchen ?? false,
			petFriendly: body.pet_friendly ?? false,
			towRequired: body.tow_required ?? false,
			mileagePolicy: body.mileage_policy || 'unlimited',
			includedMiles: body.included_miles ?? null,
			perMileRate: body.per_mile_rate ?? null,
			suggestedAmount: body.suggested_amount ?? null,
			currency: body.currency ?? null,
			slidingMin: body.sliding_min ?? null,
			slidingMax: body.sliding_max ?? null,
			exchangeDescription: body.exchange_description ?? null,
			pickupLocationName: body.pickup_location_name || '',
			pickupLocationLat: body.pickup_location_lat ?? null,
			pickupLocationLng: body.pickup_location_lng ?? null,
			dropoffSameAsPickup: body.dropoff_same_as_pickup ?? true,
			dropoffLocationName: body.dropoff_location_name ?? null,
			dropoffLocationLat: body.dropoff_location_lat ?? null,
			dropoffLocationLng: body.dropoff_location_lng ?? null,
			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<VnbDoc>(docId)!;
	return c.json(vehicleToRow(updated.vehicles[vehicleId]), 201);
});

routes.get("/api/vehicles/: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 vehicle = doc.vehicles[id];
	if (!vehicle) return c.json({ error: "Vehicle not found" }, 404);
	return c.json(vehicleToRow(vehicle));
});

routes.patch("/api/vehicles/: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 = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.vehicles[id]) return c.json({ error: "Not found" }, 404);

	const fieldMap: Record<string, keyof Vehicle> = {
		title: 'title', description: 'description', type: 'type', economy: 'economy',
		year: 'year', make: 'make', model: 'model', length_feet: 'lengthFeet',
		sleeps: 'sleeps', fuel_type: 'fuelType',
		has_generator: 'hasGenerator', has_solar: 'hasSolar', has_ac: 'hasAC',
		has_heating: 'hasHeating', has_shower: 'hasShower', has_toilet: 'hasToilet',
		has_kitchen: 'hasKitchen', pet_friendly: 'petFriendly', tow_required: 'towRequired',
		mileage_policy: 'mileagePolicy', included_miles: 'includedMiles', per_mile_rate: 'perMileRate',
		suggested_amount: 'suggestedAmount', currency: 'currency',
		sliding_min: 'slidingMin', sliding_max: 'slidingMax',
		exchange_description: 'exchangeDescription',
		pickup_location_name: 'pickupLocationName', pickup_location_lat: 'pickupLocationLat',
		pickup_location_lng: 'pickupLocationLng',
		dropoff_same_as_pickup: 'dropoffSameAsPickup',
		dropoff_location_name: 'dropoffLocationName', dropoff_location_lat: 'dropoffLocationLat',
		dropoff_location_lng: 'dropoffLocationLng',
		photos: 'photos', cover_photo: 'coverPhoto',
		trust_threshold: 'trustThreshold', instant_accept: 'instantAccept',
		is_active: 'isActive',
	};

	const updates: Array<{ field: keyof Vehicle; 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<VnbDoc>(docId, `update vehicle ${id}`, (d) => {
		const v = d.vehicles[id];
		for (const { field, value } of updates) {
			(v as any)[field] = value;
		}
		v.updatedAt = Date.now();
	});

	const updated = _syncServer!.getDoc<VnbDoc>(docId)!;
	return c.json(vehicleToRow(updated.vehicles[id]));
});

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

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

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

// ── API: Availability ──

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

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

	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/vehicles/: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 vehicleId = 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 = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.vehicles[vehicleId]) return c.json({ error: "Vehicle not found" }, 404);

	const awId = crypto.randomUUID();

	_syncServer!.changeDoc<VnbDoc>(docId, `add availability ${awId}`, (d) => {
		d.availability[awId] = {
			id: awId,
			vehicleId,
			startDate: new Date(body.start_date).getTime(),
			endDate: new Date(body.end_date).getTime(),
			status: body.status || 'available',
			pickupLocationName: body.pickup_location_name ?? null,
			pickupLat: body.pickup_lat ?? null,
			pickupLng: body.pickup_lng ?? null,
			dropoffLocationName: body.dropoff_location_name ?? null,
			dropoffLat: body.dropoff_lat ?? null,
			dropoffLng: body.dropoff_lng ?? null,
			notes: body.notes ?? null,
			createdAt: Date.now(),
		};
	});

	const updated = _syncServer!.getDoc<VnbDoc>(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 = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.availability[id]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<VnbDoc>(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.pickup_location_name !== undefined) aw.pickupLocationName = body.pickup_location_name;
		if (body.pickup_lat !== undefined) aw.pickupLat = body.pickup_lat;
		if (body.pickup_lng !== undefined) aw.pickupLng = body.pickup_lng;
		if (body.dropoff_location_name !== undefined) aw.dropoffLocationName = body.dropoff_location_name;
		if (body.dropoff_lat !== undefined) aw.dropoffLat = body.dropoff_lat;
		if (body.dropoff_lng !== undefined) aw.dropoffLng = body.dropoff_lng;
		if (body.notes !== undefined) aw.notes = body.notes;
	});

	const updated = _syncServer!.getDoc<VnbDoc>(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 = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.availability[id]) return c.json({ error: "Not found" }, 404);

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

// ── API: Rentals ──

routes.get("/api/rentals", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const { vehicle_id, status, renter_did, owner_did } = c.req.query();

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

	if (vehicle_id) rentals = rentals.filter(r => r.vehicleId === vehicle_id);
	if (status) rentals = rentals.filter(r => r.status === status);
	if (renter_did) rentals = rentals.filter(r => r.renterDid === renter_did);
	if (owner_did) rentals = rentals.filter(r => r.ownerDid === owner_did);

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

routes.post("/api/rentals", 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.vehicle_id || !body.pickup_date || !body.dropoff_date) {
		return c.json({ error: "vehicle_id, pickup_date, and dropoff_date required" }, 400);
	}

	const docId = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	const vehicle = doc.vehicles[body.vehicle_id];
	if (!vehicle) return c.json({ error: "Vehicle not found" }, 404);

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

	// Check trust threshold for auto-accept
	const renterTrustScore = body.renter_trust_score ?? 0;
	const autoAccept = vehicle.instantAccept && vehicle.trustThreshold !== null
		&& renterTrustScore >= vehicle.trustThreshold;

	const initialMessage: RentalMessage | null = body.message ? {
		id: crypto.randomUUID(),
		senderDid: body.renter_did || '',
		senderName: body.renter_name || 'Renter',
		body: body.message,
		sentAt: now,
	} : null;

	_syncServer!.changeDoc<VnbDoc>(docId, `create rental ${rentalId}`, (d) => {
		d.rentals[rentalId] = {
			id: rentalId,
			vehicleId: body.vehicle_id,
			renterDid: body.renter_did || '',
			renterName: body.renter_name || 'Renter',
			ownerDid: vehicle.ownerDid,
			pickupDate: new Date(body.pickup_date).getTime(),
			dropoffDate: new Date(body.dropoff_date).getTime(),
			estimatedMiles: body.estimated_miles ?? null,
			requestedPickupLocation: body.requested_pickup_location ?? null,
			requestedPickupLat: body.requested_pickup_lat ?? null,
			requestedPickupLng: body.requested_pickup_lng ?? null,
			requestedDropoffLocation: body.requested_dropoff_location ?? null,
			requestedDropoffLat: body.requested_dropoff_lat ?? null,
			requestedDropoffLng: body.requested_dropoff_lng ?? null,
			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<VnbDoc>(docId)!;
	return c.json(rentalToRow(updated.rentals[rentalId]), 201);
});

routes.get("/api/rentals/: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 rental = doc.rentals[id];
	if (!rental) return c.json({ error: "Rental not found" }, 404);
	return c.json(rentalToRow(rental));
});

// ── Rental status transitions ──

function rentalTransition(statusTarget: RentalStatus, 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 = vnbDocId(dataSpace);
		const doc = ensureDoc(dataSpace);
		if (!doc.rentals[id]) return c.json({ error: "Not found" }, 404);

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

		const updated = _syncServer!.getDoc<VnbDoc>(docId)!;
		return c.json(rentalToRow(updated.rentals[id]));
	};
}

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

// ── Rental messages ──

routes.post("/api/rentals/: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 = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	if (!doc.rentals[id]) return c.json({ error: "Rental not found" }, 404);

	const msgId = crypto.randomUUID();

	_syncServer!.changeDoc<VnbDoc>(docId, `add message to rental ${id}`, (d) => {
		d.rentals[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<VnbDoc>(docId)!;
	return c.json(rentalToRow(updated.rentals[id]));
});

// ── API: Endorsements ──

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

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

	if (vehicle_id) endorsements = endorsements.filter(e => e.vehicleId === vehicle_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.rental_id || !body.body?.trim()) {
		return c.json({ error: "rental_id and body required" }, 400);
	}

	const docId = vnbDocId(dataSpace);
	const doc = ensureDoc(dataSpace);
	const rental = doc.rentals[body.rental_id];
	if (!rental) return c.json({ error: "Rental not found" }, 404);
	if (rental.status !== 'completed' && rental.status !== 'endorsed') {
		return c.json({ error: "Can only endorse completed rentals" }, 400);
	}

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

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

		// Update rental status to endorsed
		if (d.rentals[body.rental_id].status === 'completed') {
			d.rentals[body.rental_id].status = 'endorsed';
		}
	});

	const updated = _syncServer!.getDoc<VnbDoc>(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 asOwner = endorsements.filter(e => e.direction === 'renter_to_owner');
	const asRenter = endorsements.filter(e => e.direction === 'owner_to_renter');

	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_owner: {
			count: asOwner.length,
			avg_rating: avgRating(asOwner),
			tags: tagCounts(asOwner),
			avg_trust_weight: asOwner.length ? asOwner.reduce((s, e) => s + e.trustWeight, 0) / asOwner.length : 0,
		},
		as_renter: {
			count: asRenter.length,
			avg_rating: avgRating(asRenter),
			tags: tagCounts(asRenter),
			avg_trust_weight: asRenter.length ? asRenter.reduce((s, e) => s + e.trustWeight, 0) / asRenter.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, sleeps, lat, lng, radius, pickup_date, dropoff_date } = c.req.query();

	const doc = ensureDoc(dataSpace);
	let vehicles = Object.values(doc.vehicles).filter(v => v.isActive);

	// Text search
	if (q) {
		const term = q.toLowerCase();
		vehicles = vehicles.filter(v =>
			v.title.toLowerCase().includes(term) ||
			v.description.toLowerCase().includes(term) ||
			v.pickupLocationName.toLowerCase().includes(term) ||
			v.ownerName.toLowerCase().includes(term) ||
			(v.make || '').toLowerCase().includes(term) ||
			(v.model || '').toLowerCase().includes(term)
		);
	}

	if (type) vehicles = vehicles.filter(v => v.type === type);
	if (economy) vehicles = vehicles.filter(v => v.economy === economy);

	if (sleeps) {
		const s = parseInt(sleeps);
		vehicles = vehicles.filter(v => v.sleeps >= s);
	}

	// Location proximity
	if (lat && lng && radius) {
		const cLat = parseFloat(lat);
		const cLng = parseFloat(lng);
		const r = parseFloat(radius);
		vehicles = vehicles.filter(v => {
			if (!v.pickupLocationLat || !v.pickupLocationLng) return false;
			const dLat = (v.pickupLocationLat - cLat) * Math.PI / 180;
			const dLng = (v.pickupLocationLng - cLng) * Math.PI / 180;
			const a = Math.sin(dLat / 2) ** 2 +
				Math.cos(cLat * Math.PI / 180) * Math.cos(v.pickupLocationLat * 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 (pickup_date && dropoff_date) {
		const puMs = new Date(pickup_date).getTime();
		const doMs = new Date(dropoff_date).getTime();
		const availability = Object.values(doc.availability);
		vehicles = vehicles.filter(v => {
			const windows = availability.filter(a =>
				a.vehicleId === v.id && a.status === 'available'
			);
			return windows.some(w => w.startDate <= puMs && w.endDate >= doMs);
		});
	}

	const rows = vehicles.map(vehicleToRow);
	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 vehicles = Object.values(doc.vehicles);
	const rentals = Object.values(doc.rentals);
	const endorsements = Object.values(doc.endorsements);

	return c.json({
		vehicles: vehicles.length,
		active_vehicles: vehicles.filter(v => v.isActive).length,
		rentals: rentals.length,
		pending_rentals: rentals.filter(r => r.status === 'pending').length,
		completed_rentals: rentals.filter(r => r.status === 'completed' || r.status === 'endorsed').length,
		endorsements: endorsements.length,
		economy_breakdown: {
			gift: vehicles.filter(v => v.economy === 'gift').length,
			suggested: vehicles.filter(v => v.economy === 'suggested').length,
			fixed: vehicles.filter(v => v.economy === 'fixed').length,
			sliding_scale: vehicles.filter(v => v.economy === 'sliding_scale').length,
			exchange: vehicles.filter(v => v.economy === 'exchange').length,
		},
		type_breakdown: {
			motorhome: vehicles.filter(v => v.type === 'motorhome').length,
			camper_van: vehicles.filter(v => v.type === 'camper_van').length,
			travel_trailer: vehicles.filter(v => v.type === 'travel_trailer').length,
			truck_camper: vehicles.filter(v => v.type === 'truck_camper').length,
			skoolie: vehicles.filter(v => v.type === 'skoolie').length,
			other: vehicles.filter(v => v.type === 'other').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 = vnbDocId(dataSpace);
	ensureDoc(dataSpace);

	_syncServer!.changeDoc<VnbDoc>(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.endorsement_tag_catalog) d.config.endorsementTagCatalog = body.endorsement_tag_catalog;
		if (body.require_endorsement !== undefined) d.config.requireEndorsement = body.require_endorsement;
		if (body.max_rental_days !== undefined) d.config.maxRentalDays = body.max_rental_days;
	});

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

// ── Page route ──

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

// ── MI export ──

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

// ── Module export ──

export const vnbModule: RSpaceModule = {
	id: "rvnb",
	name: "rVnb",
	icon: "\u{1F690}",
	description: "Can't afford a house? Live in a van down by the river. Camper rental, lending & parking.",
	scoping: { defaultScope: 'space', userConfigurable: true },
	docSchemas: [{ pattern: '{space}:vnb:vehicles', description: 'Vehicles, rentals, endorsements', init: vnbSchema.init }],
	routes,
	landingPage: renderLanding,
	seedTemplate: seedDemoIfEmpty,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		seedDemoIfEmpty("demo");
	},
	feeds: [
		{
			id: "vehicles",
			name: "Vehicles",
			kind: "data",
			description: "RV and camper listings with specs, location, and mileage policy",
			filterable: true,
		},
		{
			id: "rentals",
			name: "Rentals",
			kind: "data",
			description: "Rental requests and their status (pending, accepted, completed)",
		},
		{
			id: "endorsements",
			name: "Endorsements",
			kind: "trust",
			description: "Trust-based endorsements from completed trips, feeds rNetwork",
		},
		{
			id: "rental-value",
			name: "Rental Value",
			kind: "economic",
			description: "Economic value of rentals (contributions, exchanges, gifts)",
		},
	],
	acceptsFeeds: ["data", "trust", "economic"],
	outputPaths: [
		{ path: "vehicles", name: "Vehicles", icon: "\u{1F690}", description: "Community vehicle listings" },
		{ path: "rentals", name: "Rentals", icon: "\u{1F697}", description: "Rental requests and history" },
		{ path: "endorsements", name: "Endorsements", icon: "\u{2B50}", description: "Trust endorsements from trips" },
	],
	onboardingActions: [
		{ label: "Add a Vehicle", icon: "🚗", description: "Share a vehicle with your community", type: 'create', href: '/{space}/rvnb' },
	],
};
