import { readdir, readFile } from 'node:fs/promises';
import { isAbsolute, join, relative } from 'node:path';
import { buildCommandContext, } from '../../app/command-context.js';
import { getFrontmatterBlock, getFrontmatterField, parseGeneratedTime, } from '../shared/frontmatter.js';
import { readGlobalOptions } from '../shared/shared.utils.js';
import { readOatLocalConfig } from '../../config/oat-config.js';
import { normalizeToPosixPath, resolveProjectRoot } from '../../fs/paths.js';
import { Command } from 'commander';
const DEFAULT_DEPENDENCIES = {
    buildCommandContext,
    resolveProjectRoot,
    readOatLocalConfig,
};
const EMPTY_RESULT = {
    path: null,
    scope: null,
    generatedAt: null,
    kind: null,
    archived: null,
    actionable: null,
};
function normalizeRepoRelativePath(repoRoot, pathValue) {
    const rawPath = pathValue.trim();
    if (!rawPath) {
        return rawPath;
    }
    if (!isAbsolute(rawPath)) {
        return normalizeToPosixPath(rawPath).replace(/^\.\//, '');
    }
    return normalizeToPosixPath(relative(repoRoot, rawPath));
}
function reviewLifecycleRank(scope) {
    const normalizedScope = scope.trim().toLowerCase();
    if (normalizedScope === 'final') {
        return Number.MAX_SAFE_INTEGER;
    }
    const phases = [...normalizedScope.matchAll(/p(\d+)/g)].map((match) => Number.parseInt(match[1] ?? '0', 10));
    if (phases.length === 0) {
        return 0;
    }
    const tasks = [...normalizedScope.matchAll(/(?:^|[-_])t(\d+)/g)].map((match) => Number.parseInt(match[1] ?? '0', 10));
    const latestPhase = Math.max(...phases);
    const latestTask = tasks.length > 0 ? Math.max(...tasks) : 9999;
    return latestPhase * 10_000 + latestTask;
}
async function listMarkdownFiles(repoRoot, relativeDir) {
    try {
        const entries = await readdir(join(repoRoot, relativeDir), {
            withFileTypes: true,
        });
        return entries
            .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
            .map((entry) => normalizeToPosixPath(join(relativeDir, entry.name)));
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            return [];
        }
        throw error;
    }
}
async function readReviewCandidate(repoRoot, relativePath, kind, priority, archived, actionable) {
    const content = await readFile(join(repoRoot, relativePath), 'utf8');
    const frontmatter = getFrontmatterBlock(content);
    if (!frontmatter) {
        return null;
    }
    const generatedAt = getFrontmatterField(frontmatter, 'oat_generated_at');
    if (!generatedAt) {
        return null;
    }
    const generatedTime = parseGeneratedTime(generatedAt);
    if (Number.isNaN(generatedTime)) {
        return null;
    }
    const scope = getFrontmatterField(frontmatter, 'oat_review_scope') ?? '';
    const reviewType = getFrontmatterField(frontmatter, 'oat_review_type') ?? 'code';
    return {
        path: relativePath,
        scope,
        reviewType,
        generatedAt,
        kind,
        archived,
        actionable,
        generatedTime,
        lifecycleRank: reviewLifecycleRank(scope),
        priority,
    };
}
function parseReviewLedgerEvents(planContent) {
    const heading = /^## Reviews[ \t]*\r?$/m.exec(planContent);
    if (!heading) {
        return [];
    }
    const remaining = planContent.slice(heading.index + heading[0].length);
    const nextHeadingIndex = remaining.search(/^##(?!#)[ \t]+\S.*\r?$/m);
    const section = nextHeadingIndex === -1 ? remaining : remaining.slice(0, nextHeadingIndex);
    return section
        .split('\n')
        .filter((line) => line.trim().startsWith('|'))
        .slice(2)
        .map((line) => line
        .split('|')
        .slice(1, -1)
        .map((cell) => cell.trim()))
        .filter((cells) => cells.length === 5)
        .map(([scope = '', type = '', status = '', , artifact = '']) => ({
        scope,
        type,
        status,
        artifact,
    }));
}
async function correlateProjectActionability(repoRoot, projectPath, candidates) {
    let planContent;
    try {
        planContent = await readFile(join(repoRoot, projectPath, 'plan.md'), 'utf8');
    }
    catch (error) {
        if (typeof error === 'object' &&
            error !== null &&
            'code' in error &&
            error.code === 'ENOENT') {
            return candidates;
        }
        throw error;
    }
    const events = parseReviewLedgerEvents(planContent);
    if (events.length === 0) {
        return candidates;
    }
    return candidates.map((candidate) => {
        if (candidate.kind !== 'project' || candidate.archived) {
            return candidate;
        }
        const artifact = normalizeToPosixPath(relative(projectPath, candidate.path));
        const matchingEvent = events.find((event) => event.scope.toLowerCase() === candidate.scope.toLowerCase() &&
            event.type.toLowerCase() === candidate.reviewType.toLowerCase() &&
            normalizeToPosixPath(event.artifact) === artifact);
        return {
            ...candidate,
            actionable: matchingEvent?.status.toLowerCase() === 'received',
        };
    });
}
function sortReviewCandidates(a, b) {
    if (a.generatedTime !== b.generatedTime) {
        return b.generatedTime - a.generatedTime;
    }
    if (a.priority !== b.priority) {
        return a.priority - b.priority;
    }
    if (a.lifecycleRank !== b.lifecycleRank) {
        return b.lifecycleRank - a.lifecycleRank;
    }
    // Same scope, same generated time: fall back to a stable path order. Review
    // artifacts carry a seconds-precision `oat_generated_at`, so distinct re-gate
    // rounds differ on generatedTime above and never reach this branch.
    return a.path.localeCompare(b.path);
}
export async function findLatestReview(options) {
    const projectPath = options.projectPath
        ? normalizeRepoRelativePath(options.repoRoot, options.projectPath)
        : null;
    const scanTargets = [];
    if (projectPath) {
        scanTargets.push({
            dir: `${projectPath}/reviews`,
            kind: 'project',
            priority: 0,
            archived: false,
            actionable: true,
        });
        if (!options.actionableProjectOnly) {
            scanTargets.push({
                dir: `${projectPath}/reviews/archived`,
                kind: 'project',
                priority: 1,
                archived: true,
                actionable: false,
            });
        }
    }
    if (!options.actionableProjectOnly) {
        scanTargets.push({
            dir: '.oat/repo/reviews',
            kind: 'adhoc',
            priority: 2,
            archived: false,
            actionable: true,
        }, {
            dir: '.oat/projects/local/orphan-reviews',
            kind: 'adhoc',
            priority: 3,
            archived: false,
            actionable: true,
        });
    }
    let candidates = (await Promise.all(scanTargets.map(async (target) => {
        const files = await listMarkdownFiles(options.repoRoot, target.dir);
        return Promise.all(files.map((file) => readReviewCandidate(options.repoRoot, file, target.kind, target.priority, target.archived, target.actionable)));
    })))
        .flat()
        .filter((candidate) => candidate !== null);
    if (projectPath) {
        candidates = await correlateProjectActionability(options.repoRoot, projectPath, candidates);
    }
    if (options.actionableProjectOnly) {
        candidates = candidates.filter((candidate) => candidate.actionable);
    }
    candidates.sort(sortReviewCandidates);
    const latest = candidates[0];
    if (!latest) {
        return null;
    }
    return {
        path: latest.path,
        scope: latest.scope,
        generatedAt: latest.generatedAt,
        kind: latest.kind,
        archived: latest.archived,
        actionable: latest.actionable,
    };
}
async function resolveProjectPath(repoRoot, explicitProject, dependencies) {
    if (explicitProject) {
        return explicitProject;
    }
    const localConfig = await dependencies.readOatLocalConfig(repoRoot);
    return localConfig.activeProject ?? null;
}
async function runReviewLatest(context, options, dependencies) {
    try {
        const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
        const projectPath = await resolveProjectPath(repoRoot, options.project, dependencies);
        const result = await findLatestReview({
            repoRoot,
            projectPath,
            actionableProjectOnly: options.actionableProject,
        });
        if (context.json) {
            context.logger.json(result ?? EMPTY_RESULT);
        }
        else if (result) {
            context.logger.info(result.path);
        }
        process.exitCode = 0;
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        if (context.json) {
            context.logger.json({ status: 'error', message });
        }
        else {
            context.logger.error(message);
        }
        process.exitCode = 1;
    }
}
export function createReviewLatestCommand(overrides = {}) {
    const dependencies = {
        ...DEFAULT_DEPENDENCIES,
        ...overrides,
    };
    return new Command('latest')
        .description('Find the most recent OAT review artifact')
        .option('--project <path>', 'Project path to scan in addition to ad-hoc review locations')
        .option('--actionable-project', 'Scan only active project reviews, excluding archived and ad-hoc history')
        .action(async (options, command) => {
        const context = dependencies.buildCommandContext(readGlobalOptions(command));
        await runReviewLatest(context, options, dependencies);
    });
}
