/**
 * Pure deterministic computation of coverage gaps for periodic_coverage validation rules.
 * No external dependencies, no I/O. Given expected period bounds and a list of file periods,
 * returns the gaps where no file claims coverage.
 */
/** Compute coverage gaps. Returns gaps in chronological order. Empty array = full coverage. */
export function compute_coverage_gaps(input) {
    const expected_periods = enumerate_expected_periods(input.expected_start, input.expected_end, input.cadence, input.alignment);
    const gaps = [];
    for (const expected of expected_periods) {
        if (!is_period_covered(expected, input.files)) {
            gaps.push(expected);
        }
    }
    return gaps;
}
function enumerate_expected_periods(start, end, cadence, alignment) {
    const start_date = parse_iso_date(start);
    const end_date = parse_iso_date(end);
    if (start_date > end_date)
        return [];
    if (alignment === 'calendar_aligned' && cadence === 'monthly') {
        return tile_calendar_months(start_date, end_date);
    }
    if (alignment === 'calendar_aligned' && cadence === 'quarterly') {
        return tile_calendar_quarters(start_date, end_date);
    }
    const step_days = cadence_step_days(cadence);
    return tile_flexible(start_date, end_date, step_days);
}
function cadence_step_days(cadence) {
    switch (cadence) {
        case 'biweekly':
        case 'fortnightly':
            return 14;
        case 'monthly':
            return 30;
        case 'quarterly':
            return 91;
    }
}
function tile_flexible(start, end, step_days) {
    const periods = [];
    // Strict `<` so we don't emit a degenerate single-day cycle when cursor lands
    // exactly on `end`. Example: AU FY (2024-07-01 to 2025-06-30) at 14-day step
    // produces 26 cycles, not 27.
    let cursor = new Date(start);
    while (cursor < end) {
        const next = add_days(cursor, step_days);
        const period_end = new Date(Math.min(next.getTime() - DAY_MS, end.getTime()));
        periods.push({ start: format_iso_date(cursor), end: format_iso_date(period_end) });
        cursor = next;
    }
    return periods;
}
function tile_calendar_months(start, end) {
    const periods = [];
    let cursor = new Date(start.getFullYear(), start.getMonth(), 1);
    while (cursor <= end) {
        const month_end = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0);
        const period_end = month_end > end ? end : month_end;
        const period_start = cursor < start ? start : cursor;
        periods.push({ start: format_iso_date(period_start), end: format_iso_date(period_end) });
        cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1);
    }
    return periods;
}
function tile_calendar_quarters(start, end) {
    const periods = [];
    const quarter_start_month = Math.floor(start.getMonth() / 3) * 3;
    let cursor = new Date(start.getFullYear(), quarter_start_month, 1);
    while (cursor <= end) {
        const quarter_end = new Date(cursor.getFullYear(), cursor.getMonth() + 3, 0);
        const period_end = quarter_end > end ? end : quarter_end;
        const period_start = cursor < start ? start : cursor;
        periods.push({ start: format_iso_date(period_start), end: format_iso_date(period_end) });
        cursor = new Date(cursor.getFullYear(), cursor.getMonth() + 3, 1);
    }
    return periods;
}
/**
 * A cycle is "covered" when some file's period_start falls within the cycle's
 * range. Each file claims exactly one cycle — the one its start date lands in.
 *
 * This is stricter than "any overlap": a Jan 15 statement covering Jan 15–28
 * does NOT count as covering the next cycle (Jan 27 – Feb 9) just because the
 * tail of one overlaps the head of the other. That kind of "tail-touch" hides
 * genuinely missing files — e.g. when the Feb 1 statement is missing, the Jan
 * 15 statement's last 2 days would otherwise mask the gap.
 *
 * Edge case: a long file (an annual summary instead of monthly bills) claims
 * the cycle its start date falls in, and only that one. The other 11 cycles
 * register as missing — usually the desired behaviour.
 */
function is_period_covered(expected, files) {
    const exp_start = parse_iso_date(expected.start);
    const exp_end = parse_iso_date(expected.end);
    return files.some(file => {
        if (!file.period_start)
            return false;
        const f_start = parse_iso_date(file.period_start);
        return f_start >= exp_start && f_start <= exp_end;
    });
}
const DAY_MS = 24 * 60 * 60 * 1000;
function parse_iso_date(s) {
    const matched = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
    if (!matched)
        throw new Error(`Invalid ISO date: ${s}`);
    return new Date(parseInt(matched[1], 10), parseInt(matched[2], 10) - 1, parseInt(matched[3], 10));
}
function format_iso_date(d) {
    const yyyy = d.getFullYear();
    const mm = String(d.getMonth() + 1).padStart(2, '0');
    const dd = String(d.getDate()).padStart(2, '0');
    return `${yyyy}-${mm}-${dd}`;
}
function add_days(d, days) {
    return new Date(d.getFullYear(), d.getMonth(), d.getDate() + days);
}
//# sourceMappingURL=periodic_coverage_runner.js.map