// OpenAPI 3.0 specification for the third-party REST API mounted under /api/v1.
// Kept as a plain factory so the serving route can inject the real `servers`
// (request host) before returning the document to /swagger.json.

const ART_OPTIONS = [
  'E-Mail', 'Online-Portal', 'Indeed', 'StepStone',
  'Firmenwebsite', 'Post', 'Initiativbewerbung',
  'Arbeitsagentur', 'Sonstiges',
];
const STATUS_OPTIONS = [
  'Entwurf', 'Gesendet', 'Eingangsbestätigung',
  'In Bearbeitung', 'Interessiert', 'Telefonat', 'Warten auf Rückmeldung', 'Warten auf meine Antwort',
  'Vorstellungsgespräch', 'Vertragsverhandlung',
  'Absage', 'Absage von meiner Seite', 'Einstellung', 'Keine Rückmeldung',
];
const { LABEL_OPTIONS } = require('./labels');

// Reusable schema for the labels array (work-location labels of a Stelle).
const labelsSchema = {
  type: 'array',
  items: { type: 'string', enum: LABEL_OPTIONS },
  description: 'Labels der Stelle (Arbeitsort). Mehrere möglich.',
};

// Shared reusable schemas -----------------------------------------------

const errorResponse = {
  description: 'Fehlerantwort',
  content: {
    'application/json': {
      schema: { $ref: '#/components/schemas/Error' },
    },
  },
};

function buildOpenApiSpec(baseUrl = '') {
  return {
    openapi: '3.0.3',
    info: {
      title: 'NextJobs REST-API',
      version: '1.0.0',
      description:
        'REST-API für Drittanbietersoftware zum Lesen und Verwalten von ' +
        'Bewerbungen, Statusverlauf, generierten Unterlagen (PDFs), ' +
        'E-Mail-Korrespondenz, Einstellungen und Statistiken.\n\n' +
        'Alle Endpunkte sind unter `/api/v1` gemountet und erfordern ' +
        'Authentifizierung über einen API-Key im Header `X-API-Key`.\n\n' +
        '### Der API-Key gehört immer genau einem Benutzer\n\n' +
        'Der Tracker ist eine Multi-User-Plattform. Jeder Benutzer legt seinen ' +
        'eigenen Token unter **Einstellungen → REST-API** an („Neu generieren“) — ' +
        'es gibt keinen globalen oder serverweiten Key.\n\n' +
        'Der übergebene `X-API-Key` bestimmt, **wessen** Daten die Anfrage sieht: ' +
        'jeder Endpunkt liest und schreibt ausschließlich die Daten des Benutzers, ' +
        'dem der Token gehört. Fremde Bewerbungen, E-Mails, Anhänge, Jobangebote ' +
        'oder Einstellungen sind über die API nicht erreichbar — auch nicht durch ' +
        'Angabe einer fremden `id`. In dem Fall antwortet die API mit `404`, als ' +
        'gäbe es den Datensatz nicht. Ein unbekannter, leerer oder mehrdeutiger ' +
        'Token führt zu `401`.\n\n' +
        'Ein neu generierter Token macht den vorherigen sofort ungültig.',
    },
    servers: baseUrl ? [{ url: `${baseUrl}/api/v1` }] : [{ url: '/api/v1' }],
    tags: [
      { name: 'Applications', description: 'Bewerbungen (CRUD + Statusverlauf)' },
      { name: 'Attachments', description: 'Generierte Bewerbungsunterlagen (PDFs)' },
      { name: 'Emails', description: 'E-Mail-Korrespondenz zu einer Bewerbung' },
      { name: 'Generation', description: 'KI-Generierung der Unterlagen anstoßen/abfragen' },
      { name: 'Settings', description: 'Benutzer- / Jobcenter-Einstellungen' },
      { name: 'Statistics', description: 'Statistiken und Export' },
      { name: 'Templates', description: 'Basis-Unterlagen (Vorlagen)' },
      { name: 'JobOffers', description: 'Jobangebote (Einspielung durch Drittanbietersoftware)' },
      { name: 'System', description: 'System-Endpunkte' },
    ],
    paths: {
      '/health': {
        get: {
          tags: ['System'],
          summary: 'Verfügbarkeit prüfen',
          description: 'Liefert den Status der API. Ohne Authentifizierung aufrufbar.',
          security: [],
          responses: {
            '200': {
              description: 'API erreichbar',
              content: {
                'application/json': {
                  schema: { $ref: '#/components/schemas/Health' },
                },
              },
            },
          },
        },
      },

      '/applications': {
        get: {
          tags: ['Applications'],
          summary: 'Bewerbungen auflisten',
          description: 'Liefert alle Bewerbungen, optional gefiltert nach Datum, Art, Status oder Freitext.',
          parameters: [
            { name: 'month', in: 'query', schema: { type: 'string', pattern: '^[0-9]{1,2}$' }, description: 'Monat (01–12)' },
            { name: 'year', in: 'query', schema: { type: 'string', pattern: '^[0-9]{4}$' }, description: 'Jahr (z. B. 2026)' },
            { name: 'status', in: 'query', schema: { type: 'string', enum: STATUS_OPTIONS }, description: 'Filter nach Status' },
            { name: 'art', in: 'query', schema: { type: 'string', enum: ART_OPTIONS }, description: 'Filter nach Bewerbungsart' },
            { name: 'label', in: 'query', schema: { type: 'string', enum: LABEL_OPTIONS }, description: 'Filter nach Label (Stellen mit diesem Label)' },
            { name: 'search', in: 'query', schema: { type: 'string' }, description: 'Freitextsuche in Firma und Stelle' },
            { name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 500, default: 100 }, description: 'Max. Anzahl Ergebnisse' },
            { name: 'offset', in: 'query', schema: { type: 'integer', minimum: 0, default: 0 }, description: 'Ergebnis-Offset (Paging)' },
          ],
          responses: {
            '200': {
              description: 'Liste der Bewerbungen (mit Statusverlauf)',
              content: {
                'application/json': {
                  schema: {
                    type: 'array',
                    items: { $ref: '#/components/schemas/Application' },
                  },
                },
              },
            },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
        post: {
          tags: ['Applications'],
          summary: 'Neue Bewerbung anlegen',
          description:
            'Legt eine neue Bewerbung an. Ein Duplicate-Guard warnt (HTTP 409), ' +
            'falls bereits eine Bewerbung für dieselbe Firma + Stelle existiert; ' +
            'mit `force=true` wird sie trotzdem angelegt.',
          requestBody: {
            required: true,
            content: {
              'application/json': {
                schema: { $ref: '#/components/schemas/ApplicationCreate' },
              },
            },
          },
          responses: {
            '200': {
              description: 'Bewerbung angelegt',
              content: {
                'application/json': {
                  schema: { $ref: '#/components/schemas/ApplicationCreated' },
                },
              },
            },
            '400': errorResponse,
            '401': errorResponse,
            '409': {
              description: 'Mögliche Dublette erkannt',
              content: {
                'application/json': { schema: { $ref: '#/components/schemas/DuplicateConflict' } },
              },
            },
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}': {
        get: {
          tags: ['Applications'],
          summary: 'Einzelne Bewerbung abrufen',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          responses: {
            '200': {
              description: 'Bewerbung',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/Application' } } },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
        put: {
          tags: ['Applications'],
          summary: 'Bewerbung aktualisieren',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          requestBody: {
            required: true,
            content: { 'application/json': { schema: { $ref: '#/components/schemas/ApplicationUpdate' } } },
          },
          responses: {
            '200': {
              description: 'Aktualisierte Bewerbung',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/ApplicationCreated' } } },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
        delete: {
          tags: ['Applications'],
          summary: 'Bewerbung löschen',
          description: 'Löscht die Bewerbung inkl. Statusverlauf und verknüpfter Anhänge (kaskadierend).',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          responses: {
            '200': { description: 'Gelöscht', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/timeline': {
        get: {
          tags: ['Applications'],
          summary: 'Statusverlauf abrufen',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          responses: {
            '200': {
              description: 'Statusverlauf (chronologisch)',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/TimelineEntry' } },
                },
              },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
        post: {
          tags: ['Applications'],
          summary: 'Statusverlauf-Eintrag hinzufügen',
          description: 'Fügt einen neuen Status-Verlaufseintrag hinzu und aktualisiert den aktuellen Status der Bewerbung.',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          requestBody: {
            required: true,
            content: { 'application/json': { schema: { $ref: '#/components/schemas/TimelineEntryCreate' } } },
          },
          responses: {
            '200': {
              description: 'Angelegter Eintrag',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/TimelineEntry' } } },
            },
            '400': errorResponse,
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/timeline/{eintragId}': {
        delete: {
          tags: ['Applications'],
          summary: 'Statusverlauf-Eintrag löschen',
          parameters: [
            { $ref: '#/components/parameters/ApplicationId' },
            { name: 'eintragId', in: 'path', required: true, schema: { type: 'integer' } },
          ],
          responses: {
            '200': { description: 'Gelöscht', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/attachments': {
        get: {
          tags: ['Attachments'],
          summary: 'Generierte Anhänge auflisten',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          responses: {
            '200': {
              description: 'Anhänge (Metadaten, ohne Dateiinhalt)',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } },
                },
              },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/attachments/{attachmentId}': {
        get: {
          tags: ['Attachments'],
          summary: 'Anhang herunterladen',
          description: 'Liefert die Binärdatei (i. d. R. ein generiertes PDF) als Download.',
          parameters: [
            { $ref: '#/components/parameters/ApplicationId' },
            { name: 'attachmentId', in: 'path', required: true, schema: { type: 'integer' } },
          ],
          responses: {
            '200': {
              description: 'Binärdatei',
              content: {
                'application/octet-stream': { schema: { type: 'string', format: 'binary' } },
              },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/emails': {
        get: {
          tags: ['Emails'],
          summary: 'E-Mail-Korrespondenz abrufen',
          description: 'Gesendete und empfangene E-Mails zur Bewerbung (chronologisch), inkl. Anhang-Metadaten.',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          responses: {
            '200': {
              description: 'E-Mails',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/Email' } },
                },
              },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/emails/{emailId}/attachments/{attachmentId}': {
        get: {
          tags: ['Emails'],
          summary: 'E-Mail-Anhang herunterladen',
          parameters: [
            { name: 'emailId', in: 'path', required: true, schema: { type: 'integer' } },
            { name: 'attachmentId', in: 'path', required: true, schema: { type: 'integer' } },
          ],
          responses: {
            '200': {
              description: 'Binärdatei',
              content: {
                'application/octet-stream': { schema: { type: 'string', format: 'binary' } },
              },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/generate': {
        post: {
          tags: ['Generation'],
          summary: 'Unterlagen-Generierung anstoßen',
          description:
            'Startet die asynchrone KI-Generierung der Bewerbungsunterlagen (Anschreiben + Lebenslauf als PDF). ' +
            'Bestehende Anhänge werden vorher gelöscht. Den Fortschritt via ' +
            '`GET /applications/{id}/generation-status` abfragen.',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          requestBody: {
            required: false,
            content: {
              'application/json': {
                schema: {
                  type: 'object',
                  properties: {
                    llm_notizen: { type: 'string', description: 'Freitext-Kontext für die KI (optional)' },
                    anlagen: {
                      type: 'array',
                      items: { type: 'integer' },
                      description:
                        'IDs der zusätzlich beizulegenden statischen Anlagen (basis_anhaenge). ' +
                        'Weglassen = Vorauswahl aus den Einstellungen (GEN_ANLAGEN_DEFAULT, Standard: keine), ' +
                        'leeres Array = ausdrücklich keine. Die Auswahl erscheint auch im Anschreiben unter "Anlagen".',
                    },
                    dokumente: {
                      type: 'array',
                      items: { type: 'string', enum: ['anschreiben', 'lebenslauf'] },
                      description:
                        'Welche Unterlagen erzeugt werden sollen. Weglassen = Vorauswahl aus den Einstellungen ' +
                        '(GEN_DOKUMENTE_DEFAULT, Standard: beide). Wird der Lebenslauf nicht erzeugt, führt ihn ' +
                        'das Anschreiben auch nicht unter "Anlagen" auf, und der E-Mail-Begleittext kündigt ihn nicht an.',
                      example: ['anschreiben'],
                    },
                  },
                },
              },
            },
          },
          responses: {
            '202': {
              description: 'Generierung gestartet',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/applications/{id}/generation-status': {
        get: {
          tags: ['Generation'],
          summary: 'Generierungsstatus abfragen',
          parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
          responses: {
            '200': {
              description: 'Status + aktuelle Anhänge',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/GenerationStatus' } } },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/settings': {
        get: {
          tags: ['Settings'],
          summary: 'Persönliche Angaben abrufen',
          description:
            'Liefert die persönlichen Angaben **des Benutzers, dem der API-Key gehört**. ' +
            'Hat er noch nichts gespeichert, ist die Antwort ein leeres Objekt `{}`.',
          responses: {
            '200': { description: 'Persönliche Angaben (ggf. `{}`)', content: { 'application/json': { schema: { $ref: '#/components/schemas/Settings' } } } },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
        put: {
          tags: ['Settings'],
          summary: 'Persönliche Angaben speichern',
          description:
            'Speichert die persönlichen Angaben des eigenen Benutzers. Es werden nur die ' +
            'Felder geändert, die im Body vorkommen — weggelassene Felder behalten ihren ' +
            'Wert. Existiert noch keine Zeile, wird sie angelegt.',
          requestBody: {
            required: true,
            content: {
              'application/json': {
                schema: { $ref: '#/components/schemas/Settings' },
                example: { name: 'Max Mustermann', ort: 'Berlin' },
              },
            },
          },
          responses: {
            '200': {
              description: 'Gespeichert; gibt den neuen Stand zurück',
              content: {
                'application/json': {
                  schema: {
                    type: 'object',
                    properties: {
                      success: { type: 'boolean', example: true },
                      settings: { $ref: '#/components/schemas/Settings' },
                    },
                  },
                },
              },
            },
            '400': errorResponse,
            '401': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/statistics': {
        get: {
          tags: ['Statistics'],
          summary: 'Statistiken abrufen',
          description: 'Gesamtzahl sowie Aufschlüsselung nach Art und Status.',
          responses: {
            '200': {
              description: 'Statistiken',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/Statistics' } } },
            },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/export': {
        get: {
          tags: ['Statistics'],
          summary: 'Bewerbungen exportieren',
          description: 'Wie `GET /applications`, aber ohne interne Notizen – geeignet für PDF-/Jobcenter-Export.',
          parameters: [
            { name: 'month', in: 'query', schema: { type: 'string', pattern: '^[0-9]{1,2}$' } },
            { name: 'year', in: 'query', schema: { type: 'string', pattern: '^[0-9]{4}$' } },
          ],
          responses: {
            '200': {
              description: 'Bewerbungen (ohne interne_notizen)',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/Application' } },
                },
              },
            },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/templates': {
        get: {
          tags: ['Templates'],
          summary: 'Basis-Unterlagen (Vorlagen) auflisten',
          responses: {
            '200': {
              description: 'Vorlagen',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/Template' } },
                },
              },
            },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/joboffers': {
        get: {
          tags: ['JobOffers'],
          summary: 'Jobangebote auflisten',
          responses: {
            '200': {
              description: 'Jobangebote (neueste zuerst)',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/JobOffer' } },
                },
              },
            },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
        post: {
          tags: ['JobOffers'],
          summary: 'Jobangebot einspielen (Upsert)',
          description:
            'Spielt ein Jobangebot ein. Wird von Drittanbietersoftware genutzt, um ' +
            'Stellen in den Tracker zu übernehmen. De-Dup: Upsert über (quelle, ' +
            'external_id) oder – falls keine external_id passt – über die normalisierte ' +
            'URL (quelle_url). Dieselbe Stelle landet damit nie doppelt. Steht das ' +
            'Angebot auf der Blacklist (siehe /joboffers/blacklist), wird es mit 409 ' +
            'abgelehnt und NICHT aufgenommen. Rückgabe 201 (created) oder 200 (updated).',
          requestBody: {
            required: true,
            content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferCreate' } } },
          },
          responses: {
            '200': {
              description: 'Angebot aktualisiert',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferResult' } } },
            },
            '201': {
              description: 'Angebot angelegt',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferResult' } } },
            },
            '400': errorResponse,
            '401': errorResponse,
            '409': {
              description: 'Angebot steht auf der Blacklist und wurde nicht aufgenommen',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/BlacklistConflict' } } },
            },
            '500': errorResponse,
          },
        },
      },

      // NOTE: registered before /joboffers/{id} so "blacklist" is not read as an id.
      '/joboffers/blacklist': {
        get: {
          tags: ['JobOffers'],
          summary: 'Blacklist auflisten',
          description: 'Alle Blacklist-Einträge (neueste zuerst). Blockierte Angebote werden nie eingespielt.',
          responses: {
            '200': {
              description: 'Blacklist-Einträge',
              content: {
                'application/json': {
                  schema: { type: 'array', items: { $ref: '#/components/schemas/BlacklistEntry' } },
                },
              },
            },
            '401': errorResponse,
            '500': errorResponse,
          },
        },
        post: {
          tags: ['JobOffers'],
          summary: 'Blacklist-Eintrag anlegen',
          description:
            'Blockiert Angebote anhand einer URL, einer ganzen Domain, einer Firma ' +
            'oder einer Firma + Stelle. Passende Angebote werden danach nicht mehr eingespielt.',
          requestBody: {
            required: true,
            content: { 'application/json': { schema: { $ref: '#/components/schemas/BlacklistEntryCreate' } } },
          },
          responses: {
            '201': {
              description: 'Eintrag angelegt',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/BlacklistEntryResult' } } },
            },
            '400': errorResponse,
            '401': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/joboffers/blacklist/{id}': {
        delete: {
          tags: ['JobOffers'],
          summary: 'Blacklist-Eintrag entfernen',
          description: 'Entfernt einen Block. Betroffene Angebote können danach wieder eingespielt werden.',
          parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
          responses: {
            '200': { description: 'Entfernt', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },

      '/joboffers/{id}': {
        get: {
          tags: ['JobOffers'],
          summary: 'Einzelnes Jobangebot abrufen',
          parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
          responses: {
            '200': {
              description: 'Jobangebot',
              content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOffer' } } },
            },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
        delete: {
          tags: ['JobOffers'],
          summary: 'Jobangebot löschen',
          description:
            'Löscht ein Jobangebot. Standardmäßig wird es zuvor automatisch auf die ' +
            'Blacklist gesetzt, damit dieselbe Stelle nie erneut eingespielt wird. Mit ' +
            '?blacklist=false wird ohne Blockierung hart gelöscht.',
          parameters: [
            { name: 'id', in: 'path', required: true, schema: { type: 'integer' } },
            {
              name: 'blacklist',
              in: 'query',
              required: false,
              schema: { type: 'boolean', default: true },
              description: 'false = löschen ohne Blacklisting (Standard: true).',
            },
          ],
          responses: {
            '200': { description: 'Gelöscht', content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferDeleteResult' } } } },
            '401': errorResponse,
            '404': errorResponse,
            '500': errorResponse,
          },
        },
      },
    },

    components: {
      parameters: {
        ApplicationId: {
          name: 'id',
          in: 'path',
          required: true,
          schema: { type: 'integer' },
          description: 'Bewerbungs-ID',
        },
      },
      securitySchemes: {
        ApiKeyAuth: {
          type: 'apiKey',
          in: 'header',
          name: 'X-API-Key',
          description:
            'Persönlicher API-Token des Benutzers, zu finden unter Einstellungen → ' +
            'REST-API (dort per „Neu generieren“ erzeugen und einblenden). Der Token ' +
            'identifiziert den Benutzer — die Anfrage sieht und ändert nur dessen ' +
            'eigene Daten.',
        },
      },
      schemas: {
        Health: {
          type: 'object',
          properties: {
            status: { type: 'string', example: 'ok' },
            api: { type: 'string', example: 'bewerbungs-tracker/v1' },
          },
        },
        Ok: {
          type: 'object',
          properties: { success: { type: 'boolean', example: true } },
        },
        Error: {
          type: 'object',
          properties: { error: { type: 'string' } },
        },
        Application: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            datum: { type: 'string', format: 'date' },
            firma: { type: 'string' },
            stelle: { type: 'string' },
            art: { type: 'string', enum: ART_OPTIONS, nullable: true },
            status: { type: 'string', enum: STATUS_OPTIONS, nullable: true },
            labels: { ...labelsSchema, nullable: true },
            notizen: { type: 'string', nullable: true },
            interne_notizen: { type: 'string', nullable: true },
            ort: { type: 'string', nullable: true },
            stellenbeschreibung: { type: 'string', nullable: true },
            quelle_url: { type: 'string', nullable: true },
            email_empfaenger: { type: 'string', nullable: true },
            email_betreff: { type: 'string', nullable: true },
            email_anschreiben: { type: 'string', nullable: true },
            llm_notizen: { type: 'string', nullable: true },
            generierung_status: { type: 'string', nullable: true },
            generierung_fehler: { type: 'string', nullable: true },
            verlauf: {
              type: 'array',
              nullable: true,
              items: { $ref: '#/components/schemas/TimelineEntry' },
            },
            created_at: { type: 'string', format: 'date-time' },
            updated_at: { type: 'string', format: 'date-time' },
          },
        },
        ApplicationCreate: {
          type: 'object',
          required: ['datum', 'firma', 'stelle'],
          properties: {
            datum: { type: 'string', format: 'date' },
            firma: { type: 'string' },
            stelle: { type: 'string' },
            art: { type: 'string', enum: ART_OPTIONS },
            status: { type: 'string', enum: STATUS_OPTIONS },
            labels: labelsSchema,
            notizen: { type: 'string' },
            interne_notizen: { type: 'string' },
            ort: { type: 'string' },
            stellenbeschreibung: { type: 'string' },
            quelle_url: { type: 'string' },
            llm_notizen: { type: 'string' },
            kommentar: { type: 'string', description: 'Kommentar zum initialen Status-Eintrag' },
            force: { type: 'boolean', description: 'Dubletten-Prüfung überspringen' },
          },
        },
        ApplicationUpdate: {
          type: 'object',
          required: ['datum', 'firma', 'stelle'],
          properties: {
            datum: { type: 'string', format: 'date' },
            firma: { type: 'string' },
            stelle: { type: 'string' },
            art: { type: 'string', enum: ART_OPTIONS },
            status: { type: 'string', enum: STATUS_OPTIONS },
            labels: labelsSchema,
            notizen: { type: 'string' },
            interne_notizen: { type: 'string' },
            ort: { type: 'string' },
            stellenbeschreibung: { type: 'string' },
            quelle_url: { type: 'string' },
            llm_notizen: { type: 'string' },
          },
        },
        ApplicationCreated: {
          type: 'object',
          properties: {
            success: { type: 'boolean', example: true },
            application: { $ref: '#/components/schemas/Application' },
          },
        },
        DuplicateConflict: {
          type: 'object',
          properties: {
            duplicate: { type: 'boolean', example: true },
            error: { type: 'string' },
            matches: {
              type: 'array',
              items: {
                type: 'object',
                properties: {
                  id: { type: 'integer' },
                  datum: { type: 'string', format: 'date' },
                  firma: { type: 'string' },
                  stelle: { type: 'string' },
                  ort: { type: 'string', nullable: true },
                  status: { type: 'string', nullable: true },
                  reason: { type: 'string' },
                },
              },
            },
          },
        },
        TimelineEntry: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            bewerbung_id: { type: 'integer' },
            datum: { type: 'string', format: 'date' },
            status: { type: 'string', enum: STATUS_OPTIONS },
            kommentar: { type: 'string', nullable: true },
            created_at: { type: 'string', format: 'date-time' },
          },
        },
        TimelineEntryCreate: {
          type: 'object',
          required: ['status'],
          properties: {
            datum: { type: 'string', format: 'date', description: 'Standard: heute' },
            status: { type: 'string', enum: STATUS_OPTIONS },
            kommentar: { type: 'string' },
        },
        },
        Attachment: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            bewerbung_id: { type: 'integer' },
            name: { type: 'string', nullable: true },
            dateiname: { type: 'string' },
            mime: { type: 'string', nullable: true },
            created_at: { type: 'string', format: 'date-time' },
          },
        },
        Email: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            bewerbung_id: { type: 'integer', nullable: true },
            direction: { type: 'string', enum: ['in', 'out'] },
            from_addr: { type: 'string', nullable: true },
            to_addr: { type: 'string', nullable: true },
            subject: { type: 'string', nullable: true },
            body_text: { type: 'string', nullable: true },
            email_date: { type: 'string', format: 'date-time', nullable: true },
            anhaenge: {
              type: 'array',
              items: {
                type: 'object',
                properties: {
                  id: { type: 'integer' },
                  name: { type: 'string', nullable: true },
                  mime: { type: 'string', nullable: true },
                },
              },
            },
            created_at: { type: 'string', format: 'date-time' },
          },
        },
        GenerationStatus: {
          type: 'object',
          properties: {
            status: { type: 'string', enum: ['nicht_gestartet', 'ausstehend', 'fertig', 'fehler'], nullable: true },
            fehler: { type: 'string', nullable: true },
            anhaenge: {
              type: 'array',
              items: { $ref: '#/components/schemas/Attachment' },
            },
          },
        },
        Settings: {
          type: 'object',
          description:
            'Persönliche Angaben des Benutzers, dem der API-Key gehört (fließen in ' +
            'die generierten Unterlagen ein). Hat der Benutzer noch nichts gespeichert, ' +
            'liefert GET ein leeres Objekt `{}` — das ist ein gültiger Zustand.',
          properties: {
            name: { type: 'string', example: 'Max Mustermann' },
            adresse: { type: 'string', example: 'Musterstraße 1, 12345 Musterstadt' },
            kundennummer: { type: 'string', description: 'Kundennummer beim Jobcenter' },
            email: { type: 'string', example: 'max@example.com' },
            telefon: { type: 'string', example: '030 12345678' },
            ort: { type: 'string', example: 'Berlin' },
            webseite: { type: 'string', example: 'example.com' },
            geburtsdatum: { type: 'string', example: '01.01.1990' },
          },
        },
        Statistics: {
          type: 'object',
          properties: {
            total: { type: 'integer' },
            byArt: {
              type: 'array',
              items: {
                type: 'object',
                properties: { art: { type: 'string' }, count: { type: 'integer' } },
              },
            },
            byStatus: {
              type: 'array',
              items: {
                type: 'object',
                properties: { status: { type: 'string' }, count: { type: 'integer' } },
              },
            },
          },
        },
        Template: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            typ: { type: 'string' },
            name: { type: 'string', nullable: true },
            inhalt: { type: 'string' },
            created_at: { type: 'string', format: 'date-time' },
          },
        },
        JobOffer: {
          type: 'object',
          properties: {
            id: { type: 'integer' },
            external_id: { type: 'string', nullable: true, description: 'ID im Drittanbietersystem (Upsert-Schlüssel)' },
            quelle: { type: 'string', example: 'drittanbieter' },
            firma: { type: 'string' },
            stelle: { type: 'string' },
            ort: { type: 'string', nullable: true },
            adresse: { type: 'string', nullable: true, description: 'Anschrift des Arbeitgebers (Straße, Hausnummer, PLZ, Ort)' },
            ansprechpartner: { type: 'string', nullable: true, description: 'Name des Ansprechpartners für die Bewerbung' },
            gehalt: { type: 'string', nullable: true },
            beschreibung: { type: 'string', nullable: true },
            quelle_url: { type: 'string', nullable: true },
            art: { type: 'string', nullable: true },
            anzeige_datum: { type: 'string', format: 'date', nullable: true, description: 'Datum der eigentlichen Stellenanzeige (vom Drittanbieter übergeben)' },
            kontakt_email: { type: 'string', format: 'email', nullable: true, description: 'Kontakt-E-Mail-Adresse der Stelle' },
            status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'] },
            favorit: {
              type: 'integer',
              enum: [0, 1],
              readOnly: true,
              description: 'Vom Nutzer in der Weboberfläche gemerktes Angebot (Stern). Wird nur dort gesetzt ' +
                'und beim erneuten Einspielen desselben Angebots nicht überschrieben. Unabhängig vom ' +
                'Favoriten-Merkmal einer Bewerbung.',
            },
            labels: { ...labelsSchema, nullable: true },
            verknuepfte_bewerbung_id: { type: 'integer', nullable: true },
            url_norm: { type: 'string', nullable: true, description: 'Normalisierte URL für die De-Duplizierung (serverseitig gesetzt)' },
            firma_slug: { type: 'string', nullable: true, description: 'Firmen-Slug für Blacklist-Abgleich; wie übergeben gespeichert, bei Fehlen aus firma abgeleitet.' },
            created_at: { type: 'string', format: 'date-time' },
            updated_at: { type: 'string', format: 'date-time' },
          },
        },
        JobOfferCreate: {
          type: 'object',
          required: ['firma', 'stelle'],
          properties: {
            external_id: { type: 'string', description: 'ID im Drittanbietersystem; bei Wiederholung wird das Angebot aktualisiert' },
            quelle: { type: 'string', example: 'drittanbieter', description: 'Name der Drittanbietersoftware (Standard: drittanbieter)' },
            firma: { type: 'string' },
            stelle: { type: 'string' },
            ort: { type: 'string' },
            adresse: { type: 'string', description: 'Anschrift des Arbeitgebers (Straße, Hausnummer, PLZ, Ort)' },
            ansprechpartner: { type: 'string', description: 'Name des Ansprechpartners für die Bewerbung' },
            gehalt: { type: 'string' },
            beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
            quelle_url: { type: 'string' },
            firma_slug: { type: 'string', description: 'Optionaler Firmen-Slug für den Blacklist-Abgleich (z. B. "bosch"). Wird wie übergeben gespeichert; fehlt er, leitet der Server ihn aus firma ab.' },
            art: { type: 'string', enum: ART_OPTIONS },
            anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' },
            kontakt_email: { type: 'string', format: 'email', description: 'Kontakt-E-Mail-Adresse der Stelle' },
            status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'], default: 'offen' },
            labels: labelsSchema,
          },
        },
        JobOfferResult: {
          type: 'object',
          properties: {
            success: { type: 'boolean', example: true },
            action: { type: 'string', enum: ['created', 'updated'] },
            joboffer: { $ref: '#/components/schemas/JobOffer' },
          },
        },
        JobOfferDeleteResult: {
          type: 'object',
          properties: {
            success: { type: 'boolean', example: true },
            blacklisted: { type: 'boolean', description: 'true, wenn das Angebot beim Löschen auf die Blacklist gesetzt wurde' },
          },
        },
        BlacklistEntry: {
          type: 'object',
          description: 'Ein Blacklist-Eintrag. Nur die zum typ passenden Felder sind gesetzt.',
          properties: {
            id: { type: 'integer' },
            typ: { type: 'string', enum: ['url', 'domain', 'firma', 'firma_stelle', 'auto'] },
            url_norm: { type: 'string', nullable: true, description: 'Normalisierte URL (Tracking-Parameter entfernt)' },
            domain: { type: 'string', nullable: true },
            quelle: { type: 'string', nullable: true },
            external_id: { type: 'string', nullable: true },
            firma_norm: { type: 'string', nullable: true },
            firma_slug: { type: 'string', nullable: true, description: 'Rechtsform-/Umlaut-robuster Firmen-Slug; primärer Abgleich für typ=firma/firma_stelle/auto.' },
            stelle_norm: { type: 'string', nullable: true },
            ort_norm: { type: 'string', nullable: true },
            firma: { type: 'string', nullable: true },
            stelle: { type: 'string', nullable: true },
            quelle_url: { type: 'string', nullable: true },
            grund: { type: 'string', nullable: true },
            created_at: { type: 'string', format: 'date-time' },
          },
        },
        BlacklistEntryCreate: {
          type: 'object',
          required: ['typ'],
          properties: {
            typ: {
              type: 'string',
              enum: ['url', 'domain', 'firma', 'firma_stelle'],
              description: 'url/domain/firma nutzen "wert"; firma_stelle nutzt "firma"+"stelle" (+optional "ort").',
            },
            wert: { type: 'string', description: 'URL (typ=url), Domain (typ=domain) oder Firmenname (typ=firma).' },
            firma: { type: 'string', description: 'Nur bei typ=firma_stelle.' },
            stelle: { type: 'string', description: 'Nur bei typ=firma_stelle.' },
            ort: { type: 'string', description: 'Optional bei typ=firma_stelle.' },
            grund: { type: 'string', description: 'Optionaler Freitext-Grund.' },
          },
        },
        BlacklistEntryResult: {
          type: 'object',
          properties: {
            success: { type: 'boolean', example: true },
            entry: { $ref: '#/components/schemas/BlacklistEntry' },
          },
        },
        BlacklistConflict: {
          type: 'object',
          properties: {
            blacklisted: { type: 'boolean', example: true },
            matched_by: { type: 'string', description: 'typ des greifenden Blacklist-Eintrags' },
            blacklist_entry: { $ref: '#/components/schemas/BlacklistEntry' },
            error: { type: 'string' },
          },
        },
      },
    },
    security: [{ ApiKeyAuth: [] }],
  };
}

module.exports = { buildOpenApiSpec };