/**
 * Pure report→channel matching for notification delivery.
 *
 * Zero I/O (D0002 / D0045): given a published report, the teams that might
 * subscribe, and a delivery cadence, decide which channels should receive it.
 * The side effects — loading teams, posting to Slack — live behind the ports
 * in `ports/notification.ts` and their eval adapters.
 *
 * @see docs/design-docs/notification-delivery-port.md § 5 (event derivation
 *   and scope semantics)
 */
/** An absent `delivery` is treated as `immediate`. */
const DEFAULT_DELIVERY = "immediate";
/** An absent `scope` defaults to `owned` (matches the Studio initialValue). */
const DEFAULT_SCOPE = { type: "owned" };
/**
 * Map a published report to the event types it satisfies. `eval.completed` is
 * always present; the rest are derived from the report's own fields. Order is
 * stable: completed, score-regressed, threshold-breached, failed.
 *
 * `eval.failed` is derived only from `report.degraded` — never from run-level
 * aborts, which never produce a published report (design doc § 5).
 */
export function deriveEvents(report) {
    const events = ["eval.completed"];
    if ((report.comparison?.regressed.length ?? 0) > 0) {
        events.push("eval.score-regressed");
    }
    if (report.summary.belowCritical.length > 0) {
        events.push("eval.threshold-breached");
    }
    if (report.degraded !== undefined) {
        events.push("eval.failed");
    }
    return events;
}
/**
 * Resolve which channels across the given teams should receive this report at
 * the requested cadence. A channel is delivered when all three filters pass:
 * its cadence equals `cadence`, its `events[]` admits the report's derived
 * events, and its scope matches. Results are deduplicated by channel key, so a
 * channel reachable through two teams yields a single delivery (first team
 * encountered wins).
 */
export function matchReportToChannels(report, teams, cadence) {
    const derived = new Set(deriveEvents(report));
    const seen = new Set();
    const out = [];
    for (const team of teams) {
        for (const channel of team.notifications ?? []) {
            if ((channel.delivery ?? DEFAULT_DELIVERY) !== cadence)
                continue;
            if (!eventsMatch(channel.events, derived))
                continue;
            if (!scopeMatches(channel.scope ?? DEFAULT_SCOPE, report, team))
                continue;
            const key = channelKey(channel);
            if (seen.has(key))
                continue;
            seen.add(key);
            out.push({
                team: { slug: team.slug, displayName: team.displayName },
                channel,
            });
        }
    }
    return out;
}
/** Empty (or absent) `events[]` means "all events"; otherwise it must intersect. */
function eventsMatch(events, derived) {
    if (!events || events.length === 0)
        return true;
    return events.some((event) => derived.has(event));
}
function scopeMatches(scope, report, team) {
    switch (scope.type) {
        case "all":
            return true;
        case "owned":
            // D0037 owner attribution. The design doc's second `owned` clause —
            // report area in a team-owned area — needs `area.team` refs the pure
            // matcher never receives, so it is deferred (near-0% populated today).
            return report.provenance.owner.team === team.slug;
        case "areas":
            return report.provenance.areas.some((area) => scope.areas.includes(area));
        case "repos":
            return scope.repos.includes(report.provenance.source.name);
        case "tags":
            // Accepted by the schema, deliberate no-op in v1: reports carry a single
            // publishTag and D0055 marked `tags` future-ready (design doc § 5).
            return false;
    }
}
/**
 * Stable per-channel identity, used both to deduplicate matches and as the
 * `notificationResults` artifact key. A slack channel is identified by its
 * Slack channel ID; email by its address set; webhook by its logical name. The
 * `type:` prefix keeps keys distinct across channel types (so a webhook named
 * `C0123` never collides with slack channel `C0123`). Two channels with the
 * same key deliver once.
 */
export function channelKey(channel) {
    switch (channel.type) {
        case "slack":
            return `slack:${channel.channelId}`;
        case "email":
            return `email:${channel.addresses.join(",")}`;
        case "webhook":
            return `webhook:${channel.logicalName}`;
    }
}
