import * as fs from 'fs/promises';
import * as path from 'path';
import { constants } from 'fs';
import { findPackageJson } from '../utils/findPackageJson.mjs';
import { exec } from 'child_process';
import { promisify } from 'util';
import ts from 'typescript';
import { Logger } from '../logger.mjs';
export const execAsync = promisify(exec);
/**
 * Support for Terraform framework
 */
export class TerraformFramework {
    /**
     * Framework name
     */
    get name() {
        return 'terraform';
    }
    /**
     * Name of the framework in logs
     */
    get logName() {
        return 'Terrform';
    }
    /**
     * Get Terraform state CI command
     */
    get stateCommand() {
        return 'terraform show --json';
    }
    /**
     *
     * @returns Get command to check if Terraform is installed
     */
    get checkInstalledCommand() {
        return 'terraform --version';
    }
    /**
     * Can this class handle the current project
     * @returns
     */
    async canHandle() {
        // check for any files with .tf or .tf.json extension
        const files = await fs.readdir(process.cwd());
        const r = files.some((f) => f.endsWith('.tf') || f.endsWith('.tf.json'));
        if (!r) {
            Logger.verbose(`[${this.logName}] This is not a ${this.logName} project. There are no *.tf or *.tf.json files in ${path.resolve('.')} folder.`);
            return false;
        }
        else {
            // check if Terraform or OpenTofu is installed
            try {
                await execAsync(this.checkInstalledCommand);
                return true;
            }
            catch {
                Logger.verbose(`[${this.logName}] This is not a ${this.logName} project. ${this.logName} is not installed.`);
                return false;
            }
        }
    }
    /**
     * Get Lambda functions
     * @param _config Configuration
     * @returns Lambda functions
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    async getLambdas(config) {
        const state = await this.readTerraformState();
        const lambdas = this.extractLambdaInfo(state);
        Logger.verbose(`[${this.logName}] Found Lambdas:`, JSON.stringify(lambdas, null, 2));
        const lambdasDiscovered = [];
        const tsOutDir = await this.getTsConfigOutDir();
        if (tsOutDir) {
            Logger.verbose(`[${this.logName}] tsOutDir:`, tsOutDir);
        }
        for (const func of lambdas) {
            const functionName = func.functionName;
            const handlerParts = func.handler.split('.');
            // get last part of the handler
            const handler = handlerParts[handlerParts.length - 1];
            const filename = func.sourceFilename;
            let pathWithourExtension;
            if (filename) {
                // remove extension
                pathWithourExtension = filename.replace(/\.[^/.]+$/, '');
            }
            else {
                pathWithourExtension = path.join(func.sourceDir, handlerParts[0]);
            }
            let possibleCodePaths = [
                `${pathWithourExtension}.ts`,
                `${pathWithourExtension}.js`,
                `${pathWithourExtension}.cjs`,
                `${pathWithourExtension}.mjs`,
            ];
            if (tsOutDir) {
                // remove outDir from path
                const pathWithourExtensionTypeScript = pathWithourExtension
                    .replace(tsOutDir, '')
                    .replace(/\/\//g, '/');
                possibleCodePaths = [
                    `${pathWithourExtensionTypeScript}.ts`,
                    `${pathWithourExtensionTypeScript}.js`,
                    `${pathWithourExtensionTypeScript}.cjs`,
                    `${pathWithourExtensionTypeScript}.mjs`,
                    ...possibleCodePaths,
                ];
            }
            let codePath;
            for (const cp of possibleCodePaths) {
                try {
                    await fs.access(cp, constants.F_OK);
                    codePath = cp;
                    break;
                }
                catch {
                    // ignore, file not found
                }
            }
            if (!codePath) {
                throw new Error(`Code path not found for handler: ${functionName}`);
            }
            const packageJsonPath = await findPackageJson(codePath);
            const lambdaResource = {
                functionName,
                codePath,
                handler,
                packageJsonPath,
                esBuildOptions: undefined,
                metadata: {
                    framework: this.name,
                },
            };
            lambdasDiscovered.push(lambdaResource);
        }
        return lambdasDiscovered;
    }
    extractLambdaInfo(resources) {
        const lambdas = [];
        for (const resource of resources) {
            if (resource.type === 'aws_lambda_function') {
                Logger.verbose(`[${this.logName}] Found Lambda:`, JSON.stringify(resource, null, 2));
                let sourceDir;
                let sourceFilename;
                const functionName = resource.values.function_name;
                const handler = resource.values.handler;
                if (!functionName) {
                    Logger.error('Failed to find function name for Lambda');
                    continue;
                }
                // get dependency "data.archive_file"
                const dependencies = resource.depends_on;
                const archiveFileResourceName = dependencies.find((dep) => dep.startsWith('data.archive_file.'));
                if (archiveFileResourceName) {
                    // get the resource
                    const name = archiveFileResourceName.split('.')[2];
                    const archiveFileResource = resources.find((r) => r.name === name);
                    // get source_dir or source_filename
                    if (archiveFileResource) {
                        sourceDir = archiveFileResource.values.source_dir;
                        sourceFilename = archiveFileResource.values.source_file;
                    }
                }
                // get dependency "archive_prepare" = serverless.tf support
                const archivePrepareResourceName = dependencies.find((dep) => dep.includes('.archive_prepare'));
                if (archivePrepareResourceName) {
                    // get the resource
                    const name = archivePrepareResourceName;
                    const archivePrepareResource = resources.find((r) => r.address?.startsWith(name));
                    // get source_dir or source_filename
                    if (archivePrepareResource) {
                        sourceDir =
                            archivePrepareResource.values.query?.source_path?.replaceAll('"', '');
                    }
                }
                if (!sourceDir && !sourceFilename) {
                    Logger.error(`Failed to find source code for Lambda ${functionName}`);
                }
                else {
                    lambdas.push({
                        functionName,
                        sourceDir,
                        sourceFilename,
                        handler: handler ?? 'handler',
                    });
                }
            }
        }
        return lambdas;
    }
    async readTerraformState() {
        // Is there a better way to get the Terraform state???
        let output;
        // get state by running "terraform show --json" command
        try {
            Logger.verbose(`[${this.logName}] Getting state with '${this.stateCommand}' command`);
            output = await execAsync(this.stateCommand);
        }
        catch (error) {
            throw new Error(`[${this.logName}] Failed to getstate from '${this.stateCommand}' command: ${error.message}`, { cause: error });
        }
        if (output.stderr) {
            throw new Error(`[${this.logName}] Failed to get state from '${this.stateCommand}' command: ${output.stderr}`);
        }
        if (!output.stdout) {
            throw new Error(`[${this.logName}] Failed to get state from '${this.stateCommand}' command`);
        }
        let jsonString = output.stdout;
        Logger.verbose(`[${this.logName}] State:`, jsonString);
        jsonString = jsonString?.split('\n').find((line) => line.startsWith('{'));
        if (!jsonString) {
            throw new Error(`[${this.logName}] Failed to get state. JSON string not found in the output.`);
        }
        try {
            const state = JSON.parse(jsonString);
            const rootResources = state.values?.root_module?.resources ?? [];
            const childResources = state.values?.root_module?.child_modules
                ?.map((m) => m.resources)
                .flat() ?? [];
            return [...rootResources, ...childResources];
        }
        catch (error) {
            //save state to file
            await fs.writeFile(`${this.name}-state.json`, jsonString);
            Logger.error(`[${this.logName}] Failed to parse state JSON:`, error);
            throw new Error(`Failed to parse ${this.logName} state JSON: ${error.message}`, { cause: error });
        }
    }
    /**
     * Get the outDir from tsconfig.json
     */
    async getTsConfigOutDir() {
        let currentDir = process.cwd();
        let tsConfigPath;
        while (currentDir !== '/') {
            tsConfigPath = path.resolve(path.join(currentDir, 'tsconfig.json'));
            try {
                await fs.access(tsConfigPath, constants.F_OK);
                break;
            }
            catch {
                // tsconfig.json not found, move up one directory
                currentDir = path.dirname(currentDir);
            }
        }
        if (!tsConfigPath) {
            Logger.verbose(`[${this.logName}] tsconfig.json not found`);
            return undefined;
        }
        Logger.verbose(`[${this.logName}] tsconfig.json found:`, tsConfigPath);
        const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
        const compilerOptions = ts.parseJsonConfigFileContent(configFile.config, ts.sys, './');
        return compilerOptions.options.outDir
            ? path.resolve(compilerOptions.options.outDir)
            : undefined;
    }
}
export const terraformFramework = new TerraformFramework();
