/**
 * Where the event-tracking documents live and what their fields are called.
 *
 * The events are produced outside this monorepo, so the document shape is not
 * enforced by anything here. Rather than scatter string literals through the
 * service, every assumption about the source data is isolated in this one file
 * and can be corrected without touching query or aggregation logic.
 *
 * Override any subset at runtime with EVENT_TRACKING_FIELD_MAP, a JSON object
 * using the keys below, e.g.
 *
 *   EVENT_TRACKING_FIELD_MAP={"occurredAt":"created_at","memberId":"member_no"}
 *
 * NOTE: `occurredAt`, `memberId`, `currency`, `propertyName` and `source` are
 * used in Firestore `where` / `orderBy` clauses, so they must each name exactly
 * one real field. Getting one wrong yields empty results rather than an error.
 *
 * The event *type* is not a field — it is the collection the document lives in.
 * See `getEventCollections`.
 */
export type EventFieldMap = {
  /** When the event happened. Range-filtered and sorted on. */
  occurredAt: string;
  /** Stable member identifier. Grouped on. */
  memberId: string;
  /** Human-readable member name, display only. Absent from the current producer. */
  memberName: string;
  /** Member email, display + search only. Absent from the current producer. */
  memberEmail: string;
  /** Booking reference, display + search only. Absent from the current producer. */
  bookingRef: string;
  /** Numeric revenue for the event. Summed, per currency. */
  revenue: string;
  /** ISO-4217 currency code for `revenue`. Grouped on. */
  currency: string;
  /** Loyalty points earned by the event. Summed. */
  points: string;
  /** Property the event happened at. Filtered and grouped on. */
  propertyName: string;
  /** Experience the event relates to, where it has one. Display only. */
  experienceName: string;
  /** Member offer the event relates to, where it has one. Display only. */
  memberOffer: string;
  /**
   * Where in the app the event came from, e.g. "properties". Filtered on.
   *
   * This is NOT the event type — an `add_to_cart` from the properties section
   * and a `purchase` from the properties section share this value. It is a
   * second dimension, cutting across event types.
   */
  source: string;
};

/**
 * Defaults matched to a real document from the `event-tracking` database:
 *
 *   amount          null      (number when the event carries revenue)
 *   createdAt       "2026-08-07T17:09:05.822772"   naive ISO string, no timezone
 *   currency        null
 *   experienceName  null
 *   memberNumber    "1278640"
 *   memberOffer     null
 *   points          11        int64
 *   propertyName    "Karma Chakra"
 *   sourceOfEvent   "properties"
 *
 * The producer writes no member name, email or booking reference, so those
 * columns are empty. They are kept in the map because it costs nothing and the
 * producer may add them.
 */
const DEFAULT_FIELD_MAP: EventFieldMap = {
  occurredAt: "createdAt",
  memberId: "memberNumber",
  memberName: "memberName",
  memberEmail: "memberEmail",
  bookingRef: "bookingRef",
  revenue: "amount",
  currency: "currency",
  points: "points",
  propertyName: "propertyName",
  experienceName: "experienceName",
  memberOffer: "memberOffer",
  source: "sourceOfEvent",
};

/**
 * Extra document keys tried when the mapped key is absent.
 *
 * Only consulted for display-only fields when reading a document that has
 * already been fetched — never for building a query, where a single concrete
 * field name is required. This makes the detail table resilient to naming
 * drift between event producers without affecting filter correctness.
 */
export const FALLBACK_KEYS: Partial<Record<keyof EventFieldMap, string[]>> = {
  occurredAt: [
    "occurredAt",
    "occurred_at",
    "created_at",
    "timestamp",
    "eventTime",
    "event_time",
    "date",
  ],
  memberId: [
    "memberId",
    "member_id",
    "member_number",
    "membershipNumber",
    "membership_number",
    "userId",
    "user_id",
    "accountId",
    "account_id",
  ],
  memberName: ["member_name", "name", "fullName", "full_name", "userName"],
  memberEmail: ["member_email", "email", "userEmail", "user_email"],
  bookingRef: [
    "booking_ref",
    "bookingReference",
    "booking_reference",
    "bookingId",
    "booking_id",
    "reservationId",
    "confirmationNumber",
  ],
  revenue: [
    "revenue",
    "totalAmount",
    "total_amount",
    "price",
    "value",
    "grandTotal",
  ],
  currency: ["currency_code", "currencyCode", "curr"],
  points: ["point", "pointsEarned", "points_earned", "loyaltyPoints"],
  propertyName: ["property_name", "property", "resortName", "resort_name"],
  experienceName: ["experience_name", "experience"],
  memberOffer: ["member_offer", "offerName", "offer_name", "offer"],
  source: ["source_of_event", "source", "eventSource", "event_source"],
};

let cachedFieldMap: EventFieldMap | null = null;

/** The active field map: defaults, with any EVENT_TRACKING_FIELD_MAP keys applied on top. */
export function getFieldMap(): EventFieldMap {
  if (cachedFieldMap) return cachedFieldMap;

  const raw = process.env.EVENT_TRACKING_FIELD_MAP;
  let overrides: Partial<EventFieldMap> = {};
  if (raw) {
    try {
      overrides = JSON.parse(raw) as Partial<EventFieldMap>;
    } catch {
      // A malformed override must not take the module down; the defaults are a
      // working configuration and the mistake is visible in the logs.
      console.error(
        "EVENT_TRACKING_FIELD_MAP is not valid JSON; using default field map.",
      );
    }
  }

  cachedFieldMap = { ...DEFAULT_FIELD_MAP, ...overrides };
  return cachedFieldMap;
}

/**
 * The event types, which are the top-level collections of the database:
 * `add_to_cart`, `booking_hold_initiated`, `purchase`.
 *
 * Returning null means "ask Firestore" — `listCollections()` discovers them at
 * runtime, so a new event type the producer adds appears in the dashboard
 * without a deploy. Set EVENT_TRACKING_COLLECTIONS (comma-separated) to pin the
 * list instead, which also skips the discovery round trip.
 */
export function getEventCollections(): string[] | null {
  const raw = process.env.EVENT_TRACKING_COLLECTIONS;
  if (!raw) return null;
  const list = raw
    .split(",")
    .map((s) => s.trim())
    .filter(Boolean);
  return list.length ? list : null;
}

/** How long a discovered collection list is reused before re-listing. */
export const COLLECTION_LIST_TTL_MS = 5 * 60 * 1000;

/**
 * How the `occurredAt` field is physically stored.
 *
 * Firestore range filters are type-sensitive: a `>=` against a Timestamp never
 * matches a field stored as an ISO string, and returns zero rows instead of an
 * error. This tells the query builder which kind of bound value to construct.
 *
 * Defaults to `string`, because the current producer writes `createdAt` as a
 * naive ISO string with no timezone ("2026-08-07T17:09:05.822772"). That format
 * is fixed-width, so lexicographic ordering matches chronological ordering and
 * range filters work directly on the text.
 *
 * Override with EVENT_TRACKING_TIMESTAMP_KIND = timestamp | string | number
 * ("number" covers epoch seconds and epoch milliseconds alike).
 */
export type TimestampKind = "timestamp" | "string" | "number";

export function getTimestampKind(): TimestampKind {
  const raw = process.env.EVENT_TRACKING_TIMESTAMP_KIND;
  if (raw === "string" || raw === "number" || raw === "timestamp") return raw;
  return "string";
}

/**
 * Ceiling on how many documents a single aggregation pass will read, across all
 * collections combined.
 *
 * Aggregates that Firestore cannot compute server-side (per-member, per-event-type,
 * per-currency revenue) require folding over matching documents. This bounds the
 * cost of one request; hitting it is reported to the caller as `truncated`, never
 * swallowed, so the UI can tell the user their range is too wide rather than show
 * quietly wrong totals.
 */
export function getMaxAggregationDocs(): number {
  const raw = Number(process.env.EVENT_TRACKING_MAX_AGGREGATION_DOCS);
  return Number.isFinite(raw) && raw > 0 ? raw : 200_000;
}
