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 { SessionKpiFilter } from 'src/common/enum/session-kpi.enum';
import { ANALYTICS_LIST_DEFAULTS } from '../constants/analytics.constants';

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

/** Same three outcome values `SessionAttendeeFiltersDto.attendanceOutcome` accepts — general attendees have no registration, but still have dropoffCount/rejoinCount/joinedAt, so this outcome check is just as meaningful for them. */
const ATTENDANCE_OUTCOME_VALUES = [SessionKpiFilter.DROPPED, SessionKpiFilter.REJOINED, SessionKpiFilter.JOINED_LATE];

/**
 * The clickable-tile values that mean something for a general (no-registration) row.
 * GENERAL/ALL are excluded — the endpoint's server-forced `generalOnly` scope already covers them
 * (see `ZoomAnalyticsFacadeService.getGeneralAttendeesV1`), so there's nothing extra to pick.
 */
const GENERAL_ATTENDEE_KPI_FILTER_VALUES = [
  SessionKpiFilter.JOINED,
  SessionKpiFilter.NOT_JOINED,
  SessionKpiFilter.DROPPED,
  SessionKpiFilter.REJOINED,
  SessionKpiFilter.JOINED_LATE,
  SessionKpiFilter.KNOWN,
  SessionKpiFilter.UNKNOWN,
];

/** Valid values for `knownStatus` — the known/unknown split, usable as a standalone checkbox filter. */
const KNOWN_STATUS_VALUES = [SessionKpiFilter.KNOWN, SessionKpiFilter.UNKNOWN];

/**
 * Every v1 general-attendee filter, bundled into one JSON `filters` query param — same wire
 * convention as `SessionAttendeeFiltersDto` (the main attendees v1 endpoint) and
 * `registration/registration-list-view`'s `filters`. `kpiFilter` is the clickable-KPI-tile
 * mechanism (see `SessionKpiTile`/`getGeneralKpisV1`); `attendanceOutcome`/`knownStatus` are
 * independent checkbox side filters, each usable without a KPI tile ever being clicked.
 */
export class GeneralAttendeeFiltersDto {
  @ApiPropertyOptional({
    enum: GENERAL_ATTENDEE_KPI_FILTER_VALUES,
    description: 'Clicking a KPI tile re-issues the query with its filter, narrowing the general-attendee rows to exactly the ones behind that count.',
  })
  @IsOptional()
  @IsIn(GENERAL_ATTENDEE_KPI_FILTER_VALUES)
  kpiFilter?: SessionKpiFilter;

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

  @ApiPropertyOptional({
    enum: KNOWN_STATUS_VALUES,
    isArray: true,
    description:
      'Checkbox multi-select (OR\'d together) over the known/unknown split, usable independently of clicking a KPI tile, e.g. ["known"] or ["known","unknown"]',
  })
  @IsOptional()
  @IsArray()
  @IsIn(KNOWN_STATUS_VALUES, { each: true })
  knownStatus?: SessionKpiFilter[];
}

/**
 * v1 `GET .../general-attendees` query — a slimmed-down `AnalyticsAttendeeQueryV1Dto`: no
 * kpiCategory (this endpoint's whole point is "only the General rows," forced server-side in
 * `ZoomAnalyticsFacadeService.getGeneralAttendeesV1`, never client-chosen) and no rmContact/
 * systemAttendance/rmAttendance/coordinatorAttendance/finalAttendance (meaningless for a row with
 * no registration — no RM owner, no manual marks, and isSystemAttended is always true by
 * construction). `filters.kpiFilter` IS exposed — the same clickable-tile mechanic as the main
 * attendees screen, layered on top of (not instead of) the server-forced general-only scope;
 * omitted means no extra narrowing beyond "general rows only." `fullName` isn't a sort option here
 * (always null for these rows) — `joinedAt` is the default instead.
 */
export class AnalyticsGeneralAttendeeQueryV1Dto {
  @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 email / mobile / Zoom-reported display name' })
  @IsOptional()
  @IsString()
  search?: string;

  // `type: String` here, NOT `type: GeneralAttendeeFiltersDto` — same Swagger reasoning as
  // `AnalyticsAttendeeQueryV1Dto.filters`: typing it as the nested DTO renders "Try it out" as a
  // struct with per-field boxes and serializes it as `filters[kpiFilter]=x` instead of a single
  // JSON string. Matching that here so Swagger UI gives one text box for the raw JSON.
  @ApiPropertyOptional({
    type: String,
    example: '{"kpiFilter":"known"}',
    description:
      'JSON-stringified filters object — paste the raw JSON as a string, e.g. {"kpiFilter":"known"}. Mirrors the main attendees endpoint\'s filters param.',
  })
  @IsOptional()
  @Transform(({ value }) => {
    if (value === undefined || value === null || value === '') return undefined;
    // Same single-transform reasoning as AnalyticsAttendeeQueryV1Dto.filters — deliberately NOT
    // paired with a separate @Type(() => GeneralAttendeeFiltersDto) on this property, to avoid the
    // @Type/@Transform race on the same property (see that DTO's own comment).
    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(GeneralAttendeeFiltersDto, raw);
  })
  @ValidateNested()
  filters?: GeneralAttendeeFiltersDto;

  @ApiPropertyOptional({ enum: ANALYTICS_GENERAL_ATTENDEE_SORT_KEYS, default: 'joinedAt' })
  @IsOptional()
  @IsIn(ANALYTICS_GENERAL_ATTENDEE_SORT_KEYS)
  sortKey: (typeof ANALYTICS_GENERAL_ATTENDEE_SORT_KEYS)[number] = 'joinedAt';

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