import { existsSync } from "node:fs";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { validatePersistedConfigDocument } from "./config-metadata.js";
import { getModuleScopedConfigPath } from "./module-scoped-config.js";
export const PROJECT_CONFIG_REL = ".workspace-kit/config.json";
export function getProjectConfigPath(workspacePath) {
    return path.join(workspacePath, PROJECT_CONFIG_REL);
}
/** Built-in defaults (lowest layer). */
export const KIT_CONFIG_DEFAULTS = {
    core: {},
    /**
     * Module enablement: `enabled` whitelist (non-empty replaces default-by-flag set), then `disabled` subtracts.
     * Empty arrays = no effect (all modules use registration.enabledByDefault).
     */
    modules: {
        enabled: [],
        disabled: []
    },
    /** Maintainer-declared current kit phase (optional); overrides YAML-derived phase for queue audits when set. */
    kit: {
        githubInvocation: {
            enabled: false,
            allowedRepositories: [],
            eventPlaybookMap: {},
            commentDebounceSeconds: 0,
            rateLimitEventsPerHour: 0,
            planOnlyRunCommands: ["get-next-actions", "list-tasks", "get-task"],
            sensitiveRunCommands: ["run-transition"]
        },
        lifecycleHooks: {
            enabled: false,
            mode: "off",
            traceRelativePath: ".workspace-kit/kit/lifecycle-hook-traces.jsonl",
            handlers: []
        },
        autoCheckpoint: {
            enabled: false,
            beforeCommands: ["run-transition"],
            stashWhenDirty: true
        },
        /** Context Activation Engine (CAE); see `.ai/cae/README.md`. */
        cae: {
            enabled: false,
            /**
             * `sqlite` — load active version from kit planning SQLite (`cae_registry_*` tables); default (**Phase 70 / CAE_PLAN**).
             * `json` — load `.ai/cae/registry/*.json` (tests / explicit bootstrap only; not authoritative runtime).
             */
            registryStore: "sqlite",
            /** When true with `enabled`, `doctor --agent-instruction-surface` includes a bounded `cae` block. */
            advisoryInstructionSurface: false,
            /** Ephemeral trace bodies stay in-process until optional persistence (**`T867`**). */
            persistence: false,
            runtime: {
                /** When true (and `enabled`), non-`cae-*` `wk run` module commands run shadow CAE preflight (**`T864`**). */
                shadowPreflight: false,
                /** Persist shadow-preflight traces to tracked planning SQLite. Default false to keep read-ish commands clean. */
                persistShadowPreflight: false
            },
            enforcement: {
                /** Allowlisted blocks only; default off (**`T866`**). */
                enabled: false
            },
            shadow: {
                /** Product default: shadow labels without blocking (**`T848`**). */
                defaultOn: false
            },
            /**
             * When true with `enabled`, SQLite registry admin mutators (`cae-create-artifact`, version CRUD, …)
             * are allowed with JSON **`caeMutationApproval`** (CAE governance lane — see **`.ai/cae/registry-mutation-governance.md`**).
             */
            adminMutations: false
        }
    },
    tasks: {
        storeRelativePath: ".workspace-kit/tasks/state.json",
        persistenceBackend: "sqlite",
        canonicalAuthority: "sqlite",
        sqliteDatabaseRelativePath: ".workspace-kit/tasks/workspace-kit.db",
        strictValidation: false,
        intakePolicy: {
            defaultProfile: "advisory",
            enforcementMode: "advisory",
            profiles: {
                advisory: {
                    requiredFields: [],
                    recommendedFields: ["title", "summary", "technicalScope", "acceptanceCriteria"],
                    enforcementMode: "advisory"
                }
            },
            moduleOverrides: {}
        },
        deliveryEvidence: {
            enforcementMode: "enforce"
        }
    },
    maintainerDelivery: {
        defaultProfile: "github-pr",
        enforcementMode: "advisory",
        profiles: {
            "github-pr": {
                requiresPhaseBranch: true,
                branchPattern: "release/phase-{phaseKey}",
                taskBranchPattern: "feature/{taskId}-{slug}",
                releaseTagPattern: "v{version}",
                review: "github-pr",
                evidenceKind: "github-pr",
                prProvider: "github",
                mergeStrategy: "merge",
                phaseToMainMode: "phase-closeout"
            }
        },
        moduleOverrides: {}
    },
    documentation: {},
    /** Agent temperament / interview preferences; mirrored to `.workspace-kit/modules/agent-behavior/config.json` when using SQLite. */
    agentBehavior: {},
    agentPresentation: {
        mode: "derived",
        workLog: "derived",
        rationale: "derived",
        technicality: "derived",
        finalAnswerDetail: "derived"
    },
    responseTemplates: {
        enforcementMode: "advisory",
        defaultTemplateId: "default",
        commandOverrides: {}
    },
    improvement: {
        transcripts: {
            sourcePath: "",
            archivePath: "agent-transcripts",
            maxFilesPerSync: 5000,
            maxBytesPerFile: 50_000_000,
            maxTotalScanBytes: 500_000_000,
            discoveryPaths: []
        },
        cadence: {
            minIntervalMinutes: 15,
            skipIfNoNewTranscripts: true,
            maxRecommendationCandidatesPerRun: 500
        },
        hooks: {
            afterTaskCompleted: "off"
        }
    }
};
/**
 * Static module-level defaults keyed by module id (merged in registry startup order).
 * Keep true defaults in KIT_CONFIG_DEFAULTS as the canonical source; use this map
 * only when a module contributes additional non-default config structure.
 */
export const MODULE_CONFIG_CONTRIBUTIONS = {
    approvals: {},
    planning: {
        defaultQuestionDepth: "adaptive",
        hardBlockCriticalUnknowns: true,
        rulePacks: {}
    },
    skills: {
        skills: {
            discoveryRoots: [".claude/skills"]
        }
    },
    plugins: {
        plugins: {
            discoveryRoots: [".claude/plugins"]
        }
    }
};
export function deepMerge(target, source) {
    const out = { ...target };
    for (const [k, v] of Object.entries(source)) {
        if (v !== null && typeof v === "object" && !Array.isArray(v)) {
            const prev = out[k];
            const prevObj = prev !== null && typeof prev === "object" && !Array.isArray(prev)
                ? prev
                : {};
            out[k] = deepMerge(prevObj, v);
        }
        else {
            out[k] = v;
        }
    }
    return out;
}
function cloneDeep(obj) {
    return JSON.parse(JSON.stringify(obj));
}
export function getAtPath(root, dotted) {
    const parts = dotted.split(".").filter(Boolean);
    let cur = root;
    for (const p of parts) {
        if (cur === null || cur === undefined || typeof cur !== "object" || Array.isArray(cur)) {
            return undefined;
        }
        cur = cur[p];
    }
    return cur;
}
export function deepEqual(a, b) {
    if (a === b)
        return true;
    if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
        return false;
    }
    if (Array.isArray(a) !== Array.isArray(b))
        return false;
    if (Array.isArray(a) && Array.isArray(b)) {
        if (a.length !== b.length)
            return false;
        return a.every((v, i) => deepEqual(v, b[i]));
    }
    const ak = Object.keys(a).sort();
    const bk = Object.keys(b).sort();
    if (ak.length !== bk.length)
        return false;
    for (let i = 0; i < ak.length; i++) {
        if (ak[i] !== bk[i])
            return false;
    }
    for (const k of ak) {
        if (!deepEqual(a[k], b[k])) {
            return false;
        }
    }
    return true;
}
/** Resolved home for user-level config (`~/.workspace-kit/config.json`). Override with `WORKSPACE_KIT_HOME` (tests). */
export function getUserConfigFilePath() {
    const home = process.env.WORKSPACE_KIT_HOME?.trim() || os.homedir();
    return path.join(home, ".workspace-kit", "config.json");
}
async function readUserConfigFile() {
    const fp = getUserConfigFilePath();
    if (!existsSync(fp)) {
        return {};
    }
    const raw = await fs.readFile(fp, "utf8");
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error(`config-invalid(user): ${fp} must be a JSON object`);
    }
    const obj = parsed;
    validatePersistedConfigDocument(obj, "user config");
    return obj;
}
async function readProjectConfigFile(workspacePath) {
    const fp = path.join(workspacePath, PROJECT_CONFIG_REL);
    if (!existsSync(fp)) {
        return {};
    }
    const raw = await fs.readFile(fp, "utf8");
    const parsed = JSON.parse(raw);
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
        throw new Error("config-invalid: .workspace-kit/config.json must be a JSON object");
    }
    const obj = parsed;
    validatePersistedConfigDocument(obj, ".workspace-kit/config.json");
    return obj;
}
/** Read `.workspace-kit/config.json` for mutation (validated). Returns `{}` when missing. */
export async function readProjectConfigDocument(workspacePath) {
    return readProjectConfigFile(workspacePath);
}
/** Atomically write project config after validation. */
export async function writeProjectConfigDocument(workspacePath, doc) {
    validatePersistedConfigDocument(doc, ".workspace-kit/config.json");
    const fp = getProjectConfigPath(workspacePath);
    await fs.mkdir(path.dirname(fp), { recursive: true });
    const tmp = `${fp}.${process.pid}.${Date.now()}.tmp`;
    await fs.writeFile(tmp, stableStringifyConfig(doc), "utf8");
    await fs.rename(tmp, fp);
}
export async function loadUserLayer() {
    const data = await readUserConfigFile();
    return { id: "user", data };
}
/**
 * Parse WORKSPACE_KIT_* env into a nested object (double-underscore → path under domains).
 * Example: WORKSPACE_KIT_TASKS__STORE_PATH -> { tasks: { storeRelativePath: "..." } }
 * Uses segment after prefix; known domain prefix sets first key.
 */
export function envToConfigOverlay(env) {
    const prefix = "WORKSPACE_KIT_";
    const out = {};
    for (const [key, val] of Object.entries(env)) {
        if (!key.startsWith(prefix) || val === undefined)
            continue;
        const rest = key.slice(prefix.length);
        if (!rest || rest === "ACTOR")
            continue;
        const segments = rest.split("__").map((s) => camelCaseEnvSegment(s));
        if (segments.length === 0)
            continue;
        setDeep(out, segments, coerceEnvValue(val));
    }
    return out;
}
function camelCaseEnvSegment(s) {
    const lower = s.toLowerCase().replace(/_/g, "");
    // tasks -> tasks; STORE_PATH segments already split — first segment may be TASKS
    if (lower === "tasks")
        return "tasks";
    if (lower === "documentation")
        return "documentation";
    if (lower === "core")
        return "core";
    // e.g. STORE_PATH -> storePath
    return s
        .toLowerCase()
        .split("_")
        .filter(Boolean)
        .map((w, i) => (i === 0 ? w : w.charAt(0).toUpperCase() + w.slice(1)))
        .join("");
}
function setDeep(target, segments, value) {
    let cur = target;
    for (let i = 0; i < segments.length - 1; i++) {
        const seg = segments[i];
        const next = cur[seg];
        if (next === undefined || typeof next !== "object" || Array.isArray(next)) {
            cur[seg] = {};
        }
        cur = cur[seg];
    }
    cur[segments[segments.length - 1]] = value;
}
function coerceEnvValue(val) {
    if (val === "true")
        return true;
    if (val === "false")
        return false;
    if (/^-?\d+$/.test(val))
        return Number(val);
    return val;
}
/** Kit defaults + module contributions (topological order). Project/env/invocation added separately. */
export function buildBaseConfigLayers(registry) {
    const layers = [];
    layers.push({ id: "kit-default", data: cloneDeep(KIT_CONFIG_DEFAULTS) });
    for (const mod of registry.getStartupOrder()) {
        const contrib = MODULE_CONFIG_CONTRIBUTIONS[mod.registration.id];
        if (contrib && Object.keys(contrib).length > 0) {
            layers.push({ id: `module:${mod.registration.id}`, data: cloneDeep(contrib) });
        }
    }
    return layers;
}
export async function loadProjectLayer(workspacePath) {
    const data = await readProjectConfigFile(workspacePath);
    return { id: "project", data };
}
export function mergeConfigLayers(layers) {
    let acc = {};
    for (const layer of layers) {
        acc = deepMerge(acc, layer.data);
    }
    return acc;
}
export async function resolveWorkspaceConfigWithLayers(options) {
    const { workspacePath, registry, env = process.env, invocationConfig } = options;
    const layers = [...buildBaseConfigLayers(registry)];
    layers.push(await loadUserLayer());
    for (const mod of registry.getStartupOrder()) {
        const id = mod.registration.id;
        const fp = getModuleScopedConfigPath(workspacePath, id);
        try {
            await fs.access(fp);
        }
        catch {
            continue;
        }
        const raw = await fs.readFile(fp, "utf8");
        const parsed = JSON.parse(raw);
        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
            throw new Error(`module-config-invalid: ${fp} must be a JSON object`);
        }
        layers.push({ id: `module-file:${id}`, data: cloneDeep(parsed) });
    }
    layers.push(await loadProjectLayer(workspacePath));
    layers.push({ id: "env", data: envToConfigOverlay(env) });
    if (invocationConfig && Object.keys(invocationConfig).length > 0) {
        layers.push({ id: "invocation", data: cloneDeep(invocationConfig) });
    }
    return { effective: mergeConfigLayers(layers), layers };
}
/** Atomic write for `.workspace-kit/modules/<moduleId>/config.json` (sorted JSON, trailing newline). */
export async function writeModuleScopedConfigDocument(workspacePath, moduleId, doc) {
    const fp = getModuleScopedConfigPath(workspacePath, moduleId);
    await fs.mkdir(path.dirname(fp), { recursive: true });
    const tmp = `${fp}.${process.pid}.${Date.now()}.tmp`;
    await fs.writeFile(tmp, stableStringifyConfig(doc), "utf8");
    await fs.rename(tmp, fp);
}
export function normalizeConfigForExport(value) {
    return sortKeysDeep(value);
}
function sortKeysDeep(value) {
    if (value === null || typeof value !== "object") {
        return value;
    }
    if (Array.isArray(value)) {
        return value.map(sortKeysDeep);
    }
    const o = value;
    const out = {};
    for (const k of Object.keys(o).sort()) {
        out[k] = sortKeysDeep(o[k]);
    }
    return out;
}
/** Deterministic JSON for agents and tests (sorted keys, trailing newline). */
export function stableStringifyConfig(value) {
    return `${JSON.stringify(sortKeysDeep(value), null, 2)}\n`;
}
export function explainConfigPath(dottedPath, layers) {
    const mergedFull = mergeConfigLayers(layers);
    const effectiveValue = getAtPath(mergedFull, dottedPath);
    let winningLayer = "kit-default";
    let prevMerged = {};
    for (let i = 0; i < layers.length; i++) {
        const slice = layers.slice(0, i + 1);
        const nextMerged = mergeConfigLayers(slice);
        const prevVal = getAtPath(prevMerged, dottedPath);
        const nextVal = getAtPath(nextMerged, dottedPath);
        if (!deepEqual(prevVal, nextVal)) {
            winningLayer = layers[i].id;
        }
        prevMerged = nextMerged;
    }
    const alternates = [];
    for (let i = 0; i < layers.length; i++) {
        const slice = layers.slice(0, i + 1);
        const m = mergeConfigLayers(slice);
        alternates.push({ layer: layers[i].id, value: getAtPath(m, dottedPath) });
    }
    return { path: dottedPath, effectiveValue, winningLayer, alternates };
}
