import { safeJsonStringify } from "../../../util/log-sanitizer.js";
import { getLogger } from "../../../util/logger.js";
/**
 * Cache for expensive TypeScript type checking
 * - Longer cache duration since type checking is expensive
 * - Intelligent invalidation based on file changes
 * - Debouncing to prevent multiple checks during rapid editing
 */
export class TypeScriptCache {
    pendingCheck = null;
    cachedResult = null;
    abortController = null;
    debounceTimer = null;
    // Cache TypeScript results for 5 minutes since they're expensive to compute
    CACHE_TTL = 5 * 60 * 1000;
    DEBOUNCE_DELAY = 1000; // 1 second debounce
    /**
     * Start a debounced TypeScript check
     */
    startEagerCheck(typeCheckFunction) {
        // Clear any existing debounce timer
        if (this.debounceTimer) {
            clearTimeout(this.debounceTimer);
        }
        // Debounce the type check to avoid multiple checks during rapid editing
        this.debounceTimer = setTimeout(() => {
            this.startImmediateCheck(typeCheckFunction);
        }, this.DEBOUNCE_DELAY);
    }
    /**
     * Start an immediate TypeScript check (used internally and for testing)
     */
    startImmediateCheck(typeCheckFunction) {
        // Cancel existing check
        if (this.abortController) {
            this.abortController.abort();
        }
        this.abortController = new AbortController();
        this.pendingCheck = this.runTypeCheck(typeCheckFunction);
    }
    /**
     * Get cached TypeScript result if available, otherwise wait for pending check
     */
    async getResult() {
        if (this.pendingCheck) {
            const result = await this.pendingCheck;
            this.cachedResult = result;
            this.pendingCheck = null;
            getLogger().info("[TypeScriptCache] Returning fresh TypeScript check result");
            return result;
        }
        if (this.cachedResult) {
            const age = Date.now() - this.cachedResult.timestamp;
            if (age < this.CACHE_TTL) {
                getLogger().info(`[TypeScriptCache] Returning cached TypeScript result (age: ${Math.round(age / 1000)}s)`);
                return this.cachedResult;
            }
        }
        // No cached result or it's too old
        throw new Error("No TypeScript check result available");
    }
    /**
     * Check if we have a fresh cached result
     */
    hasFreshResult() {
        if (!this.cachedResult)
            return false;
        const age = Date.now() - this.cachedResult.timestamp;
        return age < this.CACHE_TTL;
    }
    /**
     * Invalidate the cache (e.g., when significant code changes occur)
     */
    invalidate() {
        const hadCache = !!this.cachedResult;
        const cacheAge = this.cachedResult
            ? Date.now() - this.cachedResult.timestamp
            : 0;
        this.cachedResult = null;
        if (this.abortController) {
            this.abortController.abort();
            this.abortController = null;
        }
        if (this.pendingCheck) {
            this.pendingCheck = null;
        }
        if (hadCache) {
            getLogger().info(`[TypeScriptCache] Cache invalidated (was ${Math.round(cacheAge / 1000)}s old)`);
        }
    }
    async runTypeCheck(typeCheckFunction) {
        try {
            const result = await typeCheckFunction();
            return {
                ...result,
                timestamp: Date.now(),
            };
        }
        catch (error) {
            if (error instanceof Error && error.name === "AbortError") {
                return {
                    status: "success",
                    typeErrors: "",
                    timestamp: Date.now(),
                };
            }
            throw error;
        }
    }
}
/**
 * Cache for context errors (build system and runtime errors)
 * - Short cache duration since these are cheap to read from context
 * - Updated whenever errors are added/removed from context
 */
export class ContextErrorsCache {
    cachedResult = null;
    // Cache context errors for 10 seconds since they're cheap to compute
    CACHE_TTL = 10 * 1000;
    /**
     * Get context errors, using cache if fresh, but always recalculate error ages
     */
    getResult(contextErrorsFunction) {
        if (this.cachedResult) {
            const age = Date.now() - this.cachedResult.timestamp;
            if (age < this.CACHE_TTL) {
                // Return cached result but recalculate ages for accurate reporting
                return this.addFreshAges(this.cachedResult);
            }
        }
        // Compute fresh result
        const result = contextErrorsFunction();
        this.cachedResult = {
            ...result,
            timestamp: Date.now(),
        };
        return this.addFreshAges(this.cachedResult);
    }
    /**
     * Add fresh age calculations to build system errors
     */
    addFreshAges(result) {
        const now = Date.now();
        return {
            ...result,
            buildSystemErrors: result.buildSystemErrors.map((error) => ({
                ...error,
                age: error.timestamp ? now - error.timestamp : 0,
            })),
        };
    }
    /**
     * Invalidate the cache when context errors change
     */
    invalidate() {
        this.cachedResult = null;
    }
}
/**
 * Tracks runtime errors over the entire session to detect when errors
 * mysteriously disappear without being fixed.
 */
export class RuntimeErrorHistoryTracker {
    errorHistory = new Map();
    /**
     * Record that an error was seen in this debug cycle
     */
    recordError(errorKey) {
        const existing = this.errorHistory.get(errorKey);
        const now = Date.now();
        if (existing) {
            existing.lastSeen = now;
            existing.seenCount++;
            existing.resolved = false;
        }
        else {
            this.errorHistory.set(errorKey, {
                errorKey,
                firstSeen: now,
                lastSeen: now,
                seenCount: 1,
                resolved: false,
            });
        }
    }
    /**
     * Check for errors that were previously seen but are no longer appearing
     * Returns warnings about potentially unresolved errors
     */
    getUnresolvedDisappearances(currentErrorKeys) {
        const disappeared = [];
        const now = Date.now();
        const DISAPPEARANCE_THRESHOLD = 30 * 1000; // 30 seconds
        for (const [errorKey, history] of this.errorHistory.entries()) {
            // Error was seen before but isn't in current errors
            if (!currentErrorKeys.has(errorKey) && !history.resolved) {
                const timeSinceLastSeen = now - history.lastSeen;
                // Only flag if it disappeared recently (might have been fixed)
                if (timeSinceLastSeen < DISAPPEARANCE_THRESHOLD) {
                    disappeared.push(history);
                }
            }
        }
        return disappeared;
    }
    /**
     * Mark errors as resolved when code changes fix them
     */
    markResolved(errorKeys) {
        const now = Date.now();
        for (const key of errorKeys) {
            const history = this.errorHistory.get(key);
            if (history) {
                history.resolved = true;
                history.resolvedAt = now;
            }
        }
    }
    /**
     * Mark all tracked errors that are NOT present in the provided current set as resolved.
     * Useful after a clean debug run where no runtime errors are present.
     */
    markAllNotInCurrentAsResolved(currentErrorKeys) {
        const now = Date.now();
        for (const [key, history] of this.errorHistory.entries()) {
            if (!currentErrorKeys.has(key)) {
                history.resolved = true;
                history.resolvedAt = now;
            }
        }
    }
    /**
     * Clear history (called on new generation)
     */
    clear() {
        this.errorHistory.clear();
    }
}
/**
 * Combined debug cache that manages both TypeScript and context error caches
 */
export class DebugCache {
    typeScriptCache = new TypeScriptCache();
    contextErrorsCache = new ContextErrorsCache();
    errorHistoryTracker = new RuntimeErrorHistoryTracker();
    /**
     * Start an eager TypeScript check (debounced)
     */
    startEagerTypeScriptCheck(typeCheckFunction) {
        this.typeScriptCache.startEagerCheck(typeCheckFunction);
    }
    /**
     * Invalidate context errors cache when errors are added/removed
     */
    invalidateContextErrors() {
        this.contextErrorsCache.invalidate();
    }
    /**
     * Invalidate TypeScript cache when significant code changes occur
     */
    invalidateTypeScript() {
        this.typeScriptCache.invalidate();
    }
    /**
     * Get combined debug result
     */
    async getCombinedResult(typeCheckFunction, contextErrorsFunction) {
        // Get TypeScript errors (may be cached or trigger new check)
        let typeScriptResult;
        try {
            typeScriptResult = await this.typeScriptCache.getResult();
        }
        catch {
            // No cached result, start immediate check
            this.typeScriptCache.startImmediateCheck(typeCheckFunction);
            typeScriptResult = await this.typeScriptCache.getResult();
        }
        // Get context errors (cached or fresh)
        const contextResult = this.contextErrorsCache.getResult(contextErrorsFunction);
        // Combine results
        const hasErrors = Boolean(typeScriptResult.typeErrors ||
            contextResult.buildSystemErrors.length ||
            contextResult.runtimeErrors.length);
        return {
            status: hasErrors ? "errors_found" : "success",
            message: hasErrors
                ? "Found errors that need to be fixed"
                : "All checks passed successfully",
            typeErrors: typeScriptResult.typeErrors,
            buildSystemErrors: contextResult.buildSystemErrors,
            runtimeErrors: contextResult.runtimeErrors,
            timestamp: Math.max(typeScriptResult.timestamp, contextResult.timestamp),
        };
    }
    /**
     * Check if we have fresh TypeScript results (useful for optimization)
     */
    hasFreshTypeScriptResult() {
        return this.typeScriptCache.hasFreshResult();
    }
    /**
     * Get the error history tracker for runtime error persistence
     */
    getErrorHistoryTracker() {
        return this.errorHistoryTracker;
    }
}
// Global debug cache instance
export const debugCache = new DebugCache();
/**
 * Create TypeScript check function
 */
export function createTypeScriptCheckFunction(appShell, abortSignal) {
    return async () => {
        const tsErrors = await appShell.runTypecheckRawOutput(abortSignal);
        const maxLines = 30;
        let clampedTsErrors = tsErrors ?? "";
        if (clampedTsErrors) {
            const lines = clampedTsErrors.split("\n");
            if (lines.length > maxLines) {
                clampedTsErrors =
                    lines.slice(0, maxLines).join("\n") +
                        `\n... (${lines.length - maxLines} more lines truncated)`;
            }
        }
        return {
            status: clampedTsErrors ? "errors_found" : "success",
            typeErrors: clampedTsErrors,
            timestamp: Date.now(),
        };
    };
}
/**
 * Create context errors function
 */
export function createRuntimeAndBuildErrorsFunction(clark, appShell) {
    return () => {
        const buildSystemErrors = [];
        const now = Date.now();
        const lastFileModificationTime = clark.context.lastFileModificationTime ?? 0;
        if (clark.context.buildSystemErrors?.length) {
            const rootDir = new RegExp(appShell.appRootDirPath, "g");
            const MAX_ERROR_AGE = 5 * 60 * 1000;
            // Filter out errors older than MAX_ERROR_AGE
            const freshErrors = clark.context.buildSystemErrors.filter((e) => {
                const errorAge = now - (e.timestamp || 0);
                return errorAge < MAX_ERROR_AGE;
            });
            getLogger().info("createRuntimeAndBuildErrorsFunction:FRESHERRORS", ...freshErrors.map((e) => safeJsonStringify(e)));
            buildSystemErrors.push(...freshErrors.map((e) => ({
                message: e.message?.replace(rootDir, "") || "",
                stack: e.stack?.replace(rootDir, ""),
                name: e.name,
                code: e.code,
                timestamp: e.timestamp,
                age: now - (e.timestamp || 0),
            })));
            // If we filtered out stale errors, update the context
            if (freshErrors.length !== clark.context.buildSystemErrors.length) {
                clark.updateContext((context) => ({
                    ...context,
                    buildSystemErrors: freshErrors,
                }));
            }
        }
        let runtimeErrors = [];
        const MAX_RUNTIME_ERRORS = 10;
        if (clark.context.runtimeErrors?.length) {
            const freshRuntimeErrors = clark.context.runtimeErrors.filter((e) => {
                const timestamp = e?.timestamp;
                if (typeof timestamp !== "number") {
                    return true;
                }
                return timestamp >= lastFileModificationTime;
            });
            if (freshRuntimeErrors.length !== clark.context.runtimeErrors.length) {
                clark.updateContext((context) => ({
                    ...context,
                    runtimeErrors: freshRuntimeErrors,
                }));
            }
            // Build stable fingerprints to dedupe by message + type + instance + top frame
            const seenErrors = new Set();
            const dedupedErrors = [];
            const getTopFrame = (stack) => {
                if (!stack)
                    return "";
                const text = String(stack);
                const firstLine = text.split("\n").find((l) => l.trim().length > 0);
                return firstLine ? firstLine.trim() : "";
            };
            for (const e of freshRuntimeErrors) {
                const componentType = e?.identifier?.type || "";
                const instanceId = e?.identifier?.instanceId;
                const sourceId = e?.identifier?.sourceId;
                const sourceIdStr = sourceId ? String(JSON.stringify(sourceId)) : "";
                const errorString = String(e?.error ?? "");
                const errorMessage = errorString.split("\n")[0] || errorString;
                const topFrame = getTopFrame(e?.errorInfo?.componentStack);
                const fingerprintParts = [
                    errorMessage.trim(),
                    componentType,
                    instanceId ?? sourceIdStr,
                    topFrame,
                ].filter(Boolean);
                const fingerprint = fingerprintParts.join("::");
                const dedupeKey = fingerprint || `${errorMessage}::${componentType}`;
                if (!seenErrors.has(dedupeKey)) {
                    seenErrors.add(dedupeKey);
                    // Attach fingerprint so downstream tools can use stable identity
                    dedupedErrors.push({ ...e, fingerprint });
                }
            }
            // Limit the number of runtime errors to avoid overwhelming the model
            const totalErrors = dedupedErrors.length;
            const limitedErrors = dedupedErrors.slice(0, MAX_RUNTIME_ERRORS);
            runtimeErrors = limitedErrors.map((e) => ({
                error: e.error,
                errorInfo: e.errorInfo,
                componentType: e.identifier?.type,
                sourceId: e.identifier?.sourceId,
                fingerprint: e.fingerprint,
            }));
            // Add a note if we truncated errors
            if (totalErrors > MAX_RUNTIME_ERRORS) {
                runtimeErrors.push({
                    error: `... and ${totalErrors - MAX_RUNTIME_ERRORS} more runtime error(s). Fix the errors above first, then re-run to see remaining errors.`,
                    errorInfo: {},
                    componentType: undefined,
                    sourceId: undefined,
                });
            }
        }
        const hasErrors = Boolean(buildSystemErrors.length || runtimeErrors.length);
        return {
            buildSystemErrors,
            runtimeErrors,
            status: hasErrors ? "errors_found" : "success",
            timestamp: Date.now(),
        };
    };
}
//# sourceMappingURL=debug-cache.js.map