import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import * as crypto from "crypto";
import { logger } from "./logger.js";
/**
 * Per-process registry mapping sessionId → stateFilePath.
 * Resource list callbacks iterate this map so each stdio process only
 * advertises its own sessions, eliminating cross-process leakage when
 * multiple MCP clients share the same filesystem (e.g. /tmp).
 */
const processSessionRegistry = new Map();
/**
 * Cross-repo test directory set by skyramp_analyze_changes when testsRepoDir
 * is provided. Test generation tools read this to rewrite outputDir so files
 * land in the test repo clone instead of the source repo.
 */
let _testsRepoDir;
export function setTestsRepoDir(dir) {
    _testsRepoDir = dir;
}
export function getTestsRepoDir() {
    return _testsRepoDir;
}
/**
 * In-memory session store: sessionId → { data, storedAt }.
 * Eliminates the need for the LLM to read/write state files on disk.
 * The analyze tool stores the full RecommendationState here; the
 * recommend prompt and MCP resources read it.
 *
 * Bounded by TTL (SESSION_TTL_MS) and max entries (MAX_SESSIONS).
 * Eviction runs on every storeSessionData call.
 */
const SESSION_TTL_MS = 2 * 60 * 60 * 1000; // 2 hours
const MAX_SESSIONS = 5;
const inMemorySessionStore = new Map();
function evictStaleSessions() {
    const now = Date.now();
    for (const [id, entry] of inMemorySessionStore) {
        if (now - entry.storedAt > SESSION_TTL_MS) {
            inMemorySessionStore.delete(id);
            processSessionRegistry.delete(id);
        }
    }
    if (inMemorySessionStore.size > MAX_SESSIONS) {
        const sorted = [...inMemorySessionStore.entries()]
            .sort((a, b) => a[1].storedAt - b[1].storedAt);
        const toDrop = sorted.slice(0, sorted.length - MAX_SESSIONS);
        for (const [id] of toDrop) {
            inMemorySessionStore.delete(id);
            processSessionRegistry.delete(id);
        }
    }
}
export function registerSession(sessionId, stateFilePath) {
    processSessionRegistry.set(sessionId, stateFilePath);
}
export function getSessionFilePath(sessionId) {
    return processSessionRegistry.get(sessionId);
}
export function getRegisteredSessions() {
    return processSessionRegistry;
}
export function storeSessionData(sessionId, data) {
    inMemorySessionStore.set(sessionId, { data, storedAt: Date.now() });
    evictStaleSessions();
}
export function getSessionData(sessionId) {
    const entry = inMemorySessionStore.get(sessionId);
    return entry?.data;
}
export function hasSessionData(sessionId) {
    return inMemorySessionStore.has(sessionId);
}
/**
 * Normalize a loaded RecommendationState: if the LLM saved a raw
 * RepositoryAnalysis at the root (without the wrapper), detect it
 * and wrap it properly.
 */
export function normalizeRecommendationState(data) {
    if (!data.analysis && data.apiEndpoints) {
        logger.info("Detected unwrapped RepositoryAnalysis — adapting");
        return {
            repositoryPath: data.repositoryPath || data.metadata?.repositoryName || "unknown",
            analysisScope: data.analysisScope || data.metadata?.analysisScope,
            analysis: data,
        };
    }
    return data;
}
/**
 * File prefix mapping for each state type
 */
const STATE_FILE_PREFIXES = {
    analysis: "skyramp-analysis", // For test maintenance workflow
    recommendation: "skyramp-recommendation", // For test recommendation workflow
};
/**
 * Generic State Manager for persisting workflow data
 * Reduces token usage by storing intermediate results in filesystem
 *
 * Supports two state types:
 * - analysis: Test maintenance workflow (discovery, drift, execution, health)
 * - recommendation: Test recommendation workflow (analyze repo, map tests, recommendations)
 */
export class StateManager {
    stateFile;
    sessionId;
    stateType;
    /**
     * Create a new state manager
     * @param stateType Type of state (analysis, recommendation)
     * @param sessionId Unique session identifier (defaults to UUID)
     * @param stateDir Directory to store state files (defaults to /tmp)
     */
    constructor(stateType = "analysis", sessionId, stateDir, stateFilePath) {
        this.stateType = stateType;
        this.sessionId = sessionId || crypto.randomUUID();
        if (stateFilePath) {
            this.stateFile = stateFilePath;
        }
        else {
            const baseDir = stateDir || os.tmpdir();
            const prefix = STATE_FILE_PREFIXES[stateType];
            this.stateFile = path.join(baseDir, `${prefix}-${this.sessionId}.json`);
        }
    }
    /**
     * Create state manager from a sessionId (resolves the state file path internally)
     */
    static fromSessionId(sessionId, stateType = "recommendation", stateDir) {
        return new StateManager(stateType, sessionId, stateDir);
    }
    /**
     * Create state manager from existing state file path
     */
    static fromStatePath(stateFilePath) {
        const basename = path.basename(stateFilePath, ".json");
        const stateDir = path.dirname(stateFilePath);
        // Determine state type from filename
        let stateType = "analysis";
        let sessionId = basename;
        for (const [type, prefix] of Object.entries(STATE_FILE_PREFIXES)) {
            if (basename.startsWith(prefix)) {
                stateType = type;
                sessionId = basename.replace(`${prefix}-`, "");
                break;
            }
        }
        // Pass stateFilePath as the 4th arg so the constructor uses it directly
        // instead of reconstructing a potentially-different path from the parsed parts.
        return new StateManager(stateType, sessionId, stateDir, stateFilePath);
    }
    /**
     * Read data from state file (excludes metadata)
     */
    async readData() {
        if (!this.exists()) {
            logger.debug(`State file does not exist: ${this.stateFile}`);
            return null;
        }
        try {
            const content = await fs.promises.readFile(this.stateFile, "utf-8");
            const { metadata, ...data } = JSON.parse(content);
            logger.debug(`Read data from state file: ${this.stateFile}`);
            return data;
        }
        catch (error) {
            logger.error(`Failed to read state file: ${error.message}`);
            throw new Error(`Failed to read state file: ${error.message}`);
        }
    }
    /**
     * Read full state including metadata
     */
    async readFullState() {
        if (!this.exists()) {
            return null;
        }
        try {
            const content = await fs.promises.readFile(this.stateFile, "utf-8");
            return JSON.parse(content);
        }
        catch (error) {
            logger.error(`Failed to read state file: ${error.message}`);
            throw new Error(`Failed to read state file: ${error.message}`);
        }
    }
    /**
     * Write data to state file
     * Data fields are spread at root level alongside metadata
     */
    async writeData(data, options) {
        try {
            // Read existing metadata if file exists
            let existingMetadata;
            if (this.exists()) {
                const existing = await this.readFullState();
                existingMetadata = existing?.metadata;
            }
            const state = {
                ...data,
                metadata: {
                    sessionId: this.sessionId,
                    stateType: this.stateType,
                    repositoryPath: options?.repositoryPath || existingMetadata?.repositoryPath,
                    createdAt: existingMetadata?.createdAt || new Date().toISOString(),
                    updatedAt: new Date().toISOString(),
                    step: options?.step,
                },
            };
            await fs.promises.mkdir(path.dirname(this.stateFile), { recursive: true });
            await fs.promises.writeFile(this.stateFile, JSON.stringify(state, null, 2), "utf-8");
            logger.debug(`Wrote data to state file: ${this.stateFile}`);
        }
        catch (error) {
            logger.error(`Failed to write state file: ${error.message}`);
            throw new Error(`Failed to write state file: ${error.message}`);
        }
    }
    getStatePath() {
        return this.stateFile;
    }
    getSessionId() {
        return this.sessionId;
    }
    getStateType() {
        return this.stateType;
    }
    exists() {
        return fs.existsSync(this.stateFile);
    }
    async delete() {
        if (this.exists()) {
            await fs.promises.unlink(this.stateFile);
            logger.debug(`Deleted state file: ${this.stateFile}`);
        }
    }
    async getSize() {
        if (!this.exists()) {
            return 0;
        }
        const stats = await fs.promises.stat(this.stateFile);
        return stats.size;
    }
    async getSizeFormatted() {
        const bytes = await this.getSize();
        if (bytes === 0)
            return "0 B";
        const k = 1024;
        const sizes = ["B", "KB", "MB", "GB"];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
    }
    /**
     * Cleanup old state files
     * @param maxAgeHours Maximum age in hours
     * @param stateDir Directory to clean (defaults to /tmp)
     * @param stateTypes Which state types to clean (defaults to all)
     * @returns Number of files deleted
     */
    static async cleanupOldFiles(maxAgeHours = 24, stateDir, stateTypes) {
        const baseDir = stateDir || os.tmpdir();
        const files = await fs.promises.readdir(baseDir).catch(() => []);
        const statePrefixes = stateTypes
            ? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
            : Object.values(STATE_FILE_PREFIXES);
        const candidates = files.filter((f) => statePrefixes.some((prefix) => f.startsWith(prefix)) ||
            (f.startsWith("skyramp-diff-") && f.endsWith(".diff")));
        let deletedCount = 0;
        const now = Date.now();
        const maxAge = maxAgeHours * 60 * 60 * 1000;
        for (const file of candidates) {
            const filePath = path.join(baseDir, file);
            try {
                const stats = await fs.promises.stat(filePath);
                if (now - stats.mtimeMs > maxAge) {
                    await fs.promises.unlink(filePath);
                    deletedCount++;
                    logger.debug(`Deleted old temp file: ${filePath}`);
                }
            }
            catch (error) {
                logger.error(`Failed to delete temp file ${filePath}: ${error.message}`);
            }
        }
        if (deletedCount > 0) {
            logger.info(`Cleaned up ${deletedCount} old temp files`);
        }
        return deletedCount;
    }
    /**
     * List all state files in directory
     * @param stateDir Directory to search (defaults to /tmp)
     * @param stateTypes Which state types to list (defaults to all)
     */
    static async listStateFiles(stateDir, stateTypes) {
        const baseDir = stateDir || os.tmpdir();
        const files = await fs.promises.readdir(baseDir);
        // Get prefixes to list
        const prefixesToList = stateTypes
            ? stateTypes.map((t) => STATE_FILE_PREFIXES[t])
            : Object.values(STATE_FILE_PREFIXES);
        const stateFiles = files.filter((f) => prefixesToList.some((prefix) => f.startsWith(prefix)));
        const fileInfos = await Promise.all(stateFiles.map(async (file) => {
            const filePath = path.join(baseDir, file);
            const stats = await fs.promises.stat(filePath);
            // Determine state type and session ID
            let stateType = "analysis";
            let sessionId = file.replace(".json", "");
            for (const [type, prefix] of Object.entries(STATE_FILE_PREFIXES)) {
                if (file.startsWith(prefix)) {
                    stateType = type;
                    sessionId = file.replace(`${prefix}-`, "").replace(".json", "");
                    break;
                }
            }
            return {
                sessionId,
                stateType,
                path: filePath,
                size: stats.size,
                createdAt: stats.birthtime,
                modifiedAt: stats.mtime,
            };
        }));
        return fileInfos.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
    }
}
