import type { StylableExports } from './stylable-transformer';

interface InjectCSSOptions {
    /**
     *  omitting the id will fallback to module.id/import.meta.url
     */
    id?: string | undefined;
    /**
     * css string to inject
     */
    css: string;
    /**
     * code to generate the css string to inject
     */
    cssCode?: string;
    /**
     *  calculated style depth
     */
    depth: number | string;
    /**
     *  use code to get the depth
     */
    depthCode?: string;
    /**
     *  reconciliation will happen only for style with the same runtimeId
     */
    runtimeId: string;
}

interface ModuleOptions {
    /**
     * module namespace
     */
    namespace: string;
    /**
     * static imports for the module
     */
    imports?: Array<{ from: string; defaultImport?: string }>;
    /**
     * Stylable transforms exports
     */
    jsExports: StylableExports | { [K in keyof StylableExports]: string };
    /**
     * the request of the module runtime api e.g @stylable/runtime
     */
    runtimeRequest?: string;
    /**
     * target module format
     */
    moduleType: 'esm' | 'cjs';
    /**
     * es3 compat mode
     */
    varType?: 'const' | 'var';
    /**
     * inject code immediately after imports
     */
    header?: string;
    /**
     * inject code after the entire module code
     */
    footer?: string;
}

export function generateStylableJSModuleSource(
    moduleOptions: ModuleOptions,
    injectOptions?: InjectCSSOptions
) {
    const {
        namespace,
        imports = [],
        jsExports,
        moduleType,
        runtimeRequest,
        varType = 'const',
        header = '',
        footer = '',
    } = moduleOptions;

    const { classes, keyframes, layers, containers, stVars, vars } = jsExports;
    const exportKind = moduleType === 'esm' ? `export ${varType} ` : 'module.exports.';
    return `
${imports.map(moduleRequest(moduleType)).join('\n')}
${runtimeImport(moduleType, runtimeRequest, injectOptions)}

${header}

${varType} _namespace_ = ${JSON.stringify(namespace)};
${varType} _style_ = (...args) => classesRuntime(_namespace_, ...args);

${exportKind}cssStates = (stateMapping) => statesRuntime(_namespace_, stateMapping);
${exportKind}style = _style_;
${exportKind}st = _style_;

${exportKind}namespace = _namespace_;
${exportKind}classes = ${JSON.stringify(classes)};
${exportKind}keyframes = ${JSON.stringify(keyframes)}; 
${exportKind}layers = ${JSON.stringify(layers)};
${exportKind}containers = ${JSON.stringify(containers)};
${exportKind}stVars = ${JSON.stringify(stVars)}; 
${exportKind}vars = ${JSON.stringify(vars)}; 

${runtimeExecuteInject(moduleType, injectOptions)}

${footer}
`;
}

function moduleRequest(moduleType: 'esm' | 'cjs') {
    return ({ from, defaultImport }: { from: string; defaultImport?: string }) => {
        const request = JSON.stringify(from);
        if (defaultImport) {
            return moduleType === 'esm'
                ? `import ${defaultImport} from ${request};`
                : `const ${defaultImport} = require(${request});`;
        }
        return moduleType === 'esm' ? `import ${request};` : `require(${request});`;
    };
}

function runtimeImport(
    moduleType: 'esm' | 'cjs',
    runtimeRequest: string | undefined,
    injectOptions: InjectCSSOptions | undefined
) {
    const importInjectCSS = injectOptions?.css ? `, injectCSS` : '';
    const request = JSON.stringify(runtimeRequest ?? '@stylable/runtime');
    return moduleType === 'esm'
        ? `import { classesRuntime, statesRuntime${importInjectCSS} } from ${request};`
        : `const { classesRuntime, statesRuntime${importInjectCSS} } = require(${request});`;
}

function runtimeExecuteInject(
    moduleType: 'esm' | 'cjs',
    injectOptions: InjectCSSOptions | undefined
) {
    if (!injectOptions?.css) {
        return '';
    }
    const { id, css, cssCode, depthCode, depth, runtimeId } = injectOptions;

    let out = 'injectCSS(';
    out += id ? JSON.stringify(id) : moduleType === 'esm' ? 'import.meta.url' : 'module.id';
    out += ', ';
    out += cssCode || JSON.stringify(css);
    out += ', ';
    out += depthCode || JSON.stringify(depth) || '-1';
    out += ', ';
    out += JSON.stringify(runtimeId);
    out += ');';
    return out;
}
