import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
import path from "node:path";

const repoRoot = process.cwd();
const badgeDir = path.join(repoRoot, "docs", "badges");
const manifestPath = path.join(badgeDir, "badges.json");

const resultStyles = {
	success: { message: "passing", color: "#2ea043" },
	failure: { message: "failing", color: "#cf222e" },
	cancelled: { message: "cancelled", color: "#bf8700" },
	skipped: { message: "skipped", color: "#6e7781" },
	unknown: { message: "unknown", color: "#6e7781" },
};

const badgeDefinitions = {
	"dev-sha": { label: "dev", message: "unknown", color: "#0969da" },
	"code-check": { label: "code check", message: "unknown", color: "#6e7781" },
	biome: { label: "biome", message: "unknown", color: "#6e7781" },
	"backend-tests": {
		label: "backend",
		message: "unknown",
		color: "#6e7781",
	},
	userfront: {
		label: "userfront",
		message: "unknown",
		color: "#6e7781",
	},
	adminfront: {
		label: "adminfront",
		message: "unknown",
		color: "#6e7781",
	},
	devfront: {
		label: "devfront",
		message: "unknown",
		color: "#6e7781",
	},
	orgfront: {
		label: "orgfront",
		message: "unknown",
		color: "#6e7781",
	},
	"userfront-chrome": {
		label: "chrome",
		message: "unknown",
		color: "#6e7781",
	},
	"userfront-firefox": {
		label: "firefox",
		message: "unknown",
		color: "#6e7781",
	},
	"userfront-safari": {
		label: "safari",
		message: "unknown",
		color: "#6e7781",
	},
};

const deprecatedBadgeKeys = [
	"userfront-e2e-fast",
	"userfront-e2e-full",
	"adminfront-e2e",
	"devfront-e2e",
	"orgfront-e2e",
	"userfront-coverage",
	"adminfront-coverage",
	"devfront-coverage",
	"orgfront-coverage",
];

function normalizeResult(result) {
	return resultStyles[result] ? result : "unknown";
}

function styleForResult(result) {
	return resultStyles[normalizeResult(result)];
}

function compactResult(result) {
	const normalized = normalizeResult(result);
	return {
		success: "pass",
		failure: "fail",
		cancelled: "cancel",
		skipped: "skip",
		unknown: "unknown",
	}[normalized];
}

function colorForParts(parts) {
	const normalized = parts.map(normalizeResult);
	if (normalized.includes("failure")) return resultStyles.failure.color;
	if (normalized.includes("cancelled")) return resultStyles.cancelled.color;
	if (normalized.every((part) => part === "success"))
		return resultStyles.success.color;
	return resultStyles.unknown.color;
}

function colorForCoverage(percent) {
	if (percent >= 80) return "#2ea043";
	if (percent >= 35) return "#bf8700";
	return "#cf222e";
}

function escapeXml(value) {
	return String(value)
		.replace(/&/g, "&amp;")
		.replace(/</g, "&lt;")
		.replace(/>/g, "&gt;")
		.replace(/"/g, "&quot;");
}

function textWidth(text) {
	return Math.max(38, Math.ceil(String(text).length * 6.8 + 10));
}

function renderBadge({ label, message, color }) {
	const labelWidth = textWidth(label);
	const messageWidth = textWidth(message);
	const width = labelWidth + messageWidth;
	const labelCenter = labelWidth / 2;
	const messageCenter = labelWidth + messageWidth / 2;

	return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="20" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
  <title>${escapeXml(label)}: ${escapeXml(message)}</title>
  <linearGradient id="s" x2="0" y2="100%">
    <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
    <stop offset="1" stop-opacity=".1"/>
  </linearGradient>
  <clipPath id="r"><rect width="${width}" height="20" rx="3" fill="#fff"/></clipPath>
  <g clip-path="url(#r)">
    <rect width="${labelWidth}" height="20" fill="#555"/>
    <rect x="${labelWidth}" width="${messageWidth}" height="20" fill="${color}"/>
    <rect width="${width}" height="20" fill="url(#s)"/>
  </g>
  <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
    <text x="${labelCenter}" y="15" fill="#010101" fill-opacity=".3">${escapeXml(label)}</text>
    <text x="${labelCenter}" y="14">${escapeXml(label)}</text>
    <text x="${messageCenter}" y="15" fill="#010101" fill-opacity=".3">${escapeXml(message)}</text>
    <text x="${messageCenter}" y="14">${escapeXml(message)}</text>
  </g>
</svg>
`;
}

async function readJsonIfExists(filePath) {
	try {
		return JSON.parse(await readFile(filePath, "utf8"));
	} catch {
		return null;
	}
}

async function findCoverageSummaries(directory) {
	const entries = await readdir(directory, { withFileTypes: true }).catch(
		() => [],
	);
	const results = [];

	for (const entry of entries) {
		const entryPath = path.join(directory, entry.name);
		if (
			entry.isFile() &&
			[
				"backend-coverage-summary.json",
				"package-coverage-summary.json",
				"vitest-coverage-summary.json",
			].includes(entry.name)
		) {
			results.push(entryPath);
			continue;
		}
		if (entry.isDirectory()) {
			results.push(...(await findCoverageSummaries(entryPath)));
		}
	}

	return results;
}

function updateResultBadge(manifest, key, result) {
	const style = styleForResult(result);
	manifest.badges[key] = {
		...(manifest.badges[key] ?? badgeDefinitions[key]),
		message: style.message,
		color: style.color,
	};
}

function updateCompactResultBadge(manifest, key, result) {
	const style = styleForResult(result);
	manifest.badges[key] = {
		...(manifest.badges[key] ?? badgeDefinitions[key]),
		label: badgeDefinitions[key]?.label ?? manifest.badges[key]?.label,
		message: compactResult(result),
		color: style.color,
	};
}

function coveragePart(result, statements) {
	const normalized = normalizeResult(result);
	if (normalized !== "success") {
		return {
			message: compactResult(normalized),
			color: styleForResult(normalized).color,
			result: normalized,
		};
	}
	const value = Number(statements);
	if (!Number.isFinite(value)) {
		return {
			message: "unknown",
			color: resultStyles.unknown.color,
			result: "unknown",
		};
	}
	return {
		message: `${value.toFixed(2)}%`,
		color: colorForCoverage(value),
		result: "success",
	};
}

function updatePackageBadge(
	manifest,
	key,
	testResult,
	coverageResult,
	statements,
) {
	if (!badgeDefinitions[key]) return;
	const test = normalizeResult(testResult);
	const coverage = coveragePart(coverageResult, statements);
	manifest.badges[key] = {
		...(manifest.badges[key] ?? badgeDefinitions[key]),
		label: badgeDefinitions[key].label,
		message: `${compactResult(test)} | ${coverage.message}`,
		color:
			test === "failure" || coverage.result === "failure"
				? resultStyles.failure.color
				: test === "cancelled" || coverage.result === "cancelled"
					? resultStyles.cancelled.color
					: coverage.result === "success"
						? coverage.color
						: colorForParts([test, coverage.result]),
	};
}

function updateBrowserBadge(manifest, key, desktopResult, mobileResult) {
	if (!badgeDefinitions[key]) return;
	const desktop = normalizeResult(desktopResult);
	const mobile = normalizeResult(mobileResult);
	manifest.badges[key] = {
		...(manifest.badges[key] ?? badgeDefinitions[key]),
		label: badgeDefinitions[key].label,
		message: `${compactResult(desktop)} | ${compactResult(mobile)}`,
		color: colorForParts([desktop, mobile]),
	};
}

function normalizeManifestForComparison(value) {
	return {
		...value,
		updatedAt: null,
		source: {
			...(value?.source ?? {}),
			runId: null,
			runNumber: null,
		},
	};
}

function manifestsMatchIgnoringRunMetadata(left, right) {
	return (
		JSON.stringify(normalizeManifestForComparison(left)) ===
		JSON.stringify(normalizeManifestForComparison(right))
	);
}

function shortSha(value) {
	return String(value ?? "")
		.trim()
		.slice(0, 12);
}

const existingManifest =
	process.env.RESET_BADGES === "true"
		? null
		: await readJsonIfExists(manifestPath);
const sourceSha = shortSha(
	process.env.BADGE_SOURCE_SHA || process.env.GITHUB_SHA,
);
const manifest = {
	schemaVersion: 1,
	generatedBy: "scripts/update_code_check_badges.mjs",
	updatedAt: new Date().toISOString(),
	source: {
		branch: process.env.BADGE_SOURCE_BRANCH || "dev",
		sha: process.env.BADGE_SOURCE_SHA || process.env.GITHUB_SHA || null,
		shortSha: sourceSha || null,
		runId: process.env.GITHUB_RUN_ID || null,
		runNumber: process.env.GITHUB_RUN_NUMBER || null,
	},
	badges: {
		...badgeDefinitions,
		...(existingManifest?.badges ?? {}),
	},
};
for (const key of deprecatedBadgeKeys) {
	delete manifest.badges[key];
}

manifest.badges["dev-sha"] = {
	...badgeDefinitions["dev-sha"],
	message: sourceSha || "unknown",
};

const jobResults = {
	lint: process.env.LINT_RESULT,
	biome: process.env.BIOME_RESULT,
	backend: process.env.BACKEND_RESULT,
	userfront: process.env.USERFRONT_RESULT,
	userfrontE2e: process.env.USERFRONT_E2E_RESULT,
	adminfront: process.env.ADMINFRONT_RESULT,
	devfront: process.env.DEVFRONT_RESULT,
	orgfront: process.env.ORGFRONT_RESULT,
};
const e2eWasFull = process.env.USERFRONT_E2E_FULL === "true";
const userfrontFastResult = e2eWasFull ? undefined : jobResults.userfrontE2e;
const browserResults = {
	chrome: {
		desktop: process.env.USERFRONT_E2E_CHROMIUM_DESKTOP_RESULT,
		mobile: process.env.USERFRONT_E2E_CHROMIUM_MOBILE_RESULT,
	},
	firefox: {
		desktop: process.env.USERFRONT_E2E_FIREFOX_DESKTOP_RESULT,
		mobile: process.env.USERFRONT_E2E_FIREFOX_MOBILE_RESULT,
	},
	safari: {
		desktop: process.env.USERFRONT_E2E_WEBKIT_DESKTOP_RESULT,
		mobile: process.env.USERFRONT_E2E_WEBKIT_MOBILE_RESULT,
	},
};

const legacyCoverageResult = process.env.COVERAGE_RESULT;
const coverageJobResults = {
	backend: process.env.BACKEND_COVERAGE_RESULT,
	userfront: process.env.USERFRONT_COVERAGE_RESULT,
	adminfront: process.env.ADMINFRONT_COVERAGE_RESULT || legacyCoverageResult,
	devfront: process.env.DEVFRONT_COVERAGE_RESULT || legacyCoverageResult,
	orgfront: process.env.ORGFRONT_COVERAGE_RESULT || legacyCoverageResult,
};

const overallResults = [
	...Object.values(jobResults),
	...Object.values(coverageJobResults),
	...Object.values(browserResults).flatMap((result) => [
		result.desktop,
		result.mobile,
	]),
].filter(Boolean);
const hasFailure = overallResults.some((result) =>
	["failure", "cancelled"].includes(result),
);
const allSkipped =
	overallResults.length > 0 &&
	overallResults.every((result) => result === "skipped");
if (process.env.BADGE_UPDATE_CODE_CHECK !== "false") {
	updateResultBadge(
		manifest,
		"code-check",
		overallResults.length === 0
			? "unknown"
			: hasFailure
				? "failure"
				: allSkipped
					? "skipped"
					: "success",
	);
}

if (jobResults.biome) {
	updateResultBadge(manifest, "biome", jobResults.biome);
}

const coverageSummaries = process.env.COVERAGE_SUMMARY_PATH
	? [process.env.COVERAGE_SUMMARY_PATH]
	: await findCoverageSummaries(path.join(repoRoot, "badge-artifacts"));
const coverageByPackage = new Map();
for (const summaryPath of coverageSummaries) {
	const coverageSummary = await readJsonIfExists(summaryPath);
	for (const row of coverageSummary?.packages ?? []) {
		coverageByPackage.set(row.package, row.statements);
	}
	if (coverageSummary?.package) {
		coverageByPackage.set(coverageSummary.package, coverageSummary.statements);
	}
}

if (coverageJobResults.backend) {
	updatePackageBadge(
		manifest,
		"backend-tests",
		jobResults.backend,
		coverageJobResults.backend,
		coverageByPackage.get("backend"),
	);
} else if (jobResults.backend) {
	updateCompactResultBadge(manifest, "backend-tests", jobResults.backend);
}

for (const [key, testResult, coverageResult] of [
	["userfront", userfrontFastResult, coverageJobResults.userfront],
	["adminfront", jobResults.adminfront, coverageJobResults.adminfront],
	["devfront", jobResults.devfront, coverageJobResults.devfront],
	["orgfront", jobResults.orgfront, coverageJobResults.orgfront],
]) {
	if (testResult || coverageResult) {
		updatePackageBadge(
			manifest,
			key,
			testResult,
			coverageResult,
			coverageByPackage.get(key),
		);
	}
}

for (const [browser, result] of Object.entries(browserResults)) {
	if (result.desktop || result.mobile) {
		updateBrowserBadge(
			manifest,
			`userfront-${browser}`,
			result.desktop,
			result.mobile,
		);
	}
}

if (
	existingManifest &&
	manifestsMatchIgnoringRunMetadata(manifest, existingManifest)
) {
	manifest.updatedAt = existingManifest.updatedAt;
	manifest.source = existingManifest.source ?? manifest.source;
}

await mkdir(badgeDir, { recursive: true });
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);

for (const [key, badge] of Object.entries(manifest.badges)) {
	await writeFile(path.join(badgeDir, `${key}.svg`), renderBadge(badge));
}
for (const key of deprecatedBadgeKeys) {
	await unlink(path.join(badgeDir, `${key}.svg`)).catch(() => {});
}

console.log(`Updated ${Object.keys(manifest.badges).length} badge files.`);
