import path from "path";
import { traverse } from "@babel/core";
import { parse } from "@babel/parser";
import { SOURCE_ID_ATTRIBUTE } from "@superblocksteam/library-shared";
import { AiGenerationState, } from "@superblocksteam/library-shared/types";
import { isEqual } from "lodash-es";
import { getProperties } from "../../../parsing/jsx.js";
import { getErrorMeta, getLogger } from "../../../util/logger.js";
import { debugCache } from "../../agent/tools/debug-cache.js";
import { Paths } from "../../const.js";
import { matchMultiplePaths } from "../../transform/shared.js";
import { APP_RUNTIME_UPDATED_WITHOUT_EDITS, LLM_FINISHED, POST_PROCESSING_ERRORED, POST_PROCESSING_FINISHED, } from "../clark-fsm.js";
import { sendChangeInfoHelper } from "../helpers/change-info.js";
import { classificationHelper } from "../helpers/classification.js";
import { sendUserGenerationStateChannel, sendUserMessageChannel, } from "../helpers/peer.js";
import { transitionFrom } from "../helpers/transition.js";
export const doPostProcessing = (clark, { draftInterface, appShell, chatSessionStore, fileSystemInterface: sourceTrackerInterface, signals, }) => {
    const transitionTo = transitionFrom(clark);
    const sendChangeInfo = sendChangeInfoHelper(clark, chatSessionStore);
    const sendUserMessage = sendUserMessageChannel(clark, chatSessionStore);
    const sendUserGenerationState = sendUserGenerationStateChannel(clark);
    const { isFullAppOrBroadUpdate } = classificationHelper(clark);
    const createDraftFiles = async (files) => {
        for (const file of files) {
            // ui > page/api change infos: ready dot
            void sendChangeInfo({
                type: "ready",
                filePath: file.filePath,
            });
        }
        await Promise.all(files.map((file) => draftInterface.createDraftFile(file.filePath, file.content)));
        // Immediately invalidate TypeScript cache since we just wrote files
        if (files.length > 0) {
            debugCache.invalidateTypeScript();
            // Update last modification time
            clark.updateContext({
                lastFileModificationTime: Date.now(),
            });
        }
    };
    // send ui messages and invoke package installer
    const handleDependencies = async (_packageJson) => {
        // NOTE(colin): change info for packages is disabled for the demo
        // const packageChanges = await parsePackageJsonChanges(
        //   draftInterface,
        //   packageJson.content,
        // );
        // ui > deps change infos: spin
        // for (const change of packageChanges) {
        //   void sendChangeInfo({
        //     type: "pending",
        //     filePath: renderPath(DependencyInfoPath, {
        //       packageName: change.package,
        //     }),
        //     action: change.type,
        //   });
        // }
        // any previous package changes that are still marked ready
        // should be treated as deleted
        // const previousReadyChanges = Object.values(
        //   clark.context.generatedChangeInfo ?? {},
        // ).filter(
        //   (change) =>
        //     change.status === "ready" &&
        //     change.action !== "delete" &&
        //     change.filePath.startsWith(DependencyInfoPath),
        // );
        // // ui > deps change infos: delete
        // for (const change of previousReadyChanges) {
        //   sendChangeInfo({
        //     type: "ready",
        //     filePath: change.filePath,
        //     action: "delete",
        //   });
        // }
        await appShell.runNpmInstall(clark.context.abortController?.signal);
        // ui > deps change infos: checkmark
        // for (const change of packageChanges) {
        //   sendChangeInfo({
        //     type: "validated",
        //     filePath: renderPath(DependencyInfoPath, {
        //       packageName: change.package,
        //     }),
        //   });
        // }
    };
    return async ({ event }) => {
        switch (event.type) {
            case LLM_FINISHED: {
                const { files } = event;
                // we have already drafted the generated apis into yaml at this point,
                // but the api builder files are still in the event's files list
                const scopePaths = matchMultiplePaths(Paths.PageScopes, Paths.AppScope);
                const pageAndComponentPaths = matchMultiplePaths(Paths.PageIndexes, Paths.ComponentsFile, Paths.ComponentsFolder);
                try {
                    const { packageJson, scopes, pagesAndComponents, specificEdits, themeFiles, } = files.reduce((acc, file) => {
                        if (file.filePath === Paths.PackageJson) {
                            acc.packageJson = file;
                        }
                        else if (scopePaths(file.filePath).matched) {
                            acc.scopes.push(file);
                        }
                        else if (pageAndComponentPaths(file.filePath).matched) {
                            if (file.type === "component") {
                                acc.specificEdits.push(file);
                            }
                            else {
                                acc.pagesAndComponents.push(file);
                            }
                        }
                        else if (file.filePath === Paths.AppTheme) {
                            acc.themeFiles.push(file);
                        }
                        return acc;
                    }, {
                        packageJson: null,
                        scopes: [],
                        pagesAndComponents: [],
                        specificEdits: [],
                        themeFiles: [],
                    });
                    const useSecurityAgent = !clark.context.usedDebugging && isFullAppOrBroadUpdate();
                    if (useSecurityAgent) {
                        void sendUserMessage({
                            type: "text",
                            group: "agent-engineer",
                            text: "Okay, my first pass at this app is complete. Let me pass off to the Security Agent to review.",
                        });
                        // ui > control message A: spin
                        void sendUserMessage({
                            type: "control",
                            group: "agent-security/libraries",
                            status: "pending",
                            text: "Checking external libraries",
                        }, 500);
                        // ui > control message A: checkmark
                        void sendUserMessage({
                            type: "control",
                            group: "agent-security/libraries",
                            status: "validated",
                            text: "Checking external libraries",
                        }, 1000);
                    }
                    if (packageJson) {
                        await createDraftFiles([packageJson]);
                        await handleDependencies(packageJson);
                    }
                    await createDraftFiles(scopes);
                    await createDraftFiles(pagesAndComponents);
                    await createDraftFiles(themeFiles);
                    if (specificEdits.length > 0) {
                        let editedCount = 0;
                        try {
                            for (const componentEdit of specificEdits) {
                                const wasEdited = await handleComponentEdit(componentEdit, sourceTrackerInterface);
                                if (wasEdited) {
                                    editedCount++;
                                }
                            }
                            if (editedCount > 0) {
                                getLogger().info(`Successfully edited ${editedCount} out of ${specificEdits.length} components`);
                            }
                            else {
                                getLogger().info(`No component edits were necessary (${specificEdits.length} components processed)`);
                            }
                        }
                        catch (error) {
                            getLogger().error(`Error handling component edits: ${error}`, getErrorMeta(error));
                        }
                        if (editedCount > 0) {
                            const changes = await sourceTrackerInterface.getAndFlushChanges();
                            const changesFileArtifacts = changes.map((change) => ({
                                type: "file",
                                filePath: path.relative(appShell.appRootDirPath, change.fileName),
                                content: change.source,
                            }));
                            await createDraftFiles(changesFileArtifacts);
                            // this allows the UI to show provisional specific edits before the debug loop
                            void sendUserGenerationState({
                                state: AiGenerationState.DEBUGGING,
                                hasSuggestions: false,
                            });
                        }
                        else {
                            getLogger().warn("The LLM returned specific edits, but no properties were changed");
                        }
                    }
                    else {
                        if (packageJson &&
                            scopes.length === 0 &&
                            pagesAndComponents.length === 0) {
                            // there were no changes
                            void transitionTo({
                                type: APP_RUNTIME_UPDATED_WITHOUT_EDITS,
                            });
                        }
                    }
                    if (useSecurityAgent) {
                        // ui > control message B: spin
                        void sendUserMessage({
                            type: "control",
                            group: "agent-security/authz",
                            status: "pending",
                            text: "Reviewing authorization logic",
                        });
                        // ui > control message B: checkmark
                        void sendUserMessage({
                            type: "control",
                            group: "agent-security/authz",
                            status: "validated",
                            text: "Reviewing authorization logic",
                        }, 1000);
                        void sendUserMessage({
                            type: "text",
                            group: "agent-security",
                            text: "Looks good to me! Handing off to the QA agent to review the functionality.",
                        });
                    }
                    void transitionTo({
                        type: POST_PROCESSING_FINISHED,
                        files: files,
                    });
                }
                catch (err) {
                    const error = err instanceof Error ? err : new Error("Unknown error");
                    // Emit failure event
                    signals.emit("generationFailed", error);
                    void transitionTo({
                        type: POST_PROCESSING_ERRORED,
                        error,
                    });
                }
                break;
            }
            default:
                throw new Error(`Unhandled event: ${event}`);
        }
    };
};
// get dependency changes to be shown in the ui
export const parsePackageJsonChanges = async (draftInterface, updatedPackageJsonContent) => {
    // TODO: as an optimization, we could get the datastructure diff form rather than string
    const diff = await draftInterface.getDraftDiff([Paths.PackageJson]);
    if (!diff) {
        return [];
    }
    let updatedPackageJson;
    try {
        updatedPackageJson = JSON.parse(updatedPackageJsonContent);
    }
    catch {
        return [];
    }
    const allCurrentPackages = new Set([
        // we only surface packages in the dependencies object
        ...Object.keys(updatedPackageJson.dependencies || {}),
    ]);
    const changes = [];
    const lines = diff.split("\n");
    const addedPackages = new Set();
    const removedPackages = new Set();
    const packageRegex = /^\s*"([^"]+)":\s*"[^"]*",?$/;
    for (const line of lines) {
        // Skip non-diff content lines
        if (!line.startsWith("+") && !line.startsWith("-")) {
            continue;
        }
        // Skip diff metadata lines (like +++ or ---)
        if (line.startsWith("+++") || line.startsWith("---")) {
            continue;
        }
        const lineContent = line.slice(1); // Remove +, -, or space prefix
        const match = lineContent.match(packageRegex);
        if (match) {
            const packageName = match[1];
            if (!packageName) {
                continue;
            }
            if (line.startsWith("+")) {
                // For additions, verify the package is actually in the final dependencies
                if (allCurrentPackages.has(packageName)) {
                    addedPackages.add(packageName);
                }
            }
            else if (line.startsWith("-")) {
                // For removals, exclude obvious top-level package.json fields
                if (!commonTopLevelPackageJsonFields.has(packageName)) {
                    removedPackages.add(packageName);
                }
            }
        }
    }
    // Determine the type of change for each package
    for (const packageName of addedPackages) {
        if (removedPackages.has(packageName)) {
            // Package appears in both add and remove = edit
            changes.push({ type: "edit", package: packageName });
        }
        else {
            // Package only appears in add = addition
            changes.push({ type: "create", package: packageName });
        }
    }
    for (const packageName of removedPackages) {
        if (!addedPackages.has(packageName)) {
            changes.push({ type: "delete", package: packageName });
        }
    }
    return changes;
};
const commonTopLevelPackageJsonFields = new Set([
    "name",
    "version",
    "description",
    "main",
    "scripts",
    "keywords",
    "author",
    "license",
    "repository",
    "homepage",
    "bugs",
]);
const handleComponentEdit = async (componentEdit, sourceTrackerInterface) => {
    const ast = parse(componentEdit.content, {
        sourceType: "module",
        plugins: ["jsx", "typescript"],
    });
    let nodePath = null;
    traverse(ast, {
        JSXElement(path) {
            nodePath = path;
            path.stop();
        },
    });
    if (!nodePath) {
        getLogger().error(`Failed to generate NodePath for component edit: ${componentEdit.content}`);
        throw new Error("Failed to generate NodePath.");
    }
    const properties = getProperties(nodePath, {
        bindings: [],
        jsxVariables: [],
        libraryComponents: [],
    });
    const id = properties[SOURCE_ID_ATTRIBUTE]?.value;
    if (!id) {
        throw new Error(`Generated component does not have a ${SOURCE_ID_ATTRIBUTE} attribute.`);
    }
    let wasEdited = false;
    const existingElement = sourceTrackerInterface.getElementToLocation(id);
    if (existingElement && existingElement.parentPath?.isJSXElement()) {
        const existingProperties = getProperties(existingElement.parentPath, {
            bindings: [],
            jsxVariables: [],
            libraryComponents: [],
        });
        const changedProperties = {};
        for (const key in properties) {
            if (!isEqual(properties[key], existingProperties[key]) &&
                key !== SOURCE_ID_ATTRIBUTE) {
                changedProperties[key] = properties[key];
            }
        }
        const deletedProperties = [];
        for (const key in existingProperties) {
            if (!(key in properties)) {
                deletedProperties.push(key);
            }
        }
        if (Object.keys(changedProperties).length > 0) {
            sourceTrackerInterface.setProperties({
                source: {
                    id,
                },
                changes: changedProperties,
            });
            wasEdited = true;
        }
        if (deletedProperties.length > 0) {
            await sourceTrackerInterface.deleteProperties({
                source: {
                    id,
                },
                properties: deletedProperties,
            });
            wasEdited = true;
        }
    }
    else {
        delete properties[SOURCE_ID_ATTRIBUTE];
        // fall back to setting all properties
        if (Object.keys(properties).length > 0) {
            sourceTrackerInterface.setProperties({
                source: {
                    id,
                },
                changes: properties,
            });
            wasEdited = true;
        }
    }
    return wasEdited;
};
//# sourceMappingURL=post-processing.js.map