import { readdir as defaultReaddir, readFile as defaultReadFile, } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { getFrontmatterBlock, isProjectStateKind, isProjectStatePhase, } from '../commands/shared/frontmatter.js';
import YAML from 'yaml';
function readStringField(frontmatter, key) {
    const value = frontmatter[key];
    return typeof value === 'string' ? value : undefined;
}
function readStringArrayField(frontmatter, key, errors) {
    const value = frontmatter[key];
    if (value === undefined || value === null) {
        return [];
    }
    if (Array.isArray(value) && value.every((item) => typeof item === 'string')) {
        return [...value];
    }
    errors.push({
        code: 'invalid-string-array',
        message: `${key} must be an array of strings`,
    });
    return [];
}
function readKind(frontmatter) {
    const rawKind = readStringField(frontmatter, 'oat_kind');
    return rawKind && isProjectStateKind(rawKind) ? rawKind : 'implementation';
}
function sameStringSet(left, right) {
    if (left.length !== right.length) {
        return false;
    }
    const sortedLeft = [...left].sort();
    const sortedRight = [...right].sort();
    return sortedLeft.every((value, index) => value === sortedRight[index]);
}
function graphHasCycle(graph) {
    const visiting = new Set();
    const visited = new Set();
    function visit(slug) {
        if (visiting.has(slug)) {
            return true;
        }
        if (visited.has(slug)) {
            return false;
        }
        visiting.add(slug);
        for (const dependency of graph.get(slug) ?? []) {
            if (graph.has(dependency) && visit(dependency)) {
                return true;
            }
        }
        visiting.delete(slug);
        visited.add(slug);
        return false;
    }
    return [...graph.keys()].some((slug) => visit(slug));
}
function validateChildLinkage(input, state, errors) {
    if (!state.oat_parent) {
        return;
    }
    const relatedProjects = input.relatedProjects ?? [];
    const parent = relatedProjects.find((project) => project.slug === state.oat_parent);
    if (!parent && input.relatedProjects !== undefined) {
        errors.push({
            code: 'parent-missing',
            message: `oat_parent ${state.oat_parent} must reference an existing project`,
        });
    }
    if (parent && readKind(parent.frontmatter) !== 'coordination') {
        errors.push({
            code: 'parent-not-coordination',
            message: `oat_parent ${state.oat_parent} must reference a coordination project`,
        });
    }
    for (const dependency of state.oat_depends_on) {
        if (!state.oat_siblings.includes(dependency)) {
            errors.push({
                code: 'depends-on-non-sibling',
                message: `oat_depends_on entry ${dependency} must be listed in oat_siblings`,
            });
        }
    }
    if (input.slug && parent) {
        const parentChildren = readStringArrayField(parent.frontmatter, 'oat_children', errors);
        if (parentChildren.length > 0) {
            const expectedSiblings = parentChildren.filter((childSlug) => childSlug !== input.slug);
            if (!sameStringSet(state.oat_siblings, expectedSiblings)) {
                errors.push({
                    code: 'siblings-must-match-parent-children',
                    message: 'oat_siblings must equal parent oat_children minus the current child',
                });
            }
        }
    }
    const graph = new Map();
    if (input.slug) {
        graph.set(input.slug, state.oat_depends_on);
    }
    for (const project of relatedProjects) {
        if (project.slug === input.slug || project.slug === state.oat_parent) {
            continue;
        }
        if (readStringField(project.frontmatter, 'oat_parent') !== state.oat_parent) {
            continue;
        }
        graph.set(project.slug, readStringArrayField(project.frontmatter, 'oat_depends_on', errors));
    }
    if (graphHasCycle(graph)) {
        errors.push({
            code: 'sibling-dependency-cycle',
            message: 'oat_depends_on across sibling projects must be acyclic',
        });
    }
}
function validateInheritedContextGate(frontmatter, state, errors) {
    if (!state.oat_parent) {
        return;
    }
    if (readStringField(frontmatter, 'oat_status') === 'complete' &&
        frontmatter['oat_inherited_context_revalidated'] === false) {
        errors.push({
            code: 'inherited-context-revalidation-required',
            message: 'child discovery cannot complete until oat_inherited_context_revalidated is true',
        });
    }
}
function validateCoordinationParent(state, errors) {
    if (state.oat_phase !== 'decomposition') {
        return;
    }
    if (state.oat_children.length === 0) {
        errors.push({
            code: 'decomposition-requires-children',
            message: 'oat_phase: decomposition requires non-empty oat_children',
        });
    }
}
function parseFrontmatterObject(content, filePath) {
    const frontmatter = getFrontmatterBlock(content);
    if (!frontmatter) {
        throw new Error(`${filePath} is missing frontmatter`);
    }
    const parsed = YAML.parse(frontmatter);
    if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error(`${filePath} frontmatter must be a YAML object`);
    }
    return parsed;
}
export function validateProjectState(input) {
    const errors = [];
    const rawKind = readStringField(input.frontmatter, 'oat_kind');
    const rawPhase = readStringField(input.frontmatter, 'oat_phase');
    const rawPhaseStatus = readStringField(input.frontmatter, 'oat_phase_status');
    const rawParent = readStringField(input.frontmatter, 'oat_parent');
    let kind = 'implementation';
    if (rawKind) {
        if (isProjectStateKind(rawKind)) {
            kind = rawKind;
        }
        else {
            errors.push({
                code: 'invalid-oat-kind',
                message: `Invalid oat_kind: ${rawKind}`,
            });
        }
    }
    let phase;
    if (rawPhase) {
        if (isProjectStatePhase(rawPhase)) {
            phase = rawPhase;
        }
        else {
            errors.push({
                code: 'invalid-oat-phase',
                message: `Invalid oat_phase: ${rawPhase}`,
            });
        }
    }
    if (phase === 'decomposition' && kind !== 'coordination') {
        errors.push({
            code: 'decomposition-requires-coordination',
            message: 'oat_phase: decomposition requires oat_kind: coordination',
        });
    }
    const state = {
        oat_kind: kind,
        oat_phase: phase,
        oat_phase_status: rawPhaseStatus,
        oat_parent: rawParent,
        oat_siblings: readStringArrayField(input.frontmatter, 'oat_siblings', errors),
        oat_depends_on: readStringArrayField(input.frontmatter, 'oat_depends_on', errors),
        oat_children: readStringArrayField(input.frontmatter, 'oat_children', errors),
    };
    validateChildLinkage(input, state, errors);
    validateInheritedContextGate(input.frontmatter, state, errors);
    validateCoordinationParent(state, errors);
    return {
        ok: errors.length === 0,
        state,
        errors,
    };
}
export function assertValidProjectStateContent(content, options = {}) {
    const filePath = options.filePath ?? 'state.md';
    const frontmatter = parseFrontmatterObject(content, filePath);
    const result = validateProjectState({
        frontmatter,
        slug: options.slug,
        relatedProjects: options.relatedProjects,
    });
    if (!result.ok) {
        throw new Error(result.errors.map((error) => error.message).join('; '));
    }
}
export async function readRelatedProjectStateSnapshots(options) {
    const readdir = options.readdir ?? defaultReaddir;
    const readFile = options.readFile ?? defaultReadFile;
    const snapshots = [];
    const entries = await readdir(options.projectsRoot, { withFileTypes: true });
    for (const entry of entries) {
        if (!entry.isDirectory() || entry.name === options.currentProjectSlug) {
            continue;
        }
        const statePath = join(options.projectsRoot, entry.name, 'state.md');
        try {
            const content = await readFile(statePath, 'utf8');
            snapshots.push({
                slug: entry.name,
                frontmatter: parseFrontmatterObject(content, statePath),
            });
        }
        catch {
            continue;
        }
    }
    return snapshots;
}
export async function assertValidProjectStateFilesystemContent(content, options) {
    const slug = options.slug ?? basename(options.projectPath);
    const relatedProjects = await readRelatedProjectStateSnapshots({
        projectsRoot: options.projectsRoot ?? join(options.projectPath, '..'),
        currentProjectSlug: slug,
        readdir: options.readdir,
        readFile: options.readFile,
    });
    const frontmatter = parseFrontmatterObject(content, options.filePath ?? 'state.md');
    const result = validateProjectState({
        frontmatter,
        slug,
        relatedProjects,
    });
    const errors = [...result.errors];
    if (result.state.oat_kind === 'coordination' ||
        result.state.oat_phase === 'decomposition') {
        const readdir = options.readdir ?? defaultReaddir;
        const entries = await readdir(options.projectPath, {
            withFileTypes: true,
        });
        const executableArtifacts = new Set([
            'spec.md',
            'design.md',
            'plan.md',
            'implementation.md',
        ]);
        const presentExecutableArtifacts = entries
            .filter((entry) => entry.isFile() && executableArtifacts.has(entry.name))
            .map((entry) => entry.name)
            .sort();
        if (presentExecutableArtifacts.length > 0) {
            errors.push({
                code: 'coordination-parent-no-executable-artifacts',
                message: `coordination projects must not contain executable phase artifacts: ${presentExecutableArtifacts.join(', ')}`,
            });
        }
    }
    if (errors.length > 0) {
        throw new Error(errors.map((error) => error.message).join('; '));
    }
}
