/**
 * Project file loader
 *
 * Loads project-owned files (typically config modules) from the project root.
 *
 * Usage:
 *  - useFileLoader('config/mail.ts').get<TypeMail>()
 *  - useFileLoader('config', 'mail.ts').get<TypeMail>()
 */
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import { existsSync } from '../node-singletons/fs.js';
import pathModule, { extname, join, resolve, sep } from '../node-singletons/path.js';
import processModule from '../node-singletons/process.js';
import { pathToFileURL } from '../node-singletons/url.js';
const resolveProjectRoot = () => {
    const isTestRuntime = () => {
        const nodeEnv = processModule.env?.['NODE_ENV'];
        const isVitest = processModule.env?.['VITEST'] !== undefined ||
            processModule.env?.['VITEST_WORKER_ID'] !== undefined ||
            processModule.env?.['VITEST_POOL_ID'] !== undefined;
        return nodeEnv === 'testing' || isVitest;
    };
    const isCoreRepo = (cwdPath) => {
        const fromNpm = processModule.env?.['npm_package_name'];
        if (fromNpm === '@zintrust/core')
            return true;
        // Vitest suites (notably CoverageBoost) may mock `@node-singletons/fs` to return
        // `existsSync() === true` for everything and `readFileSync() === '{}'`, which makes
        // any package.json-based detection unreliable. Instead, detect a core repo checkout
        // by checking whether this module is being executed from within the current cwd.
        // - core repo tests: import.meta.url points into `<cwd>/src/...`
        // - consumer apps: import.meta.url points into `node_modules/@zintrust/core/...`
        try {
            const cwdAbs = resolve(cwdPath);
            const selfUrl = new URL(import.meta.url);
            const selfPath = decodeURIComponent(selfUrl.pathname);
            const selfAbs = resolve(selfPath);
            return selfAbs === cwdAbs || selfAbs.startsWith(cwdAbs + sep);
        }
        catch {
            return false;
        }
    };
    const fromEnv = processModule.env?.['ZINTRUST_PROJECT_ROOT'];
    if (typeof fromEnv === 'string' && fromEnv.trim().length > 0)
        return fromEnv.trim();
    const cwd = processModule.cwd();
    // In the ZinTrust core repo, `config/*.ts` are templates (not consumer app config).
    // During core test runs, we avoid auto-loading them to prevent unexpected overrides.
    // In normal runs, keep the historical behavior (projectRoot = cwd).
    if (isCoreRepo(cwd) && isTestRuntime()) {
        return resolve(cwd, '.zintrust-internal-project-root');
    }
    return cwd;
};
const isInternalProjectRoot = (projectRoot) => pathModule.basename(projectRoot) === '.zintrust-internal-project-root';
const normalizeProjectRelativePath = (raw) => {
    const value = String(raw ?? '').trim();
    if (value.length === 0) {
        throw ErrorFactory.createConfigError('useFileLoader() requires a non-empty path');
    }
    if (value.includes('\u0000')) {
        throw ErrorFactory.createSecurityError('Invalid file path (null byte)');
    }
    const normalized = value.replaceAll('\\', '/').replace(/^\.\/+/, '');
    if (pathModule.isAbsolute(normalized)) {
        throw ErrorFactory.createSecurityError('Absolute paths are not allowed', { requested: value });
    }
    return normalized;
};
const resolveWithinProjectRoot = (projectRoot, relativePath) => {
    const rootAbs = resolve(projectRoot);
    const candidateAbs = resolve(projectRoot, relativePath);
    if (rootAbs === sep)
        return candidateAbs;
    if (candidateAbs === rootAbs)
        return candidateAbs;
    if (!candidateAbs.startsWith(rootAbs + sep)) {
        throw ErrorFactory.createSecurityError('Invalid file path (path traversal detected)', {
            projectRoot: rootAbs,
            requested: relativePath,
            resolved: candidateAbs,
        });
    }
    return candidateAbs;
};
const replaceExtension = (filePath, nextExt) => {
    const current = extname(filePath);
    if (current.length === 0)
        return `${filePath}${nextExt}`;
    return filePath.slice(0, -current.length) + nextExt;
};
const unique = (items) => {
    const seen = new Set();
    const out = [];
    for (const item of items) {
        if (seen.has(item))
            continue;
        seen.add(item);
        out.push(item);
    }
    return out;
};
const buildCandidateAbsolutePaths = (projectRoot, relativePath) => {
    const ext = extname(relativePath);
    const baseRelCandidates = unique([
        relativePath,
        ...(ext.length === 0
            ? [`${relativePath}.ts`, `${relativePath}.js`, `${relativePath}.mjs`]
            : []),
        ...(ext === '.ts'
            ? [replaceExtension(relativePath, '.js'), replaceExtension(relativePath, '.mjs')]
            : []),
        ...(ext === '.js'
            ? [replaceExtension(relativePath, '.mjs'), replaceExtension(relativePath, '.ts')]
            : []),
        ...(ext === '.mjs'
            ? [replaceExtension(relativePath, '.js'), replaceExtension(relativePath, '.ts')]
            : []),
    ]);
    const absCandidates = baseRelCandidates.flatMap((rel) => [
        resolveWithinProjectRoot(projectRoot, rel),
        resolveWithinProjectRoot(projectRoot, join('dist', rel)),
    ]);
    return unique(absCandidates);
};
const importModule = async (filePath) => {
    const url = pathToFileURL(filePath).href;
    return (await import(url));
};
export const useFileLoader = (...args) => {
    const isWorkersRuntime = () => {
        const g = globalThis;
        return g.CF !== undefined || g.caches !== undefined || g.WebSocketPair !== undefined;
    };
    if (isWorkersRuntime()) {
        return Object.freeze({
            candidates: () => [],
            path: () => '',
            exists: () => false,
            get() {
                throw ErrorFactory.createConfigError('File loading is not supported in Workers runtime');
            },
        });
    }
    const relativePath = args.length === 1
        ? normalizeProjectRelativePath(args[0])
        : normalizeProjectRelativePath(args.join('/'));
    const projectRoot = resolveProjectRoot();
    const isInternalRoot = isInternalProjectRoot(projectRoot);
    const candidates = buildCandidateAbsolutePaths(projectRoot, relativePath);
    // The core repo uses `.zintrust-internal-project-root` as a sentinel during core test runs.
    // In this mode, we must *never* import project config templates.
    // This also avoids test suites that mock `existsSync()` globally (e.g. CoverageBoost).
    const exists = () => (isInternalRoot ? false : candidates.some((c) => existsSync(c)));
    const resolveFirstExistingPath = () => {
        if (isInternalRoot)
            return candidates[0] ?? resolveWithinProjectRoot(projectRoot, relativePath);
        for (const candidate of candidates) {
            if (existsSync(candidate))
                return candidate;
        }
        return candidates[0] ?? resolveWithinProjectRoot(projectRoot, relativePath);
    };
    const get = async () => {
        if (isInternalRoot) {
            throw ErrorFactory.createNotFoundError('Project file not found', {
                projectRoot,
                relativePath,
                candidates,
            });
        }
        const candidate = candidates.find((c) => existsSync(c));
        if (candidate === undefined) {
            throw ErrorFactory.createNotFoundError('Project file not found', {
                projectRoot,
                relativePath,
                candidates,
            });
        }
        try {
            const mod = await importModule(candidate);
            if (Object.hasOwn(mod, 'default') && mod.default !== undefined) {
                return mod.default;
            }
            return mod;
        }
        catch (error) {
            throw ErrorFactory.createTryCatchError('Failed to import project file', {
                candidate,
                projectRoot,
                relativePath,
                error,
            });
        }
    };
    return Object.freeze({
        candidates: () => candidates,
        path: resolveFirstExistingPath,
        exists,
        get,
    });
};
export default useFileLoader;
