import { readFile, writeFile, resolve } from "../../utils/file-utils";
import { ManifestSchema, DEFAULT_CONFIG_PATH } from "./schemas";
import { definePlugin } from "../../utils/plugin-utils";
import { normalizeImplementationMetaToAppItem, splitVersionedKey, isSnakeCasedSlug, dashifySnakeCasedSlug, toAppLocator, isResolvedAppLocator, } from "../../utils/domain-utils";
import { extractCursor } from "../../utils/function-utils";
import { paginate } from "../../utils/pagination-utils";
import { toArrayFromAsync } from "../../utils/array-utils";
/**
 * Parse manifest content from a string
 */
function parseManifestContent(content, source) {
    try {
        const parsed = JSON.parse(content);
        if (parsed == null || typeof parsed !== "object") {
            return null;
        }
        const result = ManifestSchema.safeParse(parsed);
        if (result.success) {
            return result.data;
        }
        console.warn(`⚠️ Invalid manifest format in ${source}: ${result.error}`);
        return null;
    }
    catch (error) {
        console.warn(`⚠️ Failed to parse manifest from ${source}:`, error);
        return null;
    }
}
/**
 * Read manifest from a file path asynchronously
 * Supports local files (Node.js) and browser environments (fallback to global filesystem)
 */
export async function readManifestFromFile(filePath) {
    try {
        // Resolve relative paths relative to current working directory
        const resolvedPath = await resolve(filePath);
        const content = await readFile(resolvedPath);
        return parseManifestContent(content, resolvedPath);
    }
    catch (error) {
        // Silently return null for "file not found" errors (expected for first-time users)
        const isFileNotFound = (error &&
            typeof error === "object" &&
            "code" in error &&
            error.code === "ENOENT") ||
            (error instanceof Error && error.message.includes("File not found"));
        if (!isFileNotFound) {
            console.warn(`⚠️ Failed to read manifest from ${filePath}`);
        }
        return null;
    }
}
/**
 * Write manifest to a file path asynchronously
 * Supports local files (Node.js) and browser environments (fallback to global filesystem)
 */
async function writeManifestToFile(manifest, filePath) {
    const resolvedPath = await resolve(filePath);
    await writeFile(resolvedPath, JSON.stringify(manifest, null, 2));
}
/**
 * Get the preferred key for storing an app in the manifest
 * Extracted from plugin to make it easier to test
 */
export async function getPreferredManifestEntryKey({ appKey, api, }) {
    const locator = toAppLocator(appKey);
    // If we have a slug, prefer it
    if (locator.slug) {
        return locator.slug;
    }
    // If we have implementation name, look up the latest to get the slug
    if (locator.implementationName) {
        try {
            // API call to get app metadata by implementation name using selected_apis parameter
            const implementationsEnvelope = await api.get(`/zapier/api/v4/implementations-meta/lookup/`, {
                searchParams: {
                    selected_apis: locator.implementationName,
                },
            });
            if (implementationsEnvelope.results.length > 0 &&
                implementationsEnvelope.results[0].slug) {
                return implementationsEnvelope.results[0].slug;
            }
        }
        catch {
            // Fall back to implementation name if lookup fails
        }
        return locator.implementationName;
    }
    // Fall back to original key if we can't determine anything better
    return locator.lookupAppKey;
}
async function listAppsForSlugsPage({ slugs, cursor, api, }) {
    const searchParams = {};
    if (slugs.length > 0) {
        searchParams.slugs = slugs.join(",");
    }
    if (cursor) {
        searchParams.offset = cursor;
    }
    const implementationsEnvelope = await api.get("/zapier/api/v4/implementations-meta/lookup/", {
        searchParams,
    });
    return {
        data: implementationsEnvelope.results.map(normalizeImplementationMetaToAppItem),
        nextCursor: extractCursor(implementationsEnvelope),
    };
}
/**
 * Find a manifest entry by any app key (implementation name, slug, etc.).
 */
export function findManifestEntry({ appKey, manifest, }) {
    if (!manifest.apps) {
        return null;
    }
    const [appKeyWithoutVersion] = splitVersionedKey(appKey);
    // Direct match by key
    if (manifest.apps[appKeyWithoutVersion]) {
        return [appKeyWithoutVersion, manifest.apps[appKeyWithoutVersion]];
    }
    // Handle snake-cased slugs (convert to dashed)
    if (isSnakeCasedSlug(appKey)) {
        const slug = dashifySnakeCasedSlug(appKey);
        if (manifest.apps[slug]) {
            return [slug, manifest.apps[slug]];
        }
    }
    // Search by implementation name in manifest values
    for (const [key, entry] of Object.entries(manifest.apps)) {
        if (entry.implementationName === appKeyWithoutVersion) {
            return [key, entry];
        }
    }
    // No match found
    return null;
}
/**
 * Turn any app keys (slugs, implementation names, etc.) into app locators, which are objects that will be guaranteed to
 * have implementation names. Note that this function will not actually guarantee that implementation names are valid!
 * It will attempt to find the app key in the manifest, or if it's a slug, actually do an API call, or otherwise just
 * assume it's an implementation name that will be used for an API call.
 */
async function resolveAppKeys({ appKeys, api, manifest, }) {
    // Step 1: Convert all app keys to locators
    const locators = appKeys.map(toAppLocator);
    // Step 2: Map to resolved/unresolved via manifest
    const locatorsWithManifest = locators.map((locator) => {
        const manifestEntryResult = findManifestEntry({
            appKey: locator.lookupAppKey,
            manifest,
        });
        if (manifestEntryResult) {
            const [, manifestEntry] = manifestEntryResult;
            // Use manifest entry to resolve implementation name
            const resolvedVersion = locator.version || manifestEntry.version;
            const resolvedLocator = {
                ...locator,
                implementationName: manifestEntry.implementationName,
                version: resolvedVersion,
            };
            return resolvedLocator;
        }
        // If these have an implementationName, they're already resolved.
        return locator;
    });
    // Step 3: Separate resolved from unresolved
    const unresolvedLocators = locatorsWithManifest.filter((locator) => !isResolvedAppLocator(locator));
    // Step 4: Resolve slugs via API
    const slugsToResolve = unresolvedLocators
        .map((locator) => locator.slug)
        .filter((slug) => !!slug)
        .filter((slug, index, array) => array.indexOf(slug) === index); // dedupe
    if (slugsToResolve.length === 0) {
        return locatorsWithManifest.filter(isResolvedAppLocator);
    }
    const iterator = paginate(listAppsForSlugsPage, {
        slugs: slugsToResolve,
        api,
    });
    const pages = await toArrayFromAsync(iterator);
    const apps = pages.flatMap((page) => page.data);
    const slugToAppData = new Map();
    for (const app of apps) {
        if (app.slug) {
            slugToAppData.set(app.slug, app);
        }
    }
    const slugResolvedLocators = locatorsWithManifest.map((locator) => {
        if (isResolvedAppLocator(locator)) {
            return locator;
        }
        if (locator.slug) {
            const appData = slugToAppData.get(locator.slug);
            if (appData) {
                // After resolving from API, check if we have a manifest entry for this implementation name
                const manifestEntryByImplementationName = findManifestEntry({
                    appKey: appData.key, // appData.key is the implementation name
                    manifest,
                });
                let version = locator.version;
                if (!version) {
                    // Prefer manifest version if available, otherwise use API version
                    version = manifestEntryByImplementationName
                        ? manifestEntryByImplementationName[1].version || appData.version
                        : appData.version;
                }
                return {
                    ...locator,
                    implementationName: appData.key,
                    version,
                };
            }
        }
        return locator;
    });
    // Return all resolved locators.
    return slugResolvedLocators.filter(isResolvedAppLocator);
}
export { DEFAULT_CONFIG_PATH } from "./schemas";
export const manifestPlugin = definePlugin((sdk) => {
    const { api, options } = sdk.context;
    const { manifestPath = DEFAULT_CONFIG_PATH, manifest } = options || {};
    let resolvedManifest;
    async function resolveManifest() {
        // If manifest is provided directly, use it
        if (manifest) {
            return manifest;
        }
        // If manifestPath is provided, load from file
        if (manifestPath) {
            return await readManifestFromFile(manifestPath);
        }
        return null;
    }
    const getResolvedManifest = async () => {
        if (typeof resolvedManifest === "undefined") {
            resolvedManifest = (await resolveManifest()) ?? null;
        }
        return resolvedManifest;
    };
    const getVersionedImplementationId = async (appKey) => {
        const resolvedApps = await resolveAppKeys({
            appKeys: [appKey],
            api,
            manifest: (await getResolvedManifest()) ?? { apps: {} },
        });
        const resolvedApp = resolvedApps[0];
        if (!resolvedApp)
            return null;
        return `${resolvedApp.implementationName}@${resolvedApp.version || "latest"}`;
    };
    const updateManifestEntry = async (options) => {
        const { appKey, entry, configPath = DEFAULT_CONFIG_PATH, skipWrite = false, manifest: inputManifest, } = options;
        // Use provided manifest or read from file
        const manifest = inputManifest ||
            (await readManifestFromFile(configPath)) || { apps: {} };
        // Try to find existing entry by direct key first
        let existingEntry = findManifestEntry({
            appKey,
            manifest,
        });
        // If not found directly, try to resolve the app key and search by implementation name
        if (!existingEntry) {
            try {
                const resolvedApps = await resolveAppKeys({
                    appKeys: [appKey],
                    api,
                    manifest,
                });
                if (resolvedApps.length > 0) {
                    const resolvedImplementationName = resolvedApps[0].implementationName;
                    // Try to find entry using the resolved implementation name
                    existingEntry = findManifestEntry({
                        appKey: resolvedImplementationName,
                        manifest,
                    });
                }
            }
            catch {
                // If resolution fails, continue with original logic
            }
        }
        let manifestKey;
        if (existingEntry) {
            // Use existing key to maintain consistency with types files
            manifestKey = existingEntry[0];
        }
        else {
            // Get preferred key for new entries
            manifestKey = await getPreferredManifestEntryKey({
                appKey,
                api,
            });
        }
        const updatedManifest = {
            ...manifest,
            apps: {
                ...manifest.apps,
                [manifestKey]: entry,
            },
        };
        // Conditionally write to file
        if (!skipWrite) {
            await writeManifestToFile(updatedManifest, configPath);
            // Clear the cached manifest so it gets reloaded with the new data
            resolvedManifest = undefined;
        }
        return {
            key: manifestKey,
            entry,
            manifest: updatedManifest,
        };
    };
    const addActionEntry = async (options) => {
        const { name, entry, configPath = DEFAULT_CONFIG_PATH, skipWrite = false, manifest: inputManifest, } = options;
        // Use provided manifest or read from file
        const manifest = inputManifest ||
            (await readManifestFromFile(configPath)) || { apps: {} };
        // Ensure actions exists
        const actions = manifest.actions || {};
        // Validate uniqueness - if action with this name already exists, throw error
        if (actions[name] && !skipWrite) {
            throw new Error(`Action "${name}" already exists. Please choose a different name or remove the existing action first.`);
        }
        const updatedManifest = {
            ...manifest,
            actions: {
                ...actions,
                [name]: entry,
            },
        };
        // Conditionally write to file
        if (!skipWrite) {
            await writeManifestToFile(updatedManifest, configPath);
            // Clear the cached manifest so it gets reloaded with the new data
            resolvedManifest = undefined;
        }
        return {
            name,
            entry,
            manifest: updatedManifest,
        };
    };
    const findActionEntry = ({ name, manifest, }) => {
        return manifest.actions?.[name] || null;
    };
    const listActionEntries = async ({ configPath = DEFAULT_CONFIG_PATH, } = {}) => {
        const manifest = (await readManifestFromFile(configPath)) || { apps: {} };
        return Object.entries(manifest.actions || {});
    };
    const deleteActionEntry = async ({ name, configPath = DEFAULT_CONFIG_PATH, skipWrite = false, }) => {
        const manifest = (await readManifestFromFile(configPath)) || { apps: {} };
        if (!manifest.actions?.[name]) {
            throw new Error(`Action "${name}" does not exist.`);
        }
        const { [name]: removed, ...remainingActions } = manifest.actions;
        const updatedManifest = {
            ...manifest,
            actions: remainingActions,
        };
        if (!skipWrite) {
            await writeManifestToFile(updatedManifest, configPath);
            resolvedManifest = undefined;
        }
        return updatedManifest;
    };
    const hasActionEntry = ({ name, manifest, }) => {
        return !!manifest.actions?.[name];
    };
    return {
        context: {
            getResolvedManifest,
            getVersionedImplementationId,
            resolveAppKeys: async ({ appKeys }) => resolveAppKeys({
                appKeys,
                api,
                manifest: (await getResolvedManifest()) ?? { apps: {} },
            }),
            updateManifestEntry,
            addActionEntry,
            findActionEntry,
            listActionEntries,
            deleteActionEntry,
            hasActionEntry,
            findManifestEntry,
            readManifestFromFile,
            getManifestConnections: async () => {
                const manifest = await getResolvedManifest();
                return manifest?.connections ?? null;
            },
        },
    };
});
