import type { Dirent } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { join, relative } from "node:path";

import type {
	LighthouseData,
	LighthouseDiagnostic,
	LighthouseOpportunity,
	ResourceBreakdown,
	TestMetrics,
	TestScreenshots,
} from "./types.ts";

/**
 * Parsed bits of a sitespeed run, ready to be assembled into a TestResult.
 * Each field is independently nullable so a partial run still yields the
 * useful pieces.
 */
export interface ParsedRun {
	metrics: TestMetrics | null;
	lighthouse: LighthouseData | null;
	har: string | null;
	reportUrl: string | null;
	screenshots: TestScreenshots | null;
}

const num = (v: unknown): number => (typeof v === "number" && Number.isFinite(v) ? v : 0);

/** Recursively walk `dir` and return absolute paths of every file. */
async function walk(dir: string): Promise<string[]> {
	const out: string[] = [];
	let entries: Dirent[];
	try {
		entries = (await readdir(dir, { withFileTypes: true })) as unknown as Dirent[];
	} catch {
		return out;
	}
	for (const entry of entries) {
		const full = join(dir, String(entry.name));
		if (entry.isDirectory()) {
			out.push(...(await walk(full)));
		} else if (entry.isFile()) {
			out.push(full);
		}
	}
	return out;
}

async function readJson<T>(path: string): Promise<T | null> {
	try {
		const raw = await readFile(path, "utf8");
		return JSON.parse(raw) as T;
	} catch {
		return null;
	}
}

interface BrowsertimeStat {
	median?: number;
	mean?: number;
	max?: number;
	min?: number;
}

interface BrowsertimeJson {
	statistics?: {
		timings?: {
			firstPaint?: BrowsertimeStat;
			fullyLoaded?: BrowsertimeStat;
			pageTimings?: {
				domContentLoadedTime?: BrowsertimeStat;
				domInteractiveTime?: BrowsertimeStat;
				backEndTime?: BrowsertimeStat;
			};
			paintTiming?: {
				"first-contentful-paint"?: BrowsertimeStat;
			};
			largestContentfulPaint?: { renderTime?: BrowsertimeStat };
			cumulativeLayoutShift?: BrowsertimeStat;
			totalBlockingTime?: BrowsertimeStat;
			firstInputDelay?: BrowsertimeStat;
			ttfb?: BrowsertimeStat;
		};
		visualMetrics?: {
			SpeedIndex?: BrowsertimeStat;
		};
	};
}

interface HarLog {
	log?: {
		entries?: Array<{
			response?: {
				status?: number;
				content?: { mimeType?: string; size?: number };
				_transferSize?: number;
			};
			time?: number;
		}>;
	};
}

function mimeBucket(mime: string | undefined): string {
	if (!mime) return "other";
	if (mime.includes("html")) return "html";
	if (mime.includes("css")) return "css";
	if (mime.includes("javascript") || mime.includes("ecmascript")) return "js";
	if (mime.startsWith("image/")) return "image";
	if (mime.startsWith("font/") || mime.includes("font")) return "font";
	if (mime.includes("json") || mime.includes("xml")) return "xhr";
	if (mime.startsWith("video/") || mime.startsWith("audio/")) return "media";
	return "other";
}

function summarizeHar(har: HarLog): {
	requests: number;
	transferSize: number;
	resourceBreakdown: ResourceBreakdown;
} {
	const entries = har.log?.entries ?? [];
	const breakdown: ResourceBreakdown = {};
	let totalSize = 0;

	for (const e of entries) {
		const type = mimeBucket(e.response?.content?.mimeType);
		const size = num(e.response?._transferSize ?? e.response?.content?.size);
		const time = num(e.time);
		totalSize += size;
		const bucket = breakdown[type] ?? { count: 0, transferSize: 0, time: 0 };
		bucket.count += 1;
		bucket.transferSize += size;
		bucket.time += time;
		breakdown[type] = bucket;
	}

	return {
		requests: entries.length,
		transferSize: totalSize,
		resourceBreakdown: breakdown,
	};
}

function pickStat(stat: BrowsertimeStat | undefined): number {
	if (!stat) return 0;
	return num(stat.median ?? stat.mean ?? 0);
}

/**
 * Parse the contents of an output directory produced by `sitespeed.io`.
 * `publicBaseUrl` is the URL prefix under which `/results/<id>` is served
 * (e.g. http://localhost:3001/results/<id>).
 */
export async function parseRun(outputDir: string, publicBaseUrl: string): Promise<ParsedRun> {
	const allFiles = await walk(outputDir);

	// Sitespeed writes per-page metrics to `browsertime.pageSummary.json`
	// when the analysisstorer plugin is enabled. Fall back to the legacy
	// `browsertime.json` for older versions / non-default setups.
	const browsertimeJsonPath =
		allFiles.find((f) => f.endsWith("browsertime.pageSummary.json")) ??
		allFiles.find((f) => f.endsWith("browsertime.json"));
	// `lighthouse.pageSummary.json` is the actual lighthouse audit JSON.
	// `lighthouse.report.json` exists too but holds a stringified HTML —
	// a quirk of the lighthouse plugin's html/json multi-output mode.
	const lighthouseJsonPath =
		allFiles.find((f) => f.endsWith("lighthouse.pageSummary.json")) ??
		allFiles.find((f) => f.endsWith("lighthouse.report.json"));
	const lighthouseHtmlPath =
		allFiles.find((f) => f.endsWith("lighthouse.html")) ??
		allFiles.find((f) => f.endsWith("lighthouse.report.html"));
	const harPath =
		allFiles.find((f) => f.endsWith("browsertime.har")) ??
		allFiles.find((f) => f.endsWith(".har"));
	const indexHtmlPath = allFiles.find((f) => f.endsWith("/index.html"));

	const relUrl = (abs: string): string => `${publicBaseUrl}/${relative(outputDir, abs)}`;

	let metrics: TestMetrics | null = null;
	if (browsertimeJsonPath) {
		const data = await readJson<BrowsertimeJson[] | BrowsertimeJson>(browsertimeJsonPath);
		const page = Array.isArray(data) ? data[0] : data;
		const t = page?.statistics?.timings;
		const harData = harPath ? await readJson<HarLog>(harPath) : null;
		const harSummary = harData
			? summarizeHar(harData)
			: { requests: 0, transferSize: 0, resourceBreakdown: {} };

		metrics = {
			firstContentfulPaint: pickStat(t?.paintTiming?.["first-contentful-paint"]),
			largestContentfulPaint: pickStat(t?.largestContentfulPaint?.renderTime),
			totalBlockingTime: pickStat(t?.totalBlockingTime),
			cumulativeLayoutShift: pickStat(t?.cumulativeLayoutShift),
			timeToFirstByte: pickStat(t?.ttfb ?? t?.pageTimings?.backEndTime),
			firstInputDelay: pickStat(t?.firstInputDelay),
			speedIndex: pickStat(page?.statistics?.visualMetrics?.SpeedIndex),
			fullyLoaded: pickStat(t?.fullyLoaded),
			domContentLoaded: pickStat(t?.pageTimings?.domContentLoadedTime),
			domInteractive: pickStat(t?.pageTimings?.domInteractiveTime),
			requests: harSummary.requests,
			transferSize: harSummary.transferSize,
			resourceBreakdown: harSummary.resourceBreakdown,
		};
	}

	let lighthouse: LighthouseData | null = null;
	if (lighthouseJsonPath) {
		const lh = await readJson<LighthouseReport>(lighthouseJsonPath);
		if (lh) {
			lighthouse = mapLighthouse(
				lh,
				lighthouseHtmlPath ? relUrl(lighthouseHtmlPath) : "",
			);
		}
	}

	const screenshots = await collectScreenshots(allFiles, outputDir, publicBaseUrl);

	return {
		metrics,
		lighthouse,
		har: harPath ? relUrl(harPath) : null,
		reportUrl: indexHtmlPath ? relUrl(indexHtmlPath) : null,
		screenshots,
	};
}

interface LighthouseAuditRef {
	id: string;
	weight?: number;
	group?: string;
}

interface LighthouseAudit {
	id: string;
	title?: string;
	description?: string;
	score?: number | null;
	displayValue?: string | null;
	details?: { overallSavingsMs?: number; overallSavingsBytes?: number };
}

interface LighthouseCategory {
	id: string;
	score?: number | null;
	auditRefs?: LighthouseAuditRef[];
}

interface LighthouseReport {
	categories?: Record<string, LighthouseCategory>;
	audits?: Record<string, LighthouseAudit>;
}

function mapLighthouse(lh: LighthouseReport, reportUrl: string): LighthouseData {
	const cats = lh.categories ?? {};
	const audits = lh.audits ?? {};
	const scores = {
		performance: Math.round(num(cats.performance?.score) * 100),
		accessibility: Math.round(num(cats.accessibility?.score) * 100),
		"best-practices": Math.round(num(cats["best-practices"]?.score) * 100),
		seo: Math.round(num(cats.seo?.score) * 100),
	};

	const perfRefs = cats.performance?.auditRefs ?? [];
	const opportunities: LighthouseOpportunity[] = [];
	const diagnostics: LighthouseDiagnostic[] = [];

	for (const ref of perfRefs) {
		const audit = audits[ref.id];
		if (!audit) continue;
		const score = num(audit.score);
		if (ref.group === "load-opportunities") {
			opportunities.push({
				id: audit.id,
				title: audit.title ?? audit.id,
				description: audit.description ?? "",
				savingsMs: num(audit.details?.overallSavingsMs),
				savingsBytes: num(audit.details?.overallSavingsBytes),
				score,
			});
		} else if (ref.group === "diagnostics") {
			diagnostics.push({
				id: audit.id,
				title: audit.title ?? audit.id,
				description: audit.description ?? "",
				score,
				displayValue: audit.displayValue ?? null,
			});
		}
	}

	opportunities.sort((a, b) => b.savingsMs - a.savingsMs);

	return { scores, opportunities, diagnostics, reportUrl };
}

async function collectScreenshots(
	allFiles: string[],
	outputDir: string,
	publicBaseUrl: string,
): Promise<TestScreenshots | null> {
	const pngs = allFiles.filter((f) => f.endsWith(".png"));
	if (pngs.length === 0) return null;

	const find = (needle: string): string | undefined =>
		pngs.find((f) => f.toLowerCase().includes(needle.toLowerCase()));

	const after = find("afterPageCompleteCheck") ?? find("pageLoad");
	const lcp = find("largestContentfulPaint") ?? find("LCP");
	const cls = find("layoutShift") ?? find("CLS");

	const rel = (abs: string | undefined): string | undefined =>
		abs ? `${publicBaseUrl}/${relative(outputDir, abs)}` : undefined;

	const result: TestScreenshots = {};
	const a = rel(after);
	const l = rel(lcp);
	const c = rel(cls);
	if (a) result.afterPageCompleteCheck = a;
	if (l) result.largestContentfulPaint = l;
	if (c) result.layoutShift = c;
	return Object.keys(result).length > 0 ? result : null;
}

