// ── OpenAPI spec generator (swagger-autogen) ───────────────────────────────
//
// Static-analyzes the Express routers and emits `swagger-output.json`, which is
// served by swagger-ui-express at /api/docs (see src/app.js). Per-endpoint
// details — tags, summaries, parameters, request bodies, security and response
// codes — live as `#swagger.*` comments next to each route in
// src/router/**. This file supplies everything shared: API metadata, servers,
// tag descriptions, the two auth schemes (session cookie + mobile bearer), and
// the reusable component schemas the annotations reference.
//
// Regenerate with:  npm run swagger   (from the server/ directory)
// The generated JSON is committed so the docs work without a build step.

const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' })
const pkg = require('../package.json')
const brand = require('../src/config/brand')

// Cookie name is env-configurable (COOKIE_NAME); the spec documents whatever this
// build targets. This is a build-time artifact — regenerate with `npm run swagger`.
const COOKIE_NAME = process.env.COOKIE_NAME || 'rg_token'

const outputFile = './swagger/swagger-output.json'

// Entry point of the routing graph. swagger-autogen follows the `app.use(...)`
// mount chain from here (/api → /v1 → auth|public|admin), so generated paths are
// fully-qualified (e.g. /api/v1/auth/login).
const routes = ['./src/app.js']

const doc = {
  info: {
    title: `${brand.name} API`,
    version: pkg.version,
    description:
      `REST API for the ${brand.name} website, wiki and admin panel — a private ` +
      'Ultima Online shard.\n\n' +
      '### Authentication\n' +
      `- **Web / admin panel** uses an httpOnly session cookie (\`${COOKIE_NAME}\`) issued by ` +
      '`POST /api/v1/auth/login` (plus `/login/totp` when 2FA is enabled).\n' +
      '- **Native / mobile clients** use bearer access tokens from ' +
      '`POST /api/v1/auth/mobile/login`, refreshed via `/auth/mobile/refresh`.\n\n' +
      'Endpoints under `/api/v1/admin/**` require a valid session; some are further ' +
      'restricted to the `admin` role (editors are limited to content).',
  },
  servers: [
    { url: '/', description: 'Same-origin (current host)' },
    { url: 'http://localhost:3000', description: 'Local development' },
  ],
  tags: [
    { name: 'Health', description: 'Liveness probe' },
    { name: 'Auth', description: 'Web session login/logout (cookie + TOTP)' },
    { name: 'Auth · Mobile', description: 'Native bearer-token login, refresh and logout' },
    { name: 'Auth · SSO', description: 'OAuth2 / OIDC provider discovery and redirect flow' },
    { name: 'Public', description: 'Unauthenticated site content (settings, posts, wiki, contact)' },
    { name: 'Public · Shard', description: 'Live shard data ingested from the uo-link sidecar (status, feed, economy, IDOC, characters)' },
    { name: 'Admin · Account', description: 'Self-service account security (2FA, linked identities)' },
    { name: 'Player', description: 'Self-service player accounts (register, credentials, 2FA, linked identities)' },
    { name: 'Player · Shard', description: 'Link an in-game account and read its roster / vendors (uo-link)' },
    { name: 'Admin · Dashboard', description: 'Dashboard summary and site mode' },
    { name: 'Admin · Posts', description: 'News / five-on-friday / newsletter / screenshots + uploads' },
    { name: 'Admin · Wiki', description: 'Wiki pages, categories, tags and revisions' },
    { name: 'Admin · Settings', description: 'Site settings (admin only)' },
    { name: 'Admin · Activity', description: 'Admin activity log' },
    { name: 'Admin · Bot Activity', description: 'Bot-scoring/ban state and emergency unban (admin only)' },
    { name: 'Admin · Discord Bot', description: 'Discord bot token/config and live status (admin only)' },
    { name: 'Admin · Shard', description: 'uo-link sidecar connection config, live status and town crier (admin only)' },
    { name: 'Admin · Auth Providers', description: 'SSO provider configuration (admin only)' },
    { name: 'Admin · Users', description: 'User management (admin only)' },
  ],
  components: {
    securitySchemes: {
      // Web/admin session — httpOnly cookie set by the login endpoints.
      cookieAuth: {
        type: 'apiKey',
        in: 'cookie',
        name: COOKIE_NAME,
        description: 'Session JWT set as an httpOnly cookie by POST /api/v1/auth/login.',
      },
      // Native/mobile clients — Authorization: Bearer <accessToken>.
      bearerAuth: {
        type: 'http',
        scheme: 'bearer',
        bearerFormat: 'JWT',
        description: 'Access token from POST /api/v1/auth/mobile/login (or /refresh).',
      },
    },
    schemas: {
      Error: {
        type: 'object',
        properties: { message: { type: 'string', example: 'Not found' } },
      },
      ValidationError: {
        type: 'object',
        properties: {
          errors: {
            type: 'array',
            items: {
              type: 'object',
              properties: {
                type: { type: 'string', example: 'field' },
                msg: { type: 'string', example: 'Invalid value' },
                path: { type: 'string', example: 'username' },
                location: { type: 'string', example: 'body' },
              },
            },
          },
        },
      },
      SafeUser: {
        type: 'object',
        properties: {
          id: { type: 'integer', example: 1 },
          username: { type: 'string', example: 'admin' },
          role: { type: 'string', enum: ['admin', 'editor'], example: 'admin' },
        },
      },
      LoginRequest: {
        type: 'object',
        required: ['username', 'password'],
        properties: {
          username: { type: 'string', example: 'admin' },
          password: { type: 'string', format: 'password', example: 'super-secret' },
          company: { type: 'string', description: 'Honeypot — must be empty for humans.', example: '' },
        },
      },
      RegisterRequest: {
        type: 'object',
        required: ['username', 'password'],
        properties: {
          username: { type: 'string', minLength: 3, maxLength: 32, example: 'newplayer' },
          password: { type: 'string', format: 'password', minLength: 8, maxLength: 64 },
          email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
          company: { type: 'string', description: 'Honeypot — must be empty for humans.', example: '' },
        },
      },
      LoginResponse: {
        type: 'object',
        description:
          'Either a session (user) or, for 2FA accounts, a TOTP challenge to complete at /login/totp.',
        properties: {
          user: { $ref: '#/components/schemas/SafeUser' },
          totpRequired: { type: 'boolean', example: true },
          challenge: { type: 'string', description: 'Signed challenge token for the TOTP step.' },
        },
      },
      TotpLoginRequest: {
        type: 'object',
        required: ['challenge', 'code'],
        properties: {
          challenge: { type: 'string', description: 'Token returned by /login when totpRequired.' },
          code: { type: 'string', example: '123456' },
        },
      },
      MobileLoginRequest: {
        type: 'object',
        required: ['username', 'password'],
        properties: {
          username: { type: 'string', example: 'admin' },
          password: { type: 'string', format: 'password', example: 'super-secret' },
          code: { type: 'string', description: 'TOTP code (only when 2FA is enabled).', example: '123456' },
        },
      },
      MobileTokenResponse: {
        type: 'object',
        properties: {
          accessToken: { type: 'string', description: 'Short-lived bearer JWT.' },
          refreshToken: { type: 'string', description: 'Long-lived, revocable refresh token.' },
          expiresIn: {
            type: 'string',
            description: 'Access token lifetime as a duration string (zeit/ms format, e.g. "15m").',
            example: '15m',
          },
          user: { $ref: '#/components/schemas/SafeUser' },
        },
      },
      MobileRefreshRequest: {
        type: 'object',
        required: ['refreshToken'],
        properties: { refreshToken: { type: 'string' } },
      },
      MobileLogoutRequest: {
        type: 'object',
        properties: {
          refreshToken: { type: 'string', description: 'Revoke a single session.' },
          all: { type: 'boolean', description: 'Revoke every session for the user.', example: false },
        },
      },
      Message: {
        type: 'object',
        properties: { message: { type: 'string', example: 'Logged out.' } },
      },
      ContactRequest: {
        type: 'object',
        required: ['message'],
        properties: {
          message: { type: 'string', maxLength: 5000, example: 'When does the shard launch?' },
          email: { type: 'string', format: 'email', example: 'player@example.com' },
          name: { type: 'string', maxLength: 100, example: 'Lord British' },
        },
      },
      // Public discovery shape (GET /auth/providers) — enough for the login page
      // to render a button and start the flow. Never exposes secrets or endpoints.
      Provider: {
        type: 'object',
        properties: {
          id: { type: 'string', example: 'google' },
          name: { type: 'string', example: 'Google' },
          icon: {
            type: 'string',
            description: "Icon hint — the provider kind ('google' | 'discord' | 'oidc' | 'oauth2').",
            example: 'google',
          },
          loginUrl: {
            type: 'string',
            description: 'Relative URL to begin the redirect flow.',
            example: '/api/v1/auth/sso/google/start',
          },
          priority: { type: 'integer', description: 'Sort order (ascending).', example: 1 },
        },
      },
      // Admin-facing provider config (GET/POST/PUT /admin/auth/providers). The
      // client secret is write-only and NEVER returned — `hasSecret` reports
      // whether one is stored. `builtin` marks google/discord (fixed kind/name),
      // and `health` is the config-completeness check used to gate visibility.
      ProviderConfig: {
        type: 'object',
        properties: {
          id: { type: 'string', example: 'okta' },
          kind: { type: 'string', enum: ['google', 'discord', 'oidc', 'oauth2'], example: 'oidc' },
          name: { type: 'string', example: 'Okta' },
          enabled: { type: 'boolean', example: true },
          clientId: { type: 'string' },
          hasSecret: { type: 'boolean', description: 'Whether a client secret is stored (the secret itself is never returned).', example: true },
          authorizeUrl: { type: 'string', format: 'uri' },
          tokenUrl: { type: 'string', format: 'uri' },
          userinfoUrl: { type: 'string', format: 'uri' },
          scopes: { type: 'string', example: 'openid email profile' },
          priority: { type: 'integer', example: 10 },
          builtin: { type: 'boolean', description: 'True for the fixed google/discord providers.', example: false },
          health: { $ref: '#/components/schemas/ProviderHealth' },
        },
      },
      ProviderHealth: {
        type: 'object',
        description: 'Config-completeness check that gates whether a provider is offered to end users.',
        properties: {
          valid: { type: 'boolean', example: true },
          missing: {
            type: 'array',
            description: 'Names of required config fields that are still missing.',
            items: { type: 'string' },
            example: [],
          },
        },
      },
      ProviderCreateRequest: {
        type: 'object',
        required: ['id', 'kind', 'name'],
        properties: {
          id: { type: 'string', pattern: '^[a-z0-9-]+$', example: 'okta' },
          kind: { type: 'string', enum: ['oidc', 'oauth2'], example: 'oidc' },
          name: { type: 'string', maxLength: 80, example: 'Okta' },
          enabled: { type: 'boolean', example: true },
          clientId: { type: 'string' },
          secret: { type: 'string', format: 'password' },
          authorizeUrl: { type: 'string', format: 'uri' },
          tokenUrl: { type: 'string', format: 'uri' },
          userinfoUrl: { type: 'string', format: 'uri' },
          scopes: { type: 'string', maxLength: 500, example: 'openid email profile' },
          priority: { type: 'integer', example: 10 },
        },
      },
      Post: {
        type: 'object',
        properties: {
          id: { type: 'integer', example: 12 },
          category: { type: 'string', example: 'news' },
          title: { type: 'string', example: 'Server maintenance this weekend' },
          slug: { type: 'string', example: 'server-maintenance-this-weekend' },
          excerpt: { type: 'string', nullable: true },
          body: { type: 'string', nullable: true },
          image_url: { type: 'string', nullable: true, example: '/uploads/1700000000-abcd.png' },
          published: { type: 'boolean', example: true },
          author_id: { type: 'integer', nullable: true, example: 1 },
          created_at: { type: 'string', format: 'date-time' },
          updated_at: { type: 'string', format: 'date-time' },
          published_at: { type: 'string', format: 'date-time', nullable: true },
        },
      },
      PostCreateRequest: {
        type: 'object',
        required: ['category', 'title'],
        properties: {
          category: { type: 'string', example: 'news' },
          title: { type: 'string', maxLength: 200, example: 'Server maintenance this weekend' },
          body: { type: 'string' },
          image_url: { type: 'string', description: 'Required for the screenshots category.' },
          published: { type: 'boolean', example: false },
        },
      },
      PublishRequest: {
        type: 'object',
        required: ['published'],
        properties: { published: { type: 'boolean', example: true } },
      },
      UploadResponse: {
        type: 'object',
        properties: { url: { type: 'string', example: '/uploads/1700000000-abcd.png' } },
      },
      WikiPage: {
        type: 'object',
        properties: {
          id: { type: 'integer', example: 3 },
          slug: { type: 'string', example: 'getting-started' },
          title: { type: 'string', example: 'Getting Started' },
          excerpt: { type: 'string' },
          body: { type: 'string' },
          category_id: { type: 'integer', nullable: true, example: 2 },
          published: { type: 'boolean', example: true },
          tags: { type: 'array', items: { type: 'string' }, example: ['newbie', 'guide'] },
          created_at: { type: 'string', format: 'date-time' },
          updated_at: { type: 'string', format: 'date-time' },
        },
      },
      WikiPageCreateRequest: {
        type: 'object',
        required: ['slug', 'title'],
        properties: {
          slug: { type: 'string', pattern: '^[a-z0-9-]+$', example: 'getting-started' },
          title: { type: 'string', maxLength: 200, example: 'Getting Started' },
          excerpt: { type: 'string', maxLength: 400 },
          body: { type: 'string' },
          category_id: { type: 'integer', nullable: true },
          published: { type: 'boolean', example: false },
          tags: { type: 'array', items: { type: 'string' } },
        },
      },
      WikiCategory: {
        type: 'object',
        properties: {
          id: { type: 'integer', example: 2 },
          slug: { type: 'string', example: 'guides' },
          title: { type: 'string', example: 'Guides' },
          description: { type: 'string' },
          sort_order: { type: 'integer', example: 0 },
        },
      },
      WikiCategoryCreateRequest: {
        type: 'object',
        required: ['slug', 'title'],
        properties: {
          slug: { type: 'string', pattern: '^[a-z0-9-]+$', example: 'guides' },
          title: { type: 'string', maxLength: 200, example: 'Guides' },
          description: { type: 'string', maxLength: 400 },
          sort_order: { type: 'integer', example: 0 },
        },
      },
      User: {
        type: 'object',
        properties: {
          id: { type: 'integer', example: 1 },
          username: { type: 'string', example: 'admin' },
          role: { type: 'string', enum: ['admin', 'editor', 'moderator', 'player'], example: 'admin' },
          status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
          email: { type: 'string', format: 'email', nullable: true },
          email_verified: { type: 'boolean', example: false },
          totp_enabled: { type: 'boolean', example: true },
          last_login_at: { type: 'string', format: 'date-time', nullable: true },
          created_at: { type: 'string', format: 'date-time' },
        },
      },
      UserCreateRequest: {
        type: 'object',
        required: ['username', 'password'],
        properties: {
          username: { type: 'string', minLength: 3, maxLength: 32, example: 'editor1' },
          password: { type: 'string', format: 'password', minLength: 8, maxLength: 64 },
          role: { type: 'string', enum: ['admin', 'editor', 'moderator', 'player'], example: 'editor' },
          status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
          email: { type: 'string', format: 'email', nullable: true },
        },
      },
      // Player self-service credential changes (/api/v1/player/account/*).
      ChangeUsernameRequest: {
        type: 'object',
        required: ['username'],
        properties: {
          username: { type: 'string', minLength: 3, maxLength: 32, example: 'newname' },
        },
      },
      ChangePasswordRequest: {
        type: 'object',
        required: ['newPassword'],
        properties: {
          newPassword: { type: 'string', format: 'password', minLength: 8, maxLength: 64 },
          currentPassword: {
            type: 'string',
            format: 'password',
            description:
              'Required when the account already has a password. Omit only for an SSO-provisioned account setting its first password.',
          },
        },
      },
      PlayerAccount: {
        type: 'object',
        description: 'Self-service player account (GET /player/account).',
        properties: {
          id: { type: 'integer', example: 42 },
          username: { type: 'string', example: 'newplayer' },
          role: { type: 'string', enum: ['player'], example: 'player' },
          email: { type: 'string', format: 'email', nullable: true, example: 'player@example.com' },
          status: { type: 'string', enum: ['active', 'disabled', 'banned', 'pending'], example: 'active' },
          totp_enabled: { type: 'boolean', example: false },
          has_password: {
            type: 'boolean',
            description: 'False for an SSO-provisioned account that has not set a password yet.',
            example: true,
          },
        },
      },
      OkFlag: {
        type: 'object',
        properties: { ok: { type: 'boolean', example: true } },
      },
      TotpCodeRequest: {
        type: 'object',
        required: ['code'],
        properties: { code: { type: 'string', example: '123456' } },
      },
      SiteModeRequest: {
        type: 'object',
        required: ['mode'],
        properties: { mode: { type: 'string', enum: ['live', 'maintenance'], example: 'live' } },
      },
      UnbanRequest: {
        type: 'object',
        required: ['ip'],
        properties: { ip: { type: 'string', example: '203.0.113.5' } },
      },
      // ── Actual mutation-response shapes ─────────────────────────────────────
      // These endpoints do NOT return the generic { message } envelope; they echo
      // the affected resource id/slug or a boolean flag. Documented here as-is so
      // the spec matches the controllers. (The shapes are intentionally recorded
      // rather than normalized — see the audit note if standardizing later.)
      AccountStatus: {
        type: 'object',
        description: 'Self-service account security status (GET /admin/account).',
        properties: {
          id: { type: 'integer', example: 1 },
          username: { type: 'string', example: 'admin' },
          role: { type: 'string', enum: ['admin', 'editor'], example: 'admin' },
          totp_enabled: { type: 'boolean', example: true },
        },
      },
      TotpSetup: {
        type: 'object',
        description: 'Enrollment material returned by POST /account/totp/setup.',
        properties: {
          otpauthUrl: { type: 'string', example: `otpauth://totp/${brand.name}:admin?secret=...` },
          qr: { type: 'string', description: 'QR code as a data: URL.', example: 'data:image/png;base64,iVBORw0KGgo...' },
        },
      },
      TotpState: {
        type: 'object',
        description: 'Result of enabling/disabling 2FA.',
        properties: { totp_enabled: { type: 'boolean', example: true } },
      },
      LinkedIdentity: {
        type: 'object',
        properties: {
          provider: { type: 'string', example: 'google' },
          email: { type: 'string', format: 'email', nullable: true, example: 'user@example.com' },
          linked_at: { type: 'string', format: 'date-time' },
        },
      },
      SiteModeState: {
        type: 'object',
        description: 'Result of PUT /admin/site-mode.',
        properties: {
          site_mode: { type: 'string', enum: ['live', 'maintenance'], example: 'maintenance' },
          changed_at: { type: 'string', format: 'date-time' },
          changed_by: { type: 'string', example: 'admin' },
        },
      },
      PublicStatus: {
        type: 'object',
        description: 'Public site status (GET /public/status).',
        properties: {
          mode: { type: 'string', enum: ['live', 'maintenance'], example: 'live' },
          status_message: { type: 'string', example: '' },
        },
      },
      // Delete/mutation acknowledgements — each echoes the affected resource key
      // or a boolean flag rather than a { message } string.
      DeletedId: {
        type: 'object',
        properties: { id: { type: 'integer', example: 12 } },
      },
      DeletedSlug: {
        type: 'object',
        properties: { slug: { type: 'string', example: 'getting-started' } },
      },
      DeletedFlag: {
        type: 'object',
        properties: { deleted: { type: 'boolean', example: true } },
      },
      UnlinkedFlag: {
        type: 'object',
        properties: { unlinked: { type: 'boolean', example: true } },
      },
      UnbanResult: {
        type: 'object',
        properties: {
          ip: { type: 'string', example: '203.0.113.5' },
          removed: { type: 'boolean', description: 'Whether the IP had an entry that was cleared.', example: true },
        },
      },
      // ── uo-link shard data ──────────────────────────────────────────────
      ShardStatus: {
        type: 'object',
        description: 'Public shard status (GET /public/shard/status).',
        properties: {
          enabled: { type: 'boolean', example: true },
          status: { type: 'string', example: 'connected', description: 'connected | reconnecting | disconnected | error' },
          pluginConnected: { type: 'boolean', description: 'Is the shard link up right now?', example: true },
          lastEventAt: { type: 'string', format: 'date-time', nullable: true },
          onlineCount: { type: 'integer', example: 12 },
          economy: { $ref: '#/components/schemas/ShardEconomyPoint' },
        },
      },
      ShardEvent: {
        type: 'object',
        description: 'A logged shard event.',
        properties: {
          id: { type: 'integer', example: 4821 },
          kind: { type: 'string', example: 'vendor.sale' },
          t: { type: 'integer', description: 'Event time, epoch ms.', example: 1783720195626 },
          bootId: { type: 'string', nullable: true, example: 'boot-abc123' },
          payload: { type: 'object', additionalProperties: true, description: 'The full event object.' },
          createdAt: { type: 'string', format: 'date-time' },
        },
      },
      ShardEconomyPoint: {
        type: 'object',
        nullable: true,
        description: 'One gold-supply sample.',
        properties: {
          accounts: { type: 'integer', nullable: true, example: 240 },
          gold: { type: 'integer', nullable: true, example: 1028983421 },
          t: { type: 'integer', description: 'Sample time, epoch ms.', example: 1783720000000 },
        },
      },
      ShardOnlinePlayer: {
        type: 'object',
        description: 'A LINKED player online now (only accounts linked to a website user are listed).',
        properties: {
          serial: { type: 'string', example: '0x24C' },
          name: { type: 'string', example: 'Darrow' },
          map: { type: 'string', nullable: true, example: 'Trammel' },
          x: { type: 'integer', nullable: true, example: 1402 },
          y: { type: 'integer', nullable: true, example: 1604 },
          z: { type: 'integer', nullable: true, example: 0 },
        },
      },
      ShardVendorSale: {
        type: 'object',
        description: 'A player-vendor sale (visible only to the linked owner).',
        properties: {
          t: { type: 'integer', description: 'Sale time, epoch ms.', example: 1783720195626 },
          itemType: { type: 'string', example: 'Longsword' },
          amount: { type: 'integer', example: 1 },
          price: { type: 'integer', example: 100 },
          commission: { type: 'integer', nullable: true, example: 5 },
          ownerAcct: { type: 'string', example: 'whitlocktech' },
        },
      },
      ShardHouse: {
        type: 'object',
        description: 'A house at its current decay stage.',
        properties: {
          serial: { type: 'string', example: '0x4004705F' },
          stage: { type: 'string', example: 'IDOC' },
          map: { type: 'string', nullable: true, example: 'Trammel' },
          x: { type: 'integer', nullable: true },
          y: { type: 'integer', nullable: true },
          z: { type: 'integer', nullable: true },
          region: { type: 'string', nullable: true },
          name: { type: 'string', nullable: true, example: 'An Unnamed House' },
          ownerSerial: { type: 'string', nullable: true },
          ownerAcct: { type: 'string', nullable: true },
          builtOn: { type: 'string', format: 'date-time', nullable: true },
          lastRefreshed: { type: 'string', format: 'date-time', nullable: true },
          isIdoc: { type: 'boolean', example: true },
          updatedAt: { type: 'string', format: 'date-time' },
        },
      },
      ShardLinkRequest: {
        type: 'object',
        required: ['code'],
        properties: {
          code: { type: 'string', description: 'The one-time code shown by [link in game.', example: 'AB12CD' },
        },
      },
      ShardLinkResult: {
        type: 'object',
        properties: {
          linked: { type: 'boolean', example: true },
          account: { type: 'string', example: 'whitlocktech' },
        },
      },
      ShardLink: {
        type: 'object',
        description: 'A linked in-game account (GET /player/shard/accounts).',
        properties: {
          account: { type: 'string', example: 'whitlocktech' },
          userId: { type: 'integer', example: 42 },
          charName: { type: 'string', nullable: true, example: 'Darrow' },
          linkedAt: { type: 'string', format: 'date-time' },
        },
      },
      TownCrierRequest: {
        type: 'object',
        required: ['id', 'lines'],
        properties: {
          id: { type: 'string', maxLength: 64, description: 'Re-posting the same id replaces the prior entry.', example: 'news-42' },
          lines: { type: 'array', items: { type: 'string', maxLength: 200 }, example: ['Hear ye!', 'Market tax is now 5%.'] },
          durationSec: { type: 'integer', minimum: 1, maximum: 86400, example: 3600 },
        },
      },
    },
  },
}

swaggerAutogen(outputFile, routes, doc).then(() => {
  // eslint-disable-next-line no-console
  console.log('swagger-output.json generated.')
})
