import { BadRequestException } from '@nestjs/common';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform, Type, plainToInstance } from 'class-transformer';
import { IsArray, IsIn, IsInt, IsOptional, IsString, Min, ValidateNested } from 'class-validator';
import { SessionKpiCategory, SessionKpiFilter } from 'src/common/enum/session-kpi.enum';
import { AttendanceStatus } from 'src/common/enum/attendance-status.enum';
import {
  USER_TYPE_FILTER_VALUES,
  UserTypeFilterValue,
} from 'src/common/utils/user-type-filter.util';
import { ANALYTICS_LIST_DEFAULTS } from '../constants/analytics.constants';

const ANALYTICS_ATTENDEE_SORT_KEYS = [
  'fullName',
  'joinedAt',
  'durationSeconds',
  'dropoffCount',
  'rejoinCount',
] as const;

/** Valid values for `attendanceOutcome` — a subset of `SessionKpiFilter`; `all`/`joined`/`notJoined` describe presence, not an outcome alongside it. */
const ATTENDANCE_OUTCOME_VALUES = [SessionKpiFilter.DROPPED, SessionKpiFilter.REJOINED, SessionKpiFilter.JOINED_LATE];

/**
 * Every v1 attendee filter, bundled into one JSON `filters` query param — same wire convention as
 * `registration/registration-list-view`'s `filters`, confirmed against that reference's own
 * `filter-config` endpoint (`GET registration/filter-config/:programId`): its `sideFilterSets`
 * (gender/age/rmContact/etc.) are a UI/config classification, not a separate query param — everything
 * still travels in the ONE `filters` object on the wire, same as here. Unlike that reference (which
 * does a raw, unvalidated `JSON.parse` on the query string), this one is still shape-validated via
 * `@ValidateNested` — an unknown/malformed `filters` value 400s instead of silently no-op'ing.
 *
 * Two different mechanisms share this one object, described by `GET v1/analytics/sessions/:sessionId/kpis`'s
 * own `sideFilterSets` field (folded into the KPIs response rather than a separate filter-config endpoint):
 * - **kpiCategory/kpiFilter** — the clickable-KPI-tile mechanism (see `SessionKpiTile`/`getKpisV1`).
 *   Must be passed together — enforced in the service layer (`ZoomAnalyticsFacadeService.getAttendeeTableV1`),
 *   since "both or neither" is a cross-field business rule rather than a per-field shape check.
 * - **rmContact/systemAttendance/rmAttendance/coordinatorAttendance/finalAttendance/attendanceOutcome** —
 *   independent "side filters" (registration's own equivalent: `rmContact`, `gender`, etc.), each
 *   filterable on its own with no such pairing requirement. Field names match `sideFilterSets`' own
 *   keys one-for-one.
 */
export class SessionAttendeeFiltersDto {
  @ApiPropertyOptional({
    enum: SessionKpiCategory,
    description: "The clicked KPI tile's category — pass together with kpiFilter",
  })
  @IsOptional()
  @IsIn(Object.values(SessionKpiCategory))
  kpiCategory?: SessionKpiCategory;

  @ApiPropertyOptional({
    enum: SessionKpiFilter,
    description: "The clicked KPI tile's filter — pass together with kpiCategory",
  })
  @IsOptional()
  @IsIn(Object.values(SessionKpiFilter))
  kpiFilter?: SessionKpiFilter;

  @ApiPropertyOptional({ description: "Admin/coordinator only — scope the table to one RM's own seekers" })
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  rmContact?: number;

  @ApiPropertyOptional({
    enum: AttendanceStatus,
    description: 'Filter by Zoom\'s own System attendance state — "present"/"absent"',
  })
  @IsOptional()
  @IsIn(Object.values(AttendanceStatus))
  systemAttendance?: AttendanceStatus;

  @ApiPropertyOptional({
    enum: AttendanceStatus,
    description: 'Filter by the RM marker\'s own state — "present"/"absent" for a decided mark, "unknown" for never-marked',
  })
  @IsOptional()
  @IsIn(Object.values(AttendanceStatus))
  rmAttendance?: AttendanceStatus;

  @ApiPropertyOptional({
    enum: AttendanceStatus,
    description: 'Filter by the Coordinator marker\'s own state — same tri-state as rmAttendance',
  })
  @IsOptional()
  @IsIn(Object.values(AttendanceStatus))
  coordinatorAttendance?: AttendanceStatus;

  @ApiPropertyOptional({
    enum: AttendanceStatus,
    description: 'Filter by the resolved effective status (the "Final" column) — coordinator > RM > Zoom > QR > join-click',
  })
  @IsOptional()
  @IsIn(Object.values(AttendanceStatus))
  finalAttendance?: AttendanceStatus;

  @ApiPropertyOptional({
    enum: ATTENDANCE_OUTCOME_VALUES,
    isArray: true,
    description:
      'Checkbox multi-select (OR\'d together) — usable without clicking a KPI tile, e.g. ["dropped","rejoined"]',
  })
  @IsOptional()
  @IsArray()
  @IsIn(ATTENDANCE_OUTCOME_VALUES, { each: true })
  attendanceOutcome?: SessionKpiFilter[];

  /**
   * The registrant's own user-account type (`users.user_type`). Multiple selections are OR'd — a
   * plain IN over the chosen values, so picking both means "Org or Seeker" (see
   * `resolveUserTypeFilter`). Accepts a bare string as well as an array so a single-box selection
   * can be sent unwrapped. Not applicable to general attendees, which have no user account.
   */
  @ApiPropertyOptional({
    enum: USER_TYPE_FILTER_VALUES,
    isArray: true,
    description:
      'Checkbox set over the registrant\'s own account type — "Org" and/or "Seeker". Multiple selections are OR\'d together. "Seeker" also includes registrations made on someone else\'s behalf, which have no user account of their own.',
  })
  @IsOptional()
  @Transform(({ value }) => (value == null || Array.isArray(value) ? value : [value]))
  @IsArray()
  @IsIn(USER_TYPE_FILTER_VALUES, { each: true })
  userType?: UserTypeFilterValue[];
}

export class AnalyticsAttendeeQueryV1Dto {
  @ApiPropertyOptional({ default: ANALYTICS_LIST_DEFAULTS.PAGE })
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  page: number = ANALYTICS_LIST_DEFAULTS.PAGE;

  @ApiPropertyOptional({ default: ANALYTICS_LIST_DEFAULTS.LIMIT })
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  limit: number = ANALYTICS_LIST_DEFAULTS.LIMIT;

  @ApiPropertyOptional({ description: 'Filter by full name / email / mobile' })
  @IsOptional()
  @IsString()
  search?: string;

  // `type: String` here, NOT `type: SessionAttendeeFiltersDto` — this is a query param, and typing
  // it as the nested DTO tells Swagger it's an object, which renders "Try it out" as a struct with
  // per-field boxes and serializes it as `filters[kpiCategory]=x&filters[kpiFilter]=y` instead of
  // a single JSON string. registration.controller.ts's own `filters` param is `type: String` for
  // exactly this reason (see e.g. its `registration-list-view` route) — matching that here so
  // Swagger UI gives a single text box you paste the raw JSON string into.
  @ApiPropertyOptional({
    type: String,
    example: '{"kpiCategory":"attendance","kpiFilter":"dropped"}',
    description:
      'JSON-stringified filters object — paste the raw JSON as a string, e.g. {"kpiCategory":"attendance","kpiFilter":"dropped"}. Mirrors registration-list-view\'s filters param.',
  })
  @IsOptional()
  @Transform(({ value }) => {
    if (value === undefined || value === null || value === '') return undefined;
    // NOTE: deliberately NOT paired with a separate `@Type(() => SessionAttendeeFiltersDto)` on this
    // property — class-transformer's `@Type` and `@Transform` on the SAME property race (the type
    // conversion can run before this transform sees the raw query string, handing this function an
    // already-instantiated-but-empty DTO instead of the string, which then silently passed straight
    // through as `{}` — every filter looked accepted but never reached the query). Doing the
    // JSON-parse AND the class instantiation in this one transform sidesteps the ordering entirely.
    let raw: unknown = value;
    if (typeof value === 'string') {
      try {
        raw = JSON.parse(decodeURIComponent(value));
      } catch {
        throw new BadRequestException('filters must be a valid JSON object');
      }
    }
    return plainToInstance(SessionAttendeeFiltersDto, raw);
  })
  @ValidateNested()
  filters?: SessionAttendeeFiltersDto;

  @ApiPropertyOptional({
    enum: ANALYTICS_ATTENDEE_SORT_KEYS,
    description:
      'Explicit column sort (e.g. a clicked table header) — omit to get the server default: join time desc for the Present tile, registration date desc for every other tile/tab.',
  })
  @IsOptional()
  @IsIn(ANALYTICS_ATTENDEE_SORT_KEYS)
  sortKey?: (typeof ANALYTICS_ATTENDEE_SORT_KEYS)[number];

  @ApiPropertyOptional({ enum: ['ASC', 'DESC'] })
  @IsOptional()
  @IsIn(['ASC', 'DESC'])
  sortOrder?: 'ASC' | 'DESC';
}
