/**
 * @license
 * Copyright 2026 Balena Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import type * as SDK from 'balena-sdk';
import { getBalenaSdk, getCliForm, getVisuals } from './lazy';

import { ExpectedError } from '../errors';

/**
 * Heuristically determine whether the given semver version is a balenaOS
 * ESR version.
 *
 * @param {string} version Semver version. If invalid or range, return false.
 */
export const isESR = (version: string) => {
	const match = version.match(/^v?(\d+)\.\d+\.\d+/);
	const major = parseInt(match?.[1] ?? '', 10);
	return major >= 2018; // note: (NaN >= 2018) is false
};

/**
 * Returns whether the OS version is 'esr' or 'default'
 */
export const getOsType = (version: string) =>
	isESR(version) ? 'esr' : 'default';

/**
 * Download balenaOS image for the specified `deviceType`.
 * `OSVersion` may be one of:
 *  - exact version number,
 *  - valid semver range,
 *  - `latest` (exludes invalidated & pre-releases),
 *  - `menu`/'menu-esr' (will show the interactive menu )
 * If not provided, OSVersion will be set to `default`
 * `type` may be one of:
 * - `installation-media`
 * - `disk-image`
 *
 * @param deviceType
 * @param outputPath
 * @param OSVersion
 * @param type
 */
export async function downloadOSImage(
	deviceType: string,
	outputPath: string,
	OSVersion: string | undefined,
	type?: 'installation-media' | 'disk-image',
) {
	console.info(`Getting device operating system for ${deviceType}`);

	if (!OSVersion) {
		console.warn('OS version not specified: using latest released version');
		OSVersion = 'latest';
	}
	OSVersion = await resolveOSVersion(deviceType, OSVersion);

	// Override the default zlib flush value as we've seen cases of
	// incomplete files being identified as successful downloads when using Z_SYNC_FLUSH.
	// Using Z_NO_FLUSH results in a Z_BUF_ERROR instead of a corrupt image file.
	// https://github.com/nodejs/node/blob/master/doc/api/zlib.md#zlib-constants
	// Hopefully this is a temporary workaround until we can resolve
	// some ongoing issues with the os download stream.
	process.env.ZLIB_FLUSH = 'Z_NO_FLUSH';

	try {
		const { getStream } = await import('./image-manager');
		const stream = await getStream(deviceType, OSVersion, {
			type,
		});

		const displayVersion = await new Promise((resolve, reject) => {
			stream.on('error', reject);
			stream.on('balena-image-manager:resolved-version', resolve);
		});

		const visuals = getVisuals();
		const bar = new visuals.Progress(
			`Downloading balenaOS version ${displayVersion}`,
		);
		const spinner = new visuals.Spinner(
			`Downloading balenaOS version ${displayVersion} (size unknown)`,
		);

		stream.on('progress', (state: any) => {
			if (state != null) {
				return bar.update(state);
			}
			spinner.start();
		});

		stream.on('end', () => {
			spinner.stop();
		});

		// We completely rely on the `mime` custom property
		// to make this decision.
		// The actual stream should be checked instead.
		let output;
		if (stream.mime === 'application/zip') {
			const unzip = await import('node-unzip-2');
			output = unzip.Extract({ path: outputPath });
		} else {
			const fs = await import('fs');
			output = fs.createWriteStream(outputPath);
		}

		const { pipeline } = await import('node:stream/promises');
		await pipeline(stream, output);

		console.info(
			`balenaOS image version ${displayVersion} downloaded successfully`,
		);

		return outputPath;
	} catch (e) {
		const { getBalenaSdk } = await import('../utils/lazy');
		const balenaSdk = getBalenaSdk();
		if (e instanceof balenaSdk.errors.OSImageNotFound) {
			if (type != null) {
				throw new ExpectedError(
					'The requested OS download type is unavailable for this device type and version.',
				);
			}
			// The else here is unexpected, and it means that the img-maker was not able to find the
			// OS release artifacts, so we let the SDK's error be thrown as an unexpected error,
			// which should also report it to Sentry.
		}
		throw e;
	}
}

async function resolveOSVersion(
	deviceType: string,
	version: string,
): Promise<string> {
	if (['menu', 'menu-esr'].includes(version)) {
		return await selectOSVersionFromMenu(
			deviceType,
			version === 'menu-esr',
			false,
		);
	}
	const { normalizeOsVersion } = await import('./normalization');
	version = normalizeOsVersion(version);
	return version;
}

async function selectOSVersionFromMenu(
	deviceType: string,
	esr: boolean,
	includeDraft: boolean,
): Promise<string> {
	const vs = await getOsVersions(deviceType, esr, includeDraft);

	const choices = vs.map((v) => ({
		value: v.raw_version,
		name: formatOsVersion(v),
	}));

	return getCliForm().ask({
		message: 'Select the OS version:',
		type: 'list',
		choices,
		default: vs[0]?.raw_version,
	});
}

/**
 * Return the output of sdk.models.os.getAvailableOsVersions(), resolving
 * device type aliases and filtering with regard to ESR versions.
 */
export async function getOsVersions(
	deviceType: string,
	esr: boolean,
	includeDraft: boolean,
): Promise<SDK.OsVersion[]> {
	const sdk = getBalenaSdk();
	let slug = deviceType;
	let versions: SDK.OsVersion[] = await sdk.models.os.getAvailableOsVersions(
		slug,
		undefined,
		{ includeDraft },
	);
	// if slug is an alias, fetch the real slug
	if (!versions.length) {
		// unalias device type slug
		slug = (await sdk.models.deviceType.get(slug, { $select: 'slug' })).slug;
		if (slug !== deviceType) {
			versions = await sdk.models.os.getAvailableOsVersions(slug, undefined, {
				includeDraft,
			});
		}
	}
	versions = versions.filter(
		(v: SDK.OsVersion) => v.osType === (esr ? 'esr' : 'default'),
	);
	if (!versions.length) {
		const vType = esr ? 'ESR versions' : 'versions';
		throw new ExpectedError(
			`Error: No balenaOS ${vType} found for device type '${deviceType}'.`,
		);
	}
	return versions;
}

export function formatOsVersion(osVersion: SDK.OsVersion): string {
	return osVersion.line
		? `v${osVersion.raw_version} (${osVersion.line})`
		: `v${osVersion.raw_version}`;
}
