/**
 * Cal module — temporal coordination calendar.
 *
 * Group calendars with lunar/solar/seasonal time systems,
 * location-aware events, and temporal-spatial zoom coupling.
 */

import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";

const routes = new Hono();

// ── DB initialization ──
const SCHEMA_SQL = readFileSync(resolve(import.meta.dir, "db/schema.sql"), "utf-8");

async function initDB() {
	try {
		await sql.unsafe(SCHEMA_SQL);
		console.log("[Cal] DB schema initialized");
	} catch (e) {
		console.error("[Cal] DB init error:", e);
	}
}

async function seedDemoIfEmpty() {
	try {
		const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rcal.events");
		if (parseInt(count[0].cnt) > 0) return;

		// Create calendar sources
		const community = await sql.unsafe(
			`INSERT INTO rcal.calendar_sources (name, source_type, color, is_active, is_visible)
			 VALUES ('Community Events', 'MANUAL', '#6366f1', true, true) RETURNING id`
		);
		const sprints = await sql.unsafe(
			`INSERT INTO rcal.calendar_sources (name, source_type, color, is_active, is_visible)
			 VALUES ('Development Sprints', 'MANUAL', '#f59e0b', true, true) RETURNING id`
		);
		const communityId = community[0].id;
		const sprintsId = sprints[0].id;

		// Create location hierarchy
		const world = await sql.unsafe(
			`INSERT INTO rcal.locations (name, granularity) VALUES ('Earth', 1) RETURNING id`
		);
		const europe = await sql.unsafe(
			`INSERT INTO rcal.locations (name, granularity, parent_id, lat, lng) VALUES ('Europe', 2, $1, 48.8566, 2.3522) RETURNING id`,
			[world[0].id]
		);
		const berlin = await sql.unsafe(
			`INSERT INTO rcal.locations (name, granularity, parent_id, lat, lng) VALUES ('Berlin', 4, $1, 52.52, 13.405) RETURNING id`,
			[europe[0].id]
		);

		// Seed events — past, current week, and future
		const now = new Date();
		const events = [
			{
				title: "rSpace Launch Party",
				desc: "Celebrating the launch of the unified rSpace platform with all 22 modules live.",
				start: daysFromNow(-21, 18, 0), end: daysFromNow(-21, 22, 0),
				sourceId: communityId, locationName: "Radiant Hall, Pittsburgh",
			},
			{
				title: "Provider Onboarding Workshop",
				desc: "Hands-on session for print providers joining the cosmolocal network.",
				start: daysFromNow(-12, 14, 0), end: daysFromNow(-12, 17, 0),
				sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rspace-providers", virtualPlatform: "Jitsi",
			},
			{
				title: "Weekly Community Standup",
				desc: "Open standup — share what you're working on, ask for help, coordinate.",
				start: daysFromNow(0, 16, 0), end: daysFromNow(0, 16, 45),
				sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rspace-standup", virtualPlatform: "Jitsi",
			},
			{
				title: "Sprint: Module Seeding & Polish",
				desc: "Focus sprint on populating demo data and improving UX across all modules.",
				start: daysFromNow(0, 9, 0), end: daysFromNow(5, 18, 0),
				sourceId: sprintsId, allDay: true,
			},
			{
				title: "rFunds Budget Review",
				desc: "Quarterly review of treasury flows, enoughness thresholds, and overflow routing.",
				start: daysFromNow(6, 15, 0), end: daysFromNow(6, 17, 0),
				sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rfunds-review", virtualPlatform: "Jitsi",
			},
			{
				title: "Cosmolocal Design Sprint",
				desc: "Two-day design sprint on the next generation of cosmolocal tooling.",
				start: daysFromNow(11, 9, 0), end: daysFromNow(12, 18, 0),
				sourceId: sprintsId, locationId: berlin[0].id, locationName: "Druckwerkstatt Berlin",
			},
			{
				title: "Q1 Retrospective",
				desc: "Looking back at what we built, what worked, and what to improve.",
				start: daysFromNow(21, 16, 0), end: daysFromNow(21, 18, 0),
				sourceId: communityId, virtual: true, virtualUrl: "https://meet.jit.si/rspace-retro", virtualPlatform: "Jitsi",
			},
		];

		for (const e of events) {
			await sql.unsafe(
				`INSERT INTO rcal.events (title, description, start_time, end_time, all_day, source_id,
				 location_id, location_name, is_virtual, virtual_url, virtual_platform)
				 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
				[e.title, e.desc, e.start.toISOString(), e.end.toISOString(), e.allDay || false,
				 e.sourceId, e.locationId || null, e.locationName || null,
				 e.virtual || false, e.virtualUrl || null, e.virtualPlatform || null]
			);
		}

		console.log("[Cal] Demo data seeded: 2 sources, 3 locations, 7 events");
	} catch (e) {
		console.error("[Cal] Seed error:", e);
	}
}

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

initDB().then(seedDemoIfEmpty);

// ── API: Events ──

// GET /api/events — query events with filters
routes.get("/api/events", async (c) => {
	const { start, end, source, search, rTool, rEntityId, upcoming } = c.req.query();

	let where = "WHERE 1=1";
	const params: any[] = [];
	let idx = 1;

	if (start) { where += ` AND e.start_time >= $${idx}`; params.push(start); idx++; }
	if (end) { where += ` AND e.start_time <= ($${idx}::date + interval '1 day')`; params.push(end); idx++; }
	if (source) { where += ` AND e.source_id = $${idx}`; params.push(source); idx++; }
	if (search) { where += ` AND (e.title ILIKE $${idx} OR e.description ILIKE $${idx})`; params.push(`%${search}%`); idx++; }
	if (rTool) { where += ` AND e.r_tool_source = $${idx}`; params.push(rTool); idx++; }
	if (rEntityId) { where += ` AND e.r_tool_entity_id = $${idx}`; params.push(rEntityId); idx++; }
	if (upcoming) {
		where += ` AND e.start_time >= NOW() AND e.start_time <= NOW() + ($${idx} || ' days')::interval`;
		params.push(upcoming);
		idx++;
	}

	const rows = await sql.unsafe(
		`SELECT e.*, cs.name as source_name, cs.color as source_color, l.name as location_label
		 FROM rcal.events e
		 LEFT JOIN rcal.calendar_sources cs ON cs.id = e.source_id
		 LEFT JOIN rcal.locations l ON l.id = e.location_id
		 ${where}
		 ORDER BY e.start_time ASC LIMIT 500`,
		params
	);
	return c.json({ count: rows.length, results: rows });
});

// POST /api/events — create event
routes.post("/api/events", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json();
	const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name,
		is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id } = body;
	if (!title?.trim() || !start_time) return c.json({ error: "Title and start_time required" }, 400);

	const rows = await sql.unsafe(
		`INSERT INTO rcal.events (title, description, start_time, end_time, all_day, timezone, source_id,
		 location_id, location_name, is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id, created_by)
		 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING *`,
		[title.trim(), description || null, start_time, end_time || null, all_day || false, timezone || "UTC",
		 source_id || null, location_id || null, location_name || null, is_virtual || false,
		 virtual_url || null, virtual_platform || null, r_tool_source || null, r_tool_entity_id || null, claims.sub]
	);
	return c.json(rows[0], 201);
});

// GET /api/events/:id
routes.get("/api/events/:id", async (c) => {
	const rows = await sql.unsafe(
		`SELECT e.*, cs.name as source_name, cs.color as source_color
		 FROM rcal.events e LEFT JOIN rcal.calendar_sources cs ON cs.id = e.source_id
		 WHERE e.id = $1`,
		[c.req.param("id")]
	);
	if (rows.length === 0) return c.json({ error: "Event not found" }, 404);
	return c.json(rows[0]);
});

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

	const id = c.req.param("id");
	const body = await c.req.json();

	const fields: string[] = [];
	const params: any[] = [];
	let idx = 1;
	const allowed = ["title", "description", "start_time", "end_time", "all_day", "timezone",
		"status", "visibility", "location_name", "is_virtual", "virtual_url"];

	for (const key of allowed) {
		if (body[key] !== undefined) {
			fields.push(`${key} = $${idx}`);
			params.push(body[key]);
			idx++;
		}
	}

	if (fields.length === 0) return c.json({ error: "No fields" }, 400);
	fields.push("updated_at = NOW()");
	params.push(id);

	const rows = await sql.unsafe(
		`UPDATE rcal.events SET ${fields.join(", ")} WHERE id = $${idx} RETURNING *`,
		params
	);
	if (rows.length === 0) return c.json({ error: "Not found" }, 404);
	return c.json(rows[0]);
});

// DELETE /api/events/:id
routes.delete("/api/events/:id", async (c) => {
	const result = await sql.unsafe("DELETE FROM rcal.events WHERE id = $1 RETURNING id", [c.req.param("id")]);
	if (result.length === 0) return c.json({ error: "Not found" }, 404);
	return c.json({ ok: true });
});

// ── API: Sources ──

routes.get("/api/sources", async (c) => {
	const { is_active, is_visible, source_type } = c.req.query();
	let where = "WHERE 1=1";
	const params: any[] = [];
	let idx = 1;

	if (is_active !== undefined) { where += ` AND is_active = $${idx}`; params.push(is_active === "true"); idx++; }
	if (is_visible !== undefined) { where += ` AND is_visible = $${idx}`; params.push(is_visible === "true"); idx++; }
	if (source_type) { where += ` AND source_type = $${idx}`; params.push(source_type); idx++; }

	const rows = await sql.unsafe(`SELECT * FROM rcal.calendar_sources ${where} ORDER BY name`, params);
	return c.json({ count: rows.length, results: rows });
});

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

	const body = await c.req.json();
	const rows = await sql.unsafe(
		`INSERT INTO rcal.calendar_sources (name, source_type, url, color, is_active, is_visible)
		 VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`,
		[body.name, body.source_type || "MANUAL", body.url || null, body.color || "#6366f1",
		 body.is_active ?? true, body.is_visible ?? true]
	);
	return c.json(rows[0], 201);
});

// ── API: Locations ──

routes.get("/api/locations", async (c) => {
	const { granularity, parent, search, root } = c.req.query();
	let where = "WHERE 1=1";
	const params: any[] = [];
	let idx = 1;

	if (root === "true") { where += " AND parent_id IS NULL"; }
	if (granularity) { where += ` AND granularity = $${idx}`; params.push(parseInt(granularity)); idx++; }
	if (parent) { where += ` AND parent_id = $${idx}`; params.push(parent); idx++; }
	if (search) { where += ` AND name ILIKE $${idx}`; params.push(`%${search}%`); idx++; }

	const rows = await sql.unsafe(`SELECT * FROM rcal.locations ${where} ORDER BY name`, params);
	return c.json(rows);
});

routes.get("/api/locations/tree", async (c) => {
	const rows = await sql.unsafe(
		`WITH RECURSIVE tree AS (
			SELECT id, name, granularity, parent_id, 0 as depth FROM rcal.locations WHERE parent_id IS NULL
			UNION ALL
			SELECT l.id, l.name, l.granularity, l.parent_id, t.depth + 1
			FROM rcal.locations l JOIN tree t ON l.parent_id = t.id
		)
		SELECT * FROM tree ORDER BY depth, name`
	);
	return c.json(rows);
});

// ── API: Lunar data (computed, not stored) ──

routes.get("/api/lunar", async (c) => {
	const { start, end } = c.req.query();
	if (!start || !end) return c.json({ error: "start and end required" }, 400);

	// Simple lunar phase approximation based on synodic month
	const SYNODIC_MONTH = 29.53059;
	const KNOWN_NEW_MOON = new Date("2024-01-11T11:57:00Z").getTime();
	const phases: Record<string, { phase: string; illumination: number }> = {};

	const startDate = new Date(start);
	const endDate = new Date(end);
	const current = new Date(startDate);

	while (current <= endDate) {
		const daysSinceNewMoon = (current.getTime() - KNOWN_NEW_MOON) / (1000 * 60 * 60 * 24);
		const lunation = ((daysSinceNewMoon % SYNODIC_MONTH) + SYNODIC_MONTH) % SYNODIC_MONTH;
		const fraction = lunation / SYNODIC_MONTH;
		const illumination = 0.5 * (1 - Math.cos(2 * Math.PI * fraction));

		let phase = "waxing_crescent";
		if (fraction < 0.0625) phase = "new_moon";
		else if (fraction < 0.1875) phase = "waxing_crescent";
		else if (fraction < 0.3125) phase = "first_quarter";
		else if (fraction < 0.4375) phase = "waxing_gibbous";
		else if (fraction < 0.5625) phase = "full_moon";
		else if (fraction < 0.6875) phase = "waning_gibbous";
		else if (fraction < 0.8125) phase = "last_quarter";
		else if (fraction < 0.9375) phase = "waning_crescent";
		else phase = "new_moon";

		phases[current.toISOString().split("T")[0]] = { phase, illumination: Math.round(illumination * 100) / 100 };
		current.setDate(current.getDate() + 1);
	}

	return c.json(phases);
});

// ── API: Stats ──

routes.get("/api/stats", async (c) => {
	const [eventCount, sourceCount, locationCount] = await Promise.all([
		sql.unsafe("SELECT count(*)::int as cnt FROM rcal.events"),
		sql.unsafe("SELECT count(*)::int as cnt FROM rcal.calendar_sources WHERE is_active = true"),
		sql.unsafe("SELECT count(*)::int as cnt FROM rcal.locations"),
	]);
	return c.json({
		events: eventCount[0]?.cnt || 0,
		sources: sourceCount[0]?.cnt || 0,
		locations: locationCount[0]?.cnt || 0,
	});
});

// ── API: Context (r* tool bridge) ──

routes.get("/api/context/:tool", async (c) => {
	const tool = c.req.param("tool");
	const entityId = c.req.query("entityId");
	if (!entityId) return c.json({ error: "entityId required" }, 400);

	const rows = await sql.unsafe(
		"SELECT * FROM rcal.events WHERE r_tool_source = $1 AND r_tool_entity_id = $2 ORDER BY start_time",
		[tool, entityId]
	);
	return c.json({ count: rows.length, results: rows });
});

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

export const calModule: RSpaceModule = {
	id: "rcal",
	name: "rCal",
	icon: "📅",
	description: "Temporal coordination calendar with lunar, solar, and seasonal systems",
	scoping: { defaultScope: 'global', userConfigurable: true },
	routes,
	standaloneDomain: "rcal.online",
	landingPage: renderLanding,
	feeds: [
		{
			id: "events",
			name: "Events",
			kind: "data",
			description: "Calendar events with times, locations, and virtual meeting links",
			filterable: true,
		},
		{
			id: "availability",
			name: "Availability",
			kind: "data",
			description: "Free/busy windows and scheduling availability across calendars",
		},
	],
	acceptsFeeds: ["data", "governance"],
	outputPaths: [
		{ path: "saved-views", name: "Saved Views", icon: "👁️", description: "Custom calendar views and filters" },
		{ path: "events", name: "Events", icon: "📅", description: "Calendar events across all systems" },
	],
};
