import { createHash } from 'node:crypto';
import { readdir, readFile, rm, writeFile } from 'node:fs/promises';
import { basename, dirname, join, relative, resolve } from 'node:path';
import { parseCanonicalAgentFile } from '../../../agents/canonical/index.js';
import { isCodexMaterializedRouteTarget, isWorkflowDispatchCandidateLadder, isWorkflowDispatchFallbackRoute, normalizeProjectPath, resolveActiveProject, validateDispatchRouteTarget, } from '../../../config/oat-config.js';
import { resolveEffectiveConfig } from '../../../config/resolve.js';
import { CliError } from '../../../errors/index.js';
import { ensureDir, fileExists } from '../../../fs/io.js';
import { validateRealPathWithinScope } from '../../../fs/paths.js';
import TOML from '@iarna/toml';
import YAML from 'yaml';
import { SUPPORTED_CODEX_BASE_ROLES, SUPPORTED_CODEX_ROLE_TARGETS, } from './catalog.js';
import { mergeCodexConfig, readCodexMaxDepth, } from './config-merge.js';
import { exportCanonicalAgentToCodexRole } from './export-to-codex.js';
import { materializeCodexRole } from './materialize.js';
import { isOatManagedCodexRoleFile, readOatManagedCodexRoleOwner, withOatManagedCodexRoleOwner, } from './shared.js';
const CODEX_MATERIALIZED_BASE_ROLES = new Set(SUPPORTED_CODEX_BASE_ROLES);
function hashContent(content) {
    return createHash('sha256').update(content).digest('hex');
}
function configPath(scopeRoot) {
    return join(scopeRoot, '.codex', 'config.toml');
}
function toRelativePath(scopeRoot, absolutePath) {
    return relative(scopeRoot, absolutePath).replaceAll('\\', '/');
}
function canonicalPathAllowed(scopeRoot, canonicalEntry, allowedCanonicalPaths) {
    if (!allowedCanonicalPaths?.length) {
        return true;
    }
    const allowedSet = new Set(allowedCanonicalPaths);
    const relativeCanonicalPath = toRelativePath(scopeRoot, canonicalEntry.canonicalPath);
    return allowedSet.has(relativeCanonicalPath);
}
async function readOptionalFile(path) {
    if (!(await fileExists(path))) {
        return null;
    }
    return readFile(path, 'utf8');
}
function normalizeManagedRolesConfig(desiredRoles) {
    return desiredRoles.map((role) => ({
        roleName: role.roleName,
        description: role.description,
        configFile: role.configFile,
    }));
}
async function desiredRolesFromCanonical(canonicalEntries, scopeRoot, materializationTargets) {
    const roles = [];
    for (const entry of canonicalEntries) {
        if (entry.type !== 'agent' ||
            !entry.isFile ||
            !entry.name.endsWith('.md')) {
            continue;
        }
        const parsed = await parseCanonicalAgentFile(entry.canonicalPath);
        const exported = exportCanonicalAgentToCodexRole(parsed);
        roles.push({
            roleName: exported.roleName,
            description: exported.description,
            configFile: exported.configFile,
            rolePath: join(scopeRoot, '.codex', exported.configFile),
            content: exported.content,
        });
        if (CODEX_MATERIALIZED_BASE_ROLES.has(exported.roleName)) {
            for (const target of materializationTargets) {
                const materialized = materializeCodexRole({
                    agent: parsed,
                    model: target.model,
                    effort: target.effort,
                });
                roles.push({
                    roleName: materialized.roleName,
                    description: materialized.description,
                    configFile: materialized.configFile,
                    rolePath: join(scopeRoot, '.codex', materialized.configFile),
                    content: withOatManagedCodexRoleOwner(materialized.content, target.owner),
                });
            }
        }
    }
    const roleContents = new Map();
    for (const role of roles) {
        const existing = roleContents.get(role.roleName);
        if (existing !== undefined && existing !== role.content) {
            throw new CliError(`Distinct Codex targets produced the same role name ${role.roleName}. Refusing ambiguous role writes.`);
        }
        roleContents.set(role.roleName, role.content);
    }
    return roles.sort((left, right) => left.roleName.localeCompare(right.roleName));
}
function collectCodexTargetFromEntry(entry, targets, owner) {
    if (typeof entry === 'string') {
        return;
    }
    if (!isCodexMaterializedRouteTarget('codex', entry)) {
        return;
    }
    const validation = validateDispatchRouteTarget('codex', entry);
    if (!validation.valid || !entry.model || !entry.effort) {
        return;
    }
    const key = `${entry.model}\0${entry.effort}`;
    if (!targets.has(key)) {
        targets.set(key, {
            model: entry.model,
            effort: entry.effort,
            owner,
        });
    }
}
function collectCodexTargetsFromCell(cell, targets, owner) {
    if (typeof cell === 'string') {
        return;
    }
    if (isWorkflowDispatchCandidateLadder(cell)) {
        for (const candidate of cell.candidates) {
            if (isWorkflowDispatchFallbackRoute(candidate)) {
                for (const entry of candidate.route) {
                    collectCodexTargetFromEntry(entry, targets, owner);
                }
                continue;
            }
            collectCodexTargetFromEntry(candidate, targets, owner);
        }
        return;
    }
    for (const entry of cell) {
        collectCodexTargetFromEntry(entry, targets, owner);
    }
}
function collectCodexMaterializationTargetsFromProvider(providerValue, targets, owner) {
    if (providerValue === undefined || typeof providerValue === 'string') {
        return;
    }
    for (const cell of Object.values(providerValue)) {
        if (cell !== undefined) {
            collectCodexTargetsFromCell(cell, targets, owner);
        }
    }
}
function sortCodexMaterializationTargets(targets) {
    return [...targets.values()].sort((left, right) => {
        const modelOrder = left.model.localeCompare(right.model);
        return modelOrder === 0
            ? left.effort.localeCompare(right.effort)
            : modelOrder;
    });
}
function routeTargetFromUnknown(value) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
        return null;
    }
    const record = value;
    const target = {};
    for (const key of ['harness', 'model', 'effort']) {
        const rawValue = record[key];
        if (typeof rawValue === 'string' && rawValue.trim()) {
            target[key] = rawValue.trim();
        }
    }
    return Object.keys(target).length > 0 ? target : null;
}
function collectCodexTargetsFromUnknownCell(value, targets, owner) {
    const collectCandidate = (candidate) => {
        if (!candidate ||
            typeof candidate !== 'object' ||
            Array.isArray(candidate)) {
            return;
        }
        const route = candidate['route'];
        if (Array.isArray(route)) {
            for (const entry of route) {
                const target = routeTargetFromUnknown(entry);
                if (target) {
                    collectCodexTargetFromEntry(target, targets, owner);
                }
            }
            return;
        }
        const target = routeTargetFromUnknown(candidate);
        if (target) {
            collectCodexTargetFromEntry(target, targets, owner);
        }
    };
    if (Array.isArray(value)) {
        for (const entry of value) {
            collectCandidate(entry);
        }
        return;
    }
    if (!value || typeof value !== 'object') {
        return;
    }
    const candidates = value['candidates'];
    if (!Array.isArray(candidates)) {
        return;
    }
    for (const entry of candidates) {
        if (typeof entry === 'string') {
            continue;
        }
        collectCandidate(entry);
    }
}
function collectCodexTargetsFromUnknownProvider(value, targets, owner) {
    if (!value || typeof value !== 'object' || Array.isArray(value)) {
        return;
    }
    for (const cell of Object.values(value)) {
        collectCodexTargetsFromUnknownCell(cell, targets, owner);
    }
}
function frontmatterBlock(content) {
    const normalized = content.startsWith('\uFEFF') ? content.slice(1) : content;
    if (!normalized.startsWith('---\n')) {
        return null;
    }
    const end = normalized.indexOf('\n---', 4);
    return end > 0 ? normalized.slice(4, end) : null;
}
async function collectProjectStateCodexTargets(scopeRoot, options, targets) {
    const projectPathCandidate = options.projectPath === undefined
        ? (await resolveActiveProject(scopeRoot)).path
        : options.projectPath;
    if (!projectPathCandidate) {
        return;
    }
    const projectPath = normalizeProjectPath(scopeRoot, projectPathCandidate);
    if (!projectPath) {
        if (options.projectPath !== undefined) {
            throw new CliError(`Project-scoped Codex materialization path must be repo-relative or inside repo root: ${projectPathCandidate}`);
        }
        return;
    }
    let realProjectPath;
    try {
        const validated = await validateRealPathWithinScope(join(scopeRoot, projectPath), scopeRoot);
        realProjectPath = validated.realPath;
    }
    catch {
        if (options.projectPath !== undefined) {
            throw new CliError(`Project-scoped Codex materialization path must be repo-relative or inside repo root: ${projectPathCandidate}`);
        }
        return;
    }
    const statePath = join(realProjectPath, 'state.md');
    const stateContent = await readOptionalFile(statePath);
    if (!stateContent) {
        return;
    }
    const frontmatter = frontmatterBlock(stateContent);
    if (!frontmatter) {
        return;
    }
    const parsed = YAML.parse(frontmatter);
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        return;
    }
    const policy = parsed['oat_dispatch_policy'];
    if (!policy || typeof policy !== 'object' || Array.isArray(policy)) {
        return;
    }
    const matrix = policy['matrix'];
    if (!matrix || typeof matrix !== 'object' || Array.isArray(matrix)) {
        return;
    }
    collectCodexTargetsFromUnknownProvider(matrix['codex'], targets, 'project-config');
}
function isUserCodexScope(scopeRoot, options) {
    if (!options.userConfigDir) {
        return false;
    }
    return resolve(scopeRoot) === resolve(options.userConfigDir, '..');
}
async function readInheritedCodexMaxDepth(scopeRoot, options) {
    if (!options.userConfigDir || isUserCodexScope(scopeRoot, options)) {
        return undefined;
    }
    const userScopeRoot = resolve(options.userConfigDir, '..');
    const userConfigContent = await readOptionalFile(configPath(userScopeRoot));
    return readCodexMaxDepth(userConfigContent) ?? undefined;
}
async function readCodexMaterializationTargets(scopeRoot, options = {}) {
    const targets = new Map();
    const effectiveConfig = await resolveEffectiveConfig(scopeRoot, options.userConfigDir ?? join(scopeRoot, '.oat', '__no-user-config__'), options.env);
    if (isUserCodexScope(scopeRoot, options)) {
        collectCodexMaterializationTargetsFromProvider(effectiveConfig.user.workflow?.dispatchCeiling?.providers?.codex, targets, 'user-config');
        return sortCodexMaterializationTargets(targets);
    }
    for (const target of SUPPORTED_CODEX_ROLE_TARGETS) {
        targets.set(`${target.model}\0${target.effort}`, {
            ...target,
            owner: 'supported-catalogue',
        });
    }
    for (const providerValue of [
        effectiveConfig.shared.workflow?.dispatchCeiling?.providers?.codex,
        effectiveConfig.local.workflow?.dispatchCeiling?.providers?.codex,
    ]) {
        collectCodexMaterializationTargetsFromProvider(providerValue, targets, 'project-config');
    }
    await collectProjectStateCodexTargets(scopeRoot, options, targets);
    return sortCodexMaterializationTargets(targets);
}
function parseConfigAgentTable(content) {
    if (!content || content.trim() === '') {
        return {};
    }
    try {
        const parsed = TOML.parse(content);
        const agents = parsed.agents;
        if (!agents || typeof agents !== 'object' || Array.isArray(agents)) {
            return {};
        }
        const normalized = {};
        for (const [roleName, roleConfig] of Object.entries(agents)) {
            if (!roleConfig ||
                typeof roleConfig !== 'object' ||
                Array.isArray(roleConfig)) {
                continue;
            }
            normalized[roleName] = roleConfig;
        }
        return normalized;
    }
    catch {
        return {};
    }
}
async function collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames, cleanupOwner, removeLegacyProjectRoles) {
    const agents = parseConfigAgentTable(existingConfigContent);
    const stale = new Set();
    for (const [roleName, roleConfig] of Object.entries(agents)) {
        if (desiredRoleNames.has(roleName)) {
            continue;
        }
        const configFile = roleConfig.config_file;
        if (typeof configFile !== 'string' || !configFile.startsWith('agents/')) {
            continue;
        }
        const rolePath = join(scopeRoot, '.codex', configFile);
        const roleContent = await readOptionalFile(rolePath);
        if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
            continue;
        }
        const owner = readOatManagedCodexRoleOwner(roleContent);
        const legacyProjectRole = removeLegacyProjectRoles &&
            owner === null &&
            SUPPORTED_CODEX_BASE_ROLES.some((baseRole) => roleName.startsWith(`${baseRole}-`));
        if (owner !== cleanupOwner && !legacyProjectRole) {
            continue;
        }
        stale.add(roleName);
    }
    const agentsDir = join(scopeRoot, '.codex', 'agents');
    let roleFiles;
    try {
        roleFiles = await readdir(agentsDir);
    }
    catch {
        roleFiles = [];
    }
    for (const roleFile of roleFiles) {
        if (!roleFile.endsWith('.toml')) {
            continue;
        }
        const roleName = basename(roleFile, '.toml');
        if (desiredRoleNames.has(roleName) || stale.has(roleName)) {
            continue;
        }
        const rolePath = join(agentsDir, roleFile);
        const roleContent = await readOptionalFile(rolePath);
        if (!roleContent || !isOatManagedCodexRoleFile(roleContent, roleName)) {
            continue;
        }
        const owner = readOatManagedCodexRoleOwner(roleContent);
        const legacyProjectRole = removeLegacyProjectRoles &&
            owner === null &&
            SUPPORTED_CODEX_BASE_ROLES.some((baseRole) => roleName.startsWith(`${baseRole}-`));
        if (owner !== cleanupOwner && !legacyProjectRole) {
            continue;
        }
        stale.add(roleName);
    }
    return [...stale].sort((left, right) => left.localeCompare(right));
}
export async function computeCodexProjectExtensionPlan(scopeRoot, canonicalEntries, allowedCanonicalPaths, options = {}) {
    const isPartialSync = allowedCanonicalPaths !== undefined && allowedCanonicalPaths.length > 0;
    const materializationTargets = await readCodexMaterializationTargets(scopeRoot, options);
    if (!isPartialSync &&
        isUserCodexScope(scopeRoot, options) &&
        materializationTargets.length > 0) {
        const canonicalBaseRoles = new Set(canonicalEntries
            .filter((entry) => entry.type === 'agent' && entry.isFile)
            .map((entry) => entry.name.replace(/\.md$/i, '')));
        const missingBaseRoles = SUPPORTED_CODEX_BASE_ROLES.filter((role) => !canonicalBaseRoles.has(role));
        if (missingBaseRoles.length > 0) {
            throw new CliError(`Bundled managed Codex role definitions are unavailable for user sync: ${missingBaseRoles.join(', ')}. Refusing stale user-role cleanup.`);
        }
    }
    const desiredRoles = await desiredRolesFromCanonical(canonicalEntries.filter((entry) => canonicalPathAllowed(scopeRoot, entry, allowedCanonicalPaths)), scopeRoot, materializationTargets);
    const desiredRoleNames = new Set(desiredRoles.map((role) => role.roleName));
    const existingConfigPath = configPath(scopeRoot);
    const existingConfigContent = await readOptionalFile(existingConfigPath);
    if (isPartialSync && desiredRoles.length === 0) {
        return {
            operations: [],
            managedRoles: [],
            aggregateConfigHash: hashContent(existingConfigContent ?? ''),
        };
    }
    const inheritedMaxDepth = await readInheritedCodexMaxDepth(scopeRoot, options);
    const staleRoles = isPartialSync
        ? []
        : await collectStaleManagedRoles(scopeRoot, existingConfigContent, desiredRoleNames, isUserCodexScope(scopeRoot, options) ? 'user-config' : 'project-config', !isUserCodexScope(scopeRoot, options));
    const operations = [];
    for (const role of desiredRoles) {
        const existingRoleContent = await readOptionalFile(role.rolePath);
        if (existingRoleContent === null) {
            operations.push({
                action: 'create',
                target: 'role',
                path: toRelativePath(scopeRoot, role.rolePath),
                reason: 'managed role file missing',
                roleName: role.roleName,
                content: role.content,
            });
            continue;
        }
        if (existingRoleContent.trimEnd() !== role.content.trimEnd()) {
            operations.push({
                action: 'update',
                target: 'role',
                path: toRelativePath(scopeRoot, role.rolePath),
                reason: 'managed role file differs from canonical export',
                roleName: role.roleName,
                content: role.content,
            });
            continue;
        }
        operations.push({
            action: 'skip',
            target: 'role',
            path: toRelativePath(scopeRoot, role.rolePath),
            reason: 'managed role file already in sync',
            roleName: role.roleName,
        });
    }
    for (const staleRole of staleRoles) {
        const staleRolePath = join(scopeRoot, '.codex', 'agents', `${staleRole}.toml`);
        const staleRoleContent = await readOptionalFile(staleRolePath);
        if (staleRoleContent &&
            isOatManagedCodexRoleFile(staleRoleContent, staleRole)) {
            operations.push({
                action: 'remove',
                target: 'role',
                path: toRelativePath(scopeRoot, staleRolePath),
                reason: 'stale managed role removed',
                roleName: staleRole,
            });
        }
    }
    const configMerge = mergeCodexConfig({
        existingContent: existingConfigContent,
        desiredRoles: normalizeManagedRolesConfig(desiredRoles),
        staleManagedRoles: staleRoles,
        inheritedMaxDepth,
    });
    operations.push({
        action: existingConfigContent === null
            ? 'create'
            : configMerge.changed
                ? 'update'
                : 'skip',
        target: 'config',
        path: '.codex/config.toml',
        reason: existingConfigContent === null
            ? 'codex config missing'
            : configMerge.changed
                ? 'codex config differs from desired managed state'
                : 'codex config already in sync',
        content: configMerge.mergedContent,
    });
    return {
        operations,
        managedRoles: [...desiredRoles.map((role) => role.roleName), ...staleRoles],
        aggregateConfigHash: hashContent(configMerge.mergedContent),
    };
}
export async function applyCodexProjectExtensionPlan(scopeRoot, plan) {
    const result = {
        applied: 0,
        failed: 0,
        skipped: 0,
    };
    for (const operation of plan.operations) {
        if (operation.action === 'skip') {
            result.skipped += 1;
            continue;
        }
        const absolutePath = resolve(scopeRoot, operation.path);
        try {
            if (operation.action === 'remove') {
                await rm(absolutePath, { recursive: true, force: true });
            }
            else {
                await ensureDir(dirname(absolutePath));
                await writeFile(absolutePath, operation.content ?? '', 'utf8');
            }
            result.applied += 1;
        }
        catch {
            result.failed += 1;
        }
    }
    return result;
}
export function hasCodexExtensionChanges(plan) {
    return plan.operations.some((operation) => operation.action !== 'skip');
}
export function summarizeCodexExtension(plan) {
    let plannedOperations = 0;
    let skipped = 0;
    for (const operation of plan.operations) {
        if (operation.action === 'skip') {
            skipped += 1;
            continue;
        }
        plannedOperations += 1;
    }
    return { plannedOperations, skipped };
}
export function toCodexExtensionOperations(plan) {
    return plan.operations.map((operation) => ({
        action: operation.action,
        target: operation.target,
        path: operation.path,
        reason: operation.reason,
        roleName: operation.roleName,
    }));
}
