/**
 * Adapter: load notification-subscribed teams from the Content Lake.
 *
 * Implements the `TeamSubscriptionSource` read port (D0002): fetch active
 * teams that have at least one notification channel via GROQ, parse each
 * through `SubscribedTeamSchema`, and return the valid ones. This is pure data
 * access — report→channel matching lives in the pure core matcher, not here.
 *
 * Parsing uses `SubscribedTeamSchema`, the canonical `TeamSchema` with the
 * `members.min(1)` roster requirement relaxed: the matcher routes by report
 * ownership + channel scope and never reads `members`, and the team-side roster
 * is empty for nearly every team (membership lives user-side as
 * `ailf.user.teams[]`, personalization-only — D0075). Requiring a roster here
 * would silently drop every channel-bearing but member-less team. `slug`,
 * `status`, and the `notifications[]` discriminated union are still validated.
 *
 * A document that fails validation is logged and skipped, never thrown: one
 * malformed team doc must not silence notifications for every other team.
 *
 * Constructed in the composition root from the report store's Sanity client
 * (dataset `ailf-prod-private`) — the same client that writes reports.
 *
 * @see packages/core/src/ports/notification.ts — TeamSubscriptionSource port
 * @see packages/core/src/schemas/team.ts — SubscribedTeamSchema / TeamSchema
 * @see docs/decisions/D0002-hexagonal-architecture.md
 */
import { SubscribedTeamSchema } from "../_vendor/ailf-core/index.js";
// ---------------------------------------------------------------------------
// GROQ query
// ---------------------------------------------------------------------------
/**
 * Select active teams that have at least one notification channel.
 *
 * The projection maps the persisted Sanity shape onto the flat domain shape
 * `TeamSchema` expects. The Studio schema is a deliberate persistence subset
 * (D0045), so the query does three things that a fake-client unit test cannot
 * guard (it returns post-projection docs); the GROQ here is verified against
 * live `ailf-prod-private` data instead:
 *
 * 1. Spread (`...`) carries every persisted field rather than naming each one.
 *    This is load-bearing, not a shortcut: GROQ projects an *explicitly named
 *    but absent* attribute as `null`, and every optional `TeamSchema` field
 *    (`description`, `repos`, channel `purpose` / `channelName` / `events` /
 *    `scope` / `delivery`) is `string | undefined`, which rejects `null`. So a
 *    team with no description, or a channel with no purpose, would fail parsing
 *    and be silently skipped. Spread omits absent attributes entirely, so they
 *    arrive as `undefined` and parse cleanly. The schema strips the extra
 *    system fields the spread carries (`_type`, `_rev`, `_createdAt`, ...).
 * 2. `slug` persists as the slug object `{ _type: "slug", current }`, but
 *    `TeamSchema.slug` is a plain (branded) string, so `"slug": slug.current`
 *    overrides the spread with the scalar. Ordering uses `slug.current` for the
 *    same reason (ordering by the object is meaningless).
 * 3. A notification channel's variant is its array-member `_type`
 *    (`slackChannel` / `emailChannel` / `webhookChannel`), but the schema's
 *    discriminated union keys on a domain `type` (`slack` / `email` /
 *    `webhook`). A channel authored directly in Studio carries only `_type` and
 *    no `type`, so deriving `type` from `_type` via `select(...)` lets both
 *    Studio-authored and dashboard-authored channels parse. `_type` is always
 *    present (Sanity writes it for every array member), so it is the canonical
 *    source; a channel variant the `select` does not map projects `type: null`
 *    and is then skipped by the discriminated-union parse (fail-safe).
 *
 * Cost note (project memory `reference_groq_array_predicate_fetch`):
 * `count(notifications) > 0` forces a full Content Lake fetch off the indexed
 * aggregate. Team docs are few (~16) so the cost is immaterial here — do NOT
 * copy this predicate onto a hot path. `notifications != []` would stay indexed
 * but is wrong (it does not mean "has at least one element").
 */
const SUBSCRIBED_TEAMS_QUERY = /* groq */ `
*[_type == "ailf.team" && status == "active" && count(notifications) > 0]
  | order(slug.current asc) {
  ...,
  "id": _id,
  "slug": slug.current,
  notifications[] {
    ...,
    "type": select(
      _type == "slackChannel" => "slack",
      _type == "emailChannel" => "email",
      _type == "webhookChannel" => "webhook"
    )
  }
}
`;
// ---------------------------------------------------------------------------
// Adapter
// ---------------------------------------------------------------------------
export class ContentLakeTeamSubscriptionSource {
    client;
    constructor(client) {
        this.client = client;
    }
    async loadSubscribedTeams() {
        const raw = await this.client.fetch(SUBSCRIBED_TEAMS_QUERY);
        if (!Array.isArray(raw)) {
            console.warn("ContentLakeTeamSubscriptionSource: GROQ query did not return an array; returning no teams");
            return [];
        }
        const teams = [];
        for (const doc of raw) {
            const parsed = SubscribedTeamSchema.safeParse(doc);
            if (!parsed.success) {
                const id = teamIdOf(doc);
                const issues = parsed.error.issues
                    .slice(0, 5)
                    .map((i) => `  [${i.path.join(".")}]: ${i.message}`)
                    .join("\n");
                console.warn(`ContentLakeTeamSubscriptionSource: team "${id}" failed schema ` +
                    `validation; skipping:\n${issues}`);
                continue;
            }
            teams.push(parsed.data);
        }
        return teams;
    }
}
/** Best-effort id extraction for the skip diagnostic; never throws. */
function teamIdOf(doc) {
    if (typeof doc === "object" && doc !== null && "id" in doc) {
        const id = doc.id;
        if (typeof id === "string")
            return id;
    }
    return "<unknown>";
}
