import {
  ADMIN_AUTH,
  ADMIN_ERRORS,
  PUBLIC,
  anyObj,
  arr,
  bool,
  created,
  dateTime,
  err,
  errs,
  int,
  jsonBody,
  limitParam,
  multipartBody,
  num,
  obj,
  objectId,
  ok,
  pageParam,
  paginated,
  pathParam,
  queryParam,
  ref,
  str,
} from './helpers';

export const ADMIN_TAGS = {
  auth: 'Admin - Auth',
  account: 'Admin - Account & 2FA',
  dashboard: 'Admin - Dashboard',
  employees: 'Admin - Employees',
  attendance: 'Admin - Attendance & Tracker',
  releases: 'Admin - Desktop Releases',
  certificates: 'Admin - Certificates',
  questions: 'Admin - Question Bank',
  quizzes: 'Admin - Quizzes',
  psychometric: 'Admin - Psychometric',
  games: 'Admin - Games',
  rewards: 'Admin - Rewards',
  organisation: 'Admin - Departments & Designations',
  content: 'Admin - Content Pages',
};

export const adminTags = [
  { name: ADMIN_TAGS.auth, description: 'Admin login (with optional TOTP 2FA), token refresh, logout and password reset' },
  { name: ADMIN_TAGS.account, description: 'Logged-in admin profile, admin accounts and two-factor authentication' },
  { name: ADMIN_TAGS.dashboard, description: 'Admin dashboard overview and points leaderboard' },
  { name: ADMIN_TAGS.employees, description: 'Employee accounts: create/invite, edit, avatar, password, delete, overview' },
  {
    name: ADMIN_TAGS.attendance,
    description: 'Attendance and activity data reported by the Tracker App (desktop sessions, day summaries, sync data)',
  },
  { name: ADMIN_TAGS.releases, description: 'Upload and manage Tracker App installers served from /downloads' },
  { name: ADMIN_TAGS.certificates, description: 'Review employee certificate requests' },
  { name: ADMIN_TAGS.questions, description: 'MCQ question bank (CRUD + CSV import)' },
  { name: ADMIN_TAGS.quizzes, description: 'Quiz CRUD, results, CSV export and interview quiz links' },
  { name: ADMIN_TAGS.psychometric, description: 'Psychometric statements, tests and results' },
  { name: ADMIN_TAGS.games, description: 'Sudoku puzzles, Zipline puzzles, spin wheels and the games menu' },
  { name: ADMIN_TAGS.rewards, description: 'Reward catalogue and redemption approvals' },
  { name: ADMIN_TAGS.organisation, description: 'Create or update departments and designations' },
  { name: ADMIN_TAGS.content, description: 'Privacy policy, terms and user manual pages' },
];

const T = ADMIN_TAGS;
const idParam = (description = 'MongoDB ObjectId') => pathParam('id', description);
const notFound = (what: string) => err(404, `${what} not found`);

const loginResponseData = obj({ accessToken: str('eyJhbGciOiJIUzI1NiIs...'), admin: ref('AuthAdmin') });
const contentPageType = pathParam('type', 'Content page type', { type: 'string', enum: ['privacy', 'terms', 'user-manual'] });
const releaseResponse = ref('DesktopRelease');

export const adminPaths = {
  // ─── Auth ──────────────────────────────────────────────────────────────────
  '/api/auth/admin/register': {
    post: {
      tags: [T.auth],
      summary: 'Register a new admin',
      description: 'Only an existing, logged-in admin can create another admin. Password: min 8 chars, 1 uppercase, 1 number, 1 special char.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          { name: str('Jane Admin', { minLength: 2, maxLength: 20 }), email: str('jane@eglogics.com'), password: str('Str0ng!Pass') },
          ['name', 'email', 'password'],
        ),
      ),
      responses: {
        ...created('Admin registered successfully', obj({ admin: ref('AuthAdmin') })),
        ...err(400, 'Missing fields, invalid name/email, weak password or email already registered'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/auth/admin/login': {
    post: {
      tags: [T.auth],
      summary: 'Admin login',
      description:
        'If 2FA is **disabled**, returns an access token and sets the `adminRefreshToken` httpOnly cookie.\n\n' +
        'If 2FA is **enabled**, returns `{ requires_2fa: true, challenge_token }` with message "OTP verification required"; ' +
        'complete the login with `POST /api/auth/verify-otp`.',
      security: PUBLIC,
      requestBody: jsonBody(obj({ email: str('admin@eglogics.com'), password: str('Str0ng!Pass') }, ['email', 'password'])),
      responses: {
        200: {
          description: 'Login successful, or 2FA challenge issued',
          content: {
            'application/json': {
              schema: {
                oneOf: [
                  obj({
                    success: bool(true),
                    status_code: int(200),
                    message: str('Login successful'),
                    data: loginResponseData,
                    errors: { nullable: true, example: null },
                    pagination: { nullable: true, example: null },
                  }),
                  obj({
                    success: bool(true),
                    status_code: int(200),
                    message: str('OTP verification required'),
                    data: obj({ requires_2fa: bool(true), challenge_token: str('eyJhbGciOiJIUzI1NiIs...') }),
                    errors: { nullable: true, example: null },
                    pagination: { nullable: true, example: null },
                  }),
                ],
              },
            },
          },
        },
        ...errs({ 400: 'Missing fields or invalid email format', 401: 'Invalid credentials' }),
      },
    },
  },
  '/api/auth/verify-otp': {
    post: {
      tags: [T.auth],
      summary: 'Complete admin login with a 2FA code',
      description: 'Exchanges the `challenge_token` from the login response and a 6-digit TOTP code for an access token. Sets the `adminRefreshToken` cookie.',
      security: PUBLIC,
      requestBody: jsonBody(obj({ challenge_token: str('eyJhbGciOiJIUzI1NiIs...'), code: str('123456', { pattern: '^\\d{6}$' }) }, ['challenge_token', 'code'])),
      responses: {
        ...ok('Login successful', obj({ token: str('eyJhbGciOiJIUzI1NiIs...'), user: ref('AuthAdmin') }), {
          description: 'Login successful. Note: this endpoint returns `token`/`user`, not `accessToken`/`admin`.',
        }),
        ...errs({ 400: 'Missing fields or code is not 6 digits', 401: 'Invalid/expired challenge token or wrong code' }),
      },
    },
  },
  '/api/auth/admin/refresh': {
    post: {
      tags: [T.auth],
      summary: 'Refresh admin access token',
      description: 'Reads the `adminRefreshToken` httpOnly cookie, rotates it, and returns a new access token.',
      security: PUBLIC,
      parameters: [{ name: 'adminRefreshToken', in: 'cookie', required: true, schema: { type: 'string' } }],
      responses: {
        ...ok('Token refreshed', obj({ accessToken: str('eyJhbGciOiJIUzI1NiIs...') })),
        ...err(401, 'Missing, invalid or revoked refresh token'),
      },
    },
  },
  '/api/auth/admin/logout': {
    post: {
      tags: [T.auth],
      summary: 'Admin logout',
      description: 'Revokes the refresh token (if present) and clears the `adminRefreshToken` cookie.',
      security: PUBLIC,
      parameters: [{ name: 'adminRefreshToken', in: 'cookie', required: false, schema: { type: 'string' } }],
      responses: ok('Logged out successfully'),
    },
  },
  '/api/auth/admin/forgot-password': {
    post: {
      tags: [T.auth],
      summary: 'Send admin password reset email',
      description: 'Always returns 200 whether or not the email is registered.',
      security: PUBLIC,
      requestBody: jsonBody(obj({ email: str('admin@eglogics.com') }, ['email'])),
      responses: {
        ...ok('If that email is registered, a reset link has been sent.'),
        ...err(400, 'Please provide email'),
      },
    },
  },
  '/api/auth/admin/reset-password': {
    post: {
      tags: [T.auth],
      summary: 'Reset admin password with token',
      security: PUBLIC,
      requestBody: jsonBody(obj({ token: str('a3f9…'), newPassword: str('N3w!Password') }, ['token', 'newPassword'])),
      responses: {
        ...ok('Password reset successfully'),
        ...err(400, 'Missing fields, invalid/expired token or weak password'),
      },
    },
  },

  // ─── Account & 2FA ─────────────────────────────────────────────────────────
  '/api/admin/me': {
    get: {
      tags: [T.account],
      summary: 'Get logged-in admin',
      security: ADMIN_AUTH,
      responses: { ...ok('Admin profile fetched', obj({ admin: ref('Admin') })), ...ADMIN_ERRORS },
    },
    put: {
      tags: [T.account],
      summary: 'Update logged-in admin',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ name: str('Super Admin'), email: str('admin@eglogics.com') })),
      responses: { ...ok('Admin profile updated', obj({ admin: ref('Admin') })), ...err(400, 'Validation error'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/avatar': {
    put: {
      tags: [T.account],
      summary: 'Upload logged-in admin avatar',
      description: 'JPEG, PNG or WebP, max 2 MB. Stored on Cloudinary (200×200 crop).',
      security: ADMIN_AUTH,
      requestBody: multipartBody(obj({ avatar: { type: 'string', format: 'binary' } }, ['avatar'])),
      responses: {
        ...ok('Avatar uploaded', obj({ avatar: str('https://res.cloudinary.com/xxx/avatars/admins/abc.jpg'), admin: ref('Admin') })),
        ...err(400, 'No image uploaded or unsupported file type'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/change-password': {
    put: {
      tags: [T.account],
      summary: 'Change own password',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ currentPassword: str('Old!Pass1'), newPassword: str('N3w!Password') }, ['currentPassword', 'newPassword'])),
      responses: {
        ...ok('Password updated successfully'),
        ...err(400, 'Please provide currentPassword and newPassword'),
        ...err(401, 'Current password is incorrect (or invalid token)'),
        ...err(403, 'Token does not belong to an admin'),
      },
    },
  },
  '/api/admin/admins': {
    get: {
      tags: [T.account],
      summary: 'List admins',
      security: ADMIN_AUTH,
      parameters: [pageParam, limitParam(10, 50)],
      responses: {
        ...paginated('Admins fetched', obj({ count: int(2), admins: arr(ref('Admin')) })),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/admins/{id}': {
    delete: {
      tags: [T.account],
      summary: 'Delete an admin',
      description: 'An admin cannot delete their own account.',
      security: ADMIN_AUTH,
      parameters: [idParam('Admin id')],
      responses: {
        ...ok('Admin deleted successfully'),
        ...err(400, 'Invalid admin id or attempting to delete yourself'),
        ...notFound('Admin'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/2fa/status': {
    get: {
      tags: [T.account],
      summary: 'Get 2FA status',
      security: ADMIN_AUTH,
      responses: {
        ...ok('2FA status fetched successfully', obj({ enabled: bool(false), verifiedAt: dateTime({ nullable: true }) })),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/2fa/setup': {
    post: {
      tags: [T.account],
      summary: 'Start 2FA setup',
      description: 'Generates a new TOTP secret and returns it as a QR code data URL and a manual key. 2FA is not enabled until `/2fa/verify` succeeds.',
      security: ADMIN_AUTH,
      responses: {
        ...ok('2FA setup generated successfully', obj({ qrCode: str('data:image/png;base64,iVBORw0KGgo...'), manualKey: str('JBSWY3DPEHPK3PXP') })),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/2fa/verify': {
    post: {
      tags: [T.account],
      summary: 'Verify code and enable 2FA',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ code: str('123456') }, ['code'])),
      responses: {
        ...ok('2FA enabled successfully', obj({ enabled: bool(true) })),
        ...err(500, 'Setup not initiated or invalid verification code (thrown without a status code)'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/2fa/disable': {
    post: {
      tags: [T.account],
      summary: 'Disable 2FA',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ code: str('123456') }, ['code'])),
      responses: {
        ...ok('2FA disabled successfully', obj({ enabled: bool(false) })),
        ...err(500, '2FA not set up or invalid verification code (thrown without a status code)'),
        ...ADMIN_ERRORS,
      },
    },
  },

  // ─── Dashboard ─────────────────────────────────────────────────────────────
  '/api/admin/dashboard': {
    get: {
      tags: [T.dashboard],
      summary: 'Dashboard overview',
      description: 'Also expires any quizzes whose end date has passed before computing the counts.',
      security: ADMIN_AUTH,
      responses: {
        ...ok(
          'Dashboard fetched',
          obj({
            stats: obj({
              total_employees: int(48),
              new_this_month: int(3),
              active_quizzes: int(2),
              question_bank: int(320),
              total_question_points: int(410),
            }),
            quiz_overview: obj({ total: int(12), active: int(2), draft: int(4), inactive: int(6) }),
            attendance_today: obj({
              present: int(40),
              checked_in_now: int(35),
              absent: int(8),
              avg_minutes: int(312),
            }),
            top_performers: arr(
              obj({
                name: str('John Doe'),
                avatar: str(null, { nullable: true }),
                initials: str('JD'),
                earned_points: num(195),
                redeemed_points: num(50),
                total_points: num(145),
                rank: int(1),
              }),
            ),
            recent_employees: arr(
              obj({
                _id: objectId(),
                name: str('John Doe'),
                email: str('john@eglogics.com'),
                avatar: str(null, { nullable: true }),
                initials: str('JD'),
                joining_date: dateTime({ nullable: true }),
              }),
            ),
            recent_quiz_results: arr(
              obj({
                user_name: str('John Doe'),
                user_email: str('john@eglogics.com'),
                quiz_title: str('JavaScript Basics'),
                score: num(15),
                total_points: num(20),
                submitted_at: dateTime(),
              }),
            ),
            psychometric_overview: obj({ total_tests: int(3), total_completed: int(41) }),
          }),
          {
            description:
              'Dashboard fetched. `attendance_today` is computed from the web check-in (Attendance) collection, not Tracker App data.',
          },
        ),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/leaderboard': {
    get: {
      tags: [T.dashboard],
      summary: 'Points leaderboard',
      description: 'Employees with total_points > 0, highest first.',
      security: ADMIN_AUTH,
      responses: {
        ...ok(
          'Leaderboard fetched',
          obj({
            count: int(20),
            leaderboard: arr(
              obj({
                _id: objectId(),
                name: str('John Doe'),
                email: str('john@eglogics.com'),
                department: str('Engineering', { nullable: true }),
                designation: str('Developer', { nullable: true }),
                total_points: num(145),
              }),
            ),
          }),
        ),
        ...ADMIN_ERRORS,
      },
    },
  },

  // ─── Employees ─────────────────────────────────────────────────────────────
  '/api/admin/users': {
    get: {
      tags: [T.employees],
      summary: 'List employees (paginated)',
      description: 'Each employee includes `has_password` and today\'s `attendance` (Present when the Tracker App logged a CHECKIN today).',
      security: ADMIN_AUTH,
      parameters: [
        pageParam,
        limitParam(10, 50),
        queryParam('search', { type: 'string' }, 'Case-insensitive match on name, email, department or employee_id'),
      ],
      responses: {
        ...paginated('Users fetched', obj({ count: int(10), users: arr(ref('AdminUserListItem')) })),
        ...ADMIN_ERRORS,
      },
    },
    post: {
      tags: [T.employees],
      summary: 'Create an employee and send the invite email',
      description:
        'Only `@eglogics.com` addresses are allowed. The employee has no password until they follow the set-password link ' +
        '(valid 3 days) in the welcome email, which also links the latest Tracker App installer when one is active.',
      security: ADMIN_AUTH,
      requestBody: jsonBody({
        allOf: [ref('EditableUserFields'), { type: 'object', required: ['name', 'email'] }],
      }),
      responses: {
        ...created('User created successfully. A set-password email has been sent.', obj({ user: { allOf: [ref('User'), obj({ has_password: bool(false) })] } })),
        ...err(400, 'Missing name/email, non-@eglogics.com email or user already exists'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/users/all': {
    get: {
      tags: [T.employees],
      summary: 'List all employees (minimal, for dropdowns)',
      security: ADMIN_AUTH,
      responses: {
        ...ok(
          'Users fetched',
          obj({
            count: int(48),
            users: arr(
              obj({
                _id: objectId(),
                name: str('John Doe'),
                email: str('john@eglogics.com'),
                department: str('Engineering', { nullable: true }),
                employee_id: str('EG-0045', { nullable: true }),
                avatar: str(null, { nullable: true }),
                status: str('active'),
                has_password: bool(true),
              }),
            ),
          }),
        ),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/users/details/{employeeId}': {
    get: {
      tags: [T.employees],
      summary: 'Employee overview',
      description: 'Profile, streaks, quiz/game/points/reward stats and the 10 most recent activities.',
      security: ADMIN_AUTH,
      parameters: [pathParam('employeeId', 'User id')],
      responses: {
        ...ok(
          'Employee details fetched successfully',
          obj({
            employee: obj({
              _id: objectId(),
              name: str('John Doe'),
              email: str('john@eglogics.com'),
              phone: str(null, { nullable: true }),
              department: str('Engineering', { nullable: true }),
              designation: str('Developer', { nullable: true }),
              profileImage: str(null, { nullable: true }),
              is_active: bool(true),
              createdAt: dateTime(),
            }),
            attendance: obj({
              currentStreak: int(3),
              longestStreak: int(9),
              totalCheckIns: int(54),
              lastCheckInDate: str('2026-09-30', { nullable: true }),
            }),
            quizStats: obj({
              totalQuizAttempts: int(8),
              completedQuizzes: int(7),
              averageQuizScore: num(12.43),
              highestQuizScore: num(19),
              totalQuizPoints: num(87),
            }),
            gameStats: obj({
              totalGamesPlayed: int(14),
              totalGamesCompleted: int(11),
              averageGameScore: num(71.5),
              highestGameScore: num(98),
              fastestCompletionTime: num(42, { nullable: true }),
            }),
            points: obj({ totalPointsEarned: num(87), totalPointsRedeemed: num(50), availablePoints: num(37) }),
            rewards: obj({ totalRewardsRedeemed: int(1), latestRewards: arr(ref('RewardRedemption')) }),
            recentActivity: arr(
              obj({
                type: str('quiz_completed', {
                  enum: ['attendance_check_in', 'quiz_started', 'quiz_completed', 'zipline_started', 'zipline_completed', 'reward_redeemed'],
                }),
                title: str('Quiz Completed'),
                message: str('Completed JavaScript Basics'),
                createdAt: dateTime(),
                meta: anyObj(),
              }),
            ),
          }),
        ),
        ...err(400, 'Invalid employeeId'),
        ...notFound('Employee'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/users/{id}': {
    get: {
      tags: [T.employees],
      summary: 'Get an employee',
      security: ADMIN_AUTH,
      parameters: [idParam('User id')],
      responses: {
        ...ok('User fetched', obj({ user: { allOf: [ref('User'), obj({ has_password: bool(true) })] } })),
        ...notFound('User'),
        ...ADMIN_ERRORS,
      },
    },
    put: {
      tags: [T.employees],
      summary: 'Update an employee',
      description: 'Only the listed fields (plus `total_points`) are applied; anything else in the body is ignored.',
      security: ADMIN_AUTH,
      parameters: [idParam('User id')],
      requestBody: jsonBody({ allOf: [ref('EditableUserFields'), obj({ total_points: num(100) })] }),
      responses: {
        ...ok('User updated', obj({ user: ref('User') })),
        ...err(400, 'Validation error'),
        ...notFound('User'),
        ...ADMIN_ERRORS,
      },
    },
    delete: {
      tags: [T.employees],
      summary: 'Delete an employee',
      security: ADMIN_AUTH,
      parameters: [idParam('User id')],
      responses: { ...ok('User deleted successfully'), ...notFound('User'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/users/{id}/avatar': {
    put: {
      tags: [T.employees],
      summary: "Upload an employee's avatar",
      description: 'JPEG, PNG or WebP, max 2 MB.',
      security: ADMIN_AUTH,
      parameters: [idParam('User id')],
      requestBody: multipartBody(obj({ avatar: { type: 'string', format: 'binary' } }, ['avatar'])),
      responses: {
        ...ok('Avatar uploaded', obj({ avatar: str('https://res.cloudinary.com/xxx/avatars/users/abc.jpg'), user: ref('User') })),
        ...err(400, 'No image uploaded or unsupported file type'),
        ...notFound('User'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/users/{id}/change-password': {
    put: {
      tags: [T.employees],
      summary: "Set an employee's password",
      security: ADMIN_AUTH,
      parameters: [idParam('User id')],
      requestBody: jsonBody(obj({ newPassword: str('N3w!Password') }, ['newPassword'])),
      responses: { ...ok('Password updated successfully'), ...err(400, 'Please provide newPassword'), ...notFound('User'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/users/{id}/resend-invite': {
    post: {
      tags: [T.employees],
      summary: 'Resend the set-password invite',
      description: 'Issues a fresh 3-day set-password link. Fails if the employee already set a password.',
      security: ADMIN_AUTH,
      parameters: [idParam('User id')],
      responses: {
        ...ok('Invitation email resent successfully'),
        ...err(400, 'Invalid user id or the employee already set a password'),
        ...notFound('User'),
        ...ADMIN_ERRORS,
      },
    },
  },

  // ─── Attendance & Tracker data ─────────────────────────────────────────────
  '/api/attendance/date/{date}': {
    get: {
      tags: [T.attendance],
      summary: 'Attendance for all employees on a date',
      description:
        'One row per active employee (absent employees included), sorted by name, built from Tracker App logs. ' +
        'Durations are in seconds. `summary` counts the whole day; `records` holds only the requested page.',
      security: ADMIN_AUTH,
      parameters: [
        pathParam('date', 'Day in yyyy-MM-dd (Asia/Kolkata)', { type: 'string', example: '2026-09-30', pattern: '^\\d{4}-\\d{2}-\\d{2}$' }),
        pageParam,
        limitParam(20, 50),
      ],
      responses: {
        ...paginated(
          'Attendance fetched',
          obj({
            count: int(20, { description: 'Rows on this page' }),
            date: str('2026-09-30'),
            summary: obj({ total: int(48), present: int(40), absent: int(8), autoCheckedOut: int(2) }),
            records: arr(ref('DailyAttendanceRow')),
          }),
        ),
        ...err(400, 'Invalid date, expected yyyy-MM-dd'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/attendance/users/{id}': {
    get: {
      tags: [T.attendance],
      summary: "An employee's web check-in history",
      description: 'Reads the Attendance collection, which is only written by the web check-in (`/api/attendance/check-in`), not by the Tracker App.',
      security: ADMIN_AUTH,
      parameters: [idParam('User id'), queryParam('month', { type: 'string', example: '2026-09' }, 'Filter by month (yyyy-MM)')],
      responses: {
        ...ok(
          'User attendance fetched',
          obj({
            user: obj({ _id: objectId(), name: str('John Doe'), email: str('john@eglogics.com') }),
            streak: obj({ current: int(3), longest: int(9) }),
            total_days: int(18),
            total_hours: num(142.5),
            records: arr(obj({ _id: objectId(), date: str('2026-09-30'), sessions: arr(obj({ check_in: dateTime(), check_out: dateTime({ nullable: true }) })), total_minutes: int(480) })),
          }),
        ),
        ...notFound('User'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/desktop/sessions': {
    get: {
      tags: [T.attendance],
      summary: 'Day summaries for all employees',
      description: 'Tracker App events grouped per employee per day (newest day first, then by name). Durations are in seconds.',
      security: ADMIN_AUTH,
      parameters: [pageParam, limitParam(50, 100)],
      responses: {
        ...paginated('Sessions fetched', obj({ count: int(50), sessions: arr(ref('DaySummary')) })),
        ...ADMIN_ERRORS,
      },
    },
    delete: {
      tags: [T.attendance],
      summary: 'Delete tracker events older than one month',
      security: ADMIN_AUTH,
      responses: {
        ...ok('Deleted 120 session record(s) older than one month', obj({ deletedCount: int(120), cutoff: dateTime() })),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/desktop/session/{employee_id}': {
    get: {
      tags: [T.attendance],
      summary: 'Day summaries for one employee',
      security: ADMIN_AUTH,
      parameters: [pathParam('employee_id', 'User id (MongoDB ObjectId, not the EG-xxxx code)'), pageParam, limitParam(50, 100)],
      responses: {
        ...paginated('Sessions fetched', obj({ count: int(12), sessions: arr(ref('DaySummary')) })),
        ...err(400, 'Invalid employee_id'),
        ...notFound('Employee'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/desktop/sync/{employee_id}': {
    get: {
      tags: [T.attendance],
      summary: "An employee's synced Tracker App data",
      description: 'Latest records from each sync collection (see `POST /api/desktop/sync`).',
      security: ADMIN_AUTH,
      parameters: [
        pathParam('employee_id', 'User id (MongoDB ObjectId)'),
        queryParam('limit', { type: 'integer', minimum: 1, maximum: 500, default: 100 }, 'Max records per collection'),
      ],
      responses: {
        ...ok(
          'Sync data fetched',
          { allOf: [obj({ employee: obj({ _id: objectId(), name: str('John Doe'), email: str('john@eglogics.com'), department: str('Engineering', { nullable: true }) }) }), ref('SyncData')] },
        ),
        ...err(400, 'Invalid employee_id'),
        ...notFound('Employee'),
        ...ADMIN_ERRORS,
      },
    },
  },

  // ─── Desktop releases ──────────────────────────────────────────────────────
  '/api/admin/desktop-release': {
    get: {
      tags: [T.releases],
      summary: 'List Tracker App releases',
      security: ADMIN_AUTH,
      responses: { ...ok('Desktop releases fetched', obj({ count: int(3), releases: arr(releaseResponse) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.releases],
      summary: 'Upload a Tracker App release',
      description:
        'Max 500 MB. Allowed extensions per platform: windows `.exe`; macos `.dmg`/`.pkg`; linux `.appimage`/`.deb`/`.rpm`. ' +
        'Setting `is_active=true` deactivates the previous active release for the same platform.',
      security: ADMIN_AUTH,
      requestBody: multipartBody(
        obj(
          {
            installer: { type: 'string', format: 'binary' },
            version: str('1.0.4', { pattern: '^\\d+(\\.\\d+){1,3}(-[0-9A-Za-z.]+)?$' }),
            platform: str('windows', { enum: ['windows', 'macos', 'linux'], default: 'windows' }),
            release_notes: str('Fixed idle detection\nFaster sync', { description: 'One note per line (or a JSON array)' }),
            mandatory: bool(false),
            is_active: bool(true),
          },
          ['installer', 'version'],
        ),
      ),
      responses: {
        ...created('Desktop release uploaded', releaseResponse),
        ...err(400, 'Missing/invalid version or file, wrong extension for platform, or version already exists'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/desktop-release/{id}': {
    delete: {
      tags: [T.releases],
      summary: 'Delete a Tracker App release',
      description: 'Also removes the installer file from /downloads.',
      security: ADMIN_AUTH,
      parameters: [idParam('Release id')],
      responses: { ...ok('Desktop release deleted'), ...notFound('Desktop release'), ...ADMIN_ERRORS },
    },
  },

  // ─── Certificates ──────────────────────────────────────────────────────────
  '/api/admin/certificate-requests': {
    get: {
      tags: [T.certificates],
      summary: 'List certificate requests',
      security: ADMIN_AUTH,
      parameters: [queryParam('status', { type: 'string', enum: ['pending', 'approved', 'rejected'] }), pageParam, limitParam(10, 50)],
      responses: {
        ...paginated('Certificate requests fetched', obj({ requests: arr(ref('CertificateRequest')) })),
        ...err(400, 'Invalid status'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/certificate-requests/{id}': {
    get: {
      tags: [T.certificates],
      summary: 'Get a certificate request',
      security: ADMIN_AUTH,
      parameters: [idParam('Certificate request id')],
      responses: { ...ok('Certificate request fetched', obj({ request: ref('CertificateRequest') })), ...notFound('Certificate request'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/certificate-requests/{id}/approve': {
    patch: {
      tags: [T.certificates],
      summary: 'Approve a certificate request',
      description: 'Creates the employee certificate and notifies the employee.',
      security: ADMIN_AUTH,
      parameters: [idParam('Certificate request id')],
      requestBody: jsonBody(obj({ admin_comment: str('Verified') }), false),
      responses: {
        ...ok('Certificate request approved', obj({ request: ref('CertificateRequest'), certificate: ref('UserCertificate') })),
        ...err(400, 'Only pending requests can be reviewed'),
        ...notFound('Certificate request'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/certificate-requests/{id}/reject': {
    patch: {
      tags: [T.certificates],
      summary: 'Reject a certificate request',
      security: ADMIN_AUTH,
      parameters: [idParam('Certificate request id')],
      requestBody: jsonBody(obj({ admin_comment: str('Certificate is not legible') }), false),
      responses: {
        ...ok('Certificate request rejected', obj({ request: ref('CertificateRequest') })),
        ...err(400, 'Only pending requests can be reviewed'),
        ...notFound('Certificate request'),
        ...ADMIN_ERRORS,
      },
    },
  },

  // ─── Question bank ─────────────────────────────────────────────────────────
  '/api/questions': {
    get: {
      tags: [T.questions],
      summary: 'List questions',
      security: ADMIN_AUTH,
      parameters: [pageParam, limitParam(20, 100)],
      responses: { ...paginated('Questions fetched', obj({ questions: arr(ref('Question')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.questions],
      summary: 'Create a question',
      security: ADMIN_AUTH,
      requestBody: jsonBody(ref('QuestionInput')),
      responses: {
        ...created('Question created', obj({ question: ref('Question') })),
        ...err(400, 'Missing fields, correct_option not one of the options, or duplicate question_text'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/questions/import': {
    post: {
      tags: [T.questions],
      summary: 'Import questions from CSV',
      description:
        'CSV (max 5 MB) with header `question_text,option_a,option_b,option_c,option_d,correct_option` and optional ' +
        '`difficulty,category,tags,points` (tags comma-separated). Invalid or duplicate rows are skipped and reported in `failed`.',
      security: ADMIN_AUTH,
      requestBody: multipartBody(obj({ file: { type: 'string', format: 'binary' } }, ['file'])),
      responses: { ...ok('Questions imported', ref('ImportResult')), ...err(400, 'No file uploaded, not a CSV, or empty CSV'), ...ADMIN_ERRORS },
    },
  },
  '/api/questions/{id}': {
    get: {
      tags: [T.questions],
      summary: 'Get a question',
      security: ADMIN_AUTH,
      parameters: [idParam('Question id')],
      responses: { ...ok('Question fetched', obj({ question: ref('Question') })), ...notFound('Question'), ...ADMIN_ERRORS },
    },
    put: {
      tags: [T.questions],
      summary: 'Update a question',
      description: 'Partial update — empty strings are ignored. The resulting correct_option must still match one of the options.',
      security: ADMIN_AUTH,
      parameters: [idParam('Question id')],
      requestBody: jsonBody({ ...ref('QuestionInput') }),
      responses: {
        ...ok('Question updated', obj({ question: ref('Question') })),
        ...err(400, 'correct_option must match one of the four options'),
        ...notFound('Question'),
        ...ADMIN_ERRORS,
      },
    },
    delete: {
      tags: [T.questions],
      summary: 'Delete a question',
      security: ADMIN_AUTH,
      parameters: [idParam('Question id')],
      responses: { ...ok('Question deleted successfully'), ...notFound('Question'), ...ADMIN_ERRORS },
    },
  },

  // ─── Quizzes ───────────────────────────────────────────────────────────────
  '/api/quizzes': {
    get: {
      tags: [T.quizzes],
      summary: 'List quizzes',
      description: 'Questions are populated with question_text, correct_option and points.',
      security: ADMIN_AUTH,
      parameters: [queryParam('status', { type: 'string', enum: ['draft', 'active', 'inactive'] }), pageParam, limitParam(10, 50)],
      responses: { ...paginated('Quizzes fetched', obj({ quizzes: arr(ref('Quiz')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.quizzes],
      summary: 'Create a quiz',
      description: 'Emits a `new_quiz` socket event. When created as `active`, employees are notified.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          {
            title: str('JavaScript Basics'),
            questions: arr(objectId('Question id')),
            start_datetime: dateTime(),
            end_datetime: dateTime(),
            status: str('draft', { enum: ['draft', 'active', 'inactive'], default: 'draft' }),
          },
          ['title', 'questions', 'start_datetime', 'end_datetime'],
        ),
      ),
      responses: {
        ...created('Quiz created successfully', obj({ quiz: ref('Quiz') })),
        ...err(400, 'Missing fields, start ≥ end, invalid question ids, or duplicate title'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/quizzes/interview/quiz/assign': {
    post: {
      tags: [T.quizzes],
      summary: 'Send an interview quiz link to a candidate',
      description: 'Creates a single-use token and emails the link `{INTERVIEW_BASE_URL}/interview/quiz/attempt/{token}`. See the Public - Interview Quiz section for the candidate endpoints.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ quizId: objectId(), candidateEmail: str('candidate@example.com') }, ['quizId', 'candidateEmail'])),
      responses: {
        ...created(
          'Interview quiz assignment created and link sent to candidate',
          obj({
            assignment: obj({
              _id: objectId(),
              quiz: objectId(),
              candidateEmail: str('candidate@example.com'),
              token: str('9b2f6f0e-2a4d-4c1e-9d8a-3c1f5e7b9a10'),
              status: str('pending', { enum: ['pending', 'started', 'submitted', 'expired'] }),
            }),
            quizLink: str('https://interview.example.com/interview/quiz/attempt/9b2f6f0e-2a4d-4c1e-9d8a-3c1f5e7b9a10'),
          }),
        ),
        ...err(400, 'quizId and candidateEmail are required'),
        ...notFound('Quiz'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/quizzes/{id}': {
    get: {
      tags: [T.quizzes],
      summary: 'Get a quiz',
      description: 'Questions are populated with all options, correct_option and points.',
      security: ADMIN_AUTH,
      parameters: [idParam('Quiz id')],
      responses: { ...ok('Quiz fetched', obj({ quiz: ref('Quiz') })), ...notFound('Quiz'), ...ADMIN_ERRORS },
    },
    put: {
      tags: [T.quizzes],
      summary: 'Update a quiz',
      description: 'Partial update. Switching status to `active` notifies employees.',
      security: ADMIN_AUTH,
      parameters: [idParam('Quiz id')],
      requestBody: jsonBody(
        obj({
          title: str('JavaScript Basics'),
          questions: arr(objectId('Question id')),
          start_datetime: dateTime(),
          end_datetime: dateTime(),
          status: str('active', { enum: ['draft', 'active', 'inactive'] }),
        }),
      ),
      responses: {
        ...ok('Quiz updated', obj({ quiz: ref('Quiz') })),
        ...err(400, 'start ≥ end, invalid question ids, or duplicate title'),
        ...notFound('Quiz'),
        ...ADMIN_ERRORS,
      },
    },
    delete: {
      tags: [T.quizzes],
      summary: 'Delete a quiz',
      security: ADMIN_AUTH,
      parameters: [idParam('Quiz id')],
      responses: { ...ok('Quiz deleted successfully'), ...notFound('Quiz'), ...ADMIN_ERRORS },
    },
  },
  '/api/quizzes/{id}/results': {
    get: {
      tags: [T.quizzes],
      summary: 'Results for a quiz',
      description: 'Sorted by score (highest first); users and answered questions are populated.',
      security: ADMIN_AUTH,
      parameters: [idParam('Quiz id')],
      responses: {
        ...ok('Quiz results fetched', obj({ quiz: obj({ _id: objectId(), title: str('JavaScript Basics') }), count: int(14), results: arr(ref('QuizResult')) })),
        ...notFound('Quiz'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/quizzes/{id}/results/export': {
    get: {
      tags: [T.quizzes],
      summary: 'Export quiz results as CSV',
      description: 'Columns: Name, Email, Score, Total Points, Percentage, Submitted At. Sent as an attachment named `{quiz_title}_results.csv`.',
      security: ADMIN_AUTH,
      parameters: [idParam('Quiz id')],
      responses: {
        200: { description: 'CSV file', content: { 'text/csv': { schema: { type: 'string', example: '"Name","Email","Score","Total Points","Percentage","Submitted At"' } } } },
        ...err(404, 'Quiz not found or no results for this quiz'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/quizzes/{quizId}/submissions/{userId}': {
    get: {
      tags: [T.quizzes],
      summary: "One employee's submission for a quiz",
      security: ADMIN_AUTH,
      parameters: [pathParam('quizId', 'Quiz id'), pathParam('userId', 'User id')],
      responses: { ...ok('Submission fetched', obj({ submission: ref('QuizResult') })), ...notFound('Submission'), ...ADMIN_ERRORS },
    },
  },

  // ─── Psychometric ──────────────────────────────────────────────────────────
  '/api/psychometric/statements': {
    get: {
      tags: [T.psychometric],
      summary: 'List statements',
      security: ADMIN_AUTH,
      parameters: [pageParam, limitParam(20, 100), queryParam('dimension', { type: 'string', enum: ['EI', 'SN', 'TF', 'JP'] })],
      responses: { ...paginated('Statements fetched', obj({ statements: arr(ref('PsychometricStatement')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.psychometric],
      summary: 'Create a statement',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          {
            statement_text: str('I enjoy meeting new people'),
            dimension: str('EI', { enum: ['EI', 'SN', 'TF', 'JP'] }),
            direction: str('positive', { enum: ['positive', 'negative'] }),
            sequence: int(1),
          },
          ['statement_text', 'dimension', 'direction'],
        ),
      ),
      responses: { ...created('Statement created', obj({ statement: ref('PsychometricStatement') })), ...err(400, 'Missing fields or duplicate statement'), ...ADMIN_ERRORS },
    },
  },
  '/api/psychometric/statements/import': {
    post: {
      tags: [T.psychometric],
      summary: 'Import statements from CSV',
      description: 'CSV (max 5 MB) with `statement_text,dimension,direction` and optional `sequence`. Invalid or duplicate rows are reported in `failed`.',
      security: ADMIN_AUTH,
      requestBody: multipartBody(obj({ file: { type: 'string', format: 'binary' } }, ['file'])),
      responses: { ...ok('Statements imported', ref('ImportResult')), ...err(400, 'No file uploaded or invalid CSV'), ...ADMIN_ERRORS },
    },
  },
  '/api/psychometric/statements/{id}': {
    get: {
      tags: [T.psychometric],
      summary: 'Get a statement',
      security: ADMIN_AUTH,
      parameters: [idParam('Statement id')],
      responses: { ...ok('Statement fetched', obj({ statement: ref('PsychometricStatement') })), ...notFound('Statement'), ...ADMIN_ERRORS },
    },
    put: {
      tags: [T.psychometric],
      summary: 'Update a statement',
      security: ADMIN_AUTH,
      parameters: [idParam('Statement id')],
      requestBody: jsonBody(
        obj({
          statement_text: str('I enjoy meeting new people'),
          dimension: str('EI', { enum: ['EI', 'SN', 'TF', 'JP'] }),
          direction: str('positive', { enum: ['positive', 'negative'] }),
          sequence: int(1),
        }),
      ),
      responses: { ...ok('Statement updated', obj({ statement: ref('PsychometricStatement') })), ...notFound('Statement'), ...ADMIN_ERRORS },
    },
    delete: {
      tags: [T.psychometric],
      summary: 'Delete a statement',
      security: ADMIN_AUTH,
      parameters: [idParam('Statement id')],
      responses: { ...ok('Statement deleted'), ...notFound('Statement'), ...ADMIN_ERRORS },
    },
  },
  '/api/psychometric/tests': {
    get: {
      tags: [T.psychometric],
      summary: 'List tests',
      security: ADMIN_AUTH,
      parameters: [queryParam('status', { type: 'string', enum: ['draft', 'active', 'inactive'] }), pageParam, limitParam(10, 100)],
      responses: { ...paginated('Tests fetched', obj({ tests: arr(ref('PsychometricTest')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.psychometric],
      summary: 'Create a test',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          {
            title: str('MBTI Assessment'),
            description: str('40-statement personality test'),
            statements: arr(objectId('Statement id')),
            status: str('draft', { enum: ['draft', 'active', 'inactive'] }),
          },
          ['title', 'statements'],
        ),
      ),
      responses: { ...created('Test created', obj({ test: ref('PsychometricTest') })), ...err(400, 'Missing fields, invalid statements or duplicate title'), ...ADMIN_ERRORS },
    },
  },
  '/api/psychometric/tests/{id}': {
    get: {
      tags: [T.psychometric],
      summary: 'Get a test',
      security: ADMIN_AUTH,
      parameters: [idParam('Test id')],
      responses: { ...ok('Test fetched', obj({ test: ref('PsychometricTest') })), ...notFound('Test'), ...ADMIN_ERRORS },
    },
    put: {
      tags: [T.psychometric],
      summary: 'Update a test',
      security: ADMIN_AUTH,
      parameters: [idParam('Test id')],
      requestBody: jsonBody(
        obj({
          title: str('MBTI Assessment'),
          description: str('40-statement personality test'),
          statements: arr(objectId('Statement id')),
          status: str('active', { enum: ['draft', 'active', 'inactive'] }),
        }),
      ),
      responses: { ...ok('Test updated', obj({ test: ref('PsychometricTest') })), ...notFound('Test'), ...ADMIN_ERRORS },
    },
    delete: {
      tags: [T.psychometric],
      summary: 'Delete a test',
      security: ADMIN_AUTH,
      parameters: [idParam('Test id')],
      responses: { ...ok('Test deleted'), ...notFound('Test'), ...ADMIN_ERRORS },
    },
  },
  '/api/psychometric/tests/{id}/results': {
    get: {
      tags: [T.psychometric],
      summary: 'Results for a test',
      security: ADMIN_AUTH,
      parameters: [idParam('Test id')],
      responses: {
        ...ok('Test results fetched', obj({ test: obj({ _id: objectId(), title: str('MBTI Assessment') }), count: int(12), results: arr(ref('PsychometricResult')) })),
        ...notFound('Test'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/psychometric/tests/{testId}/results/{userId}': {
    get: {
      tags: [T.psychometric],
      summary: "One employee's result for a test",
      description: 'Includes each response with its populated statement.',
      security: ADMIN_AUTH,
      parameters: [pathParam('testId', 'Test id'), pathParam('userId', 'User id')],
      responses: { ...ok('User result fetched', obj({ result: ref('PsychometricResult') })), ...notFound('Result'), ...ADMIN_ERRORS },
    },
  },

  // ─── Games ─────────────────────────────────────────────────────────────────
  '/api/admin/sudoku': {
    get: {
      tags: [T.games],
      summary: 'List sudoku puzzles',
      description: 'Includes solutions.',
      security: ADMIN_AUTH,
      responses: { ...ok('All sudoku puzzles fetched', obj({ puzzles: arr(ref('SudokuPuzzle')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.games],
      summary: 'Add a sudoku puzzle',
      description: 'New puzzles are created inactive (`is_active: false`); activate them with PUT.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          {
            difficulty: str('medium', { enum: ['easy', 'medium', 'hard'] }),
            puzzle: str('530070000600195000098000060800060003400803001700020006060000280000419005000080079'),
            solution: str('534678912672195348198342567859761423426853791713924856961537284287419635345286179'),
          },
          ['difficulty', 'puzzle', 'solution'],
        ),
      ),
      responses: { ...created('Sudoku puzzle added', obj({ puzzle: ref('SudokuPuzzle') })), ...err(400, 'Missing required fields'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/sudoku/{id}': {
    get: {
      tags: [T.games],
      summary: 'Get a sudoku puzzle',
      security: ADMIN_AUTH,
      parameters: [idParam('Puzzle id')],
      responses: { ...ok('Sudoku puzzle detail fetched', obj({ puzzle: ref('SudokuPuzzle') })), ...notFound('Sudoku puzzle'), ...ADMIN_ERRORS },
    },
    put: {
      tags: [T.games],
      summary: 'Update a sudoku puzzle',
      security: ADMIN_AUTH,
      parameters: [idParam('Puzzle id')],
      requestBody: jsonBody(
        obj({
          difficulty: str('hard', { enum: ['easy', 'medium', 'hard'] }),
          puzzle: str('530070000…'),
          solution: str('534678912…'),
          is_active: bool(true),
        }),
      ),
      responses: { ...ok('Sudoku puzzle updated', obj({ puzzle: ref('SudokuPuzzle') })), ...notFound('Sudoku puzzle'), ...ADMIN_ERRORS },
    },
    delete: {
      tags: [T.games],
      summary: 'Delete a sudoku puzzle',
      security: ADMIN_AUTH,
      parameters: [idParam('Puzzle id')],
      responses: { ...ok('Sudoku puzzle deleted', obj({ id: objectId() })), ...notFound('Sudoku puzzle'), ...ADMIN_ERRORS },
    },
  },
  '/api/games/zipline': {
    get: {
      tags: [T.games],
      summary: 'List Zipline puzzles',
      security: ADMIN_AUTH,
      responses: { ...ok('Zipline puzzles fetched', obj({ puzzles: arr(ref('ZiplinePuzzle')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.games],
      summary: 'Add a Zipline puzzle',
      security: ADMIN_AUTH,
      requestBody: jsonBody(ref('ZiplinePuzzleInput')),
      responses: {
        ...created('Zipline puzzle added', obj({ puzzle: ref('ZiplinePuzzle') })),
        ...err(400, 'Invalid puzzle structure'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/games/zipline/{puzzleId}': {
    put: {
      tags: [T.games],
      summary: 'Update a Zipline puzzle',
      security: ADMIN_AUTH,
      parameters: [pathParam('puzzleId', 'Puzzle id')],
      requestBody: jsonBody({ ...ref('ZiplinePuzzleInput') }),
      responses: { ...ok('Zipline puzzle updated', obj({ puzzle: ref('ZiplinePuzzle') })), ...err(400, 'Invalid puzzle structure'), ...notFound('Puzzle'), ...ADMIN_ERRORS },
    },
    delete: {
      tags: [T.games],
      summary: 'Delete a Zipline puzzle',
      security: ADMIN_AUTH,
      parameters: [pathParam('puzzleId', 'Puzzle id')],
      responses: { ...ok('Zipline puzzle deleted', obj({ puzzleId: objectId() })), ...notFound('Puzzle'), ...ADMIN_ERRORS },
    },
  },
  '/api/games/spin': {
    post: {
      tags: [T.games],
      summary: 'Create a spin wheel',
      description: 'Unless `isActive` is false, the new wheel becomes the only active wheel.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          {
            name: str('October Wheel'),
            isActive: bool(true, { default: true }),
            segments: { ...arr(ref('WheelSegment')), minItems: 1 },
          },
          ['name', 'segments'],
        ),
      ),
      responses: { ...created('Wheel created', obj({ wheel: ref('Wheel') })), ...err(400, 'name and segments are required, or total weight is 0'), ...ADMIN_ERRORS },
    },
  },
  '/api/games/spin/wheels': {
    get: {
      tags: [T.games],
      summary: 'List spin wheels',
      security: ADMIN_AUTH,
      responses: { ...ok('Wheels fetched', obj({ wheels: arr(ref('Wheel')) })), ...ADMIN_ERRORS },
    },
  },
  '/api/games/spin/wheels/{wheelId}': {
    put: {
      tags: [T.games],
      summary: 'Update a spin wheel',
      security: ADMIN_AUTH,
      parameters: [pathParam('wheelId', 'Wheel id')],
      requestBody: jsonBody(obj({ name: str('October Wheel'), isActive: bool(true), segments: arr(ref('WheelSegment')) })),
      responses: { ...ok('Wheel updated', obj({ wheel: ref('Wheel') })), ...err(400, 'No valid fields, empty segments or total weight is 0'), ...notFound('Wheel'), ...ADMIN_ERRORS },
    },
    delete: {
      tags: [T.games],
      summary: 'Delete a spin wheel',
      security: ADMIN_AUTH,
      parameters: [pathParam('wheelId', 'Wheel id')],
      responses: { ...ok('Wheel deleted', obj({ wheelId: objectId() })), ...err(400, 'Cannot delete an active wheel. Deactivate it first'), ...notFound('Wheel'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/games/all': {
    get: {
      tags: [T.games],
      summary: 'List games menu entries',
      description: 'All entries (active and inactive), sorted by `order`.',
      security: ADMIN_AUTH,
      responses: { ...ok('All games fetched', obj({ games: arr(ref('AvailableGame')) })), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/games/add': {
    post: {
      tags: [T.games],
      summary: 'Add a games menu entry',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          { title: str('Sudoku'), subtitle: str('Up to 500 pts'), icon: str('grid'), color: str('green'), route: str('/sudoku'), is_active: bool(true), order: int(1) },
          ['title'],
        ),
      ),
      responses: { ...created('Game added', obj({ game: ref('AvailableGame') })), ...err(400, 'Game already exists'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/games/update/{id}': {
    put: {
      tags: [T.games],
      summary: 'Update a games menu entry',
      security: ADMIN_AUTH,
      parameters: [idParam('Game entry id')],
      requestBody: jsonBody(
        obj({ title: str('Sudoku'), subtitle: str('Up to 500 pts'), icon: str('grid'), color: str('green'), route: str('/sudoku'), is_active: bool(true), order: int(1) }),
      ),
      responses: { ...ok('Game updated', obj({ game: ref('AvailableGame') })), ...notFound('Game'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/games/delete/{id}': {
    delete: {
      tags: [T.games],
      summary: 'Delete a games menu entry',
      security: ADMIN_AUTH,
      parameters: [idParam('Game entry id')],
      responses: { ...ok('Game deleted', obj({ game: ref('AvailableGame') })), ...notFound('Game'), ...ADMIN_ERRORS },
    },
  },

  // ─── Rewards ───────────────────────────────────────────────────────────────
  '/api/admin/rewards': {
    get: {
      tags: [T.rewards],
      summary: 'List rewards',
      security: ADMIN_AUTH,
      responses: { ...ok('Rewards fetched', obj({ rewards: arr(ref('Reward')) })), ...ADMIN_ERRORS },
    },
    post: {
      tags: [T.rewards],
      summary: 'Create a reward',
      description: 'Send an `image` file (JPEG/PNG/WebP, max 2 MB, uploaded to Cloudinary) or an `image_url`.',
      security: ADMIN_AUTH,
      requestBody: multipartBody(
        obj(
          {
            name: str('Amazon Gift Card'),
            description: str('₹500 Amazon voucher'),
            type: str('digital', { enum: ['physical', 'digital'] }),
            points_required: num(500),
            quantity: int(10, { description: 'Omit for unlimited' }),
            is_active: bool(true),
            image: { type: 'string', format: 'binary' },
            image_url: str('https://example.com/card.png'),
          },
          ['name', 'type', 'points_required'],
        ),
      ),
      responses: { ...created('Reward created', obj({ reward: ref('Reward') })), ...err(400, 'Validation error'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/rewards/{id}': {
    put: {
      tags: [T.rewards],
      summary: 'Update a reward',
      description: 'JSON body; any reward fields are applied as-is.',
      security: ADMIN_AUTH,
      parameters: [idParam('Reward id')],
      requestBody: jsonBody(
        obj({
          name: str('Amazon Gift Card'),
          description: str('₹500 Amazon voucher'),
          type: str('digital', { enum: ['physical', 'digital'] }),
          points_required: num(500),
          quantity: int(10),
          image_url: str('https://example.com/card.png'),
          is_active: bool(true),
        }),
      ),
      responses: { ...ok('Reward updated', obj({ reward: ref('Reward') })), ...notFound('Reward'), ...ADMIN_ERRORS },
    },
    delete: {
      tags: [T.rewards],
      summary: 'Delete a reward',
      security: ADMIN_AUTH,
      parameters: [idParam('Reward id')],
      responses: { ...ok('Reward deleted'), ...notFound('Reward'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/rewards/redemptions': {
    get: {
      tags: [T.rewards],
      summary: 'List redemption requests',
      description: 'Newest first; user (name, email) and reward (name, type, points_required) are populated.',
      security: ADMIN_AUTH,
      responses: { ...ok('Redemptions fetched', obj({ redemptions: arr(ref('RewardRedemption')) })), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/rewards/redemptions/{id}/approve': {
    post: {
      tags: [T.rewards],
      summary: 'Approve (fulfil) a redemption',
      description: "Deducts the points (increments the employee's redeemed_points), decrements limited stock, sets status `fulfilled` and notifies the employee.",
      security: ADMIN_AUTH,
      parameters: [idParam('Redemption id')],
      responses: {
        ...ok('Reward redemption fulfilled', obj({ redemption: ref('RewardRedemption') })),
        ...err(400, 'Already processed, not enough points, or out of stock'),
        ...notFound('Redemption request'),
        ...ADMIN_ERRORS,
      },
    },
  },
  '/api/admin/rewards/redemptions/{id}/reject': {
    post: {
      tags: [T.rewards],
      summary: 'Reject a redemption',
      security: ADMIN_AUTH,
      parameters: [idParam('Redemption id')],
      requestBody: jsonBody(obj({ adminNote: str('Out of budget this quarter') }), false),
      responses: {
        ...ok('Reward redemption rejected', obj({ redemption: ref('RewardRedemption') })),
        ...err(400, 'Redemption already processed'),
        ...notFound('Redemption request'),
        ...ADMIN_ERRORS,
      },
    },
  },

  // ─── Departments & designations ────────────────────────────────────────────
  '/api/admin/department': {
    post: {
      tags: [T.organisation],
      summary: 'Create or update a department',
      description: 'Updates the department with `id` when given, otherwise upserts by `name`.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ id: objectId('Existing department id (update)'), name: str('Engineering'), is_active: bool(true) }, ['name'])),
      responses: { ...ok('Department saved', obj({ department: ref('Department') })), ...err(400, 'Department name is required'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/designation': {
    post: {
      tags: [T.organisation],
      summary: 'Create or update a designation',
      description: 'Updates the designation with `id` when given, otherwise upserts by `name`.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(
        obj(
          { id: objectId('Existing designation id (update)'), name: str('Full Stack Developer'), is_active: bool(true), departmentId: objectId('Department id') },
          ['name'],
        ),
      ),
      responses: { ...ok('Designation saved', obj({ designation: ref('Designation') })), ...err(400, 'Designation name is required'), ...ADMIN_ERRORS },
    },
  },

  // ─── Content pages ─────────────────────────────────────────────────────────
  '/api/admin/legal': {
    post: {
      tags: [T.content],
      summary: 'Create or update a legal page (any type)',
      description: 'Legacy upsert by free-form `type` (stored lower-cased). Content is stored as-is (not sanitised). Prefer `PUT /api/admin/legal/{type}` for privacy/terms/user-manual.',
      security: ADMIN_AUTH,
      requestBody: jsonBody(obj({ type: str('privacy'), title: str('Privacy Policy'), content: str('Full policy text…') }, ['type', 'title', 'content'])),
      responses: { ...ok('Legal page saved', obj({ page: ref('LegalPage') })), ...err(400, 'type, title, and content are required'), ...ADMIN_ERRORS },
    },
  },
  '/api/admin/legal/{type}': {
    get: {
      tags: [T.content],
      summary: 'Get a content page',
      security: ADMIN_AUTH,
      parameters: [contentPageType],
      responses: {
        ...ok('Content page fetched', obj({ page: ref('LegalPage') })),
        ...err(400, 'type must be one of privacy, terms, user-manual'),
        ...notFound('Content page'),
        ...ADMIN_ERRORS,
      },
    },
    put: {
      tags: [T.content],
      summary: 'Update (or create) a content page',
      description:
        'Rich-text HTML is sanitised (no scripts, event handlers or iframes). Body limit is 5 MB for this route. ' +
        'If `title` is omitted the existing title is kept (or a default is used on creation).',
      security: ADMIN_AUTH,
      parameters: [contentPageType],
      requestBody: jsonBody(obj({ title: str('User Manual', { maxLength: 200 }), content: str('<h1>Getting started</h1><p>…</p>') }, ['content'])),
      responses: {
        ...ok('Content page updated', obj({ page: ref('LegalPage') })),
        ...err(400, 'Invalid type, empty content, or invalid title'),
        ...ADMIN_ERRORS,
      },
    },
  },
};
