import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as glob from '@actions/glob';
import * as tc from '@actions/tool-cache';
import { rmrf } from './fs';
import { RUST_HASH, RUST_VERSION } from './rust';

export const CARGO_HOME = process.env.CARGO_HOME ?? path.join(os.homedir(), '.cargo');

export const WORKSPACE_ROOT = process.env.GITHUB_WORKSPACE ?? process.cwd();

export const CACHE_ENABLED = core.getBooleanInput('cache') && cache.isFeatureAvailable();

export async function downloadAndInstallBinstall(binDir: string) {
	core.info('cargo-binstall does not exist, attempting to install');

	let arch;
	let file;

	switch (process.arch) {
		case 'x64':
			arch = 'x86_64';
			break;
		case 'arm':
			arch = 'armv7';
			break;
		case 'arm64':
			arch = 'aarch64';
			break;
		default:
			throw new Error(`Unsupported architecture: ${process.arch}`);
	}

	switch (process.platform) {
		case 'linux': {
			const { family } = await import('detect-libc');
			let lib = 'gnu';

			if ((await family()) === 'musl') {
				lib = 'musl';
			}

			if (process.arch === 'arm') {
				lib += 'eabihf';
			}

			file = `${arch}-unknown-linux-${lib}.tgz`;
			break;
		}
		case 'darwin':
			file = `${arch}-apple-darwin.zip`;
			break;
		case 'win32':
			file = `${arch}-pc-windows-msvc.zip`;
			break;
		default:
			throw new Error(`Unsupported platform: ${process.platform}`);
	}

	const url = `https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-${file}`;
	const dlPath = await tc.downloadTool(url);

	if (url.endsWith('.zip')) {
		await tc.extractZip(dlPath, binDir);
	} else if (url.endsWith('.tgz')) {
		await tc.extractTar(dlPath, binDir);
	}
}

export async function installBins() {
	const bins = core
		.getInput('bins')
		.split(',')
		.map((bin) => bin.trim())
		.filter(Boolean);

	if (CACHE_ENABLED) {
		bins.push('cargo-cache');
	}

	if (bins.length === 0) {
		return;
	}

	core.info('Installing additional binaries');

	const binDir = path.join(CARGO_HOME, 'bin');

	if (!fs.existsSync(path.join(binDir, 'cargo-binstall'))) {
		await downloadAndInstallBinstall(binDir);
	}

	await exec.exec('cargo', ['binstall', '--no-confirm', '--log-level', 'info', ...bins]);
}

export function getCacheTarget(): string {
	return core.getInput('cache-target') || 'debug';
}

export function getCachePaths(): string[] {
	return [
		// ~/.cargo/registry
		path.join(CARGO_HOME, 'registry'),
		// /workspace/target/debug
		path.join(WORKSPACE_ROOT, 'target', getCacheTarget()),
	];
}

export function getCachePrefixes(): string[] {
	return [`setup-rustcargo-v1-${process.platform}`, 'setup-rustcargo-v1'];
}

export async function getPrimaryCacheKey() {
	const hasher = crypto.createHash('sha1');

	core.info('Generating cache key');

	core.debug(`Hashing Rust version = ${RUST_VERSION}`);
	hasher.update(RUST_VERSION);

	core.debug(`Hashing Rust commit hash = ${RUST_HASH}`);
	hasher.update(RUST_HASH);

	const lockHash = await glob.hashFiles('Cargo.lock');

	core.debug(`Hashing Cargo.lock = ${lockHash}`);
	hasher.update(lockHash);

	const cacheTarget = getCacheTarget();

	core.debug(`Hashing target profile = ${cacheTarget}`);
	hasher.update(cacheTarget);

	const job = process.env.GITHUB_JOB;

	if (job) {
		core.debug(`Hashing GITHUB_JOB = ${job}`);
		hasher.update(job);
	}

	return `${getCachePrefixes()[0]}-${hasher.digest('hex')}`;
}

export async function cleanCargoRegistry() {
	core.info('Cleaning ~/.cargo before saving');

	const registryDir = path.join(CARGO_HOME, 'registry');

	// .cargo/registry/src - Delete entirely
	await exec.exec('cargo', ['cache', '--autoclean']);

	// .cargo/registry/index - Delete .cache directories
	const indexDir = path.join(registryDir, 'index');

	if (fs.existsSync(indexDir)) {
		await Promise.all(
			fs.readdirSync(indexDir).map(async (index) => {
				if (fs.existsSync(path.join(indexDir, index, '.git'))) {
					await rmrf(path.join(indexDir, index, '.cache'));
				}
			}),
		);
	}

	// .cargo/registry/cache - Do nothing?
}

// https://doc.rust-lang.org/cargo/guide/build-cache.html
export async function cleanTargetProfile() {
	const targetProfile = getCacheTarget();

	core.info(`Cleaning target/${targetProfile} before saving`);

	const targetDir = path.join(WORKSPACE_ROOT, 'target', targetProfile);

	// target/*/{examples,incremental} - Not required in CI
	core.info('Removing examples and incremental directories');

	await Promise.all(
		['examples', 'incremental'].map(async (dirName) => {
			const dir = path.join(targetDir, dirName);

			if (fs.existsSync(dir)) {
				await rmrf(dir);
			}
		}),
	);

	// target/**/*.d - Not required in CI?
	core.info('Removing dep-info files (*.d)');

	const globber = await glob.create(path.join(targetDir, '**/*.d'));
	const files = await globber.glob();

	await Promise.all(files.map(rmrf));
}

export async function saveCache() {
	if (!CACHE_ENABLED) {
		return;
	}

	const primaryKey = await getPrimaryCacheKey();
	const cacheHitKey = core.getState('cache-hit-key');

	if (cacheHitKey === primaryKey) {
		core.info(`Cache hit occured on the key ${cacheHitKey}, not saving cache`);
		return;
	}

	await cleanCargoRegistry();
	await cleanTargetProfile();

	core.info(`Saving cache with key ${primaryKey}`);

	await cache.saveCache(getCachePaths(), primaryKey);
}

export async function restoreCache() {
	if (!CACHE_ENABLED) {
		return;
	}

	core.info('Attempting to restore cache');

	const primaryKey = await getPrimaryCacheKey();
	const cacheKey = await cache.restoreCache(getCachePaths(), primaryKey, getCachePrefixes());

	if (cacheKey) {
		core.saveState('cache-hit-key', cacheKey);
		core.info(`Cache restored using key ${primaryKey}`);
	} else {
		core.info(`Cache does not exist using key ${primaryKey}`);
	}

	core.setOutput('cache-key', cacheKey ?? primaryKey);
	core.setOutput('cache-hit', !!cacheKey);
}