import { access, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import ts from "typescript";
import YAML from "yamljs";

import { findWorkspaceRoot } from "../../findWorkspaceRoot.js";

/**
 * Backend TypeScript builds pull workspace dependencies into one emitted `build/` tree without using
 * TypeScript **project references**.
 *
 * **What this command does:** For each `backends/*` package, it rewrites the
 * `// transpile-monorepo-dependencies:start` … `end` region in `tsconfig.json` so `include` lists every
 * transitive `@devowl-wp/*` dependency (using each package’s own `tsconfig` `include` globs, after
 * `extends`). That way `tsc` compiles backend source and linked monorepo packages together.
 *
 * **Why not project references?** One flat `build/` output is easier to ship and link (see backend task
 * `tsc-build-link-node-modules` for hoisting outputs into that folder).
 *
 * **Symlinks and “double” compilation:** `tsc` follows symlinks into `node_modules`, so the same
 * dependency is not compiled twice as separate roots. The flip side is that the dependency’s JS only
 * appears once under `build/`; the follow-up task hoists those artifacts to the backend `build/` root as
 * needed. While walking the lockfile graph we skip a dependency package the second time it appears
 * (`uniqueSet`) so its globs are not duplicated in `include`.
 */
const MAIN_PKG_SCOPE = "@devowl-wp";

type PnpmLockfileImporters = Record<
    string,
    {
        dependencies?: Record<string, unknown>;
    }
>;

async function pathExists(filePath: string): Promise<boolean> {
    try {
        await access(filePath);
        return true;
    } catch {
        return false;
    }
}

/**
 * Merged `include` globs relative to the dependency package root (for prefixing with `depRootRelative`).
 */
function getMonorepoDependencyIncludePathPatterns(
    workspaceRoot: string,
    backendImporterKey: string,
    depRootRelative: string,
): string[] {
    const depRootAbs = resolve(workspaceRoot, backendImporterKey, depRootRelative);
    const tsconfigPath = join(depRootAbs, "tsconfig.json");

    const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
    if (configFile.error) {
        throw new Error(ts.flattenDiagnosticMessageText(configFile.error.messageText, "\n"));
    }

    const parsed = ts.parseJsonConfigFileContent(
        configFile.config,
        ts.sys,
        dirname(tsconfigPath),
        undefined,
        tsconfigPath,
    );

    const include = parsed.raw?.include;
    if (!Array.isArray(include) || include.length === 0) {
        throw new Error(`tsconfig has no non-empty "include" (after extends) for monorepo dependency: ${tsconfigPath}`);
    }

    return include.map((pattern) => String(pattern).replace(/\\/g, "/"));
}

function collectTsConfigIncludeDependencies(
    workspaceRoot: string,
    importers: PnpmLockfileImporters,
    importerKeys: string[],
    backendImporterKey: string,
    pkgNameWithoutScope: string,
    cwd = "",
    uniqueSet = new Set<string>(),
    collectedDependencies: string[] = [],
): string[] {
    const importerEntry = importerKeys.find((key) => key.split("/")[1] === pkgNameWithoutScope);
    if (!importerEntry || !importers[importerEntry]?.dependencies) {
        return collectedDependencies;
    }
    const { dependencies } = importers[importerEntry];

    for (const dependency of Object.keys(dependencies)) {
        if (dependency.startsWith(`${MAIN_PKG_SCOPE}/`)) {
            const newCwdPrefix = `${cwd}/node_modules/${dependency}`;
            const depRootRelative = newCwdPrefix.replace(/^\//, "").replace(/\\/g, "/");
            const includePatterns = getMonorepoDependencyIncludePathPatterns(
                workspaceRoot,
                backendImporterKey,
                depRootRelative,
            );
            const paths = [
                ...includePatterns.map((pattern) => join(depRootRelative, pattern).replace(/\\/g, "/")),
                `${depRootRelative}/package.json`,
            ];

            if (!uniqueSet.has(dependency)) {
                uniqueSet.add(dependency);
                collectedDependencies.push(...paths);
            }
            collectTsConfigIncludeDependencies(
                workspaceRoot,
                importers,
                importerKeys,
                backendImporterKey,
                dependency.split("/")[1]!,
                newCwdPrefix,
                uniqueSet,
                collectedDependencies,
            );
        }
    }

    return collectedDependencies;
}

async function tsconfigTranspileMonorepoDependenciesExecute(): Promise<void> {
    const startedAt = performance.now();
    const workspaceRootResolved = await findWorkspaceRoot(process.cwd());

    if (!workspaceRootResolved) {
        throw new Error("Could not find workspace root (pnpm-workspace.yaml or similar).");
    }

    let updatedBackends = 0;
    const lockfileRaw = await readFile(join(workspaceRootResolved, "pnpm-lock.yaml"), "utf8");
    const { importers } = YAML.parse(lockfileRaw) as { importers: PnpmLockfileImporters };
    const importerKeys = Object.keys(importers);

    for (const importerKey of importerKeys) {
        if (!importerKey.startsWith("backends/")) {
            continue;
        }

        const tsConfigPath = join(workspaceRootResolved, importerKey, "tsconfig.json");
        if (!(await pathExists(tsConfigPath))) {
            continue;
        }

        const collectedDependencies = collectTsConfigIncludeDependencies(
            workspaceRootResolved,
            importers,
            importerKeys,
            importerKey,
            importerKey.split("/")[1]!,
        );

        const tsConfig = await readFile(tsConfigPath, "utf8");
        const tsConfigModified = tsConfig.replace(
            /(\/\/ transpile-monorepo-dependencies:start\n)(.*)(^\s*\/\/ transpile-monorepo-dependencies:end)/gms,
            `$1${collectedDependencies.map((dep) => `"${dep}",`).join("\n")}\n$3`,
        );

        if (tsConfigModified.replace(/[\s,]/g, "") !== tsConfig.replace(/[\s,]/g, "")) {
            await writeFile(tsConfigPath, tsConfigModified, "utf8");
            updatedBackends++;
        }
    }

    const elapsedMs = Math.round(performance.now() - startedAt);
    const prefix = "Backend tsconfig.json#include transitive dependencies:";

    if (updatedBackends === 0) {
        console.log(
            "\x1b[32m%s\x1b[0m",
            `${prefix} no changes under backends/ — transpile-monorepo-dependencies blocks are already up-to-date (${elapsedMs} ms).`,
        );
    } else {
        console.log(
            "\x1b[32m%s\x1b[0m",
            `${prefix} updated ${updatedBackends} backend tsconfig.json files under backends/ (transpile-monorepo-dependencies entries from pnpm-lock.yaml + transitive @devowl-wp tsconfigs, ${elapsedMs} ms).`,
        );
    }
}

export { tsconfigTranspileMonorepoDependenciesExecute };
