// Deterministic dated-statement extractor for Task 306.
//
// Reads a free-text body and returns one row per parseable dated statement.
// Strictly deterministic — no LLM, no heuristics that could collapse to a
// catch-all. The 305 typed-edge post-write hook upgrades `kind` from the
// default "mentioned" to a more specific value asynchronously; until then
// rows land as `mentioned`, which is the most-faithful default.
//
// Date forms recognised (in priority order):
//   1. ISO date           — 2026-03-10, 2026/03/10
//   2. Day-month-year     — 10 March 2026, 10 Mar 2026
//   3. Month-year         — March 2026, Mar 2026
//   4. "in YYYY"          — "in 2018"
//   5. "since YYYY"       — "since 2018"
//   6. Bare year          — 2018 (only when 1900-2099 and surrounded by word boundaries)
//   7. Relative + quarter — yesterday / last Tuesday / two weeks ago / Q3 2024
//      (Task 390 — delegated to parseRelativeDate against caller-supplied reference)
//
// Coarse forms (bare year, "in YYYY") parse to YYYY-01-01T00:00:00Z so the
// `occurredAt` index is queryable. Day-precision forms parse exactly.
// Relative forms resolve to a UTC day from the reference timestamp.
//
// A statement is the sentence containing the parsed date. Summaries
// truncate to 500 chars at sentence boundaries when possible.
import { parseRelativeDate } from "./relative-date.js";
const MONTHS = {
    jan: 1, january: 1,
    feb: 2, february: 2,
    mar: 3, march: 3,
    apr: 4, april: 4,
    may: 5,
    jun: 6, june: 6,
    jul: 7, july: 7,
    aug: 8, august: 8,
    sep: 9, sept: 9, september: 9,
    oct: 10, october: 10,
    nov: 11, november: 11,
    dec: 12, december: 12,
};
const SUMMARY_MAX = 500;
function pad(n) {
    return n < 10 ? `0${n}` : String(n);
}
function isoDay(year, month, day) {
    return `${year}-${pad(month)}-${pad(day)}T00:00:00Z`;
}
function isoMonth(year, month) {
    return `${year}-${pad(month)}-01T00:00:00Z`;
}
function isoYear(year) {
    return `${year}-01-01T00:00:00Z`;
}
function validYear(y) {
    return y >= 1900 && y <= 2099;
}
function validMonth(m) {
    return m >= 1 && m <= 12;
}
function validDay(d) {
    return d >= 1 && d <= 31;
}
/** Split body into sentences (period / question / exclamation followed by space or end). */
function splitSentences(body) {
    return body
        .split(/(?<=[.?!])\s+/)
        .map((s) => s.trim())
        .filter((s) => s.length > 0);
}
function clampSummary(s) {
    if (s.length <= SUMMARY_MAX)
        return s;
    // Truncate at the last sentence boundary inside the cap, else hard-truncate.
    const sliced = s.slice(0, SUMMARY_MAX);
    const lastBoundary = Math.max(sliced.lastIndexOf(". "), sliced.lastIndexOf("? "), sliced.lastIndexOf("! "));
    if (lastBoundary > SUMMARY_MAX * 0.5) {
        return sliced.slice(0, lastBoundary + 1);
    }
    return sliced.slice(0, SUMMARY_MAX - 1) + "…";
}
/**
 * Returns null if the sentence has no parseable date; otherwise the first
 * matching date and its ISO normalisation.
 */
function parseFirstDate(sentence, reference) {
    // 1. ISO date: 2026-03-10 or 2026/03/10
    const iso = sentence.match(/\b(\d{4})[-/](\d{1,2})[-/](\d{1,2})\b/);
    if (iso) {
        const y = Number(iso[1]);
        const m = Number(iso[2]);
        const d = Number(iso[3]);
        if (validYear(y) && validMonth(m) && validDay(d)) {
            return { occurredAt: isoDay(y, m, d), matched: iso[0], form: "iso" };
        }
    }
    // 2. Day-month-year: "10 March 2026" / "10 Mar 2026"
    const dmy = sentence.match(/\b(\d{1,2})\s+([A-Za-z]+)\s+(\d{4})\b/);
    if (dmy) {
        const d = Number(dmy[1]);
        const m = MONTHS[dmy[2].toLowerCase()];
        const y = Number(dmy[3]);
        if (m && validYear(y) && validDay(d)) {
            return { occurredAt: isoDay(y, m, d), matched: dmy[0], form: "dmy" };
        }
    }
    // 3. Month-year: "March 2026" / "Mar 2026"
    const my = sentence.match(/\b([A-Za-z]+)\s+(\d{4})\b/);
    if (my) {
        const m = MONTHS[my[1].toLowerCase()];
        const y = Number(my[2]);
        if (m && validYear(y)) {
            return { occurredAt: isoMonth(y, m), matched: my[0], form: "my" };
        }
    }
    // 4 + 5. "in YYYY" / "since YYYY"
    const inYear = sentence.match(/\b(?:in|since)\s+(\d{4})\b/i);
    if (inYear) {
        const y = Number(inYear[1]);
        if (validYear(y)) {
            return { occurredAt: isoYear(y), matched: inYear[0], form: "in-year" };
        }
    }
    // 6. Relative + quarter forms (Task 390). Delegated to the shared lib so the
    // datetime skill calls the same code path. Checked before bare-year so that
    // "Q3 2024" parses as the quarter rather than the bare year 2024.
    const rel = parseRelativeDate(sentence, reference);
    if (rel) {
        return {
            occurredAt: rel.date.toISOString().replace(/\.\d{3}Z$/, "Z"),
            matched: rel.matched,
            form: rel.form === "quarter" ? "quarter" : "relative",
        };
    }
    // 7. Bare year — restrictive: must be 4 digits surrounded by word boundaries
    // and not preceded by "$" (price), ":" (time), or "-" (already part of a
    // date we would have matched above). Cheap defensive guards.
    const bareYear = sentence.match(/(?<![$:\d-])\b(\d{4})\b(?!\s*[-/]\d)/);
    if (bareYear) {
        const y = Number(bareYear[1]);
        if (validYear(y)) {
            return { occurredAt: isoYear(y), matched: bareYear[0], form: "bare-year" };
        }
    }
    return null;
}
/**
 * Extract every dated statement from `body`. One row per sentence containing
 * the first parseable date in that sentence. Sentences without a date are
 * dropped silently (the diagnostic count is the caller's concern).
 *
 * `reference` anchors relative forms ("yesterday", "two weeks ago", "Q3 last
 * year"). Callers pass the message's createdAt; absent that, current time.
 */
export function extractDatedStatements(body, reference = new Date()) {
    if (!body || typeof body !== "string")
        return [];
    const ref = reference instanceof Date && !Number.isNaN(reference.getTime())
        ? reference
        : new Date();
    const out = [];
    const formCounts = {};
    for (const sentence of splitSentences(body)) {
        const hit = parseFirstDate(sentence, ref);
        if (hit) {
            out.push({
                occurredAt: hit.occurredAt,
                summary: clampSummary(sentence),
                matched: hit.matched,
                form: hit.form,
            });
            formCounts[hit.form] = (formCounts[hit.form] ?? 0) + 1;
        }
    }
    // Task 390 diagnostic: coverage so the rewriter team can observe parse rate.
    const forms = Object.keys(formCounts).sort().join(",");
    // eslint-disable-next-line no-console
    console.log(`[timeline-extract] coverage body-len=${body.length} dates=${out.length}${forms ? ` forms=[${forms}]` : ""}`);
    return out;
}
//# sourceMappingURL=timeline-extractor.js.map