import { RegistrationOnlineSessionActivationStatus } from 'src/common/enum/registration-online-session-activation-status.enum';
import { SessionKpiCategory, SessionKpiFilter } from 'src/common/enum/session-kpi.enum';
import { AttendanceStatus } from 'src/common/enum/attendance-status.enum';
import { GeneratedLinkSourceType } from 'src/common/enum/generated-link-source-type.enum';
import { UserTypeFilterValue } from 'src/common/utils/user-type-filter.util';
import { SessionCommunicationStatusSummary } from 'src/session-communication/session-communication.types';

/**
 * Shared response shapes returned by every ZoomAnalyticsProvider — the
 * "common abstraction introduced in new code" so the Webinar provider today
 * and any future resource-type provider always hand back the same shape to
 * the facade service / Analytics module.
 */

/** The 8 admin-screen header KPIs for a reconciled session. */
export interface ZoomSessionKpis {
  sessionId: number;
  startTime: Date | null;
  durationMinutes: number | null;
  totalPanelists: number;
  totalSeekersJoined: number;
  seekersNotJoined: number;
  seekersJoinedLate: number;
  seekersDropped: number;
  seekersRejoined: number;
  /**
   * The roster's Present/Absent split by FINAL attendance (Coordinator > RM > Zoom webhook — the
   * same precedence `OnlineAttendanceService.getManualMarksBySession` resolves the seeker table's
   * own "Final" column with), NOT raw Zoom join activity — deliberately different from
   * `totalSeekersJoined`/`seekersNotJoined` above. A seeker with no resolved status at all (never
   * marked, never joined Zoom) counts as `finalAbsent`: every roster seeker lands in exactly one of
   * these two buckets, so `finalPresent + finalAbsent === totalPanelists` always holds.
   */
  finalPresent: number;
  finalAbsent: number;
  reconciledAt: Date | null;
}

/**
 * The general-attendee (shared/common-link joiner, no registration) counterpart to
 * `ZoomSessionKpis`. No `totalSeekersJoined`/`seekersNotJoined` equivalent: general rows aren't
 * gated on a per-seeker "expected" roster the way registered seekers are — see `knownAttendees`/
 * `unknownAttendees` below for the general pool's own eligible/matched split.
 * `generalJoinedLate`/`generalDropped`/`generalRejoined` describe join OUTCOMES, so they only ever
 * count people who actually joined (known-and-joined plus unknown) — a known registrant who hasn't
 * joined yet contributes to `knownAttendees`/`totalGeneralAttendees` but not to these three.
 */
export interface ZoomGeneralAttendeeKpis {
  sessionId: number;
  /** knownAttendees + unknownAttendees — every eligible generated-link registrant (joined or not) plus every unmatched walk-in that joined. */
  totalGeneralAttendees: number;
  /** Every active `zoom_generated_registrant_link` row for this session — pre-generated ROLE/PLACEHOLDER registrants, joined or not. */
  knownAttendees: number;
  /** General-pool joiners whose email matched no generated-link row — true anonymous walk-ins. */
  unknownAttendees: number;
  /** Every general-pool row (known or unknown) that has actually joined at least once — knownAttendees still counts a not-yet-joined known registrant, this doesn't. */
  generalLoggedIn: number;
  generalJoinedLate: number;
  generalDropped: number;
  generalRejoined: number;
}

/** Live snapshot for a session currently in progress, polled roughly every minute. */
export interface ZoomLiveStatus {
  sessionId: number;
  isLive: boolean;
  currentlyJoined: number;
  totalJoinedSoFar: number;
  rejoinsSoFar: number;
  lastEventAt: Date | null;
}

/**
 * v1 response for GET .../live — exactly `getAttendeeTableV1`'s own `PaginatedZoomAttendeeRowsV1`
 * (`data`/`total`/`page`/`limit`/`tableHeaders`/`appliedKpi`/`kpis`, delegated to directly, same call),
 * plus the unscoped live snapshot fields from `ZoomLiveStatus` (every currently-connected participant,
 * general attendees included), plus a "Currently active"/"Currently inactive" tile pair appended onto
 * `kpis` by `getLiveStatusV1` itself — live-monitor-only, never part of `buildAttendanceTiles`, so
 * `/kpis` and `/attendees` don't carry them. Tagged with `SessionKpiFilter.ACTIVE`/`INACTIVE` so
 * clicking one re-issues this same call narrowed to exactly those rows. No separate top-level
 * `currentlyActive`/`currentlyInactive` fields — the tiles are the only place these numbers live.
 */
export interface ZoomLiveStatusV1 extends ZoomLiveStatus, PaginatedZoomAttendeeRowsV1 {}

/**
 * One attendance signal shown per seeker: System (Zoom-derived) | RM | Coordinator | Final.
 * `final` is the resolved effective status from the SESSION_ATTENDANCE precedence engine
 * (`resolveAttendanceStatus` — coordinator > RM > Zoom > QR > join-click) — `true`/`false` for a
 * decided present/absent, `null` when no source has marked this registrant at all (`unknown`).
 */
export interface ZoomAttendanceMarks {
  system: boolean;
  rm: boolean | null;
  coordinator: boolean | null;
  final: boolean | null;
}

/** One row of the screenshot-matched seeker table. */
export interface ZoomAttendeeRow {
  attendeeId: number;
  userId: number | null;
  registrationId: number | null;
  fullName: string | null;
  /** The most recent display name Zoom reported for this seeker joining — may differ from fullName. */
  zoomDisplayName: string | null;
  /** A registered seeker's own email. For a known general row, the matched generated link's real `sourceEmail` — never Zoom's own report, which only ever has the uniquely-tagged address we registered them with. */
  email: string | null;
  /** A known general row's matched generated link's real `sourceMobile` — Zoom's own report never collects a registrant's phone number at all. */
  mobile: string | null;
  /** A registered seeker's assigned RM contact — the RM's `org_usr_name`, or when that's the "Other" placeholder, `hdb_program_registration.other_infinitheism_contact` instead (same fallback the registration list's own "RM Contact" column uses). Null for general rows — no registration to resolve an RM from. */
  rmContact: string | null;
  /** A registered seeker's `hdb_program_registration.user_profile_url`. Null for general rows — placeholders/role links have no registration to source one from. */
  profileImage: string | null;
  joinedAt: Date | null;
  noOfDevices: number | null;
  dropoffCount: number;
  rejoinCount: number;
  lastDropoffAt: Date | null;
  lastRejoinedAt: Date | null;
  /** Seconds present from the session's actual start time onward. */
  durationSeconds: number;
  /** Seconds present before the session's actual start time (e.g. joined early) — excluded from durationSeconds. */
  preSessionDurationSeconds: number;
  attendance: ZoomAttendanceMarks;
  /**
   * This registrant's join link for the session. For a registered seeker (`registrationId` set),
   * from their active `hdb_program_registration_online_session` row. For a general row
   * (`registrationId` null) matched to a `zoom_generated_registrant_link` row (see `sourceTag`),
   * that row's own pre-generated `join_url`. Null if not provisioned/matched (e.g. shared-link
   * meeting, registration removed, or a truly unknown general-pool walk-in).
   */
  joinUrl: string | null;
  /** The registrant's registration-level activation status (hdb_program_registration.activation_status — the rollup across their online sessions), not any single session's extension row. Always null for general rows (no registration to read it from). */
  activationStatus: RegistrationOnlineSessionActivationStatus | null;
  /**
   * Set only for a general row (`registrationId` null) matched by email to an active
   * `zoom_generated_registrant_link` row — a "known" general attendee, pre-generated for a
   * role-matched staff user or a placeholder batch slot. `label` is that row's `roleKey` —
   * the matched role key for a ROLE row, `'SYSTEM'` for a PLACEHOLDER row.
   * Null for registered-seeker rows and for unmatched ("unknown") general rows.
   */
  sourceTag: { sourceType: GeneratedLinkSourceType; label: string | null } | null;
  /**
   * The matched `zoom_generated_registrant_link` row's own id — set only for a "known" general row
   * (same match as `sourceTag`/`joinUrl`/`email`/`mobile`). This is the id
   * `/session-communication/general-link/single` takes as `generatedLinkId` to send a one-off
   * communication to this recipient. Null for registered-seeker rows and unmatched general rows
   * (nothing to send a general-link communication to).
   */
  generatedLinkId: number | null;
}

export interface ZoomAttendeeQuery {
  page: number;
  limit: number;
  search?: string;
  /** v1 only — clicking a KPI tile re-issues the query with its own category/filter, narrowing this table to the rows behind that count. Omitted/`all` = no narrowing. */
  kpiCategory?: SessionKpiCategory;
  kpiFilter?: SessionKpiFilter;
  /**
   * v1 only — one independent side filter per attendance signal, each `PRESENT`/`ABSENT` for a decided
   * mark or `UNKNOWN` for "this source never marked them" (same tri-state as `ZoomAttendanceMarks`/
   * `AttendanceStatus`, reused rather than a new enum). `systemAttendance` is a plain boolean column
   * (`is_system_attended`) filtered directly in the repository; `rmAttendance`/`coordinatorAttendance`/
   * `finalAttendance` live in a different table (`program_user_attendance`, resolved via the precedence
   * engine) — the provider resolves those to matching registration ids first (see
   * `ZoomLiveEventAnalyticsProviderBase.matchesSourceStateFilter`).
   */
  systemAttendance?: AttendanceStatus;
  rmAttendance?: AttendanceStatus;
  coordinatorAttendance?: AttendanceStatus;
  /** v1 only — filters by the resolved effective status (the "Final" column), from the precedence engine. */
  finalAttendance?: AttendanceStatus;
  /**
   * v1 only — a checkbox side filter (multi-select, OR'd together) over the same three
   * outcome-shaped `kpiFilter` values (`dropped`/`rejoined`/`joinedLate`), usable independently of
   * clicking a KPI tile. `joined`/`notJoined`/`all` aren't valid here — those are presence, not an
   * "outcome" alongside presence, so they stay exclusive to the `kpiFilter` tile mechanism.
   */
  attendanceOutcome?: SessionKpiFilter[];
  /**
   * v1 only — a checkbox side filter (multi-select, OR'd together) over the known/unknown split
   * (`known`/`unknown`), usable independently of clicking a KPI tile. Only meaningful for general
   * attendees — doesn't force the `registration_id IS NULL` scope itself (unlike `kpiFilter`'s own
   * KNOWN/UNKNOWN cases), since it's only ever set alongside `generalOnly: true` by
   * `getGeneralAttendeesV1`. Selecting both values applies no extra narrowing (the split is
   * exhaustive over general rows).
   */
  knownStatus?: SessionKpiFilter[];
  /**
   * v1 only — a checkbox side filter over the registrant's OWN account type (`users.user_type`):
   * `['Org']`, `['Seeker']`, or both, matched as a plain IN over the selected values (see
   * `resolveUserTypeFilter`). `Seeker` additionally matches "registered for someone else" rows,
   * which have no user account of their own. Never applies to general attendees, which have no
   * registration at all.
   */
  userType?: UserTypeFilterValue[];
  /**
   * Facade-internal only, never client-supplied — set by `getGeneralAttendeesV1` to scope every row
   * to `registration_id IS NULL` regardless of `kpiFilter`, so a general-attendees caller can still
   * pick `joined`/`notJoined`/`dropped`/`rejoined`/`joinedLate` (see `ZoomAnalyticsAttendeeSummaryRepository.applyKpiFilter`)
   * without that condition's own registration_id guard excluding every general row.
   */
  generalOnly?: boolean;
  sortKey?: string;
  sortOrder?: 'ASC' | 'DESC';
}

/**
 * v1 facade-boundary input — mirrors registration-list-view's bundled `filters` JSON param
 * convention: every filter field travels together inside `filters`, not as separate top-level query
 * params. `ZoomAttendeeQuery` (above) stays flat — it's the internal shape the repository/provider
 * layer already worked with before v1 and needs no wire-format opinion.
 */
export interface ZoomAttendeeQueryV1Input {
  page: number;
  limit: number;
  search?: string;
  filters?: {
    kpiCategory?: SessionKpiCategory;
    kpiFilter?: SessionKpiFilter;
    /** Admin/coordinator only — scopes the table to one RM's own seekers. */
    rmContact?: number;
    systemAttendance?: AttendanceStatus;
    rmAttendance?: AttendanceStatus;
    coordinatorAttendance?: AttendanceStatus;
    finalAttendance?: AttendanceStatus;
    attendanceOutcome?: SessionKpiFilter[];
    /** The registrant's own account type — see `ZoomAttendeeQuery.userType`. */
    userType?: UserTypeFilterValue[];
    /**
     * Facade-internal only — see `ZoomAttendeeQuery.knownStatus`. Only ever set from
     * `getGeneralAttendeesV1`'s own construction, not from the main `SessionAttendeeFiltersDto`.
     */
    knownStatus?: SessionKpiFilter[];
    /** Facade-internal only — see `ZoomAttendeeQuery.generalOnly`. Never set from a client-facing DTO. */
    generalOnly?: boolean;
  };
  sortKey?: string;
  sortOrder?: 'ASC' | 'DESC';
}

/**
 * v1 facade-boundary input for `GET .../general-attendees` — deliberately narrower than
 * `ZoomAttendeeQueryV1Input`: no kpiCategory/rmContact/systemAttendance/rmAttendance/
 * coordinatorAttendance/finalAttendance, since this endpoint's entire purpose ("only the
 * shared/common-link joiners") is enforced server-side by `getGeneralAttendeesV1`, and the
 * registration-only filters have no meaning for a row with no registration. `filters.kpiFilter` IS
 * exposed (restricted to joined/notJoined/dropped/rejoined/joinedLate/known/unknown) — the same
 * clickable-tile mechanic as the main attendees screen, layered on top of the server-forced
 * general-only scope rather than replacing it. Bundled into one `filters` object on the wire, same
 * convention as `ZoomAttendeeQueryV1Input.filters`/`SessionAttendeeFiltersDto`.
 */
export interface AnalyticsGeneralAttendeeQueryV1Input {
  page: number;
  limit: number;
  search?: string;
  filters?: {
    kpiFilter?: SessionKpiFilter;
    attendanceOutcome?: SessionKpiFilter[];
    /** Checkbox side filter (OR'd together) over the known/unknown split — see `ZoomAttendeeQuery.knownStatus`. */
    knownStatus?: SessionKpiFilter[];
  };
  sortKey?: string;
  sortOrder?: 'ASC' | 'DESC';
}

export interface PaginatedZoomAttendeeRows {
  data: ZoomAttendeeRow[];
  total: number;
  page: number;
  limit: number;
}

/** One v1 KPI tile — clickable when it carries a kpiCategory/kpiFilter; a plain stat otherwise (e.g. totalPanelists). */
export interface SessionKpiTile {
  label: string;
  value: number;
  kpiCategory?: SessionKpiCategory;
  kpiFilter?: SessionKpiFilter;
}

/**
 * v1 response for GET .../kpis — the existing 8 fields plus the clickable tile set derived from
 * them, plus `sideFilterSets` describing the attendees screen's independent side filters
 * (rmContact/systemAttendance/rmAttendance/coordinatorAttendance/finalAttendance/attendanceOutcome — see
 * `SessionAttendeeSideFilter`). Folded into this one response rather than a separate `filter-config`
 * endpoint — the KPIs call is already the natural "load this session's analytics screen" entry point.
 */
export interface ZoomSessionKpisV1 extends ZoomSessionKpis {
  kpis: SessionKpiTile[];
  sideFilterSets: SessionAttendeeFilterSets;
}

/**
 * v1 response for GET .../general-attendees/kpis — the general-attendee counterpart to
 * `ZoomSessionKpisV1`. `sideFilterSets` here only ever carries `knownStatus` — none of the main
 * screen's other side filters (rmContact/systemAttendance/rmAttendance/coordinatorAttendance/
 * finalAttendance) apply to a row with no registration, same reasoning as
 * `AnalyticsGeneralAttendeeQueryV1Input`.
 */
export interface ZoomGeneralAttendeeKpisV1 extends ZoomGeneralAttendeeKpis {
  kpis: SessionKpiTile[];
  sideFilterSets: SessionAttendeeFilterSets;
}

export interface SessionAttendeeTableHeader {
  key: string;
  label: string;
  sortable: boolean;
  filterable: boolean;
  type: 'string' | 'number' | 'date' | 'boolean';
}

/**
 * v1 response for GET .../attendees and .../general-attendees — the existing paginated rows plus
 * table headers, an echo of the applied KPI, and the same `kpis` tile array the dedicated KPIs
 * endpoint (`/kpis` or `/general-attendees/kpis`) returns, embedded here so the frontend can render
 * the KPI tiles and the table from one call instead of two racing requests.
 */
/** One downloadable-report option — `code` is the exact `purpose` value to pass to `POST /reports/session-attendance/generate` alongside this screen's `filters.sessionId`. Same shape as the Registrations tab's report dropdown entries. `isActive` reflects REPORT_PURPOSE_FEATURE_FLAGS for this session's program type (common/constants/constants.ts) — disabled entries stay in the array (not filtered out) so the UI can grey them out; if every entry is inactive, the UI should hide the whole download affordance for this screen. */
export interface ZoomAttendeeReportOption {
  code: string;
  label: string;
  description: string;
  isActive: boolean;
}

export interface PaginatedZoomAttendeeRowsV1 extends PaginatedZoomAttendeeRows {
  tableHeaders: SessionAttendeeTableHeader[];
  appliedKpi: { kpiCategory: SessionKpiCategory; kpiFilter: SessionKpiFilter } | null;
  kpis: SessionKpiTile[];
  /** The session's own `ProgramSession.isAttendanceLocked` — true once a Coordinator has locked attendance, blocking RM/Admin edits. `ZoomLiveStatusV1` inherits this via its own spread of this response. */
  isAttendanceLocked: boolean;
  /** Downloadable reports offered for this session — its own 4-purpose set for the General Attendees screen (`generalOnly`), see ZOOM_ANALYTICS_GENERAL_ATTENDEE_SESSION_REPORTS' docblock. */
  reports: ZoomAttendeeReportOption[];
}

/**
 * Bulk-communication summary exposed on the general-attendees response, mirroring the
 * online-session GET responses' own `communications` object — one key per purpose whose audience
 * IS this table, so the screen that triggers a general-link send can also show whether it has
 * already gone out. Only the general-link ("pre-test") Value Card applies today: the other
 * general-link purposes (Invite/Absent/Welcome/Program completion) are driven from the sessions and
 * registrations screens, not from this one. null = never triggered for the session.
 */
export interface GeneralAttendeeBulkCommunications {
  valueCard: SessionCommunicationStatusSummary | null;
}

/**
 * v1 response for GET .../general-attendees — `PaginatedZoomAttendeeRowsV1` plus the general-link
 * bulk-communication status for the session. Separate from the seeker `/attendees` response
 * because these statuses describe sends aimed at general-link recipients specifically; the seeker
 * table's equivalents live on the online-session GET responses. Also carries `sideFilterSets`
 * (unlike the base `PaginatedZoomAttendeeRowsV1`, which relies on a separate `/kpis` call for
 * that) — this screen has no separate kpis call of its own wired up client-side, so it's bundled
 * directly into this one response instead, same "single round trip" reasoning as `kpis` above it.
 */
export interface PaginatedZoomGeneralAttendeeRowsV1 extends PaginatedZoomAttendeeRowsV1 {
  communications: GeneralAttendeeBulkCommunications;
  sideFilterSets: SessionAttendeeFilterSets;
}

export interface SessionAttendeeSideFilterOption {
  label: string;
  value: string;
}

/**
 * One side-filter field definition — mirrors `registration-list-view`'s `filter-config`
 * `sideFilterSets` entry shape (key/label/type/options/excludeRoles) so the frontend can render both
 * screens' side panels the same way. `excludeRoles` hides a filter from a role for whom it's
 * meaningless (e.g. `rmContact` for an `relational_manager` caller, who is already hard-scoped to
 * themself server-side — same key/convention as registration's own `rmContact` side filter).
 */
export interface SessionAttendeeSideFilter {
  key: string;
  label: string;
  type: 'dropdown' | 'checkbox';
  options: SessionAttendeeSideFilterOption[];
  excludeRoles?: string[];
}

/**
 * Mirrors `registration-list-view`'s `filter-config` response shape one-for-one: `baseSets` is every
 * side-filter's own definition (keyed by its `key`); `contextualFilters` says WHICH of those keys
 * apply for a given `kpiFilter` selection (`all`/`joined`/`notJoined`/`joinedLate`/`dropped`/
 * `rejoined`) — e.g. `attendanceOutcome` is redundant (and omitted) once the `notJoined` KPI tile is
 * already active, since a seeker who never joined can't have dropped, rejoined, or joined late.
 */
export interface SessionAttendeeFilterSets {
  baseSets: Record<string, SessionAttendeeSideFilter[]>;
  contextualFilters: Record<string, string[]>;
}

/**
 * Roster-level attendance rollup shown as the Final Attendance widget's stat pills — just
 * present/absent (plus droppedOff/rejoined context), no separate "late comers" bucket: a late
 * joiner's OWN lateness lives on ZoomLateComerAnalysis instead, unconditional on final
 * attendance (see its own doc comment for why).
 */
export interface ZoomSessionAttendanceSummary {
  total: number;
  attended: number;
  absent: number;
  droppedOff: number;
  rejoined: number;
}

/** One time bucket of the Attendance Journey chart. */
export interface ZoomJourneyPoint {
  /** Bucket boundary instant (ISO) — the client formats it to a clock label. */
  bucketAt: Date;
  /** Seekers present at this instant. */
  attended: number;
  /** Roster members who had not joined at all by this instant. */
  absent: number;
  /** Full drop-offs (every open device disconnected) that happened inside this bucket. */
  dropOff: number;
  /** Rejoin transitions that happened inside this bucket. */
  rejoins: number;
  /** Late first-joins that happened inside this bucket. */
  lateComers: number;
}

/** One stacked segment inside a drop-off band: how many seekers in the band whose drop-offs carry this reason. */
export interface ZoomDropOffBandSegment {
  reason: string;
  count: number;
}

/** One row of the Drop-off bar chart — seekers bucketed by how many times they dropped ("1 drop-off" / "2 drop-offs" / "3+ drop-offs"). */
export interface ZoomDropOffBand {
  label: string;
  segments: ZoomDropOffBandSegment[];
}

/** The Drop-off widget: total mid-session drop-off events + per-band reason breakdown. */
export interface ZoomDropOffAnalysis {
  /** Total mid-session drop-off EVENTS (a seeker with 3 drop-offs contributes 3). */
  total: number;
  /** Distinct reasons present, in legend/dataset order. */
  reasons: string[];
  bands: ZoomDropOffBand[];
}

/** One session's row of a seeker's cross-session analytics (the seeker-detail screen). */
export interface ZoomSeekerSessionRow {
  sessionId: number;
  startsAt: Date | null;
  /** Whether the session has already taken place — unheld sessions carry no attendance data yet. */
  held: boolean;
  /** Final resolved attendance for the session (Coordinator > RM > Zoom system). */
  attended: boolean;
  joinedAt: Date | null;
  /** Minutes past the session start the first join happened, when beyond the late threshold. */
  lateByMinutes: number | null;
  durationSeconds: number;
  deviceCount: number | null;
  dropoffCount: number;
  rejoinCount: number;
}

/**
 * A registration's basic identity, read WITHOUT the usual `deleted_at IS NULL` guard — see
 * ZoomRegistrationRepository.findProfileByRegistrationId for why. Fields are raw/nullable
 * since a since-deleted or long-since-registered row may be missing any of them.
 */
export interface ZoomRegistrationProfile {
  fullName: string | null;
  email: string | null;
  mobile: string | null;
  registrationSeqNumber: string | null;
  registeredDate: Date | null;
  gender: string | null;
  rmName: string | null;
}

/** Cross-session rollup for one seeker in one program (the seeker-detail screen's KPIs + table). */
export interface ZoomSeekerAnalytics {
  programId: number;
  registrationId: number;
  /** The seeker's assigned RM contact's display name, or null when unassigned. */
  rmName: string | null;
  /**
   * Basic profile fields for the seeker-detail screen's header, sourced WITHOUT excluding a
   * since-deleted registration (same as `rmName` above, now folded into this same lookup) — a
   * registrant who was later archived/deleted still needs their name/email/phone to show
   * alongside their attendance history. Null only if the registrationId never existed at all.
   */
  profile: ZoomRegistrationProfile | null;
  sessionsHeld: number;
  sessionsAttended: number;
  attendancePercent: number | null;
  totalLearningSeconds: number;
  /** Eligibility for the session after `thresholdSession`: pending until every one of the first `thresholdSession` sessions has been held. */
  eligibility: { thresholdSession: number; status: 'pending' | 'eligible' | 'not-eligible' };
  sessions: ZoomSeekerSessionRow[];
}

/** One slice of the Multiple Logins donut — seekers who joined from `label` (e.g. "2 devices") concurrently/distinctly. */
export interface ZoomMultipleLoginsBucket {
  label: string;
  count: number;
  percentage: number;
}

/** Seekers who joined from more than one device/connection, bucketed by device count. */
export interface ZoomMultipleLoginsBreakdown {
  /** Seekers with 2+ devices. */
  total: number;
  buckets: ZoomMultipleLoginsBucket[];
}

/** One bucket of the Late Comers widget — how many late-joining seekers fell `label` past the late cutoff (e.g. "<30s" past it). */
export interface ZoomLateComerBucket {
  label: string;
  count: number;
}

/** How late the session's late-comers actually were, past the late-join cutoff — bucketed to seconds/minutes granularity. */
export interface ZoomLateComerAnalysis {
  /**
   * Total late-joining seekers, straight from the raw event log — NOT gated on final
   * attendance, so this includes anyone who joined late regardless of whether they ended
   * up finally present or finally absent (see computeLateComers's own doc comment).
   */
  total: number;
  buckets: ZoomLateComerBucket[];
}

/**
 * Every roster seeker plotted by punctuality × endurance for THIS session (its own
 * duration is 100%, not a program-wide average) — a 5x5 density grid.
 * `counts[punctualityBandIndex][enduranceBandIndex]` is the seeker count in that cell;
 * both band label arrays are parallel to their respective index dimension.
 * `punctualityBandLabels` is ordered high-to-low (matches the widget's top-to-bottom rows);
 * `enduranceBandLabels` is ordered low-to-high (matches its left-to-right columns).
 */
export interface ZoomSincerityMap {
  /** Roster size — every seeker gets a cell, including no-shows (0% punctuality, 0% endurance). */
  total: number;
  punctualityBandLabels: string[];
  enduranceBandLabels: string[];
  counts: number[][];
}

/** The session analytics dashboard (live and post), computed entirely from the locally stored webhook event log. */
export interface ZoomSessionDashboard {
  sessionId: number;
  summary: ZoomSessionAttendanceSummary;
  journey: ZoomJourneyPoint[];
  dropOff: ZoomDropOffAnalysis;
  multipleLogins: ZoomMultipleLoginsBreakdown;
  lateComers: ZoomLateComerAnalysis;
  sincerityMap: ZoomSincerityMap;
  reconciledAt: Date | null;
}

/** Program-wide KPI tile values for the Overall Analytics tab's header strip. */
export interface ZoomProgramAnalyticsKpis {
  /** Every non-deleted registration in the program with an allocated seat (ProgramRegistration.seatAllocated). */
  totalAttendees: number;
  /** Registrations whose activation rollup (ProgramRegistration.activationStatus) is ACTIVE. */
  active: number;
  /** Mean of each registration's own attendancePercent (see ZoomSeekerAnalytics), rounded; null until at least one registration has a held session. */
  avgAttendancePercent: number | null;
  /** Registrations whose eligibility.status is 'eligible' — same ELIGIBILITY_THRESHOLD_SESSIONS rule as ZoomSeekerAnalytics.eligibility, aggregated across the roster. */
  eligibleForNextSession: number;
  /** The eligibility gate itself — attend all of the first `thresholdSession` sessions or you're out. Same value as ZoomSeekerAnalytics.eligibility.thresholdSession. */
  thresholdSession: number;
}

/**
 * One completed session's KPI tiles in the Overall Analytics timeline — the
 * same 4 counts shown on that session's own Attendees screen (SessionKpis),
 * so clicking one there and clicking it here mean the same thing.
 */
export interface ZoomProgramSessionKpiTiles {
  totalAttendees: number;
  present: number;
  absent: number;
  joinedLate: number;
}

/** One communication purpose that actually went out for an upcoming session — the checklist only ever lists what's real, never a "not sent" placeholder. */
export interface ZoomProgramCommsChecklistItem {
  purpose: string;
  label: string;
}

/** One pill of the Overall Analytics session timeline, in schedule order. */
export interface ZoomProgramSessionTimelineEntry {
  sessionId: number;
  /** "S1", "S2", ... — this session's 1-based position in the program's own schedule. */
  label: string;
  startsAt: Date | null;
  status: 'completed' | 'upcoming';
  /** Present only when status is 'completed'. */
  kpis?: ZoomProgramSessionKpiTiles;
  /** Present only when status is 'upcoming'; empty array (not omitted) when nothing has been sent yet. */
  commsChecklist?: ZoomProgramCommsChecklistItem[];
}

/** One completed session's column in the Latecomers heatmap. */
export interface ZoomProgramLatecomerColumn {
  sessionId: number;
  /** "S1", "S2", ... — same label as its ZoomProgramSessionTimelineEntry. */
  label: string;
  /** Parallel to ZoomProgramLatecomersMatrix.bandLabels — 0 for a band with no latecomers that session. */
  countsByBand: number[];
}

/** How late each completed session's latecomers were, banded — the Latecomers heatmap. */
export interface ZoomProgramLatecomersMatrix {
  /** Row labels, in band order (e.g. "<30s", "30s–1 min", ...) — see ZOOM_ANALYTICS_LATE_COMER_BUCKETS. */
  bandLabels: string[];
  /** One column per completed session, in schedule order. */
  columns: ZoomProgramLatecomerColumn[];
}

/** One of a repeat-latecomer's held sessions — on time, late, or absent entirely. */
export type ZoomProgramRepeatLatecomerStatus = 'onTime' | 'late' | 'absent';

/** One row of the Repeat Latecomers table — a single seeker's pattern across every held session. */
export interface ZoomProgramRepeatLatecomerRow {
  registrationId: number;
  seekerName: string;
  /** Parallel to ZoomProgramRepeatLatecomers.sessionLabels, in schedule order. */
  statuses: ZoomProgramRepeatLatecomerStatus[];
  /** Count of 'late' entries in `statuses`. */
  lateCount: number;
  /** Mean minutes late across this seeker's own late sessions, rounded; null if lateCount is 0. */
  avgLateMinutes: number | null;
}

/** Who's consistently late — ranked by lateCount descending, capped to the worst offenders. */
export interface ZoomProgramRepeatLatecomers {
  /** "S1", "S2", ... — held sessions only, in schedule order. */
  sessionLabels: string[];
  rows: ZoomProgramRepeatLatecomerRow[];
}

/** One held session's multi-device (2+ concurrent/distinct devices) login count. */
export interface ZoomProgramMultiDeviceColumn {
  sessionId: number;
  /** "S1", "S2", ... — same label as its ZoomProgramSessionTimelineEntry. */
  label: string;
  count: number;
}

/** Multiple-logins-per-session across the program — a data-quality caveat, not an attendance metric. */
export interface ZoomProgramMultiDeviceBreakdown {
  columns: ZoomProgramMultiDeviceColumn[];
}

/** The Overall Analytics (program-level) dashboard — served entirely from locally stored data, no live Zoom polling. */
export interface ZoomProgramOverallAnalytics {
  programId: number;
  kpis: ZoomProgramAnalyticsKpis;
  timeline: ZoomProgramSessionTimelineEntry[];
  latecomers: ZoomProgramLatecomersMatrix;
  repeatLatecomers: ZoomProgramRepeatLatecomers;
  multipleLogins: ZoomProgramMultiDeviceBreakdown;
}

export type ZoomFollowUpEngagement = 'LOW' | 'MEDIUM' | 'HIGH';

/** One row of the "Seekers Who Need Attention" follow-up table. */
export interface ZoomFollowUpRow {
  attendeeId: number | null;
  registrationId: number;
  userId: number | null;
  fullName: string | null;
  email: string | null;
  mobile: string | null;
  joinedAt: Date | null;
  durationSeconds: number | null;
  deviceCount: number | null;
  rmName: string | null;
  reason: string;
  engagement: ZoomFollowUpEngagement;
}

export interface PaginatedZoomFollowUpRows {
  data: ZoomFollowUpRow[];
  total: number;
  page: number;
  limit: number;
}
