import { basename } from "node:path";
import { getErrorMeta, getLogger } from "../../util/logger.js";
import llmobs from "../llmobs/index.js";
export class FileTransformer {
    spec;
    constructor(src, dest) {
        this.spec = { src, dest };
    }
}
const matcherCache = new Map();
export const matchPath = (pattern) => {
    if (!matcherCache.has(pattern)) {
        matcherCache.set(pattern, compileMatcher(pattern));
    }
    const { regex, paramNames } = matcherCache.get(pattern);
    return (path) => {
        const match = regex.exec(path);
        if (!match)
            return { matched: false };
        const values = match.slice(1);
        const params = {};
        paramNames.forEach((name, i) => {
            params[name] = values[i] ?? "";
        });
        return { matched: true, params: params };
    };
};
export const matchMultiplePaths = (...patterns) => {
    // Pre-compile all matchers and cache them
    const matchers = patterns.map((pattern) => {
        if (!matcherCache.has(pattern)) {
            matcherCache.set(pattern, compileMatcher(pattern));
        }
        return matcherCache.get(pattern);
    });
    return (path) => {
        // Try each pattern in order, return first match
        for (const { regex, paramNames } of matchers) {
            const match = regex.exec(path);
            if (match) {
                const values = match.slice(1);
                const params = {};
                paramNames.forEach((name, i) => {
                    params[name] = values[i] ?? "";
                });
                return { matched: true, params: params };
            }
        }
        return { matched: false };
    };
};
const compileMatcher = (pattern) => {
    const paramNames = [];
    const regexParts = pattern.split("/").map((segment) => {
        if (segment.startsWith(":")) {
            paramNames.push(segment.slice(1));
            return "([^/]+)";
        }
        if (segment === "*")
            return "[^/]+";
        if (segment === "**")
            return ".*";
        if (segment.includes("*"))
            return segment.replace(/\*/g, "[^/]*");
        return segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    });
    const regex = new RegExp(`^${regexParts.join("/")}/?$`);
    return { regex, paramNames };
};
export const renderPath = llmobs.wrap({ kind: "task", name: "renderPath" }, _renderPath);
function _renderPath(path, params) {
    const renderedSegs = path.split("/").map((segment) => {
        if (segment.startsWith(":")) {
            const name = segment.slice(1);
            const value = params[name];
            if (!value)
                throw new Error(`Missing param: ${name}`);
            return encodeURIComponent(value);
        }
        return segment;
    });
    return renderedSegs.join("/");
}
const pathKey = Symbol("pathKey");
export const applyFileTransformations = llmobs.wrap({ kind: "task", name: "applyFileTransformations" }, _applyFileTransformations);
async function _applyFileTransformations(files, transformers) {
    const logger = getLogger();
    let currentFiles = files;
    logger.debug(`[ai-service] Starting file transformations for ${currentFiles.map((a) => a.filePath).join(", ")} with transformers: ${transformers
        .map((t) => t.constructor.name)
        .join(", ")}`);
    for (const transformer of transformers) {
        logger.debug(`[ai-service] Applying transformer: ${transformer.constructor.name}`);
        const matchingInput = {};
        // Determine which files match each pattern in the transformer's src
        for (const [key, path] of Object.entries(transformer.spec.src)) {
            logger.debug(`[ai-service] Matching files for pattern: ${path} (key: ${key})`);
            const exec = matchPath(path);
            matchingInput[key] = currentFiles
                .map((file) => {
                const match = exec(file.filePath);
                return match.matched
                    ? {
                        // we don't want individual transformers to know/rely on the path structure,
                        // but we track it within this function for bookkeeping
                        [pathKey]: file.filePath,
                        fileName: basename(file.filePath),
                        pathParams: match.params,
                        content: file.content,
                    }
                    : null;
            })
                .filter(Boolean);
        }
        // Skip if no files matched any patterns
        if (Object.values(matchingInput).every((arr) => arr.length === 0)) {
            logger.debug(`[ai-service] No matching files for transformer: ${transformer.constructor.name}, skipping.`);
            continue;
        }
        logger.debug("[ai-service] Transforming files", JSON.stringify({
            transformer: transformer.constructor.name,
            matchingFilesMap: Object.fromEntries(Object.entries(matchingInput).map(([key, inputs]) => [
                key,
                inputs.map((input) => ({
                    filePath: input[pathKey],
                    pathParams: input.pathParams,
                })),
            ])),
        }));
        const processedFiles = [];
        try {
            const output = await transformer.transform(matchingInput);
            for (const [key, outputForKey] of Object.entries(output)) {
                const pathSpec = transformer.spec.dest[key];
                if (!pathSpec) {
                    throw new Error(`Missing dest path for key: ${key}`);
                }
                for (const { pathParams, content } of outputForKey) {
                    const path = renderPath(pathSpec, pathParams);
                    processedFiles.push({
                        filePath: path,
                        content,
                        type: "file",
                    });
                }
            }
        }
        catch (error) {
            logger.error(`[ai-service] Error in transformer ${transformer.constructor.name}:`, getErrorMeta(error));
            logger.info("matchingFilesMap:", matchingInput);
            throw error;
        }
        // Get all unique file paths that were processed
        const matchedFilePaths = new Set();
        Object.values(matchingInput).forEach((fileArray) => {
            fileArray.forEach((input) => matchedFilePaths.add(input[pathKey]));
        });
        const unmatchedFiles = currentFiles.filter((file) => !matchedFilePaths.has(file.filePath));
        // Combine non-processed files with transformed files
        currentFiles = [...unmatchedFiles, ...processedFiles];
    }
    return currentFiles;
}
//# sourceMappingURL=shared.js.map