/**
 * Error Pages Static Assets
 * Serves /error-pages/* assets (CSS/JS/SVG/etc) used by HTML error templates.
 */
import { Cloudflare } from '../config/cloudflare.js';
import { HTTP_HEADERS, MIME_TYPES } from '../config/constants.js';
import { MIME_TYPES_MAP, resolveSafePath, tryDecodeURIComponent } from './common.js';
import { getFrameworkPublicRoots } from './publicRoot.js';
import { Router } from './Router.js';
import { ErrorFactory } from '../exceptions/ZintrustError.js';
import * as fs from '../node-singletons/fs.js';
import * as path from '../node-singletons/path.js';
let cachedCandidateRoots = null;
const getCandidatePublicRoots = () => {
    const cwd = process.cwd();
    if (cachedCandidateRoots !== null && cachedCandidateRoots.cwd === cwd) {
        return cachedCandidateRoots.roots;
    }
    const roots = [path.join(cwd, 'public'), ...getFrameworkPublicRoots()];
    const unique = new Set(roots.map((root) => root.trim()).filter((root) => root !== ''));
    const resolved = [...unique];
    cachedCandidateRoots = { cwd, roots: resolved };
    return resolved;
};
const pathExistsAsync = async (candidate) => {
    try {
        await fs.fsPromises.access(candidate);
        return true;
    }
    catch {
        return false;
    }
};
const isFileAsync = async (candidate) => {
    try {
        const stats = await fs.fsPromises.stat(candidate);
        return stats.isFile();
    }
    catch {
        return false;
    }
};
const findFirstExistingFileAsync = async (candidates) => {
    const results = await Promise.all(candidates.map(async (candidate) => ({
        candidate,
        isFile: await isFileAsync(candidate),
    })));
    return results.find((result) => result.isFile)?.candidate;
};
const resolveErrorPageFilePathAsync = async (candidateRoots, normalizedRelative) => {
    const checks = await Promise.all(candidateRoots.map(async (root) => {
        const baseDir = path.join(root, 'error-pages');
        const filePath = resolveSafePath(baseDir, normalizedRelative);
        if (filePath === undefined)
            return undefined;
        try {
            const stats = await fs.fsPromises.stat(filePath);
            if (stats.isDirectory())
                return undefined;
            return filePath;
        }
        catch {
            return undefined;
        }
    }));
    return checks.find((filePath) => typeof filePath === 'string');
};
const servePublicRootFileAsync = async (relativePath, response, contentType) => {
    const candidateRoots = getCandidatePublicRoots();
    const candidates = candidateRoots.map((root) => path.join(root, relativePath));
    const filePath = await findFirstExistingFileAsync(candidates);
    try {
        if (filePath === undefined || filePath === null) {
            response.setStatus(404);
            response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
            response.send('Not Found');
            return false;
        }
        const content = await fs.fsPromises.readFile(filePath);
        response.setStatus(200);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, contentType);
        response.send(content);
        return true;
    }
    catch (error) {
        ErrorFactory.createTryCatchError(`Error serving public file ${filePath}`, error);
        response.setStatus(500);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
        response.send('Internal Server Error');
        return false;
    }
};
const findFirstExistingFile = (candidates) => {
    for (const candidate of candidates) {
        try {
            if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
                return candidate;
        }
        catch {
            // ignore and continue
        }
    }
    return undefined;
};
const servePublicRootFile = (relativePath, response, contentType) => {
    const candidateRoots = getCandidatePublicRoots();
    const candidates = candidateRoots.map((root) => path.join(root, relativePath));
    const filePath = findFirstExistingFile(candidates);
    try {
        if (filePath === undefined || filePath === null) {
            response.setStatus(404);
            response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
            response.send('Not Found');
            return false;
        }
        const content = fs.readFileSync(filePath);
        response.setStatus(200);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, contentType);
        response.send(content);
        return true;
    }
    catch (error) {
        ErrorFactory.createTryCatchError(`Error serving public file ${filePath}`, error);
        response.setStatus(500);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
        response.send('Internal Server Error');
        return false;
    }
};
const serveAssetsFileAsync = async (urlPath, response) => {
    const assets = Cloudflare.getAssetsBinding();
    if (!assets)
        return false;
    try {
        const url = new URL(urlPath, 'https://assets.local');
        const res = await assets.fetch(url.toString());
        const contentType = res.headers.get(HTTP_HEADERS.CONTENT_TYPE) ?? MIME_TYPES.TEXT;
        const body = Buffer.from(await res.arrayBuffer());
        response.setStatus(res.status);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, contentType);
        response.send(body);
        return true;
    }
    catch (error) {
        ErrorFactory.createTryCatchError(`Error serving asset ${urlPath}`, error);
        response.setStatus(500);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
        response.send('Internal Server Error');
        return true;
    }
};
export const serveErrorPagesFile = (urlPath, response) => {
    if (urlPath === '/error-pages' || urlPath === '/error-pages/') {
        response.setStatus(404);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
        response.send('Not Found');
        return true;
    }
    if (!urlPath.startsWith('/error-pages/'))
        return false;
    const candidateRoots = getCandidatePublicRoots();
    const rawRelative = urlPath.slice('/error-pages/'.length);
    const normalizedRelative = tryDecodeURIComponent(rawRelative).replaceAll('\\', '/');
    for (const root of candidateRoots) {
        const baseDir = path.join(root, 'error-pages');
        const filePath = resolveSafePath(baseDir, normalizedRelative);
        if (filePath === undefined)
            continue;
        try {
            if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
                continue;
            }
            if (!fs.existsSync(filePath)) {
                continue;
            }
            const ext = path.extname(filePath).toLowerCase();
            const contentType = MIME_TYPES_MAP[ext] || 'application/octet-stream';
            const content = fs.readFileSync(filePath);
            response.setStatus(200);
            response.setHeader(HTTP_HEADERS.CONTENT_TYPE, contentType);
            response.send(content);
            return true;
        }
        catch (error) {
            ErrorFactory.createTryCatchError(`Error serving error-pages file ${filePath}`, error);
            response.setStatus(500);
            response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
            response.send('Internal Server Error');
            return true;
        }
    }
    response.setStatus(404);
    response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
    response.send('Not Found');
    return true;
};
export const serveErrorPagesFileAsync = async (urlPath, response) => {
    if (urlPath === '/error-pages' || urlPath === '/error-pages/') {
        response.setStatus(404);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
        response.send('Not Found');
        return true;
    }
    if (!urlPath.startsWith('/error-pages/'))
        return false;
    if (Cloudflare.getWorkersEnv() !== null) {
        return serveAssetsFileAsync(urlPath, response);
    }
    const candidateRoots = getCandidatePublicRoots();
    const rawRelative = urlPath.slice('/error-pages/'.length);
    const normalizedRelative = tryDecodeURIComponent(rawRelative).replaceAll('\\', '/');
    const filePath = await resolveErrorPageFilePathAsync(candidateRoots, normalizedRelative);
    try {
        if (filePath !== undefined && (await pathExistsAsync(filePath))) {
            const ext = path.extname(filePath).toLowerCase();
            const contentType = MIME_TYPES_MAP[ext] || 'application/octet-stream';
            const content = await fs.fsPromises.readFile(filePath);
            response.setStatus(200);
            response.setHeader(HTTP_HEADERS.CONTENT_TYPE, contentType);
            response.send(content);
            return true;
        }
    }
    catch (error) {
        ErrorFactory.createTryCatchError(`Error serving error-pages file ${filePath}`, error);
        response.setStatus(500);
        response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
        response.send('Internal Server Error');
        return true;
    }
    response.setStatus(404);
    response.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
    response.send('Not Found');
    return true;
};
const handleErrorPagesRequest = async (req, res) => {
    const urlPath = req.getPath();
    if (await serveErrorPagesFileAsync(urlPath, res))
        return;
    res.setStatus(404);
    res.setHeader(HTTP_HEADERS.CONTENT_TYPE, MIME_TYPES.TEXT);
    res.send('Not Found');
};
const handleZintrustSvgRequest = async (_req, res) => {
    if (Cloudflare.getWorkersEnv() !== null) {
        await serveAssetsFileAsync('/zintrust.svg', res);
        return;
    }
    await servePublicRootFileAsync('zintrust.svg', res, MIME_TYPES.SVG);
};
export const serveZintrustSvgFile = (response) => {
    return servePublicRootFile('zintrust.svg', response, MIME_TYPES.SVG);
};
export const serveZintrustSvgFileAsync = async (response) => {
    if (Cloudflare.getWorkersEnv() !== null) {
        return serveAssetsFileAsync('/zintrust.svg', response);
    }
    return servePublicRootFileAsync('zintrust.svg', response, MIME_TYPES.SVG);
};
export const registerErrorPagesRoutes = (router) => {
    Router.get(router, '/error-pages', handleErrorPagesRequest);
    Router.get(router, '/error-pages/', handleErrorPagesRequest);
    Router.get(router, '/error-pages/:path*', handleErrorPagesRequest);
    // Used by the HTML error templates (logo in footer).
    Router.get(router, '/zintrust.svg', handleZintrustSvgRequest);
};
export default { registerErrorPagesRoutes, serveErrorPagesFile };
