/**
 * Tube module — community video hosting & live streaming.
 *
 * Video library (Cloudflare R2 bucket), HLS live streaming,
 * and RTMP ingest support. No database — metadata from S3 listing.
 */

import { Hono } from "hono";
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 * as Automerge from "@automerge/automerge";
import type { SyncServer } from '../../server/local-first/sync-server';
import { tubeSchema, tubeDocId } from './schemas';
import type { TubeDoc } from './schemas';
import { S3Client, ListObjectsV2Command, GetObjectCommand, HeadObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";

let _syncServer: SyncServer | null = null;

function ensureTubeDoc(space: string): TubeDoc {
    const docId = tubeDocId(space);
    let doc = _syncServer!.getDoc<TubeDoc>(docId);
    if (!doc) {
        doc = Automerge.change(Automerge.init<TubeDoc>(), 'init tube', (d) => {
            const init = tubeSchema.init();
            Object.assign(d, init);
            d.meta.spaceSlug = space;
        });
        _syncServer!.setDoc(docId, doc);
    }
    return doc;
}

const routes = new Hono();

// ── 360split config ──
const SPLIT_360_URL = process.env.SPLIT_360_URL || "";

// ── R2 / S3 config ──
const R2_ENDPOINT = process.env.R2_ENDPOINT || "";
const R2_BUCKET = process.env.R2_BUCKET || "rtube-videos";
const R2_ACCESS_KEY_ID = process.env.R2_ACCESS_KEY_ID || "";
const R2_SECRET_ACCESS_KEY = process.env.R2_SECRET_ACCESS_KEY || "";
const R2_PUBLIC_URL = process.env.R2_PUBLIC_URL || "";

const VIDEO_EXTENSIONS = new Set([
	".mp4", ".mkv", ".webm", ".mov", ".avi", ".wmv", ".flv", ".m4v",
]);

// ── S3 Client (lazy init) ──
let s3: S3Client | null = null;
function getS3(): S3Client | null {
	if (!R2_ENDPOINT) return null;
	if (!s3) {
		s3 = new S3Client({
			region: "auto",
			endpoint: R2_ENDPOINT,
			credentials: {
				accessKeyId: R2_ACCESS_KEY_ID,
				secretAccessKey: R2_SECRET_ACCESS_KEY,
			},
		});
	}
	return s3;
}

// ── S3-compatible listing ──
async function listVideos(): Promise<Array<{ name: string; size: number; lastModified?: string }>> {
	const client = getS3();
	if (!client) {
		// Return demo data when R2 not configured
		return [
			{ name: "welcome-to-rtube.mp4", size: 12_500_000 },
			{ name: "community-meeting-2026.mp4", size: 85_000_000 },
			{ name: "workshop-recording.webm", size: 45_000_000 },
		];
	}

	try {
		const command = new ListObjectsV2Command({ Bucket: R2_BUCKET, MaxKeys: 200 });
		const response = await client.send(command);
		const items: Array<{ name: string; size: number; lastModified?: string }> = [];

		for (const obj of response.Contents || []) {
			if (!obj.Key) continue;
			const ext = obj.Key.substring(obj.Key.lastIndexOf(".")).toLowerCase();
			if (VIDEO_EXTENSIONS.has(ext)) {
				items.push({
					name: obj.Key,
					size: obj.Size || 0,
					lastModified: obj.LastModified?.toISOString(),
				});
			}
		}
		return items.sort((a, b) => a.name.localeCompare(b.name));
	} catch (e) {
		console.error("[Tube] S3 list error:", e);
		return [];
	}
}

// ── API routes ──

// GET /api/videos — list videos from bucket
routes.get("/api/videos", async (c) => {
	const videos = await listVideos();
	return c.json({ videos, r2Configured: !!R2_ENDPOINT });
});

// GET /api/v/:path — video streaming with HTTP range request support
routes.get("/api/v/*", async (c) => {
	const client = getS3();
	if (!client) return c.json({ error: "R2 not configured" }, 503);

	const path = c.req.path.replace(/^.*\/api\/v\//, "");
	if (!path) return c.json({ error: "Path required" }, 400);

	try {
		// Get object metadata for Content-Length
		const head = await client.send(new HeadObjectCommand({ Bucket: R2_BUCKET, Key: path }));
		const totalSize = head.ContentLength || 0;
		const contentType = head.ContentType || "video/mp4";
		const rangeHeader = c.req.header("range");

		if (rangeHeader) {
			// Parse range header
			const match = rangeHeader.match(/bytes=(\d+)-(\d*)/);
			if (!match) return c.json({ error: "Invalid range" }, 400);

			const start = parseInt(match[1]);
			const end = match[2] ? parseInt(match[2]) : Math.min(start + 5 * 1024 * 1024 - 1, totalSize - 1);
			const chunkSize = end - start + 1;

			const obj = await client.send(new GetObjectCommand({
				Bucket: R2_BUCKET,
				Key: path,
				Range: `bytes=${start}-${end}`,
			}));

			return new Response(obj.Body as ReadableStream, {
				status: 206,
				headers: {
					"Content-Type": contentType,
					"Content-Length": String(chunkSize),
					"Content-Range": `bytes ${start}-${end}/${totalSize}`,
					"Accept-Ranges": "bytes",
					"Cache-Control": "public, max-age=86400",
				},
			});
		}

		// No range — stream full file
		const obj = await client.send(new GetObjectCommand({ Bucket: R2_BUCKET, Key: path }));
		return new Response(obj.Body as ReadableStream, {
			headers: {
				"Content-Type": contentType,
				"Content-Length": String(totalSize),
				"Accept-Ranges": "bytes",
				"Cache-Control": "public, max-age=86400",
			},
		});
	} catch (e: any) {
		if (e.name === "NoSuchKey") return c.json({ error: "Video not found" }, 404);
		console.error("[Tube] Stream error:", e);
		return c.json({ error: "Stream failed" }, 500);
	}
});

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

	const client = getS3();
	if (!client) return c.json({ error: "R2 not configured" }, 503);

	const formData = await c.req.formData();
	const file = formData.get("video") as File | null;
	if (!file) return c.json({ error: "video file required" }, 400);

	const ext = file.name.substring(file.name.lastIndexOf(".")).toLowerCase();
	if (!VIDEO_EXTENSIONS.has(ext)) return c.json({ error: `Unsupported format. Allowed: ${[...VIDEO_EXTENSIONS].join(", ")}` }, 400);

	const key = formData.get("path")?.toString() || file.name;
	const buffer = Buffer.from(await file.arrayBuffer());

	await client.send(new PutObjectCommand({
		Bucket: R2_BUCKET,
		Key: key,
		Body: buffer,
		ContentType: file.type || "video/mp4",
		ContentLength: buffer.length,
	}));

	return c.json({ ok: true, key, size: buffer.length, publicUrl: R2_PUBLIC_URL ? `${R2_PUBLIC_URL}/${key}` : undefined }, 201);
});

// GET /api/info — module info
routes.get("/api/info", (c) => {
	return c.json({
		module: "tube",
		name: "rTube",
		r2Configured: !!R2_ENDPOINT,
		rtmpIngest: "rtmp://rtube.online:1936/live",
		features: ["video-library", "hls-streaming", "rtmp-ingest"],
	});
});

// GET /api/health
routes.get("/api/health", (c) => c.json({ ok: true }));

// ── 360° Split routes ──

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

	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);

	const client = getS3();
	if (!client) return c.json({ error: "R2 not configured" }, 503);

	const { videoName, numViews, hFov, vFov, overlap, outputRes } = await c.req.json();
	if (!videoName) return c.json({ error: "videoName required" }, 400);

	try {
		// Fetch video from R2
		const obj = await client.send(new GetObjectCommand({ Bucket: R2_BUCKET, Key: videoName }));
		const bytes = await obj.Body!.transformToByteArray();

		// Build multipart form for 360split
		const form = new FormData();
		form.append("video", new Blob([bytes as unknown as BlobPart]), videoName);
		if (numViews) form.append("num_views", String(numViews));
		if (hFov) form.append("h_fov", String(hFov));
		if (vFov) form.append("v_fov", String(vFov));
		if (overlap) form.append("overlap", String(overlap));
		if (outputRes) form.append("output_res", outputRes);

		const resp = await fetch(`${SPLIT_360_URL}/upload`, { method: "POST", body: form });
		if (!resp.ok) {
			const text = await resp.text();
			return c.json({ error: `360split upload failed: ${text}` }, 502);
		}
		const data = await resp.json();
		return c.json({ jobId: data.job_id });
	} catch (e: any) {
		if (e.name === "NoSuchKey") return c.json({ error: "Video not found in library" }, 404);
		console.error("[Tube] 360split start error:", e);
		return c.json({ error: "Failed to start split job" }, 500);
	}
});

// GET /api/360split/status/:jobId — poll job status
routes.get("/api/360split/status/:jobId", async (c) => {
	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);

	const jobId = c.req.param("jobId");
	try {
		const resp = await fetch(`${SPLIT_360_URL}/status/${jobId}`);
		if (!resp.ok) return c.json({ error: "Status check failed" }, resp.status as any);
		return c.json(await resp.json());
	} catch (e) {
		console.error("[Tube] 360split status error:", e);
		return c.json({ error: "Cannot reach 360split service" }, 502);
	}
});

// POST /api/360split/import/:jobId — import results to R2
routes.post("/api/360split/import/:jobId", async (c) => {
	const authToken = extractToken(c.req.raw.headers);
	if (!authToken) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
	const client = getS3();
	if (!client) return c.json({ error: "R2 not configured" }, 503);

	const jobId = c.req.param("jobId");
	const { originalName } = await c.req.json().catch(() => ({ originalName: "" }));

	try {
		// Get output file list from status
		const statusResp = await fetch(`${SPLIT_360_URL}/status/${jobId}`);
		if (!statusResp.ok) return c.json({ error: "Could not get job status" }, 502);
		const status = await statusResp.json();
		if (status.status !== "complete") return c.json({ error: "Job not complete yet" }, 400);

		const baseName = originalName
			? originalName.replace(/\.[^.]+$/, "")
			: `video-${jobId}`;
		const imported: string[] = [];

		for (const filename of status.output_files || []) {
			// Download from 360split
			const dlResp = await fetch(`${SPLIT_360_URL}/download/${jobId}/${filename}`);
			if (!dlResp.ok) {
				console.error(`[Tube] 360split download failed: ${filename}`);
				continue;
			}
			const buffer = Buffer.from(await dlResp.arrayBuffer());
			const key = `360split/${baseName}/${filename}`;

			await client.send(new PutObjectCommand({
				Bucket: R2_BUCKET,
				Key: key,
				Body: buffer,
				ContentType: "video/mp4",
				ContentLength: buffer.length,
			}));
			imported.push(key);
		}

		// Cleanup temp files on 360split
		await fetch(`${SPLIT_360_URL}/cleanup/${jobId}`, { method: "POST" }).catch(() => {});

		return c.json({ ok: true, imported });
	} catch (e) {
		console.error("[Tube] 360split import error:", e);
		return c.json({ error: "Import failed" }, 500);
	}
});

// ── Live 360° Split routes ──

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

	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);

	const { streamKey, numViews, hFov, vFov, overlap, outputRes } = await c.req.json();
	if (!streamKey) return c.json({ error: "streamKey required" }, 400);

	const streamUrl = `rtmp://rtube-rtmp:1935/live/${streamKey}`;

	try {
		const resp = await fetch(`${SPLIT_360_URL}/live-split`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({
				stream_url: streamUrl,
				num_views: numViews || 4,
				h_fov: hFov,
				v_fov: vFov,
				overlap: overlap || 0,
				output_res: outputRes || "",
			}),
		});
		if (!resp.ok) {
			const text = await resp.text();
			return c.json({ error: `live-split start failed: ${text}` }, 502);
		}
		return c.json(await resp.json());
	} catch (e: any) {
		console.error("[Tube] live-split start error:", e);
		return c.json({ error: "Cannot reach 360split service" }, 502);
	}
});

// GET /api/live-split/status/:sessionId — proxy status
routes.get("/api/live-split/status/:sessionId", async (c) => {
	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
	const sessionId = c.req.param("sessionId");
	try {
		const resp = await fetch(`${SPLIT_360_URL}/live-split/status/${sessionId}`);
		if (!resp.ok) return c.json({ error: "Status check failed" }, resp.status as any);
		return c.json(await resp.json());
	} catch (e) {
		console.error("[Tube] live-split status error:", e);
		return c.json({ error: "Cannot reach 360split service" }, 502);
	}
});

// POST /api/live-split/stop/:sessionId — stop session (auth-gated)
routes.post("/api/live-split/stop/:sessionId", async (c) => {
	const authToken = extractToken(c.req.raw.headers);
	if (!authToken) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(authToken); } catch { return c.json({ error: "Invalid token" }, 401); }

	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
	const sessionId = c.req.param("sessionId");
	try {
		const resp = await fetch(`${SPLIT_360_URL}/live-split/stop/${sessionId}`, { method: "POST" });
		if (!resp.ok) return c.json({ error: "Stop failed" }, resp.status as any);
		return c.json(await resp.json());
	} catch (e) {
		console.error("[Tube] live-split stop error:", e);
		return c.json({ error: "Cannot reach 360split service" }, 502);
	}
});

// GET /api/live-split/hls/:sessionId/* — proxy HLS segments
routes.get("/api/live-split/hls/:sessionId/*", async (c) => {
	if (!SPLIT_360_URL) return c.json({ error: "360split service not configured" }, 503);
	const sessionId = c.req.param("sessionId");
	const subpath = c.req.path.replace(new RegExp(`^.*/api/live-split/hls/${sessionId}/`), "");
	if (!subpath) return c.json({ error: "Path required" }, 400);

	try {
		const resp = await fetch(`${SPLIT_360_URL}/live-split/hls/${sessionId}/${subpath}`);
		if (!resp.ok) return new Response("Not found", { status: 404 });

		const contentType = subpath.endsWith(".m3u8")
			? "application/vnd.apple.mpegurl"
			: subpath.endsWith(".ts")
				? "video/mp2t"
				: "application/octet-stream";

		return new Response(resp.body, {
			headers: {
				"Content-Type": contentType,
				"Access-Control-Allow-Origin": "*",
				"Cache-Control": "no-cache, no-store",
			},
		});
	} catch (e) {
		console.error("[Tube] live-split HLS proxy error:", e);
		return new Response("Proxy error", { status: 502 });
	}
});

// ── CRUD: Playlists ──

routes.get("/api/playlists", (c) => {
    if (!_syncServer) return c.json({ playlists: [] });
    const space = c.req.param("space") || "demo";
    const doc = ensureTubeDoc(space);
    return c.json({ playlists: Object.values(doc.playlists || {}) });
});

routes.post("/api/playlists", 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); }
    if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
    const space = c.req.param("space") || "demo";
    const { name, entries = [] } = await c.req.json();
    if (!name) return c.json({ error: "name required" }, 400);
    const id = crypto.randomUUID();
    const docId = tubeDocId(space);
    ensureTubeDoc(space);
    _syncServer.changeDoc<TubeDoc>(docId, `create playlist ${id}`, (d) => {
        d.playlists[id] = { id, name, entries, createdBy: null, createdAt: Date.now() };
    });
    const updated = _syncServer.getDoc<TubeDoc>(docId)!;
    return c.json(updated.playlists[id], 201);
});

// ── Page route ──
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	return c.html(renderShell({
		title: `${space} — Tube | rSpace`,
		moduleId: "rtube",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-video-player space="${space}"></folk-video-player>`,
		scripts: `<script type="module" src="/modules/rtube/folk-video-player.js?v=2"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rtube/tube.css">`,
	}));
});

export function getRecentVideosForMI(space: string, limit = 5): { id: string; name: string; entryCount: number; createdAt: number }[] {
    if (!_syncServer) return [];
    const doc = _syncServer.getDoc<TubeDoc>(tubeDocId(space));
    if (!doc?.playlists) return [];
    return Object.values(doc.playlists)
        .sort((a, b) => b.createdAt - a.createdAt)
        .slice(0, limit)
        .map(p => ({ id: p.id, name: p.name, entryCount: p.entries.length, createdAt: p.createdAt }));
}

export const tubeModule: RSpaceModule = {
	id: "rtube",
	name: "rTube",
	icon: "🎬",
	description: "Community video hosting & live streaming",
	scoping: { defaultScope: 'global', userConfigurable: true },
	routes,
	docSchemas: [{ pattern: '{space}:tube:playlists', description: 'Playlists and watch party per space', init: tubeSchema.init }],
	async onInit(ctx) { _syncServer = ctx.syncServer; },
	landingPage: renderLanding,
	standaloneDomain: "rtube.online",
	feeds: [
		{
			id: "videos",
			name: "Videos",
			kind: "data",
			description: "Video library — uploaded and live-streamed content",
			filterable: true,
		},
		{
			id: "watch-activity",
			name: "Watch Activity",
			kind: "attention",
			description: "View counts, watch time, and streaming engagement metrics",
		},
	],
	acceptsFeeds: ["data", "resource"],
	outputPaths: [
		{ path: "videos", name: "Videos", icon: "🎬", description: "Hosted videos and recordings" },
		{ path: "playlists", name: "Playlists", icon: "📺", description: "Video playlists and channels" },
	],
};
