import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { resolvezmeelConfigPath, resolvezmeelEnvPath } from "./paths.js";
function nonEmpty(value) {
    return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function expandHomePrefix(value) {
    if (value === "~")
        return os.homedir();
    if (value.startsWith("~/"))
        return path.resolve(os.homedir(), value.slice(2));
    return value;
}
function resolveHomeAwarePath(value) {
    return path.resolve(expandHomePrefix(value));
}
function sanitizeWorktreeInstanceId(rawValue) {
    const trimmed = rawValue.trim().toLowerCase();
    const normalized = trimmed
        .replace(/[^a-z0-9_-]+/g, "-")
        .replace(/-+/g, "-")
        .replace(/^[-_]+|[-_]+$/g, "");
    return normalized || "worktree";
}
function isLoopbackHost(hostname) {
    const value = hostname.trim().toLowerCase();
    return value === "127.0.0.1" || value === "localhost" || value === "::1";
}
function rewriteLocalUrlPort(rawUrl, port) {
    if (!rawUrl)
        return undefined;
    try {
        const parsed = new URL(rawUrl);
        if (!isLoopbackHost(parsed.hostname))
            return rawUrl;
        parsed.port = String(port);
        return parsed.toString();
    }
    catch {
        return rawUrl;
    }
}
function parseEnvFile(contents) {
    const entries = {};
    for (const rawLine of contents.split(/\r?\n/)) {
        const line = rawLine.trim();
        if (!line || line.startsWith("#"))
            continue;
        const match = rawLine.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
        if (!match)
            continue;
        const [, key, rawValue] = match;
        const value = rawValue.trim();
        if (!value) {
            entries[key] = "";
            continue;
        }
        if ((value.startsWith("\"") && value.endsWith("\"")) ||
            (value.startsWith("'") && value.endsWith("'"))) {
            entries[key] = value.slice(1, -1);
            continue;
        }
        entries[key] = value.replace(/\s+#.*$/, "").trim();
    }
    return entries;
}
function readEnvEntries(envPath) {
    if (!fs.existsSync(envPath))
        return {};
    return parseEnvFile(fs.readFileSync(envPath, "utf8"));
}
function formatEnvEntries(entries) {
    return [
        "# zmeel environment variables",
        "# Generated by zmeel worktree repair",
        ...Object.entries(entries).map(([key, value]) => `${key}=${JSON.stringify(value)}`),
        "",
    ].join("\n");
}
function isPathInside(candidatePath, rootPath) {
    const candidate = path.resolve(candidatePath);
    const root = path.resolve(rootPath);
    return candidate === root || candidate.startsWith(`${root}${path.sep}`);
}
function resolveWorktreeRuntimeContext(env, overrideConfigPath) {
    if (env.ZMEEL_IN_WORKTREE !== "true")
        return null;
    const configPath = resolvezmeelConfigPath(overrideConfigPath);
    const envPath = resolvezmeelEnvPath(configPath);
    const worktreeRoot = path.resolve(path.dirname(configPath), "..");
    const worktreeName = nonEmpty(env.ZMEEL_WORKTREE_NAME) ?? path.basename(worktreeRoot);
    const instanceId = nonEmpty(env.ZMEEL_INSTANCE_ID) ?? sanitizeWorktreeInstanceId(worktreeName);
    const homeDir = resolveHomeAwarePath(nonEmpty(env.ZMEEL_HOME) ??
        nonEmpty(env.ZMEEL_WORKTREES_DIR) ??
        "~/.zmeel-worktrees");
    const instanceRoot = path.resolve(homeDir, "instances", instanceId);
    return {
        configPath,
        envPath,
        worktreeName,
        instanceId,
        homeDir,
        instanceRoot,
        contextPath: path.resolve(homeDir, "context.json"),
        embeddedPostgresDataDir: path.resolve(instanceRoot, "db"),
        backupDir: path.resolve(instanceRoot, "data", "backups"),
        logDir: path.resolve(instanceRoot, "logs"),
        storageDir: path.resolve(instanceRoot, "data", "storage"),
        secretsKeyFilePath: path.resolve(instanceRoot, "secrets", "master.key"),
    };
}
function writeConfigFile(configPath, config) {
    fs.mkdirSync(path.dirname(configPath), { recursive: true });
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
}
function resolveRepoManagedWorktreesRoot(worktreeRoot) {
    const normalized = path.resolve(worktreeRoot);
    const marker = `${path.sep}.zmeel${path.sep}worktrees${path.sep}`;
    const index = normalized.indexOf(marker);
    if (index === -1)
        return null;
    const repoRoot = normalized.slice(0, index);
    return path.resolve(repoRoot, ".zmeel", "worktrees");
}
function collectSiblingWorktreePorts(context) {
    const serverPorts = new Set();
    const databasePorts = new Set();
    const siblingConfigPaths = new Set();
    const instancesDir = path.resolve(context.homeDir, "instances");
    if (fs.existsSync(instancesDir)) {
        for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
            if (!entry.isDirectory() || entry.name === context.instanceId)
                continue;
            const siblingConfigPath = path.resolve(instancesDir, entry.name, "config.json");
            if (fs.existsSync(siblingConfigPath)) {
                siblingConfigPaths.add(siblingConfigPath);
            }
        }
    }
    const repoManagedWorktreesRoot = resolveRepoManagedWorktreesRoot(path.dirname(context.configPath));
    if (repoManagedWorktreesRoot && fs.existsSync(repoManagedWorktreesRoot)) {
        for (const entry of fs.readdirSync(repoManagedWorktreesRoot, { withFileTypes: true })) {
            if (!entry.isDirectory())
                continue;
            const siblingConfigPath = path.resolve(repoManagedWorktreesRoot, entry.name, ".zmeel", "config.json");
            if (path.resolve(siblingConfigPath) === path.resolve(context.configPath))
                continue;
            if (fs.existsSync(siblingConfigPath)) {
                siblingConfigPaths.add(siblingConfigPath);
            }
        }
    }
    for (const siblingConfigPath of siblingConfigPaths) {
        try {
            const siblingConfig = JSON.parse(fs.readFileSync(siblingConfigPath, "utf8"));
            if (Number.isInteger(siblingConfig.server.port) && siblingConfig.server.port > 0) {
                serverPorts.add(siblingConfig.server.port);
            }
            if (siblingConfig.database.mode === "embedded-postgres" &&
                Number.isInteger(siblingConfig.database.embeddedPostgresPort) &&
                siblingConfig.database.embeddedPostgresPort > 0) {
                databasePorts.add(siblingConfig.database.embeddedPostgresPort);
            }
        }
        catch {
            // Ignore sibling configs that are missing or malformed.
        }
    }
    return { serverPorts, databasePorts };
}
function findNextUnclaimedPort(preferredPort, claimedPorts) {
    let port = Math.max(1, Math.trunc(preferredPort));
    while (claimedPorts.has(port)) {
        port += 1;
    }
    return port;
}
function buildIsolatedWorktreeConfig(config, context, portOverrides) {
    const serverPort = portOverrides?.serverPort ?? config.server.port;
    const databasePort = config.database.mode === "embedded-postgres"
        ? portOverrides?.databasePort ?? config.database.embeddedPostgresPort
        : undefined;
    const nextConfig = {
        ...config,
        database: {
            ...config.database,
            ...(config.database.mode === "embedded-postgres"
                ? {
                    embeddedPostgresDataDir: context.embeddedPostgresDataDir,
                    embeddedPostgresPort: databasePort ?? config.database.embeddedPostgresPort,
                    backup: {
                        ...config.database.backup,
                        dir: context.backupDir,
                    },
                }
                : {}),
        },
        server: {
            ...config.server,
            port: serverPort,
        },
        logging: {
            ...config.logging,
            logDir: context.logDir,
        },
        storage: {
            ...config.storage,
            localDisk: {
                ...config.storage.localDisk,
                baseDir: context.storageDir,
            },
        },
        secrets: {
            ...config.secrets,
            localEncrypted: {
                ...config.secrets.localEncrypted,
                keyFilePath: context.secretsKeyFilePath,
            },
        },
    };
    if (config.auth.baseUrlMode === "explicit" && config.auth.publicBaseUrl) {
        nextConfig.auth = {
            ...config.auth,
            publicBaseUrl: rewriteLocalUrlPort(config.auth.publicBaseUrl, serverPort),
        };
    }
    return nextConfig;
}
function needsWorktreeConfigRepair(config, context) {
    if (config.database.mode === "embedded-postgres") {
        if (!isPathInside(config.database.embeddedPostgresDataDir, context.instanceRoot)) {
            return true;
        }
        if (!isPathInside(config.database.backup.dir, context.instanceRoot)) {
            return true;
        }
    }
    if (!isPathInside(config.logging.logDir, context.instanceRoot)) {
        return true;
    }
    if (!isPathInside(config.storage.localDisk.baseDir, context.instanceRoot)) {
        return true;
    }
    if (!isPathInside(config.secrets.localEncrypted.keyFilePath, context.instanceRoot)) {
        return true;
    }
    return false;
}
export function applyRuntimePortSelectionToConfig(config, input) {
    let changed = false;
    let nextConfig = config;
    if (input.allowServerPortWrite !== false && config.server.port !== input.serverPort) {
        nextConfig = {
            ...nextConfig,
            server: {
                ...nextConfig.server,
                port: input.serverPort,
            },
        };
        changed = true;
    }
    if (input.allowDatabasePortWrite !== false &&
        nextConfig.database.mode === "embedded-postgres" &&
        typeof input.databasePort === "number" &&
        nextConfig.database.embeddedPostgresPort !== input.databasePort) {
        nextConfig = {
            ...nextConfig,
            database: {
                ...nextConfig.database,
                embeddedPostgresPort: input.databasePort,
            },
        };
        changed = true;
    }
    if (nextConfig.auth.baseUrlMode === "explicit" && nextConfig.auth.publicBaseUrl) {
        const rewritten = rewriteLocalUrlPort(nextConfig.auth.publicBaseUrl, input.serverPort);
        if (rewritten && rewritten !== nextConfig.auth.publicBaseUrl) {
            nextConfig = {
                ...nextConfig,
                auth: {
                    ...nextConfig.auth,
                    publicBaseUrl: rewritten,
                },
            };
            changed = true;
        }
    }
    return { config: nextConfig, changed };
}
export function maybeRepairLegacyWorktreeConfigAndEnvFiles() {
    const context = resolveWorktreeRuntimeContext(process.env);
    if (!context) {
        return { repairedConfig: false, repairedEnv: false };
    }
    process.env.ZMEEL_HOME = context.homeDir;
    process.env.ZMEEL_INSTANCE_ID = context.instanceId;
    process.env.ZMEEL_CONFIG = context.configPath;
    process.env.ZMEEL_CONTEXT = context.contextPath;
    process.env.ZMEEL_WORKTREE_NAME = context.worktreeName;
    let repairedConfig = false;
    if (fs.existsSync(context.configPath)) {
        try {
            const parsed = JSON.parse(fs.readFileSync(context.configPath, "utf8"));
            const siblingPorts = collectSiblingWorktreePorts(context);
            const hasSiblingPortCollision = siblingPorts.serverPorts.has(parsed.server.port) ||
                (parsed.database.mode === "embedded-postgres" &&
                    siblingPorts.databasePorts.has(parsed.database.embeddedPostgresPort));
            if (needsWorktreeConfigRepair(parsed, context) || hasSiblingPortCollision) {
                const selectedServerPort = findNextUnclaimedPort(parsed.server.port === 3100 ? 3101 : parsed.server.port, siblingPorts.serverPorts);
                const selectedDatabasePort = parsed.database.mode === "embedded-postgres"
                    ? findNextUnclaimedPort(parsed.database.embeddedPostgresPort === 54329
                        ? 54330
                        : parsed.database.embeddedPostgresPort, new Set([...siblingPorts.databasePorts, selectedServerPort]))
                    : undefined;
                writeConfigFile(context.configPath, buildIsolatedWorktreeConfig(parsed, context, {
                    serverPort: selectedServerPort,
                    databasePort: selectedDatabasePort,
                }));
                repairedConfig = true;
            }
        }
        catch {
            // Leave invalid configs to the normal startup validation path.
        }
    }
    const existingEnvEntries = readEnvEntries(context.envPath);
    const desiredEnvEntries = {
        ...existingEnvEntries,
        ZMEEL_HOME: context.homeDir,
        ZMEEL_INSTANCE_ID: context.instanceId,
        ZMEEL_CONFIG: context.configPath,
        ZMEEL_CONTEXT: context.contextPath,
        ZMEEL_IN_WORKTREE: "true",
        ZMEEL_WORKTREE_NAME: context.worktreeName,
    };
    const repairedEnv = Object.entries(desiredEnvEntries).some(([key, value]) => existingEnvEntries[key] !== value);
    if (repairedEnv) {
        fs.mkdirSync(path.dirname(context.envPath), { recursive: true });
        fs.writeFileSync(context.envPath, formatEnvEntries(desiredEnvEntries), { mode: 0o600 });
    }
    return { repairedConfig, repairedEnv };
}
export function maybePersistWorktreeRuntimePorts(input) {
    const context = resolveWorktreeRuntimeContext(process.env);
    if (!context || !fs.existsSync(context.configPath))
        return;
    let fileConfig;
    try {
        fileConfig = JSON.parse(fs.readFileSync(context.configPath, "utf8"));
    }
    catch {
        return;
    }
    const { config, changed } = applyRuntimePortSelectionToConfig(fileConfig, {
        serverPort: input.serverPort,
        databasePort: input.databasePort,
        allowServerPortWrite: !nonEmpty(process.env.PORT),
        allowDatabasePortWrite: !nonEmpty(process.env.DATABASE_URL),
    });
    if (changed) {
        writeConfigFile(context.configPath, config);
    }
}
//# sourceMappingURL=worktree-config.js.map