import fs from 'node:fs/promises';
import { builtinModules } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');

const zones = [
    { name: 'common', root: 'packages/common/src', allowed: [] },
    { name: 'logic', root: 'packages/logic/src', allowed: ['@sammo-ts/common'] },
    { name: 'infra', root: 'packages/infra/src', allowed: ['@sammo-ts/common', '@sammo-ts/logic'] },
    {
        name: 'game-engine',
        root: 'app/game-engine/src',
        allowed: ['@sammo-ts/common', '@sammo-ts/infra', '@sammo-ts/logic'],
    },
    {
        name: 'game-api',
        root: 'app/game-api/src',
        allowed: ['@sammo-ts/common', '@sammo-ts/infra', '@sammo-ts/logic', '@sammo-ts/game-engine'],
    },
    {
        name: 'gateway-api',
        root: 'app/gateway-api/src',
        allowed: ['@sammo-ts/common', '@sammo-ts/infra', '@sammo-ts/logic', '@sammo-ts/game-engine'],
    },
    {
        name: 'release-controller',
        root: 'app/release-controller/src',
        allowed: ['@sammo-ts/gateway-api', '@sammo-ts/infra'],
    },
    {
        name: 'game-frontend',
        root: 'app/game-frontend/src',
        allowed: ['@sammo-ts/common', '@sammo-ts/logic', '@sammo-ts/game-api', '@sammo-ts/gateway-api'],
        frontend: true,
    },
    {
        name: 'gateway-frontend',
        root: 'app/gateway-frontend/src',
        allowed: ['@sammo-ts/common', '@sammo-ts/game-api', '@sammo-ts/gateway-api'],
        frontend: true,
    },
];

const persistenceModules = new Set([
    '@sammo-ts/infra',
    '@prisma/client',
    'pg',
    'redis',
    'fs',
    'fs/promises',
    'http',
    'https',
    'net',
    'child_process',
    'node:fs',
    'node:fs/promises',
    'node:http',
    'node:https',
    'node:net',
    'node:dgram',
    'node:dns',
    'node:tls',
    'node:child_process',
    'node:worker_threads',
]);

const backendPackages = new Set(['@sammo-ts/game-api', '@sammo-ts/gateway-api']);
const frontendRuntimeRootBarrels = new Set(['@sammo-ts/common', '@sammo-ts/logic']);
const frontendRuntimeRootAllowlist = new Map([
    ['app/game-frontend/src/workers/battleSimulator.worker.ts', new Set(['@sammo-ts/logic'])],
]);
const nodeBuiltins = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));

const isPersistenceModule = (specifier) =>
    persistenceModules.has(specifier) ||
    ['@sammo-ts/infra/', '@prisma/client/', 'pg/', 'redis/'].some((prefix) => specifier.startsWith(prefix));

const listSources = async (root) => {
    const result = [];
    const visit = async (directory) => {
        for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
            const absolute = path.join(directory, entry.name);
            if (entry.isDirectory()) {
                await visit(absolute);
            } else if (/\.(?:ts|tsx|vue)$/.test(entry.name)) {
                result.push(absolute);
            }
        }
    };
    await visit(root);
    return result.sort();
};

const internalPackageName = (specifier) => {
    const match = /^(@sammo-ts\/[^/]+)/.exec(specifier);
    return match?.[1] ?? null;
};

export const extractImports = (source) => {
    const imports = [];
    const fromPattern = /\b(?:import|export)\s+(type\s+)?([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
    const sideEffectPattern = /\bimport\s+['"]([^'"]+)['"]/g;
    const dynamicPattern = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
    let match;
    while ((match = fromPattern.exec(source))) {
        const clause = match[2].trim();
        const namedElements =
            clause.startsWith('{') && clause.endsWith('}')
                ? clause
                      .slice(1, -1)
                      .split(',')
                      .map((element) => element.trim())
                      .filter(Boolean)
                : [];
        const typeOnly =
            Boolean(match[1]) ||
            (namedElements.length > 0 && namedElements.every((element) => element.startsWith('type ')));
        imports.push({ specifier: match[3], typeOnly, clause: match[2] });
    }
    while ((match = sideEffectPattern.exec(source))) {
        imports.push({ specifier: match[1], typeOnly: false, clause: '' });
    }
    while ((match = dynamicPattern.exec(source))) {
        imports.push({ specifier: match[1], typeOnly: false, clause: '' });
    }
    return imports;
};

export const checkSource = ({ source, relativePath, zone }) => {
    const violations = [];
    for (const imported of extractImports(source)) {
        const packageName = internalPackageName(imported.specifier);
        if (packageName && packageName !== `@sammo-ts/${zone.name}` && !zone.allowed.includes(packageName)) {
            violations.push(`${relativePath}: ${zone.name} may not depend on ${packageName}`);
        }
        if ((zone.name === 'common' || zone.name === 'logic') && isPersistenceModule(imported.specifier)) {
            violations.push(
                `${relativePath}: ${zone.name} must access I/O through an injected port (${imported.specifier})`
            );
        }
        if (zone.frontend && nodeBuiltins.has(imported.specifier)) {
            violations.push(`${relativePath}: frontend production code may not import ${imported.specifier}`);
        }
        if (zone.frontend && packageName && backendPackages.has(packageName) && !imported.typeOnly) {
            violations.push(`${relativePath}: frontend may reference ${packageName} only through import type`);
        }
        if (
            zone.frontend &&
            frontendRuntimeRootBarrels.has(imported.specifier) &&
            !imported.typeOnly &&
            !frontendRuntimeRootAllowlist.get(relativePath)?.has(imported.specifier)
        ) {
            violations.push(`${relativePath}: frontend runtime must import a leaf subpath from ${imported.specifier}`);
        }
        if (
            (zone.name === 'game-api' || zone.name === 'gateway-api') &&
            imported.specifier === '@sammo-ts/game-engine'
        ) {
            violations.push(
                `${relativePath}: import a side-effect-free @sammo-ts/game-engine subpath instead of its process entrypoint`
            );
        }
        if (imported.specifier === '@sammo-ts/infra' && /\b(?:LogCategory|LogScope)\b/.test(imported.clause)) {
            violations.push(`${relativePath}: LogCategory and LogScope are owned by @sammo-ts/logic`);
        }
    }
    if ((zone.name === 'common' || zone.name === 'logic') && /\bprocess\.(?:env|stdout|stderr)\b/.test(source)) {
        violations.push(`${relativePath}: ${zone.name} runtime diagnostics must use an injected port`);
    }
    if ((zone.name === 'common' || zone.name === 'logic') && /\bfetch\s*\(/.test(source)) {
        violations.push(`${relativePath}: ${zone.name} network access must use an injected port`);
    }
    return violations;
};

export const checkWorkspace = async () => {
    const violations = [];
    for (const zone of zones) {
        const absoluteRoot = path.join(workspaceRoot, zone.root);
        const packageRoot = path.dirname(absoluteRoot);
        const manifest = JSON.parse(await fs.readFile(path.join(packageRoot, 'package.json'), 'utf8'));
        const importedPackages = new Map();
        for (const filename of await listSources(absoluteRoot)) {
            const source = await fs.readFile(filename, 'utf8');
            const relativePath = path.relative(workspaceRoot, filename);
            violations.push(...checkSource({ source, relativePath, zone }));
            for (const imported of extractImports(source)) {
                const packageName = internalPackageName(imported.specifier);
                if (!packageName || packageName === manifest.name) {
                    continue;
                }
                const usage = importedPackages.get(packageName) ?? { runtime: false, typeOnly: false };
                usage.typeOnly ||= imported.typeOnly;
                usage.runtime ||= !imported.typeOnly;
                importedPackages.set(packageName, usage);
            }
        }
        for (const [packageName, usage] of importedPackages) {
            const inDependencies = Object.hasOwn(manifest.dependencies ?? {}, packageName);
            const inDevDependencies = Object.hasOwn(manifest.devDependencies ?? {}, packageName);
            if (!inDependencies && !inDevDependencies) {
                violations.push(`${path.relative(workspaceRoot, packageRoot)}/package.json: missing ${packageName}`);
            }
            if (usage.runtime && !inDependencies) {
                violations.push(
                    `${path.relative(workspaceRoot, packageRoot)}/package.json: runtime dependency ${packageName} must be in dependencies`
                );
            }
            if (zone.frontend && backendPackages.has(packageName) && inDependencies) {
                violations.push(
                    `${path.relative(workspaceRoot, packageRoot)}/package.json: type-only backend ${packageName} belongs in devDependencies`
                );
            }
        }
    }
    return violations;
};

if (process.argv[1] === fileURLToPath(import.meta.url)) {
    const violations = await checkWorkspace();
    if (violations.length > 0) {
        console.error(`Package boundary check failed (${violations.length})`);
        for (const violation of violations) {
            console.error(`- ${violation}`);
        }
        process.exitCode = 1;
    } else {
        console.info('Package boundary check passed.');
    }
}
