// Refactor global variables (setTimeout, document, navigator, etc.) to use compatGlobal.
// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-globals.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";

const isDryRun = !Deno.args.includes("--run");

if (isDryRun) {
    console.log("=== DRY RUN MODE ===");
    console.log(
        "To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-globals.ts --run\n"
    );
} else {
    console.log("=== RUN MODE: WILL MODIFY FILES ===");
}

const project = new Project({ tsConfigFilePath: "../tsconfig.json" });

// Manually add files under src/ to ensure those excluded by tsconfig.json are processed if needed.
project.addSourceFilesAtPaths("../src/**/*.ts");
project.addSourceFilesAtPaths("../src/**/*.svelte");

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");

function toPosixPath(filePath: string): string {
    return filePath.replace(/\\/g, "/");
}

const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;

const TARGET_GLOBALS = new Set([
    "setTimeout",
    "clearTimeout",
    "setInterval",
    "clearInterval",
    "requestAnimationFrame",
    "cancelAnimationFrame",
    "localStorage",
    "navigator",
    "location",
    "document",
    "window",
]);

let modifiedFilesCount = 0;

for (const sourceFile of project.getSourceFiles()) {
    const filePath = sourceFile.getFilePath();
    const posixFilePath = toPosixPath(filePath);

    // Only process files inside the project src directory.
    if (!posixFilePath.startsWith(posixSrc)) {
        continue;
    }

    // Exclude coreEnvFunctions.ts to avoid self-referential definitions
    if (posixFilePath.endsWith("/coreEnvFunctions.ts") || posixFilePath.endsWith("/coreEnvFunctions")) {
        continue;
    }

    // Exclude unit and integration test files
    if (
        posixFilePath.endsWith(".spec.ts") ||
        posixFilePath.endsWith(".test.ts") ||
        posixFilePath.includes("/_test/") ||
        posixFilePath.includes("/testdeno/")
    ) {
        continue;
    }

    // Collect all identifier nodes
    const identifiers = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier);
    const nodesToReplace: { node: Node; replacement: string }[] = [];

    for (const idNode of identifiers) {
        const name = idNode.getText();
        if (!TARGET_GLOBALS.has(name)) {
            continue;
        }

        const parent = idNode.getParent();
        if (!parent) {
            continue;
        }

        // 1. Skip if it is the property name in a PropertyAccessExpression (e.g. the "setTimeout" in "obj.setTimeout")
        if (parent.getKind() === SyntaxKind.PropertyAccessExpression) {
            const propAccess = parent.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
            if (propAccess.getNameNode() === idNode) {
                continue;
            }
        }

        // 1.5. Skip if it is the right-hand side of a QualifiedName (e.g. the "requestAnimationFrame" in "typeof compatGlobal.requestAnimationFrame")
        if (parent.getKind() === SyntaxKind.QualifiedName) {
            const qualified = parent.asKindOrThrow(SyntaxKind.QualifiedName);
            if (qualified.getRight() === idNode) {
                continue;
            }
        }

        // 2. Skip if it is the operand of a typeof expression (e.g. "typeof window")
        if (parent.getKind() === SyntaxKind.TypeOfExpression) {
            continue;
        }

        // 3. Skip if it is a declaration name node
        const kind = parent.getKind();
        if (
            kind === SyntaxKind.VariableDeclaration ||
            kind === SyntaxKind.Parameter ||
            kind === SyntaxKind.FunctionDeclaration ||
            kind === SyntaxKind.MethodDeclaration ||
            kind === SyntaxKind.PropertyDeclaration ||
            kind === SyntaxKind.ClassDeclaration ||
            kind === SyntaxKind.InterfaceDeclaration ||
            kind === SyntaxKind.TypeAliasDeclaration ||
            kind === SyntaxKind.ImportSpecifier ||
            kind === SyntaxKind.ExportSpecifier ||
            kind === SyntaxKind.MethodSignature ||
            kind === SyntaxKind.PropertySignature ||
            kind === SyntaxKind.PropertyAssignment
        ) {
            if ((parent as any).getNameNode?.() === idNode || (parent as any).getName?.() === name) {
                continue;
            }
        }

        // 4. Verify it is a global variable reference using definitions
        let isGlobal = false;
        try {
            const definitions = idNode.getDefinitions();
            isGlobal =
                definitions.length === 0 ||
                definitions.every((def) => {
                    const sf = def.getSourceFile();
                    if (!sf) return true;
                    const path = sf.getFilePath();
                    return path.includes("node_modules/typescript/lib/") || path.includes("node_modules/@types/");
                });
        } catch (_err) {
            // If checking definitions fails, assume it is local/imported to be safe
            isGlobal = false;
        }

        if (!isGlobal) {
            continue;
        }

        // Determine replacement
        let replacement = "";
        if (name === "window" || name === "globalThis") {
            replacement = "compatGlobal";
        } else if (name === "document") {
            replacement = "_activeDocument";
        } else {
            replacement = `compatGlobal.${name}`;
        }

        nodesToReplace.push({ node: idNode, replacement });
    }

    if (nodesToReplace.length > 0) {
        console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
        for (const { node, replacement } of nodesToReplace) {
            const { line } = sourceFile.getLineAndColumnAtPos(node.getStart());
            console.log(`  Line ${line}: "${node.getText()}" -> "${replacement}"`);
        }

        if (!isDryRun) {
            // Apply replacements
            // Note: replaceWithText changes AST, so we replace them directly
            for (const { node, replacement } of nodesToReplace) {
                node.replaceWithText(replacement);
            }

            // Determine what needs to be imported based on replacements
            const needsCompatGlobal = nodesToReplace.some((r) => r.replacement.includes("compatGlobal"));
            const needsActiveDocument = nodesToReplace.some((r) => r.replacement.includes("_activeDocument"));

            const requiredImports: string[] = [];
            if (needsCompatGlobal) requiredImports.push("compatGlobal");
            if (needsActiveDocument) requiredImports.push("_activeDocument");

            if (requiredImports.length > 0) {
                const existingImport = sourceFile.getImportDeclarations().find((imp) => {
                    const spec = imp.getModuleSpecifierValue();
                    return spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions" || spec === "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
                });

                if (existingImport) {
                    for (const nameToImport of requiredImports) {
                        const alreadyImported = existingImport
                            .getNamedImports()
                            .some((ni) => ni.getName() === nameToImport);
                        if (!alreadyImported) {
                            existingImport.addNamedImport(nameToImport);
                        }
                    }
                } else {
                    sourceFile.addImportDeclaration({
                        namedImports: requiredImports,
                        moduleSpecifier: "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions",
                    });
                }
            }
        }

        modifiedFilesCount++;
    }
}

console.log(`\nTotal files to modify: ${modifiedFilesCount}`);

if (!isDryRun) {
    project.saveSync();
    console.log("All changes successfully saved.");
} else {
    console.log("Dry run complete. No changes were written to files.");
}
