import type { DocumentSnapshot } from "@google-cloud/firestore";
import { FALLBACK_KEYS, getFieldMap, type EventFieldMap } from "./event-analytics.config";

/** One tracked booking event, normalised for the console. */
export type EventRecord = {
  id: string;
  /**
   * The collection the document came from — `add_to_cart`,
   * `booking_hold_initiated`, `purchase`. The documents carry no event-type
   * field; which collection they live in *is* the type.
   */
  eventType: string;
  /** Where in the app the event originated, e.g. "properties". Not the type. */
  source: string | null;
  /** ISO 8601, or null when the source value was missing/unparseable. */
  occurredAt: string | null;
  /**
   * The `createdAt` value exactly as the producer wrote it, unparsed.
   *
   * Reports show this rather than `occurredAt`: it is the value in Firestore, so
   * it is right even when normalisation fails, and it is what someone comparing
   * a spreadsheet against the database expects to see.
   */
  createdAtRaw: string | null;
  memberId: string | null;
  memberName: string | null;
  memberEmail: string | null;
  bookingRef: string | null;
  /**
   * Null means "this event carries no revenue", which is not the same as zero.
   * Zero would silently drag down averages and imply a booking worth nothing.
   */
  revenue: number | null;
  currency: string | null;
  /**
   * Loyalty points earned. Null, not 0, when the event carries none — the
   * current producer leaves `amount` and `currency` empty on most events, so
   * points are frequently the only quantity an event actually has.
   */
  points: number | null;
  propertyName: string | null;
  experienceName: string | null;
  memberOffer: string | null;
  /** The untouched document, for the detail row's expand panel. */
  raw: Record<string, unknown>;
};

/** True for a Firestore Timestamp instance or a plain `{_seconds}` clone of one. */
function isTimestampLike(v: unknown): v is { toDate?: () => Date; _seconds?: number } {
  if (typeof v !== "object" || v === null) return false;
  return "toDate" in v || "_seconds" in v;
}

/**
 * Firestore Timestamp / Date / ISO string / epoch number -> ISO 8601 string.
 *
 * Timestamps must be converted here: JSON.stringify on a raw Timestamp emits
 * `{"_seconds":...,"_nanoseconds":...}`, which no date parser downstream reads.
 */
export function toIsoString(value: unknown): string | null {
  if (value === null || value === undefined) return null;

  if (value instanceof Date) {
    return Number.isNaN(value.getTime()) ? null : value.toISOString();
  }

  if (isTimestampLike(value)) {
    if (typeof value.toDate === "function") {
      const d = value.toDate();
      return Number.isNaN(d.getTime()) ? null : d.toISOString();
    }
    if (typeof value._seconds === "number") {
      return new Date(value._seconds * 1000).toISOString();
    }
    return null;
  }

  if (typeof value === "number") {
    // Firestore producers write either seconds or milliseconds. Anything below
    // ~1e11 cannot be a plausible millisecond epoch (it would be 1973), so
    // treat it as seconds.
    const ms = value < 1e11 ? value * 1000 : value;
    const d = new Date(ms);
    return Number.isNaN(d.getTime()) ? null : d.toISOString();
  }

  if (typeof value === "string") {
    const d = new Date(value);
    return Number.isNaN(d.getTime()) ? null : d.toISOString();
  }

  return null;
}

/** Read a logical field from a document, falling back to known alternate keys. */
function read(
  data: Record<string, unknown>,
  map: EventFieldMap,
  field: keyof EventFieldMap,
): unknown {
  const primary = data[map[field]];
  if (primary !== undefined && primary !== null) return primary;

  for (const key of FALLBACK_KEYS[field] ?? []) {
    const value = data[key];
    if (value !== undefined && value !== null) return value;
  }
  return undefined;
}

function toStringOrNull(value: unknown): string | null {
  if (value === null || value === undefined) return null;
  if (typeof value === "string") return value.trim() || null;
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  return null;
}

/**
 * Parse a revenue value. Strings are accepted because producers sometimes write
 * `"1,250.00"`; anything that does not resolve to a finite number becomes null
 * rather than zero.
 */
function toNumberOrNull(value: unknown): number | null {
  if (value === null || value === undefined) return null;
  if (typeof value === "number") return Number.isFinite(value) ? value : null;
  // `points` is an int64. The driver hands back a JS number by default, but
  // returns a BigInt when the client is configured with `useBigInt`.
  if (typeof value === "bigint") return Number(value);
  if (typeof value === "string") {
    const n = Number(value.replace(/[,\s]/g, ""));
    return Number.isFinite(n) ? n : null;
  }
  return null;
}

/**
 * The timestamp as written, for reports.
 *
 * A string producer value passes through untouched — no parsing, so no way for
 * a format we do not anticipate to turn into an empty cell. Other shapes have no
 * meaningful "as written" form, so they fall back to the normalised ISO value.
 */
function rawTimestampString(value: unknown): string | null {
  if (typeof value === "string") return value.trim() || null;
  return toIsoString(value);
}

/**
 * Firestore document -> EventRecord.
 *
 * `eventType` is passed in rather than read: it is the collection name, which
 * the snapshot's own data does not carry.
 */
export function mapEventDoc(doc: DocumentSnapshot, eventType: string): EventRecord {
  const data = (doc.data() ?? {}) as Record<string, unknown>;
  const map = getFieldMap();

  const currency = toStringOrNull(read(data, map, "currency"));

  return {
    id: doc.id,
    eventType,
    source: toStringOrNull(read(data, map, "source")),
    occurredAt: toIsoString(read(data, map, "occurredAt")),
    createdAtRaw: rawTimestampString(read(data, map, "occurredAt")),
    memberId: toStringOrNull(read(data, map, "memberId")),
    memberName: toStringOrNull(read(data, map, "memberName")),
    memberEmail: toStringOrNull(read(data, map, "memberEmail")),
    bookingRef: toStringOrNull(read(data, map, "bookingRef")),
    revenue: toNumberOrNull(read(data, map, "revenue")),
    currency: currency ? currency.toUpperCase() : null,
    points: toNumberOrNull(read(data, map, "points")),
    propertyName: toStringOrNull(read(data, map, "propertyName")),
    experienceName: toStringOrNull(read(data, map, "experienceName")),
    memberOffer: toStringOrNull(read(data, map, "memberOffer")),
    raw: toPlainJson(data),
  };
}

/**
 * Make a document safe to send to the browser.
 *
 * `JSON.stringify` applies each value's own `toJSON` before handing it to the
 * replacer, so a Firestore Timestamp arrives here already flattened to
 * `{_seconds, _nanoseconds}` — which `isTimestampLike` recognises and turns
 * into an ISO string. Exotic field types (GeoPoint, DocumentReference) are not
 * expected in event documents, but a document containing one must still render
 * a detail row, so failure degrades to an empty `raw` rather than a 500.
 */
function toPlainJson(data: Record<string, unknown>): Record<string, unknown> {
  try {
    return JSON.parse(
      JSON.stringify(data, (_key, value: unknown) =>
        isTimestampLike(value) ? toIsoString(value) : value,
      ),
    ) as Record<string, unknown>;
  } catch {
    return {};
  }
}
