/*
 * ******************************************************************
 * Copyright (c) 2025 Broadcom. All Rights Reserved.
 * Broadcom Confidential. The term "Broadcom" refers to Broadcom Inc.
 * and/or its subsidiaries.
 * ******************************************************************
 */
import path from "path";
import fs from "fs";
import { spawn } from "child_process";
import * as cp from "child_process"
import * as chokidar from "chokidar";
import { WebSocketServer } from "ws";

export class UiEmulatorUtils {

    private static loadJsonConfig(rootDir: string, name: string) {
        const jsonPath = path.resolve(rootDir, name);
        if (!fs.existsSync(jsonPath)) {

            return null;
        }
        const fileContent = fs.readFileSync(jsonPath).toString();
        return JSON.parse(fileContent);
    }

    private static storeJsonConfig(rootDir: string, name: string, content: any) {
        const jsonPath = path.resolve(rootDir, name);
        fs.writeFileSync(jsonPath, JSON.stringify(content, null, 2));
    }

    private static loadFile(rootDir: string, name: string) {
        const jsonPath = path.resolve(rootDir, name);
        if (!fs.existsSync(jsonPath)) {

            return null;
        }
        const fileContent = fs.readFileSync(jsonPath).toString();
        return fileContent;
    }

    private static storeFile(rootDir: string, name: string, content: any) {
        const jsonPath = path.resolve(rootDir, name);
        fs.writeFileSync(jsonPath, content);
    }

    private static discoverPluginModule(packageRoot: string, elementsRootRelativePath: string, elementFolderName: string) {
        const srcRoot = path.join(elementFolderName, 'src');
        return {
            elementsRootRelativePath,
            srcRoot,
            elementFolderName,
        };
    }

    /**
     * Configures and serves the ui-emulator angular application, hosting provided ui plugin elements.
     * @param uiEmulatorConfigRoot - absolute root path to ui plugin folder
     * @param elementsRootRelativePath - relative path to root dir containing all plugin folders
     * @param elementFolderNames - all ui plugin folders
     * @param vcdConfig - VCD Configuration object
     * @param watch - Enable or Disable the watch for changes in UI Plugin src folders
     * @param ignoredFiles - Regex that ignores certain files from the Emulator's watch for changes
     * @param serveArgs - Arguments passed to ng serve
     */
    public static async serve(uiEmulatorConfigRoot: string,
        elementsRootRelativePath: string,
        elementFolderNames: string[],
        vcdConfig: {
            jwt: string,
            authtoken?: string,
            cellUrl: string,
            scope: "tenant" | "provider",
            orgName: string,
            orgId: string,
            locale: string,
        },
        watch: boolean = true,
        ignoredFiles: string = "",
        serveArgs?: string[],
    ) {
        ignoredFiles = this.normalizeIgnoreFilesRegExp(ignoredFiles);

        const rootDir = path.join(uiEmulatorConfigRoot, '.env'); // Extract as optional parameter?

        const PROXY_TARGET_PLACEHOLDER = "https://<insert your VCD endpoint here>";
        const emulatorPath = "node_modules/@vcd/ui-emulator";
        const emulatorRoot = path.resolve(emulatorPath);

        const angularJson = this.loadJsonConfig(emulatorRoot, 'angular.json');
        const tsconfigJson = this.loadJsonConfig(emulatorRoot, 'tsconfig.app.json');
        const environment = this.loadJsonConfig(rootDir, 'environment.json');
        const proxyConfig = this.loadFile(rootDir, 'proxy.conf.mjs');
        let newProxyConfig: string = proxyConfig;

        const argIndexPort = serveArgs.findIndex((argument: string) => {
            return argument === "--port";
        });
        let defaultDevServerPort: string = "4200";
        if (argIndexPort !== -1) {
            const serverPort = serveArgs[argIndexPort + 1];
            defaultDevServerPort = serverPort;
        }

        const bootstrapDetails = {
            "devMode": false,
            "locale": vcdConfig.locale || "en",
            "scope": vcdConfig.scope,
            "builtInExtensions": [],
            "thirdPartyExtensions": {
                "hyperplugin2": "/assets/hyperplugin2",
                "hyperplugin3": "/assets/hyperplugin3",
                "hyperplugin19": "/assets/hyperplugin19",
            },
            "cell": {
                "address": `http://localhost:${defaultDevServerPort}`,
                "organization": vcdConfig.orgName,
            },
            "flexUrl": "",
        };

        let pluginModules: {
            elementsRootRelativePath: string;
            srcRoot: string;
            elementFolderName: string;
        }[] = [];

        try {
            console.log('Setting auth token');

            environment.credentials = {
                jwt: vcdConfig.jwt,
            };

            if (vcdConfig.authtoken) {
                environment.credentials.authtoken = vcdConfig.authtoken;
            }

            console.log('Updating proxy config');
            while (newProxyConfig.includes(PROXY_TARGET_PLACEHOLDER)) {
                newProxyConfig = newProxyConfig.replace(PROXY_TARGET_PLACEHOLDER, vcdConfig.cellUrl);
            }

            console.log('Updating Plugins');
            if (vcdConfig.orgId) {
                bootstrapDetails.cell["orgId"] = vcdConfig.orgId;
            }

            pluginModules = elementFolderNames.map((element) => {
                bootstrapDetails.thirdPartyExtensions[element] = `/assets/${element}`;

                return this.discoverPluginModule(uiEmulatorConfigRoot, elementsRootRelativePath, element)
            });

            console.log('Updating angular.json');
            angularJson.projects.emulator.sourceRoot = `${emulatorPath}/src`;
            angularJson.projects.emulator.architect.build.options.index = `${emulatorPath}/src/index.html`;
            angularJson.projects.emulator.architect.build.options.main = `${emulatorPath}/src/main.ts`;
            angularJson.projects.emulator.architect.build.options.tsConfig = "tsconfig.emulator.json";
            angularJson.projects.emulator.architect.build.options.styles = [
                "node_modules/@cds/core/global.min.css",
                "node_modules/@cds/core/styles/theme.dark.min.css",
                "node_modules/@clr/ui/clr-ui.min.css",
                "node_modules/@vcd/ui-emulator/src/styles.scss"
            ];
            angularJson.projects.emulator.architect.build.options.scripts = [
                "node_modules/@vcd/ui-emulator/ng-hyper-plugin-framework/polyfills/custom-window.js",
                "node_modules/@vcd/ui-emulator/ng-hyper-plugin-framework/polyfills/scoped-custom-element-registry.js",
                "node_modules/systemjs/dist/system.src.js"
            ];
            angularJson.projects.emulator.architect.serve.configurations.development = {
                "browserTarget": "emulator:build:development",
                "proxyConfig": ".env/proxy.conf.runtime.mjs"
            };

            angularJson.projects.emulator.architect.build.options.assets = [
                `${emulatorPath}/src/favicon.ico`,
                `${emulatorPath}/src/assets`,
                ...pluginModules.map((pm) => {
                    return {
                        glob: '**/*',
                        input: `./${path.join(pm.elementsRootRelativePath, pm.srcRoot, "../dist")}`,
                        output: `assets/${pm.elementFolderName}`
                    };
                })
            ];
            angularJson.projects.emulator.architect.build.options.fileReplacements = [
                {
                    "replace": "node_modules/@vcd/ui-emulator/.env/environment.runtime.json",
                    "with": ".env/environment.runtime.json"
                }
            ];
            console.log('Updating tsconfig.emulator.json');
            tsconfigJson.extends = `./${emulatorPath}/tsconfig.json`;
            tsconfigJson.files = [
                `${emulatorPath}/src/main.ts`
            ];
            tsconfigJson.include = [
                '.env/*.json',
                `${emulatorPath}/src/**/*.ts`,
                `${emulatorPath}/ng-hyper-plugin-framework/**/*.ts`
            ];
            tsconfigJson.exclude = [
                `${emulatorPath}/src/assets`,
            ];

            const windowBootstrapDetails = `
                window["_bootstrapDetails"] = ${JSON.stringify(bootstrapDetails, null, 4)}
            `.trim();
            fs.writeFileSync(path.resolve(emulatorRoot, "src", "bootstrapDetails.js"), windowBootstrapDetails);
        } catch (e) {
            console.log('Error configuring environment.', e);
        } finally {
            this.storeJsonConfig(rootDir, 'environment.runtime.json', environment);
            this.storeFile(rootDir, 'proxy.conf.runtime.mjs', newProxyConfig);
            this.storeJsonConfig(uiEmulatorConfigRoot, 'angular.json', angularJson);
            this.storeJsonConfig(uiEmulatorConfigRoot, 'tsconfig.emulator.json', tsconfigJson);
        }

        if (watch) {
            const wss = createWebSocketServer(angularJson.projects.emulator.architect.build.options.index as string);

            const uiPluginBuilds = pluginModules.map((pluginModule) => {
                const pluginSrcRoot = path.resolve(path.join(pluginModule.elementsRootRelativePath, pluginModule.srcRoot));
                const pluginRootDir = path.resolve(path.join(pluginSrcRoot, "./../"));

                return this.buildUIPlugin(pluginRootDir);
            });

            Promise.all(uiPluginBuilds)
                .then(() => {
                    this.startDevServer(uiEmulatorConfigRoot, serveArgs);

                    pluginModules.forEach((pluginModule) => {
                        const pluginSrcRoot = path.resolve(path.join(pluginModule.elementsRootRelativePath, pluginModule.srcRoot));
                        console.log(`Watching ${pluginSrcRoot} for changes`);
                        this.watchUIPluginsSources(wss, pluginSrcRoot, ignoredFiles);
                    });
                });
        } else {
            this.startDevServer(uiEmulatorConfigRoot, serveArgs);
        }

        return Promise.resolve();
    }

    private static watchUIPluginsSources(wss: WebSocketServer, pluginSrcRoot: string, ignoredFiles: string, skipFirstEvent: boolean = true) {
        let ignored = null;
        if (ignoredFiles) {
            ignored = new RegExp(ignoredFiles);
        }

        const watcher = chokidar.watch(pluginSrcRoot, { ignored, persistent: true });

        const pluginRootDir = path.resolve(path.join(pluginSrcRoot, "./../"));
        let child_process = null;
        watcher.on("all", () => {
            if (child_process || skipFirstEvent) {
                skipFirstEvent = false;
                return;
            }
            child_process = cp.spawn("npm", ["run", "build"], {
                cwd: pluginRootDir,
                stdio: "inherit",
                shell: this.isPlatformWindows(),
            });

            child_process.on('close', (code) => {
                console.log(`Build finished with code: ${code}`);
                child_process = null;

                wss.clients.forEach((client) => {
                    if (client.readyState === client.OPEN) {
                        client.send(JSON.stringify({ type: "reload" }));
                    }
                });
            });
        });
    }

    private static startDevServer(uiEmulatorConfigRoot: string, serveArgs: string[]) {
        spawn('ng', ['serve', ...(serveArgs ? serveArgs : [])], {
            cwd: path.resolve(uiEmulatorConfigRoot),
            stdio: 'inherit',
            shell: this.isPlatformWindows(),
        });
    }

    private static buildUIPlugin(pluginRootDir: string) {
        return new Promise<void>((resolve) => {
            const child_process = cp.spawn("npm", ["run", "build"], {
                cwd: pluginRootDir,
                stdio: "inherit",
                shell: this.isPlatformWindows(),
            });

            child_process.on('close', (code) => {
                console.log(`Build finished with code: ${code}`);
                resolve();
            });
        });
    }

    private static normalizeIgnoreFilesRegExp(regex: string) {
        if (!regex) {
            return "";
        }

        if (regex.startsWith("/")) {
            regex = regex.substring(1, regex.length);
        }

        if (regex.endsWith("/")) {
            regex = regex.substring(0, regex.length - 1);
        }

        return regex;
    }

    private static isPlatformWindows(): boolean {
        return process.platform === "win32";
    }
}

function createWebSocketServer(indexHtml: string) {
    const wss = new WebSocketServer({ port: 3000 });
    wss.on("connection", (ws: WebSocket) => {
        ws.send(JSON.stringify({ type: "connected" }));
    });
    updateIndexHtml(indexHtml);
    return wss;
}

function updateIndexHtml(filePath: string) {
    const scriptToInject = `
<script>
    const socket = new WebSocket("ws://localhost:3000");

    socket.onmessage = (event) => {
        const message = JSON.parse(event.data);
        if (message.type === "reload") {
            console.log("Reloading page...");
            location.reload();
        }
    };
</script>`;

    // Read the existing index.html file
    fs.readFile(filePath, "utf-8", (err: Error, data: string) => {
        if (err) {
            console.error("Error reading the file:", err);
            return;
        }

        // Check if the script is already present to prevent duplicates
        if (data.includes("new WebSocket")) {
            console.log("Wss already exists in the file.");
            return;
        }

        // Inject the script before the closing </body> tag
        const updatedHtml = data.replace("</body>", `${scriptToInject}\n</body>`);

        // Write the modified content back to index.html
        fs.writeFile(filePath, updatedHtml, "utf8", (err: Error) => {
            if (err) {
                console.error("Error writing to the file:", err);
            } else {
                console.log("Wss successfully injected into index.html.");
            }
        });
    });
}