import { access, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { stripTemplateFrontmatter } from '../shared/strip-template-frontmatter.js';
import YAML from 'yaml';
import { initializeBacklog } from './init.js';
import { regenerateBacklogIndex, } from './regenerate-index.js';
import { generateBacklogId } from './shared/generate-id.js';
const PRIORITIES = ['urgent', 'high', 'medium', 'low', 'none'];
const SCOPES = ['idea', 'task', 'feature', 'initiative'];
const SCOPE_ESTIMATES = ['XS', 'S', 'M', 'L', 'XL', 'XXL'];
const DESCRIPTION_PLACEHOLDER = '{Describe the problem, request, or capability tracked by this backlog item.}';
const DEFAULT_DEPENDENCIES = {
    initializeBacklog,
    regenerateBacklogIndex,
};
async function pathExists(path) {
    try {
        await access(path);
        return true;
    }
    catch (error) {
        const code = error && typeof error === 'object' && 'code' in error
            ? String(error.code)
            : null;
        if (code !== 'ENOENT') {
            throw error;
        }
        return false;
    }
}
async function resolveBacklogTemplate(assetsRoot, templatesRoot) {
    const candidates = [
        ...(templatesRoot ? [join(templatesRoot, 'backlog-item.md')] : []),
        join(assetsRoot, 'templates', 'backlog-item.md'),
    ];
    for (const path of candidates) {
        try {
            return { content: await readFile(path, 'utf8'), path };
        }
        catch (error) {
            const code = error && typeof error === 'object' && 'code' in error
                ? String(error.code)
                : null;
            if (code !== 'ENOENT') {
                throw error;
            }
        }
    }
    throw new Error('Template backlog-item.md was not found in repo-local templates or bundled assets.');
}
function normalizeChoice(field, value, fallback, allowed, transform) {
    const normalized = transform(value ?? fallback);
    if (!allowed.includes(normalized)) {
        throw new Error(`Invalid backlog ${field} "${value}". Valid values: ${allowed.join(', ')}.`);
    }
    return normalized;
}
function normalizeInputs(options) {
    const title = options.title.trim();
    if (!title ||
        title.includes('\r') ||
        title.includes('\n') ||
        title.includes('\0')) {
        throw new Error('Backlog title must be a non-empty single line.');
    }
    const date = new Date(options.createdAt ?? new Date().toISOString());
    if (Number.isNaN(date.getTime())) {
        throw new Error(`Invalid backlog creation timestamp: ${options.createdAt}`);
    }
    const priority = normalizeChoice('priority', options.priority, 'medium', PRIORITIES, (value) => value.trim().toLowerCase());
    const scope = normalizeChoice('scope', options.scope, 'task', SCOPES, (value) => value.trim().toLowerCase());
    const scopeEstimate = options.scopeEstimate === undefined
        ? null
        : normalizeChoice('scope estimate', options.scopeEstimate, '', SCOPE_ESTIMATES, (value) => value.trim().toUpperCase());
    const labels = (options.labels ?? []).map((label) => label.trim());
    if (labels.some((label) => !label ||
        label.includes('\r') ||
        label.includes('\n') ||
        label.includes('\0'))) {
        throw new Error('Backlog labels must be non-empty single-line values.');
    }
    if (options.description !== undefined &&
        (!options.description.trim() || options.description.includes('\0'))) {
        throw new Error('Backlog description must contain non-empty text.');
    }
    return {
        title,
        timestamp: date.toISOString(),
        priority,
        scope,
        scopeEstimate,
        labels,
        description: options.description,
    };
}
function renderBacklogItem(template, values) {
    let body = stripTemplateFrontmatter(template).replace(/^\r?\n+/, '');
    if (values.description !== undefined) {
        body = body.replace(DESCRIPTION_PLACEHOLDER, () => values.description);
    }
    const frontmatter = YAML.stringify({
        id: values.id,
        title: values.title,
        status: 'open',
        priority: values.priority,
        scope: values.scope,
        scope_estimate: values.scopeEstimate,
        labels: values.labels,
        assignee: null,
        created: values.timestamp,
        updated: values.timestamp,
        associated_issues: [],
        external_plans: [],
    }).trimEnd();
    return `---\n${frontmatter}\n---\n\n${body.trimEnd()}\n`;
}
export async function createBacklogItem(options, overrides = {}) {
    const dependencies = { ...DEFAULT_DEPENDENCIES, ...overrides };
    const values = normalizeInputs(options);
    const id = generateBacklogId(values.title, values.timestamp);
    const filePath = join(options.backlogRoot, 'items', `${id}.md`);
    const archivedPath = join(options.backlogRoot, 'archived', `${id}.md`);
    if ((await pathExists(filePath)) || (await pathExists(archivedPath))) {
        throw new Error(`Backlog item ${id} already exists. Use a more specific title to disambiguate.`);
    }
    const template = await resolveBacklogTemplate(options.assetsRoot, options.templatesRoot);
    const content = renderBacklogItem(template.content, { id, ...values });
    await dependencies.initializeBacklog(options.backlogRoot);
    const indexPath = join(options.backlogRoot, 'index.md');
    const originalIndex = await readFile(indexPath, 'utf8');
    await writeFile(filePath, content, { encoding: 'utf8', flag: 'wx' });
    let index;
    try {
        index = await dependencies.regenerateBacklogIndex(options.backlogRoot);
    }
    catch (error) {
        const rollback = await Promise.allSettled([
            rm(filePath),
            writeFile(indexPath, originalIndex, 'utf8'),
        ]);
        const rollbackErrors = rollback
            .filter((result) => result.status === 'rejected')
            .map((result) => result.reason);
        if (rollbackErrors.length > 0) {
            const rollbackError = new AggregateError([error, ...rollbackErrors], `Backlog item ${id} creation failed and rollback was incomplete.`);
            rollbackError.cause = error;
            throw rollbackError;
        }
        throw error;
    }
    return {
        id,
        backlogRoot: options.backlogRoot,
        filePath,
        templatePath: template.path,
        index,
    };
}
