#!/usr/bin/env node
import { execFile as execFileCallback } from 'node:child_process';
import { promisify } from 'node:util';
const execFile = promisify(execFileCallback);
const GH_TIMEOUT_MS = 30_000;
const LABEL_STATUS_ACTIVE = 'status:active';
const LABEL_TYPE_DEBT = 'type:debt';
const LABEL_WORK_IN_PROGRESS = 'work-in-progress';
const ACTIVE_LABELS = new Set([LABEL_STATUS_ACTIVE, LABEL_WORK_IN_PROGRESS]);
function isBrokenPipe(error) {
    return Reflect.get(error, 'code') === 'EPIPE';
}
process.stdout.on('error', (error) => {
    if (isBrokenPipe(error)) {
        process.exit(0);
    }
    throw error;
});
function issueSummary(issue) {
    return {
        number: issue.number,
        title: issue.title,
        url: issue.url,
    };
}
function issueLabels(issue) {
    return issue.labels.map((label) => label.name).sort(compareStrings);
}
function compareNumbers(left, right) {
    return left - right;
}
function compareStrings(left, right) {
    if (left < right) {
        return -1;
    }
    if (left > right) {
        return 1;
    }
    return 0;
}
function compareIssues(left, right) {
    return compareNumbers(left.number, right.number);
}
function labelCounts(issues) {
    const counts = new Map();
    for (const issue of issues) {
        for (const label of issueLabels(issue)) {
            counts.set(label, (counts.get(label) ?? 0) + 1);
        }
    }
    return [...counts.entries()]
        .map(([label, count]) => ({ label, count }))
        .sort((left, right) => compareStrings(left.label, right.label));
}
function hasAnyLabel(issue, labels) {
    return issueLabels(issue).some((label) => labels.has(label));
}
function hasLabel(issue, label) {
    return issueLabels(issue).includes(label);
}
function issueNumberSet(issues) {
    return new Set(issues.map((issue) => issue.number));
}
function coveredOpenIssueNumbers(openIssues, pullRequests) {
    const openNumbers = issueNumberSet(openIssues);
    const covered = new Set();
    for (const pr of pullRequests) {
        for (const issue of pr.closingIssuesReferences) {
            if (openNumbers.has(issue.number)) {
                covered.add(issue.number);
            }
        }
    }
    return covered;
}
function sortedIssueNumbers(issues) {
    return issues.map((issue) => issue.number).sort(compareNumbers);
}
function prCoverage(openIssues, pullRequests) {
    const openNumbers = issueNumberSet(openIssues);
    return pullRequests
        .map((pr) => ({
        number: pr.number,
        title: pr.title,
        url: pr.url,
        isDraft: pr.isDraft,
        closes: pr.closingIssuesReferences
            .filter((issue) => openNumbers.has(issue.number))
            .map(issueSummary)
            .sort(compareIssues),
    }))
        .filter((pr) => pr.closes.length > 0)
        .sort((left, right) => compareNumbers(left.number, right.number));
}
export function buildIssueTriageReport(openIssues, pullRequests) {
    const coveredNumbers = coveredOpenIssueNumbers(openIssues, pullRequests);
    const activeIssues = openIssues.filter((issue) => hasAnyLabel(issue, ACTIVE_LABELS));
    const availableIssues = openIssues.filter((issue) => !coveredNumbers.has(issue.number) && !hasAnyLabel(issue, ACTIVE_LABELS));
    const debtCandidates = availableIssues
        .filter((issue) => hasLabel(issue, LABEL_TYPE_DEBT))
        .map(issueSummary)
        .sort(compareIssues);
    return {
        rawOpenIssueCount: openIssues.length,
        prCoveredOpenIssueCount: coveredNumbers.size,
        activeOpenIssueCount: activeIssues.length,
        availableOpenIssueCount: availableIssues.length,
        prCoveredIssueNumbers: [...coveredNumbers].sort(compareNumbers),
        activeIssueNumbers: sortedIssueNumbers(activeIssues),
        availableIssueNumbers: sortedIssueNumbers(availableIssues),
        rawLabelCounts: labelCounts(openIssues),
        availableLabelCounts: labelCounts(availableIssues),
        prCoverage: prCoverage(openIssues, pullRequests),
        debtCandidates,
    };
}
export function formatIssueTriageReport(report) {
    const lines = [
        'Issue triage report',
        '',
        `open.raw ${report.rawOpenIssueCount}`,
        `open.pr_covered ${report.prCoveredOpenIssueCount}`,
        `open.active ${report.activeOpenIssueCount}`,
        `open.available ${report.availableOpenIssueCount}`,
        '',
        'PR-covered issues',
        ...formatIssueNumbers(report.prCoveredIssueNumbers),
        '',
        'Available debt candidates',
        ...report.debtCandidates.map((issue) => `#${issue.number} ${issue.title}`),
        '',
        'Available label counts',
        ...report.availableLabelCounts.map((entry) => `${entry.label} ${entry.count}`),
    ];
    return `${lines.join('\n')}\n`;
}
function formatIssueNumbers(numbers) {
    if (numbers.length === 0) {
        return ['none'];
    }
    return numbers.map((number) => `#${number}`);
}
function parseArgs(args) {
    let format = 'text';
    let limit = '1000';
    for (const arg of args) {
        if (arg === '--json') {
            format = 'json';
            continue;
        }
        if (arg === '--text') {
            format = 'text';
            continue;
        }
        if (arg.startsWith('--limit=')) {
            limit = arg.slice('--limit='.length);
            continue;
        }
        throw new Error(`Unknown argument: ${arg}`);
    }
    return { format, limit };
}
async function ghJson(args) {
    try {
        const { stdout } = await execFile('gh', args, { encoding: 'utf8', timeout: GH_TIMEOUT_MS });
        return JSON.parse(stdout);
    }
    catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        throw new Error(`ghJson failed for gh ${args.join(' ')}: ${message}`);
    }
}
async function main(args) {
    const options = parseArgs(args);
    const issues = await ghJson([
        'issue',
        'list',
        '--state',
        'open',
        '--limit',
        options.limit,
        '--json',
        'number,title,url,labels',
    ]);
    const pullRequests = await ghJson([
        'pr',
        'list',
        '--state',
        'open',
        '--limit',
        '100',
        '--json',
        'number,title,url,isDraft,closingIssuesReferences',
    ]);
    const report = buildIssueTriageReport(issues, pullRequests);
    const output = options.format === 'json'
        ? `${JSON.stringify(report, null, 2)}\n`
        : formatIssueTriageReport(report);
    process.stdout.write(output);
}
if (import.meta.url === `file://${process.argv[1]}`) {
    main(process.argv.slice(2)).catch((error) => {
        console.error(`issue-triage-report: ${error.message}`);
        process.exit(1);
    });
}
