/**
 * Constants for the new Zoom live + analytics integration. Deliberately
 * separate from `src/common/constants/zoom.constants.ts` — that file is
 * never edited by this feature; every new env key, API path, and constant
 * needed for live tracking / post-session analytics lives here instead.
 */

import { AttendanceStatus } from 'src/common/enum/attendance-status.enum';
import { SessionKpiFilter } from 'src/common/enum/session-kpi.enum';
import { ROLE_VALUES } from 'src/common/constants/strings-constants';
import { USER_TYPE_FILTER_OPTIONS } from 'src/common/utils/user-type-filter.util';
import { SessionAttendeeSideFilter, SessionAttendeeSideFilterOption } from '../interfaces/zoom-analytics.interface';

/** present/absent as a side-filter's own checkbox options — shared by systemAttendance/rmAttendance/coordinatorAttendance/finalAttendance. */
export const ATTENDEE_ATTENDANCE_STATE_OPTIONS: SessionAttendeeSideFilterOption[] = [
  { label: 'Present', value: AttendanceStatus.PRESENT },
  { label: 'Absent', value: AttendanceStatus.ABSENT },
];

/** `userType`'s checkbox options — the registrant's own account type (Org/Seeker), shared with every other screen offering this filter. */
export const ATTENDEE_USER_TYPE_OPTIONS: SessionAttendeeSideFilterOption[] = [...USER_TYPE_FILTER_OPTIONS];

/** dropoff/rejoin/late as `attendanceOutcome`'s own checkbox options — the same 3 outcome-shaped `kpiFilter` values, usable without clicking a KPI tile. */
export const ATTENDEE_ATTENDANCE_OUTCOME_OPTIONS: SessionAttendeeSideFilterOption[] = [
  { label: 'Drop Off', value: SessionKpiFilter.DROPPED },
  { label: 'Rejoin', value: SessionKpiFilter.REJOINED },
  { label: 'Joined Late', value: SessionKpiFilter.JOINED_LATE },
];

/** `knownStatus`'s own checkbox options — the known/unknown split, usable independently of clicking a KPI tile. */
export const GENERAL_ATTENDEE_KNOWN_STATUS_OPTIONS: SessionAttendeeSideFilterOption[] = [
  { label: 'Unidentified', value: SessionKpiFilter.UNKNOWN },
  { label: 'Internal Member(s)', value: SessionKpiFilter.KNOWN },
];

/**
 * v1 — the attendees screen's side filters (as opposed to the clickable-KPI-tile mechanism, see
 * `SessionKpiTile`), mirroring `registration-list-view`'s `filter-config` `sideFilterSets` shape
 * (key/label/type/options/excludeRoles) one-for-one. `rmContact` mirrors that reference's own
 * `rmContact` side filter (same key, same idea) — a dropdown with `options` populated at request
 * time from real RM contacts (see `ZoomAnalyticsFacadeService.buildSideFilterSets`), same as the
 * reference does.
 *
 * `excludeRoles` here is an INTERNAL source of truth only — `buildSideFilterSets` consumes it
 * server-side, using the caller's own actual roles, and never echoes it back on the response. Sending
 * `excludeRoles` to the client and trusting it to hide the control itself would just be client-side
 * enforcement of a server-side rule — a caller who ignores it would see (and could still submit) a
 * filter meant to be hidden from them. Since the server already knows the caller's roles on every
 * request, the correct fix is to just not include the filter at all, not describe why it's excluded.
 */
export const ATTENDEE_SIDE_FILTER_SETS: Record<string, SessionAttendeeSideFilter[]> = {
  rmContact: [
    {
      key: 'rmContact',
      label: 'RM',
      type: 'dropdown',
      excludeRoles: [ROLE_VALUES.RELATIONAL_MANAGER],
      options: [],
    },
  ],
  systemAttendance: [
    { key: 'systemAttendance', label: 'System Attendance', type: 'checkbox', options: ATTENDEE_ATTENDANCE_STATE_OPTIONS },
  ],
  rmAttendance: [
    { key: 'rmAttendance', label: 'RM Attendance', type: 'checkbox', options: ATTENDEE_ATTENDANCE_STATE_OPTIONS },
  ],
  coordinatorAttendance: [
    {
      key: 'coordinatorAttendance',
      label: 'Coordinator Attendance',
      type: 'checkbox',
      options: ATTENDEE_ATTENDANCE_STATE_OPTIONS,
    },
  ],
  finalAttendance: [
    { key: 'finalAttendance', label: 'Final Attendance', type: 'checkbox', options: ATTENDEE_ATTENDANCE_STATE_OPTIONS },
  ],
  attendanceOutcome: [
    {
      key: 'attendanceOutcome',
      label: 'Attendance Outcome',
      type: 'checkbox',
      options: ATTENDEE_ATTENDANCE_OUTCOME_OPTIONS,
    },
  ],
  // The registrant's own user-account type (Org/Seeker) — the same key/options every other screen
  // offering this filter uses, so they all speak one wire format.
  userType: [
    {
      key: 'userType',
      label: 'User Type',
      type: 'checkbox',
      options: ATTENDEE_USER_TYPE_OPTIONS,
    },
  ],
};

/**
 * Per-`kpiFilter` contextual visibility — mirrors `registration-list-view`'s own `contextualFilters`
 * (e.g. its `onlineCompleted` context drops the now-redundant `payment` filter). Same idea here:
 * `notJoined` drops `systemAttendance` (already exactly what `joined`/`notJoined` mean). Every
 * other context keeps the full set — `rmContact`/`rmAttendance`/`coordinatorAttendance`/`finalAttendance`
 * are independent facets that stay meaningful no matter which KPI tile is active. `joined`/`notJoined`
 * ("Present"/"Absent") are scoped down to just `rmContact`, `attendanceOutcome` and `userType`.
 * `userType` appears in EVERY context — the registrant's account type is orthogonal to every
 * attendance signal, so no tile ever makes it redundant.
 */
export const ATTENDEE_SIDE_FILTER_CONTEXTUAL_FILTERS: Record<string, string[]> = {
  [SessionKpiFilter.ALL]: [
    'rmContact',
    'systemAttendance',
    'rmAttendance',
    'coordinatorAttendance',
    'finalAttendance',
    'attendanceOutcome',
    'userType',
  ],
  [SessionKpiFilter.JOINED]: ['rmContact', 'attendanceOutcome', 'userType'],
  [SessionKpiFilter.NOT_JOINED]: ['rmContact', 'attendanceOutcome', 'userType'],
  [SessionKpiFilter.JOINED_LATE]: [
    'rmContact',
    'systemAttendance',
    'rmAttendance',
    'coordinatorAttendance',
    'finalAttendance',
    'attendanceOutcome',
    'userType',
  ],
  [SessionKpiFilter.DROPPED]: [
    'rmContact',
    'systemAttendance',
    'rmAttendance',
    'coordinatorAttendance',
    'finalAttendance',
    'attendanceOutcome',
    'userType',
  ],
  [SessionKpiFilter.REJOINED]: [
    'rmContact',
    'systemAttendance',
    'rmAttendance',
    'coordinatorAttendance',
    'finalAttendance',
    'attendanceOutcome',
    'userType',
  ],
  // ACTIVE already implies is_system_attended = true (same as JOINED), so systemAttendance is redundant.
  [SessionKpiFilter.ACTIVE]: [
    'rmContact',
    'rmAttendance',
    'coordinatorAttendance',
    'finalAttendance',
    'attendanceOutcome',
    'userType',
  ],
  // INACTIVE spans both never-joined and joined-then-dropped seekers, so systemAttendance still narrows it further.
  [SessionKpiFilter.INACTIVE]: [
    'rmContact',
    'systemAttendance',
    'rmAttendance',
    'coordinatorAttendance',
    'finalAttendance',
    'attendanceOutcome',
    'userType',
  ],
};

/**
 * v1 — the general-attendees ("Others") screen's side filters, the counterpart to
 * `ATTENDEE_SIDE_FILTER_SETS` above. Just `knownStatus` today — every other side filter on the
 * main screen (rmContact/systemAttendance/rmAttendance/coordinatorAttendance/finalAttendance/
 * userType) has no meaning for a row with no registration, same reasoning as
 * `AnalyticsGeneralAttendeeQueryV1Input`.
 */
export const GENERAL_ATTENDEE_SIDE_FILTER_SETS: Record<string, SessionAttendeeSideFilter[]> = {
  knownStatus: [
    {
      key: 'knownStatus',
      label: 'Type',
      type: 'checkbox',
      options: GENERAL_ATTENDEE_KNOWN_STATUS_OPTIONS,
    },
  ],
};

/**
 * Per-`kpiFilter` contextual visibility for the general-attendees screen — same idea as
 * `ATTENDEE_SIDE_FILTER_CONTEXTUAL_FILTERS`. `known`/`unknown` drop `knownStatus` itself: the
 * clicked KPI tile already IS that exact narrowing, so the checkbox would be redundant (and, set
 * to the opposite value, self-contradicting). Every other context keeps it.
 */
export const GENERAL_ATTENDEE_SIDE_FILTER_CONTEXTUAL_FILTERS: Record<string, string[]> = {
  [SessionKpiFilter.ALL]: ['knownStatus'],
  [SessionKpiFilter.GENERAL]: ['knownStatus'],
  [SessionKpiFilter.JOINED]: ['knownStatus'],
  [SessionKpiFilter.NOT_JOINED]: ['knownStatus'],
  [SessionKpiFilter.JOINED_LATE]: ['knownStatus'],
  [SessionKpiFilter.DROPPED]: ['knownStatus'],
  [SessionKpiFilter.REJOINED]: ['knownStatus'],
  [SessionKpiFilter.KNOWN]: [],
  [SessionKpiFilter.UNKNOWN]: [],
};

export const ZOOM_ANALYTICS_ENV_KEYS = {
  /** Master switch for the whole feature (webhook processing + schedulers). */
  ENABLE: 'ENABLE_ZOOM_ANALYTICS',
  /** Optional Dashboard-API self-healing poll — off until Business+ access is confirmed. */
  ENABLE_LIVE_POLL: 'ENABLE_ZOOM_ANALYTICS_LIVE_POLL',
  /** Secret used to verify the new analytics webhook's HMAC signature. */
  WEBHOOK_SECRET_TOKEN: 'ZOOM_ANALYTICS_WEBHOOK_SECRET_TOKEN',
  /** Seconds after scheduled start before a first join counts as late. Defaults to LATE_JOIN_THRESHOLD_SECONDS. */
  LATE_JOIN_THRESHOLD_SECONDS: 'ZOOM_ANALYTICS_LATE_JOIN_THRESHOLD_SECONDS',
} as const;

export const ZOOM_ANALYTICS_DEFAULTS = {
  /** Dashboard-API live self-heal poll interval. */
  LIVE_POLL_INTERVAL_MS: 60_000,
  /** A seeker whose first join is later than this many seconds after the scheduled start counts as late. */
  LATE_JOIN_THRESHOLD_SECONDS: 0,
  /** A seeker whose last leave is earlier than this many seconds before session end counts as dropped. */
  DROPOFF_GRACE_SECONDS: 0,
  /** Candidate journey bucket widths — the smallest one keeping the chart at/under JOURNEY_TARGET_BUCKETS wins, so short test sessions still get a meaningful curve. */
  JOURNEY_STEP_LADDER_MINUTES: [1, 2, 5, 10, 15, 30, 60] as readonly number[],
  /** Preferred maximum journey points per chart. */
  JOURNEY_TARGET_BUCKETS: 24,
  /** Hard cap on journey buckets — guards against a corrupt start/end pair producing a giant array. */
  JOURNEY_MAX_BUCKETS: 96,
  /** The journey chart's x-axis always starts this many minutes before the session's own start, so any early joiners' arrival is visible on the chart. */
  JOURNEY_LEAD_IN_MINUTES: 15,
  /** Eligibility for the NEXT session is decided once this many sessions have been held (attend all of them → eligible). */
  ELIGIBILITY_THRESHOLD_SESSIONS: 11,
} as const;

/**
 * The Late Comers widget's buckets — how far PAST the late-join cutoff (not past the
 * session start) a late-joining seeker's first join fell. Checked in order, first match
 * wins; the last entry's `maxMs` is unreachable (Infinity) so it always catches the rest.
 */
export const ZOOM_ANALYTICS_LATE_COMER_BUCKETS: readonly { label: string; maxMs: number }[] = [
  { label: '<30s', maxMs: 30_000 },
  { label: '30s–1 min', maxMs: 60_000 },
  { label: '1–2 min', maxMs: 120_000 },
  { label: '2–5 min', maxMs: 300_000 },
  { label: '5 min+', maxMs: Infinity },
];

/** The Sincerity Map's 5 punctuality/endurance bands (each a 0-100% score) — same bands for both axes. */
export const ZOOM_SINCERITY_MAP_BAND_LABELS: readonly string[] = [
  '0-20',
  '20-40',
  '40-60',
  '60-80',
  '80-100',
];

/**
 * Canonical labels for the KNOWN drop-off reason families on the dashboard's
 * Drop-off Analysis widget. Zoom's live-webhook `leave_reason` is a sentence
 * shaped `"<name> left the meeting. Reason : <detail>."` — the detail after
 * "Reason :" is what gets classified (see normalizeDropOffReason); details
 * outside these families surface verbatim as their own dynamic bucket.
 */
export const ZOOM_ANALYTICS_DROP_OFF_REASONS = {
  USER_LEFT: 'User Manually Left',
  REMOVED_BY_HOST: 'Removed by Host',
  NETWORK_ISSUE: 'Connection Lost / Network Issue',
  HOST_ENDED: 'Session Ended by Host',
  /** Drop-offs recorded before the deployed webhook receiver captured leave reasons, or leaves where Zoom sent none. */
  UNKNOWN: 'Unknown',
} as const;

/** Follow-up reasons + engagement labels for the "Seekers Who Need Attention" table. */
export const ZOOM_ANALYTICS_FOLLOW_UP = {
  REASON_NOT_JOINED: 'Did not join the session',
  REASON_FREQUENT_DROPOFFS: 'Frequent connectivity drop-offs during the session',
  REASON_DROPPED_ONCE: 'Dropped off during the session',
} as const;

/** New Zoom REST paths this feature needs that don't exist on the existing ZoomApiService. */
export const ZOOM_ANALYTICS_API_PATHS = {
  REPORT_WEBINAR: (webinarId: string) => `/report/webinars/${webinarId}`,
  REPORT_MEETING: (meetingId: string) => `/report/meetings/${meetingId}`,
  DASHBOARD_WEBINAR_PARTICIPANTS: (webinarId: string) =>
    `/metrics/webinars/${webinarId}/participants`,
  DASHBOARD_MEETING_PARTICIPANTS: (meetingId: string) =>
    `/metrics/meetings/${meetingId}/participants`,
} as const;

export const ZOOM_ANALYTICS_HTTP_STATUS = {
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
} as const;

/** Zoom application error codes worth special-casing — neither is a real failure of ours. */
export const ZOOM_ANALYTICS_API_CODE = {
  /** Report API: data not generated yet — treat as empty, retry later. */
  PAST_DATA_NOT_READY: 3001,
  /** Granular-scope model: the S2S app lacks the dashboard scopes (arrives as HTTP 400). */
  SCOPES_MISSING: 4711,
} as const;

/** Request headers carrying Zoom's webhook signature (see Zoom webhook docs). */
export const ZOOM_ANALYTICS_WEBHOOK_HEADER = {
  SIGNATURE: 'x-zm-signature',
  TIMESTAMP: 'x-zm-request-timestamp',
} as const;

/** Zoom webhook HMAC signing scheme. message = `${MESSAGE_PREFIX}:${timestamp}:${JSON.stringify(body)}`. */
export const ZOOM_ANALYTICS_WEBHOOK_HMAC = {
  ALGORITHM: 'sha256',
  ENCODING: 'hex' as const,
  MESSAGE_PREFIX: 'v0',
  SIGNATURE_PREFIX: 'v0=',
} as const;

/** Max age (seconds) of a webhook's timestamp header before it's rejected as a likely replay. */
export const ZOOM_ANALYTICS_WEBHOOK_MAX_AGE_SECONDS = 300;

/** Logger context `module` tag for every log line emitted while processing a Zoom webhook — lets ops filter/grep for just this flow. */
export const ZOOM_ANALYTICS_WEBHOOK_LOG_MODULE = 'zoom-webhook';

export const ZOOM_ANALYTICS_SWAGGER = {
  TAG: 'analytics',
  GET_KPIS: 'Admin: the 8 header KPIs for a reconciled webinar session',
  GET_LIVE: 'Admin: live join/leave snapshot for a webinar currently in progress',
  GET_ATTENDEES: 'Admin: paginated per-seeker attendance table for a session',
  RECONCILE: 'Admin: pull the latest data from Zoom for a session and recompute analytics',
  MARK_ATTENDANCE: "Admin: mark a seeker's RM or Coordinator attendance for a session",
  WEBHOOK: 'Receive Zoom webinar webhook events (live tracking + lifecycle)',
} as const;

export const ZOOM_ANALYTICS_LOG = {
  WEBHOOK_RECEIVED: 'Zoom analytics webhook received',
  WEBHOOK_IGNORED_DISABLED: 'Zoom analytics webhook received while ENABLE_ZOOM_ANALYTICS is off — ignoring',
  WEBHOOK_REJECTED_SIGNATURE: 'Rejected Zoom analytics webhook with invalid signature',
  WEBHOOK_REJECTED_STALE: 'Rejected Zoom analytics webhook with stale/replayed timestamp',
  RECONCILIATION_STARTED: 'Zoom analytics reconciliation started',
  RECONCILIATION_COMPLETED: 'Zoom analytics reconciliation completed',
  LIVE_POLL_UNAVAILABLE: 'Zoom Dashboard API unavailable for live poll — check Business+ plan and dashboard:read:list_meeting_participants:admin / dashboard:read:list_webinar_participants:admin scopes',
  REPORT_UNAVAILABLE:
    'Zoom Report API unavailable — session start/duration will fall back to local data until report:read:meeting:admin / report:read:webinar:admin scopes exist',
  BACKGROUND_RECONCILE_FAILED: 'Background Zoom analytics reconcile failed',
  WEBHOOK_EVENT_MISSING_DATA: 'Zoom analytics webhook event missing required data',
  WEBHOOK_SESSION_NOT_FOUND: 'Zoom analytics webhook: no session found for reported resource',
  WEBHOOK_PARTICIPANT_UNMATCHED: 'Zoom analytics webhook: participant email did not match any roster seeker',
  WEBHOOK_OCCURRENCE_ID_UNMATCHED:
    'Zoom analytics webhook: occurrence_id did not match any known sibling session',
  WEBHOOK_OCCURRENCE_AMBIGUOUS:
    'Zoom analytics webhook: could not disambiguate sibling session by occurrence_id or active time window, attached to first match',
} as const;

/**
 * The session-scoped downloadable reports offered on the Attendees/Live screens' `reports` field —
 * same {code, label, description} shape and same "dropdown built from a fixed list" pattern the
 * Registrations tab uses (`RegistrationService.getRoleBasedReportOptions`, driven by
 * REPORT_DEFINITIONS/REPORT_ACCESS_MATRIX in the registration domain) and Room Inventory copies
 * (`RoomInventoryService.getAvailableReports`) — kept as its own small list here rather than
 * importing those, since `code` must match `SessionAttendanceReportPurpose`
 * (session-attendance-report.constants.ts, reports domain) exactly and reaching into that domain's
 * constants from here would cross the module boundary this app's layering rules don't allow;
 * duplicating these 4 tiny literals is the accepted tradeoff (same one this report family's SQL
 * view already takes on knowingly — see migration 3's header on session_is_done).
 *
 * Every code here takes a single session via `filters.sessionId` on
 * `POST /reports/session-attendance/generate`, matching this screen's own scope — program-wide
 * purposes (all-sessions-attendance, eligible-registrations-attendance) belong to other screens and
 * are deliberately not listed here. Not offered on the General Attendees screen — none of these
 * purposes read general/unregistered attendees today (session-attendance family is registered-
 * seeker-only OR live-event-derived-by-registration-email, see migration 5/6 headers); that screen
 * gets its own single-purpose list instead, see `ZOOM_ANALYTICS_GENERAL_ATTENDEE_SESSION_REPORTS`
 * below, so `getAttendeeTableV1` never offers a code here that would silently produce a report with
 * no rows for that screen's audience.
 */
export const ZOOM_ANALYTICS_SESSION_REPORTS: readonly { code: string; label: string; description: string }[] = [
  {
    code: 'single-session-attendance',
    label: 'Session Attendance',
    description: 'Attendance detail for this session — one row per registrant.',
  },
  {
    code: 'session-report',
    label: 'Session Report',
    description: 'Full attendance detail for this session, merged with duration, exact minutes late, and drop-off columns.',
  },
  {
    code: 'late-comer-report',
    label: 'Late Comers Report',
    description: 'Registrants who joined this session late, with the exact number of minutes past the scheduled start they joined.',
  },
  {
    code: 'session-dropoffs',
    label: 'Drop-offs Report',
    description: 'Registrants who left this session at least once, with join/leave counts and the reasons Zoom reported.',
  },
];

// The General Attendees screen's own downloadable-report list — its own 4-purpose set,
// mirroring ZOOM_ANALYTICS_SESSION_REPORTS' 4 purposes one-for-one (attendance list /
// full session report / late comers / drop-offs), since none of THAT array's purposes
// read general/unregistered (registration_id IS NULL) rows (session-attendance family is
// registered-seeker-only OR live-event-derived-by-registration-email — see migration 5/6
// headers) — see fn_generate_general_attendees_report/fn_generate_general_session_dropoff_report
// (migrations 7/8) for the general-scoped equivalents this array's codes point to.
export const ZOOM_ANALYTICS_GENERAL_ATTENDEE_SESSION_REPORTS: readonly { code: string; label: string; description: string }[] = [
  {
    code: 'general-attendees',
    label: 'General Attendees Report',
    description: 'Every attendee who joined via the shared/common link for this session, with their resolved name/email/phone (matched staff/role or placeholder link details, else Zoom\'s own reported name), attendance, and drop-off/duration detail.',
  },
  {
    code: 'general-session-report',
    label: 'General Session Report',
    description: 'Full attendance detail for this session\'s shared/common-link attendees, merged with duration, exact minutes late, and drop-off columns.',
  },
  {
    code: 'general-late-comer-report',
    label: 'General Late Comers Report',
    description: 'Shared/common-link attendees who joined this session late, with the exact number of minutes past the scheduled start they joined.',
  },
  {
    code: 'general-session-dropoffs',
    label: 'General Drop-offs Report',
    description: 'Shared/common-link attendees who left this session at least once, with join/leave counts and the reasons Zoom reported.',
  },
];
