import type {LoaderContext} from 'webpack';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {basename} from 'path';
import camelCase from '../helpers/camel-case.js';


/**
 * Extracts CSS class names from a CSS Module into TypeScript definitions.
 *
 * Allows TS and PHPStorm to validate uses of CSS Modules in the codebase.
 *
 * Inspired by: `@teamsupercell/typings-for-css-modules-loader` but without dependencies and under our control.
 */
export default function createCssModuleTypings( this: LoaderContext<Record<string, never>>, content: string, ...args: [] ): void {
	if ( 'cacheable' in this && 'function' === typeof this.cacheable ) {
		this.cacheable();
	}

	try {
		const indexOfLocals = content.indexOf( '.locals' );
		const cssModuleKeys = -1 === indexOfLocals ? [] : getCssModuleKeys( content.substring( indexOfLocals ) );
		if ( cssModuleKeys.length > 0 ) {
			const fileName = this.resourcePath;
			const cssModuleDefinition = generateModuleTypeDefinition( cssModuleKeys, camelCase( basename( fileName ), true ) );

			const typingsPath = fileName.replace( /\.pcss$/, '.pcss.d.ts' );
			writeTypingsFile( typingsPath, cssModuleDefinition );
		}

		this.callback( null, content, ...args );
	} catch ( error ) {
		this.emitError( error as Error );
	}
}


/**
 * Formats the content of a TypeScript definition file.
 *
 * Turns a list of CSS module keys into a TypeScript interface definition.
 */
function generateModuleTypeDefinition( cssModuleKeys: string[], pascalCaseFileName: string, ): string {
	const interfaceName = `I${pascalCaseFileName}`;
	const moduleName = `${pascalCaseFileName}Module`;
	const namespaceName = `${pascalCaseFileName}Namespace`;


	const interfaceProperties = [ ...cssModuleKeys ]
		.sort()
		.map( ( key: string ) => {
			if ( key.includes( '-' ) ) {
				return `\t\t'${key}': string;`;
			}
			return `\t\t${key}: string;`;
		} )
		.join( '\n' );

	return `// Autogenerated by typings-for-css-modules-loader.
declare namespace ${namespaceName} {
	export interface I${pascalCaseFileName} {
${interfaceProperties}
	}
}

declare const ${moduleName}: ${namespaceName}.${interfaceName};

export = ${moduleName};
`;
}


/**
 * Write the typing file if it does not exist or if the content has changed.
 */
function writeTypingsFile( fileName: string, content: string ): void {
	if ( ! existsSync( fileName ) ) {
		writeTypingsFile.fileWriter( fileName, content );
	} else {
		const existingContent = readFileSync( fileName, 'utf8' );
		if ( existingContent !== content ) {
			writeTypingsFile.fileWriter( fileName, content );
		}
	}
}
writeTypingsFile.fileWriter = writeFileSync;


/**
 * Extracts CSS module keys from the content of a CSS Module file.
 *
 * @param {string} content - The content of the CSS Module file.
 * @return {string[]} - An array of unique CSS module keys.
 */
function getCssModuleKeys( content: string ): string[] {
	const keyRegex = /"([^"\n]+)":/g;
	let match: string[] | null;
	const cssModuleKeys: string[] = [];

	while ( ( match = keyRegex.exec( content ) ) ) {
		if ( -1 === cssModuleKeys.indexOf( match[ 1 ] ) ) {
			cssModuleKeys.push( match[ 1 ] );
		}
	}
	return cssModuleKeys;
}


/**
 * Modifier file writer used by the writeTypingsFile function.
 *
 * Here for unit test overrides of `writeFileSync`.
 */
export function modifyFileWriter( fn: typeof writeTypingsFile.fileWriter ): void {
	writeTypingsFile.fileWriter = fn
}
