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

export const EMPLOYEE_TAGS = {
  auth: 'Employee App - Auth',
  profile: 'Employee App - Profile',
  dashboard: 'Employee App - Dashboard',
  quizzes: 'Employee App - Quizzes',
  psychometric: 'Employee App - Psychometric',
  attendance: 'Employee App - Web Check-in',
  certificates: 'Employee App - Certificates',
  notifications: 'Employee App - Notifications',
  rewards: 'Employee App - Rewards',
  games: 'Employee App - Games',
  reference: 'Employee App - Reference Data',
};

export const employeeTags = [
  { name: EMPLOYEE_TAGS.auth, description: 'Employee login, token refresh, logout and password reset (shared with the Tracker App)' },
  { name: EMPLOYEE_TAGS.profile, description: 'Logged-in employee profile, avatar, password and recent activity' },
  {
    name: EMPLOYEE_TAGS.dashboard,
    description: 'Home dashboard and points leaderboard. available_points = earned_points − redeemed_points.',
  },
  { name: EMPLOYEE_TAGS.quizzes, description: 'Take quizzes and view results' },
  { name: EMPLOYEE_TAGS.psychometric, description: 'Take psychometric (MBTI-style) tests and view results' },
  {
    name: EMPLOYEE_TAGS.attendance,
    description:
      'Legacy web check-in/out and streaks (Attendance collection). Working-hours attendance comes from the Tracker App — see "Tracker App - Activity Logs".',
  },
  { name: EMPLOYEE_TAGS.certificates, description: 'Submit certificates for approval and view approved ones' },
  { name: EMPLOYEE_TAGS.notifications, description: 'In-app notifications and push (FCM) device tokens' },
  { name: EMPLOYEE_TAGS.rewards, description: 'Rewards store, redemption requests and history' },
  { name: EMPLOYEE_TAGS.games, description: 'Games menu, Sudoku, Zipline and the daily spin wheel' },
  { name: EMPLOYEE_TAGS.reference, description: 'Departments, designations, colleague names and content pages' },
];

const T = EMPLOYEE_TAGS;
const SHARED_AUTH = [T.auth, TRACKER_TAGS.auth];
const kolkata = (example: string) => str(example, { description: 'Asia/Kolkata local time' });
const contentPageType = pathParam('type', 'Content page type', { type: 'string', enum: ['privacy', 'terms', 'user-manual'] });
const quizIdParam = pathParam('id', 'Quiz id');

const quizQuestion = obj({
  _id: objectId(),
  question_text: str('What does HTTP stand for?'),
  option_a: str('HyperText Transfer Protocol'),
  option_b: str('High Transfer Text Protocol'),
  option_c: str('Hyperlink Text Transfer Protocol'),
  option_d: str('None of the above'),
});

export const employeePaths = {
  // ─── Auth (shared with Tracker App) ────────────────────────────────────────
  '/api/auth/user/register': {
    post: {
      tags: [T.auth],
      summary: 'Self-register an employee',
      description: 'Password: min 8 chars, 1 uppercase, 1 number, 1 special char. Employees are normally created by an admin (invite email) instead.',
      security: PUBLIC,
      requestBody: jsonBody(
        obj({ name: str('John Doe', { minLength: 2, maxLength: 20 }), email: str('john@eglogics.com'), password: str('Str0ng!Pass') }, ['name', 'email', 'password']),
      ),
      responses: {
        ...created('User registered successfully', obj({ user: obj({ id: objectId(), name: str('John Doe'), email: str('john@eglogics.com'), role: str('user') }) })),
        ...err(400, 'Missing fields, invalid name/email, weak password or email already registered'),
      },
    },
  },
  '/api/auth/user/login': {
    post: {
      tags: SHARED_AUTH,
      summary: 'Employee login',
      description: 'Returns an access token (use as `UserBearerAuth`) and sets the `userRefreshToken` httpOnly cookie (7 days). Also sends a "Login Successful" push notification.',
      security: PUBLIC,
      requestBody: jsonBody(obj({ email: str('john@eglogics.com'), password: str('Str0ng!Pass') }, ['email', 'password'])),
      responses: {
        ...ok('Login successful', obj({ accessToken: str('eyJhbGciOiJIUzI1NiIs...'), user: ref('AuthUser') })),
        ...errs({ 400: 'Missing fields or invalid email format', 401: 'Invalid credentials' }),
      },
    },
  },
  '/api/auth/user/refresh': {
    post: {
      tags: SHARED_AUTH,
      summary: 'Refresh employee access token',
      description: 'Reads the `userRefreshToken` httpOnly cookie, rotates it and returns a new access token.',
      security: PUBLIC,
      parameters: [{ name: 'userRefreshToken', 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/user/logout': {
    post: {
      tags: SHARED_AUTH,
      summary: 'Employee logout',
      description: 'Revokes the refresh token (if present) and clears the `userRefreshToken` cookie.',
      security: PUBLIC,
      parameters: [{ name: 'userRefreshToken', in: 'cookie', required: false, schema: { type: 'string' } }],
      responses: ok('Logged out successfully'),
    },
  },
  '/api/auth/user/forgot-password': {
    post: {
      tags: SHARED_AUTH,
      summary: 'Send employee password reset email',
      description: 'Always returns 200 whether or not the email is registered.',
      security: PUBLIC,
      requestBody: jsonBody(obj({ email: str('john@eglogics.com') }, ['email'])),
      responses: { ...ok('If that email is registered, a reset link has been sent.'), ...err(400, 'Missing or invalid email') },
    },
  },
  '/api/auth/user/reset-password': {
    post: {
      tags: SHARED_AUTH,
      summary: 'Reset (or first-time set) employee password with token',
      description: 'Also used to complete the admin invite (set-password link).',
      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') },
    },
  },

  // ─── Profile ───────────────────────────────────────────────────────────────
  '/api/user/me': {
    get: {
      tags: [T.profile],
      summary: 'Get my profile',
      security: USER_AUTH,
      responses: {
        ...ok(
          'User profile fetched',
          obj({
            user: {
              allOf: [
                ref('User'),
                obj({
                  available_points: num(145, { description: 'earned_points − redeemed_points' }),
                  rank: int(3, { nullable: true, description: 'null when available_points is 0' }),
                }),
              ],
            },
          }),
        ),
        ...USER_ERRORS,
      },
    },
    put: {
      tags: [T.profile],
      summary: 'Update my profile',
      description: 'Only these fields are applied. Name 2–20 chars; phone must be 10 digits and not a fake/sequential/repeated number.',
      security: USER_AUTH,
      requestBody: jsonBody(
        obj({
          name: str('John Doe'),
          email: str('john@eglogics.com'),
          phone: str('9812345670', { pattern: '^\\d{10}$' }),
          address: str('221B Baker Street'),
          department: str('Engineering'),
          designation: str('Full Stack Developer'),
          joining_date: str('2026-04-01', { format: 'date' }),
        }),
      ),
      responses: { ...ok('Profile updated', obj({ user: ref('User') })), ...err(400, 'Validation error'), ...USER_ERRORS },
    },
  },
  '/api/user/avatar': {
    put: {
      tags: [T.profile],
      summary: 'Upload my avatar',
      description: 'JPEG, PNG or WebP, max 2 MB.',
      security: USER_AUTH,
      requestBody: multipartBody(obj({ avatar: { type: 'string', format: 'binary' } }, ['avatar'])),
      responses: {
        ...ok('Avatar uploaded', obj({ avatar: str('https://res.cloudinary.com/xxx/avatars/abc.jpg'), user: ref('User') })),
        ...err(400, 'No image uploaded or unsupported file type'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/user/change-password': {
    put: {
      tags: [T.profile],
      summary: 'Change my password',
      security: USER_AUTH,
      requestBody: jsonBody(obj({ current_password: str('Old!Pass1'), new_password: str('N3w!Password') }, ['current_password', 'new_password'])),
      responses: {
        ...ok('Password updated successfully'),
        ...err(400, 'Missing fields or weak password'),
        ...err(401, 'Current password is incorrect (or invalid token)'),
        ...err(403, 'Token does not belong to an employee'),
      },
    },
  },
  '/api/user/recent-activity': {
    get: {
      tags: [T.profile],
      summary: 'My recent activity',
      description: 'Latest quiz completions, certificates and today’s web check-ins, newest first.',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Recent activity fetched',
          obj({
            activities: arr(
              obj({
                type: str('quiz', { enum: ['quiz', 'certification', 'attendance'] }),
                message: str('Completed quiz "JavaScript Basics"'),
                createdAt: dateTime(),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Dashboard ─────────────────────────────────────────────────────────────
  '/api/user/dashboard': {
    get: {
      tags: [T.dashboard],
      summary: 'My dashboard',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Dashboard fetched',
          obj({
            user: obj({ name: str('John Doe'), avatar: str(null, { nullable: true }), designation: str('Developer', { nullable: true }), initials: str('JD') }),
            streak: obj({ current: int(3), longest: int(9) }),
            points: obj({
              earned_points: num(195),
              redeemed_points: num(50),
              available_points: num(145),
              today: num(10, { description: 'Quiz points earned today' }),
              rank: int(3, { nullable: true }),
            }),
            quizzes: obj({
              taken: int(4),
              total: int(5),
              total_active: int(2),
              percentage: num(90),
              active: arr(
                obj({
                  _id: objectId(),
                  title: str('JavaScript Basics'),
                  start_datetime: dateTime(),
                  end_datetime: dateTime(),
                  total_points: num(20),
                  submitted: bool(false),
                }),
              ),
              recent: arr(obj({ quiz_id: objectId(), quiz_title: str('Quiz 3'), score: num(3), total_points: num(4), submitted_at: dateTime() })),
            }),
            personality: arr(obj({ test_id: objectId(), test_title: str('MBTI Assessment'), personality_type: str('ESTP'), submitted_at: dateTime() })),
          }),
        ),
        ...err(404, 'User not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/user/leaderboard': {
    get: {
      tags: [T.dashboard],
      summary: 'Leaderboard',
      description: 'All employees ranked by available_points (earned_points − redeemed_points).',
      security: USER_AUTH,
      parameters: [queryParam('period', { type: 'string', enum: ['today', 'weekly', 'monthly', 'all-time'], default: 'all-time' })],
      responses: {
        ...ok(
          'Leaderboard fetched',
          obj({
            leaderboard: arr(
              obj({
                user_id: objectId(),
                name: str('John Doe'),
                avatar: str(null, { nullable: true }),
                initials: str('JD'),
                earned_points: num(195),
                redeemed_points: num(50),
                available_points: num(145),
                isCurrentUser: bool(true),
                rank: int(1),
              }),
            ),
          }),
        ),
        ...err(400, 'Invalid period'),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Quizzes ───────────────────────────────────────────────────────────────
  '/api/quizzes/active': {
    get: {
      tags: [T.quizzes],
      summary: 'Active quizzes',
      description: 'Active, not-yet-ended quizzes with my submission status (unsubmitted first).',
      security: USER_AUTH,
      parameters: [queryParam('filter', { type: 'string', enum: ['live', 'upcoming', 'all'], default: 'all' }, 'live = started, upcoming = not started yet')],
      responses: {
        ...ok(
          'Quizzes fetched',
          obj({
            count: int(3),
            quizzes: arr(
              obj({
                _id: objectId(),
                title: str('JavaScript Basics'),
                start_datetime: dateTime(),
                end_datetime: dateTime(),
                total_questions: int(10),
                total_points: num(20),
                is_live: bool(true),
                submitted: bool(false),
                points_earned: num(15, { description: 'Only when submitted' }),
                submitted_at: dateTime({ description: 'Only when submitted' }),
              }),
            ),
          }),
        ),
        ...err(400, 'Invalid filter'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/quizzes/my-results': {
    get: {
      tags: [T.quizzes],
      summary: 'My quiz results',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Results fetched',
          obj({
            results: arr(
              obj({
                _id: objectId(),
                quiz_id: objectId(),
                quiz_title: str('JavaScript Basics'),
                correct_answers: int(3),
                total_questions: int(4),
                scored_points: num(15),
                total_points: num(20),
                percentage: num(75),
                submitted_at: dateTime(),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/quizzes/{id}/take': {
    get: {
      tags: [T.quizzes],
      summary: 'Get quiz questions (unshuffled, no answers)',
      security: USER_AUTH,
      parameters: [quizIdParam, pageParam, limitParam(10, 50)],
      responses: {
        ...paginated(
          'Quiz fetched',
          obj({
            alreadySubmitted: bool(false),
            quiz: obj({
              _id: objectId(),
              title: str('JavaScript Basics'),
              start_datetime: dateTime(),
              end_datetime: dateTime(),
              questions: arr({ allOf: [quizQuestion, obj({ points: num(1) })] }),
            }),
          }),
        ),
        ...err(400, 'Quiz not active, not started yet, or already ended'),
        ...err(404, 'Quiz not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/quizzes/{id}/attempt': {
    get: {
      tags: [T.quizzes],
      summary: 'Start / resume a timed quiz attempt',
      description:
        'Creates the attempt on first call (5 seconds per question) with a per-user shuffled order of questions and options; ' +
        'later calls return the same order and the remaining time. When already submitted, returns `alreadySubmitted: true` and no questions.',
      security: USER_AUTH,
      parameters: [quizIdParam, pageParam, { ...limitParam(10, 50), description: 'Page size, 1–50 (400 outside this range)' }],
      responses: {
        ...paginated(
          'Quiz attempt fetched',
          obj({
            alreadySubmitted: bool(false),
            quiz: obj({ _id: objectId(), title: str('JavaScript Basics'), questions: arr(quizQuestion) }),
            startedAt: kolkata('2026-09-30 13:10:00'),
            expiresAt: kolkata('2026-09-30 13:11:15'),
            durationTime: str('01:15', { description: 'Seconds as a string when ≤ 60, otherwise mm:ss or hh:mm:ss' }),
            remainingTime: str('60', { description: 'Same format as durationTime' }),
          }),
        ),
        ...err(400, 'Invalid page/limit, quiz not active, not started yet, or already ended'),
        ...err(404, 'Quiz not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/quizzes/{id}/submit': {
    post: {
      tags: [T.quizzes],
      summary: 'Submit quiz answers',
      description:
        'Requires a started attempt (`GET /attempt`). Missing/empty `answers` is accepted (time-out submission). Points earned are added to the employee’s quiz/earned points.',
      security: USER_AUTH,
      parameters: [quizIdParam],
      requestBody: jsonBody(
        obj({
          answers: arr(obj({ question_id: objectId(), selected_option: str('HyperText Transfer Protocol', { description: 'Option text, not a letter' }) }, ['question_id', 'selected_option'])),
        }),
      ),
      responses: {
        ...created('Quiz submitted successfully', obj({ result: { allOf: [ref('QuizResult'), obj({ totalQuestions: int(10) })] }, totalQuestions: int(10) })),
        ...err(400, 'Invalid answer format, quiz not open, already submitted, or no active attempt'),
        ...err(404, 'Quiz not found'),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Psychometric ──────────────────────────────────────────────────────────
  '/api/psychometric/tests/active': {
    get: {
      tags: [T.psychometric],
      summary: 'Active psychometric tests',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Active tests fetched',
          obj({
            tests: arr(
              obj({
                _id: objectId(),
                title: str('MBTI Assessment'),
                description: str(null, { nullable: true }),
                completed: bool(false),
                personality_type: str('INTJ', { description: 'Only when completed' }),
                submitted_at: dateTime({ description: 'Only when completed' }),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/psychometric/tests/{id}/attempt': {
    get: {
      tags: [T.psychometric],
      summary: 'Get test statements to answer',
      security: USER_AUTH,
      parameters: [pathParam('id', 'Test id')],
      responses: {
        ...ok(
          'Test attempt fetched',
          obj({
            test: obj({ _id: objectId(), title: str('MBTI Assessment'), description: str(null, { nullable: true }) }),
            statements: arr(obj({ _id: objectId(), statement_text: str('I enjoy meeting new people') })),
            total: int(40),
            scale: {
              ...arr(obj({ value: int(1), label: str('Strongly Disagree') })),
              example: [
                { value: 1, label: 'Strongly Disagree' },
                { value: 2, label: 'Disagree' },
                { value: 3, label: 'Neutral' },
                { value: 4, label: 'Agree' },
                { value: 5, label: 'Strongly Agree' },
              ],
            },
          }),
        ),
        ...err(400, 'Test not active or already completed'),
        ...err(404, 'Test not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/psychometric/tests/{id}/submit': {
    post: {
      tags: [T.psychometric],
      summary: 'Submit test responses',
      description: 'Every statement in the test must be answered with a value from 1 to 5.',
      security: USER_AUTH,
      parameters: [pathParam('id', 'Test id')],
      requestBody: jsonBody(
        obj({ responses: { ...arr(obj({ statement: objectId('Statement id'), value: int(4, { minimum: 1, maximum: 5 }) }, ['statement', 'value'])), minItems: 1 } }, ['responses']),
      ),
      responses: {
        ...created('Test submitted successfully', obj({ profile: ref('PersonalityProfile'), personality_type: str('ENTP') })),
        ...err(400, 'Missing/incomplete responses, invalid value, test not active, or already completed'),
        ...err(404, 'Test not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/psychometric/my-results': {
    get: {
      tags: [T.psychometric],
      summary: 'My psychometric results',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Results fetched',
          obj({
            results: arr(
              obj({
                _id: objectId(),
                test: obj({ _id: objectId(), title: str('MBTI Assessment'), description: str(null, { nullable: true }) }),
                profile: ref('PersonalityProfile'),
                personality_type: str('INTJ'),
                submitted_at: dateTime(),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Web check-in ──────────────────────────────────────────────────────────
  '/api/attendance/check-in': {
    post: {
      tags: [T.attendance],
      summary: 'Web check-in',
      description: 'Opens a session for today and updates the streak (one missed day keeps the streak with a warning; more resets it).',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Checked in successfully',
          obj({
            attendance: obj({ date: str('2026-09-30'), sessions: arr(ref('WebAttendanceSession')), total_minutes: int(0) }),
            streak: obj({
              current: int(5),
              longest: int(12),
              warning: str('You missed a day! Check in tomorrow to keep your 4 day streak.', { nullable: true }),
            }),
          }),
        ),
        ...err(400, 'Already checked in. Please check out first.'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/attendance/check-out': {
    post: {
      tags: [T.attendance],
      summary: 'Web check-out',
      security: USER_AUTH,
      responses: {
        ...ok('Checked out successfully', obj({ date: str('2026-09-30'), sessions: arr(ref('WebAttendanceSession')), total_minutes: int(245) })),
        ...err(400, 'No check-in for today or no active session'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/attendance/today': {
    get: {
      tags: [T.attendance],
      summary: "Today's web check-in status",
      security: USER_AUTH,
      responses: {
        ...ok(
          'Today status fetched',
          obj({
            date: str('2026-09-30'),
            is_checked_in: bool(true),
            sessions: arr(obj({ check_in: dateTime(), check_out: dateTime({ nullable: true }) })),
            total_minutes: int(120),
            streak: obj({ current: int(5), longest: int(12) }),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/attendance/my': {
    get: {
      tags: [T.attendance],
      summary: 'My web check-in history',
      security: USER_AUTH,
      parameters: [queryParam('month', { type: 'string', example: '2026-09' }, 'Filter by month (yyyy-MM)')],
      responses: {
        ...ok(
          'Attendance fetched',
          obj({
            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) })),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Certificates ──────────────────────────────────────────────────────────
  '/api/user/certificates/requests': {
    post: {
      tags: [T.certificates],
      summary: 'Submit a certificate for approval',
      security: USER_AUTH,
      requestBody: multipartBody(
        obj(
          {
            certificate_id: str('WEBDEV2234', { pattern: '^[A-Za-z0-9_-]{4,30}$', description: '4–30 letters, numbers, _ or -. Stored upper-case; must be unique.' }),
            title: str('AWS Cloud Practitioner'),
            description: str('Entry-level cloud certification'),
            issuer: str('Amazon Web Services'),
            issue_date: str('2026-03-20', { format: 'date' }),
            completion_date: str('2026-03-25', { format: 'date', description: 'On or after issue_date' }),
            certificate: { type: 'string', format: 'binary', description: 'PDF, JPEG, PNG or WebP, max 5 MB' },
          },
          ['certificate_id', 'title', 'issuer', 'issue_date', 'certificate'],
        ),
      ),
      responses: {
        ...created('Certificate request submitted', obj({ request: ref('CertificateRequest') })),
        ...err(400, 'Missing fields/file, invalid certificate_id or dates, or duplicate certificate_id'),
        ...USER_ERRORS,
      },
    },
    get: {
      tags: [T.certificates],
      summary: 'My certificate requests',
      security: USER_AUTH,
      parameters: [queryParam('status', { type: 'string', enum: ['pending', 'approved', 'rejected'] }), pageParam, limitParam(10, 50)],
      responses: {
        ...paginated('Certificate requests fetched', obj({ count: int(2), requests: arr(ref('CertificateRequest')) })),
        ...err(400, 'Invalid status'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/user/certificates': {
    get: {
      tags: [T.certificates],
      summary: 'My approved certificates',
      security: USER_AUTH,
      parameters: [pageParam, limitParam(10, 50)],
      responses: { ...paginated('Certificates fetched', obj({ count: int(1), certificates: arr(ref('UserCertificate')) })), ...USER_ERRORS },
    },
  },

  // ─── Notifications ─────────────────────────────────────────────────────────
  '/api/notifications': {
    get: {
      tags: [T.notifications],
      summary: 'My notifications',
      security: USER_AUTH,
      parameters: [pageParam, { ...limitParam(10, 50), description: 'Page size, 1–50 (400 outside this range)' }],
      responses: {
        ...paginated('Notifications fetched', obj({ notifications: arr(ref('Notification')), unreadCount: int(3) })),
        ...err(400, 'Invalid page or limit'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/notifications/read-all': {
    patch: {
      tags: [T.notifications],
      summary: 'Mark all notifications as read',
      security: USER_AUTH,
      responses: { ...ok('All notifications marked as read', obj({ modifiedCount: int(5) })), ...USER_ERRORS },
    },
  },
  '/api/notifications/{id}/read': {
    patch: {
      tags: [T.notifications],
      summary: 'Mark a notification as read',
      security: USER_AUTH,
      parameters: [pathParam('id', 'Notification id')],
      responses: { ...ok('Notification marked as read', obj({ notification: ref('Notification') })), ...err(404, 'Notification not found'), ...USER_ERRORS },
    },
  },
  '/api/user/device-token': {
    post: {
      tags: [T.notifications],
      summary: 'Save FCM device token',
      security: USER_AUTH,
      requestBody: jsonBody(obj({ token: str('fcm_token_here'), platform: str('android', { enum: ['android', 'ios', 'web'], default: 'android' }) }, ['token'])),
      responses: { ...ok('Device token saved successfully'), ...err(400, 'Device token is required'), ...USER_ERRORS },
    },
    delete: {
      tags: [T.notifications],
      summary: 'Remove FCM device token',
      security: USER_AUTH,
      requestBody: jsonBody(obj({ token: str('fcm_token_here') }, ['token'])),
      responses: { ...ok('Device token removed successfully'), ...err(400, 'Device token is required'), ...USER_ERRORS },
    },
  },

  // ─── Rewards ───────────────────────────────────────────────────────────────
  '/api/rewards/store': {
    get: {
      tags: [T.rewards],
      summary: 'Rewards I can afford',
      description:
        'Active rewards whose points_required ≤ my available points (sum of quiz scores − redeemed_points), cheapest first. ' +
        '`requested` is true when I already have a redemption request for that reward.',
      security: USER_AUTH,
      parameters: [pageParam, limitParam(12)],
      responses: {
        ...paginated(
          'Available rewards fetched',
          obj({ rewards: arr({ allOf: [ref('Reward'), obj({ requested: bool(false) })] }), available_points: num(145) }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/rewards/redeem/{rewardId}': {
    post: {
      tags: [T.rewards],
      summary: 'Request a reward',
      description: 'Creates a pending request. Points are only deducted when an admin approves it.',
      security: USER_AUTH,
      parameters: [pathParam('rewardId', 'Reward id')],
      responses: {
        ...created('Reward redemption requested', obj({ redemption: ref('RewardRedemption') })),
        ...err(400, 'Not enough points, out of stock, or already requested'),
        ...err(404, 'Reward not found or inactive'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/rewards/history': {
    get: {
      tags: [T.rewards],
      summary: 'My redemption history',
      description: 'Status is mapped for display: pending → "Pending", fulfilled → "Redeemed", rejected → "Rejected".',
      security: USER_AUTH,
      parameters: [pageParam, limitParam(10)],
      responses: {
        ...paginated(
          'Reward redemption history fetched',
          obj({
            redemptions: arr({
              allOf: [
                ref('RewardRedemption'),
                obj({ status: str('Pending', { enum: ['Pending', 'Redeemed', 'Rejected', 'approved'] }) }),
              ],
            }),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Games ─────────────────────────────────────────────────────────────────
  '/api/user/games/available': {
    get: {
      tags: [T.games],
      summary: 'Games menu',
      description: 'Active games, sorted by `order`.',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Games fetched',
          obj({
            games: arr(
              obj({
                id: objectId(),
                title: str('Sudoku'),
                subtitle: str('Up to 500 pts'),
                icon: str('grid'),
                color: str('green'),
                route: str('/sudoku'),
                available: bool(true),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/sudoku': {
    get: {
      tags: [T.games],
      summary: 'Random sudoku puzzle',
      description: 'A random active puzzle of any difficulty (no solution). Pass its `_id` as `gameId` to `/sudoku/start`.',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Sudoku puzzle fetched',
          obj({
            puzzle: obj({
              _id: objectId(),
              type: str('sudoku'),
              difficulty: str('medium', { enum: ['easy', 'medium', 'hard'] }),
              puzzle: str('530070000600195000098000060800060003400803001700020006060000280000419005000080079'),
              is_active: bool(true),
              createdAt: dateTime(),
            }),
          }),
        ),
        ...err(404, 'No puzzles available'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/sudoku/start': {
    post: {
      tags: [T.games],
      summary: 'Start a sudoku attempt',
      security: USER_AUTH,
      requestBody: jsonBody(obj({ gameId: objectId('Puzzle `_id` from GET /api/games/sudoku') }, ['gameId'])),
      responses: {
        ...created('Sudoku attempt started', obj({ attemptId: objectId(), startedAt: str('2026-09-30 01:10:00 PM', { description: 'Asia/Kolkata, yyyy-MM-dd hh:mm:ss a' }) })),
        ...err(400, 'Invalid or missing gameId'),
        ...err(500, 'Game not found (thrown without a status code)'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/sudoku/attempt/{attemptId}': {
    patch: {
      tags: [T.games],
      summary: 'Save sudoku progress',
      security: USER_AUTH,
      parameters: [pathParam('attemptId', 'Attempt id')],
      requestBody: jsonBody(obj({ answers: str('534070000600195000098000060800060003400803001700020006060000280000419005000080079', { description: 'Current 81-character grid' }) }, ['answers'])),
      responses: {
        ...ok('Sudoku attempt updated', obj({ attempt: anyObj('Game attempt (user, game, answers, status, score, timeTaken, startedAt)') })),
        ...err(500, 'Attempt not found or already finished (thrown without a status code)'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/sudoku/attempt/{attemptId}/submit': {
    post: {
      tags: [T.games],
      summary: 'Submit a sudoku attempt',
      description:
        'The grid must exactly match the solution. Correct submissions score between the difficulty’s base and max points (faster = more) and add them to earned_points; ' +
        'wrong ones mark the attempt failed with score 0 (still HTTP 200, `success: false` inside `data`).',
      security: USER_AUTH,
      parameters: [pathParam('attemptId', 'Attempt id')],
      requestBody: jsonBody(obj({ answers: str('534678912672195348198342567859761423426853791713924856961537284287419635345286179', { description: 'Final 81-character grid' }) }, ['answers'])),
      responses: {
        ...ok(
          'Sudoku attempt submitted',
          obj({
            success: bool(true),
            message: str('Incorrect solution', { description: 'Only when success is false' }),
            score: int(250),
            timeTaken: int(270, { description: 'Seconds' }),
            startedAt: str('2026-09-30 01:10:00 PM'),
            completedAt: str('2026-09-30 01:14:30 PM'),
          }),
        ),
        ...err(400, 'Attempt already completed or failed'),
        ...err(403, 'Attempt belongs to another employee (or token is not an employee)'),
        ...err(404, 'Attempt or game not found'),
        ...err(401, 'Missing, invalid or expired employee access token'),
      },
    },
  },
  '/api/games/sudoku/my-attempts': {
    get: {
      tags: [T.games],
      summary: 'My last 10 sudoku attempts',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Sudoku attempts fetched',
          obj({
            attempts: arr(
              obj({
                _id: objectId(),
                game: obj({ _id: objectId(), difficulty: str('medium') }),
                answers: str('5346789…'),
                status: str('completed', { enum: ['in_progress', 'completed', 'failed'] }),
                score: int(250),
                timeTaken: int(270),
                startedAt: str('2026-09-30 01:10:00 PM', { nullable: true }),
                completedAt: str('2026-09-30 01:14:30 PM', { nullable: true }),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/zipline/start': {
    post: {
      tags: [T.games],
      summary: 'Start a Zipline attempt',
      description: 'Without `puzzleId`, a random available puzzle is chosen (optionally of the given difficulty). The puzzle is returned without its solution.',
      security: USER_AUTH,
      requestBody: jsonBody(obj({ puzzleId: objectId(), difficulty: str('easy', { enum: ['easy', 'medium', 'hard'] }) }), false),
      responses: {
        ...ok(
          'Zipline attempt started',
          obj({
            attemptId: objectId(),
            puzzle: obj({
              _id: objectId(),
              title: str('Warm-up'),
              description: str('Connect 1 to 4'),
              difficulty: str('easy'),
              grid: arr(arr(int(0))),
              numbers: arr(ref('ZiplineCheckpoint')),
              blocks: arr(ref('ZiplineCell')),
            }),
          }),
        ),
        ...err(404, 'No available puzzles found / Puzzle not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/zipline/attempt/{attemptId}': {
    patch: {
      tags: [T.games],
      summary: 'Save Zipline progress',
      security: USER_AUTH,
      parameters: [pathParam('attemptId', 'Attempt id')],
      requestBody: jsonBody(obj({ progress: anyObj('Any client-side progress state (stored as-is)') }, ['progress'])),
      responses: {
        ...ok('Zipline attempt updated', obj({ attempt: anyObj('Zipline attempt document') })),
        ...err(500, 'Attempt not found or already completed (thrown without a status code)'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/zipline/attempt/{attemptId}/submit': {
    post: {
      tags: [T.games],
      summary: 'Submit a Zipline path',
      description:
        'The path must start at checkpoint 1, move one orthogonal cell at a time, avoid blocked cells, visit checkpoints in order and end on the last one. ' +
        'A wrong path is still HTTP 200 with `success: false` and score 0. Correct paths add the score to earned_points.',
      security: USER_AUTH,
      parameters: [pathParam('attemptId', 'Attempt id')],
      requestBody: jsonBody(
        obj(
          {
            path: {
              ...arr({ type: 'array', items: { type: 'integer' }, minItems: 2, maxItems: 2 }),
              minItems: 2,
              description: 'Visited cells in order as [row, col] pairs',
              example: [[2, 0], [2, 1], [2, 2], [2, 3], [2, 4]],
            },
          },
          ['path'],
        ),
      ),
      responses: {
        ...ok(
          'Zipline attempt submitted',
          obj({
            success: bool(true),
            isCorrect: bool(true),
            status: str('COMPLETED', { enum: ['COMPLETED', 'FAILED'] }),
            score: int(96),
            timeTaken: str('1m 23s'),
            startedAt: kolkata('2026-09-30 12:21:48'),
            completedAt: str('2026-09-30 12:23:11', { nullable: true }),
          }),
        ),
        ...err(400, 'Invalid path or attempt already submitted'),
        ...err(404, 'Attempt not found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/zipline/my-attempts': {
    get: {
      tags: [T.games],
      summary: 'My Zipline attempts',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Zipline attempts fetched',
          obj({
            attempts: arr(
              obj({
                _id: objectId(),
                puzzleId: objectId(),
                title: str('Warm-up', { nullable: true }),
                difficulty: str('easy', { nullable: true }),
                isCorrect: bool(true),
                score: int(96),
                timeTaken: int(83, { description: 'Seconds' }),
                startedAt: str('2026-09-30 12:21:48', { nullable: true }),
                completedAt: str('2026-09-30 12:23:11', { nullable: true }),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/spin': {
    get: {
      tags: [T.games],
      summary: 'Active spin wheel',
      security: USER_AUTH,
      responses: { ...ok('Wheel fetched', obj({ wheel: ref('Wheel') })), ...err(404, 'No active wheel found'), ...USER_ERRORS },
    },
  },
  '/api/games/spin/eligibility': {
    get: {
      tags: [T.games],
      summary: 'Can I spin today?',
      description: 'One spin per day (server-local day).',
      security: USER_AUTH,
      responses: {
        ...ok('Wheel eligibility fetched', obj({ eligibility: obj({ canSpin: bool(true), remainingSpins: int(1), nextSpinAt: dateTime({ nullable: true }) }) })),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/spin/spin-wheel': {
    post: {
      tags: [T.games],
      summary: 'Spin the wheel',
      description: 'Picks a weighted segment of the active wheel. Points prizes are added to earned_points and spin_wheel_points.',
      security: USER_AUTH,
      responses: {
        ...ok('Wheel spun successfully', obj({ wheelId: objectId(), segment: ref('WheelSegment') })),
        ...err(400, 'No spins remaining'),
        ...err(404, 'No active wheel found'),
        ...USER_ERRORS,
      },
    },
  },
  '/api/games/spin/history': {
    get: {
      tags: [T.games],
      summary: 'My last 20 spins',
      security: USER_AUTH,
      responses: {
        ...ok(
          'Spin history fetched',
          obj({
            history: arr(
              obj({
                _id: objectId(),
                userId: objectId(),
                wheelId: objectId(),
                segmentId: objectId(),
                prizeType: str('points'),
                prizeValue: num(50),
                createdAt: dateTime(),
              }),
            ),
          }),
        ),
        ...USER_ERRORS,
      },
    },
  },

  // ─── Reference data ────────────────────────────────────────────────────────
  '/api/departments/all': {
    get: {
      tags: [T.reference],
      summary: 'Active departments',
      security: USER_AUTH,
      responses: { ...ok('Departments fetched', obj({ departments: arr(ref('Department')) })), ...USER_ERRORS },
    },
  },
  '/api/designations/all': {
    get: {
      tags: [T.reference],
      summary: 'Active designations',
      security: USER_AUTH,
      parameters: [queryParam('departmentId', { type: 'string' }, 'Only designations of this department')],
      responses: { ...ok('Designations fetched', obj({ designations: arr(ref('Designation')) })), ...USER_ERRORS },
    },
  },
  '/api/user/active-users/names': {
    get: {
      tags: [T.reference],
      summary: 'Names of all active employees',
      security: USER_AUTH,
      responses: { ...ok('Active user names fetched', obj({ count: int(48), names: arr(str('John Doe')) })), ...USER_ERRORS },
    },
  },
  '/api/user/legal/{type}': {
    get: {
      tags: [T.reference],
      summary: 'Get a content page',
      security: USER_AUTH,
      parameters: [contentPageType],
      responses: {
        ...ok('Content page fetched', obj({ page: ref('LegalPage') })),
        ...err(400, 'type must be one of privacy, terms, user-manual'),
        ...err(404, 'Content page not found'),
        ...USER_ERRORS,
      },
    },
  },
};
