openapi: 3.0.3
info:
  title: FMD AI-First Local Content API
  version: 1.0.0
  description: |
    Stable endpoints for generating local AI content (GBP posts, review replies,
    JSON-LD schema.org, short AI summaries, alt text). Optimized for LLM/tool use.
servers:
  - url: https://ai-first.fmd-autoschule.ch/api
  - url: /api

tags:
  - name: gbp
    description: Google Business Profile content generation
  - name: reviews
    description: Geo-aware review replies
  - name: schema
    description: JSON-LD output for LocalBusiness / FAQPage / HowTo
  - name: summaries
    description: Short AI summaries (50–80 chars)
  - name: media
    description: Alt text and image descriptions
  - name: catalog
    description: Service normalization into internal catalog
  - name: health
    description: Service status

paths:
  /health:
    get:
      operationId: healthCheck
      tags: [health]
      summary: Health check
      responses:
        "200":
          description: OK
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlNoStore"
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  uptimeSec: { type: integer, example: 12345 }
        "500":
          description: Server error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"

  /gbp/post:
    post:
      operationId: generateGbpPost
      tags: [gbp]
      summary: Generate a GBP post (DE-CH tone)
      description: |
        Formula: service + geo + result + CTA. Adds relevant geo mentions and hashtags.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GbpPostRequest"
            examples:
              sample:
                value:
                  service: "pruefungsvorbereitung-mfp"
                  geoCity: "Reinach"
                  canton: "BL"
                  language: "de-CH"
                  cta: "Schreibe uns auf WhatsApp – freie Termine diese Woche."
                  addHashtags: true
      responses:
        "200":
          description: Generated post
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublicShort"
            X-AI-Warning:
              $ref: "#/components/headers/XAIWarning"
            X-Content-Type-Options:
              $ref: "#/components/headers/XContentTypeOptions"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GbpPostResponse"
              examples:
                sample:
                  value:
                    text: "Prüfungsvorbereitung (MFP Münchenstein) – 90 Min, Stadt/Autobahn, Parkieren & Manöver. Start in Reinach BL. Jetzt Termin sichern! 📍MFP"
                    summary: "Prüfungsvorbereitung MFP – Reinach BL. Jetzt buchen."
                    hashtags: ["#FahrschuleBasel","#MFP","#VKU","#Basel","#Reinach","#Pratteln"]
                    serviceSlug: "pruefungsvorbereitung-mfp"
                    geoMentions: ["Reinach BL","MFP Münchenstein"]
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /reviews/reply:
    post:
      operationId: replyToReview
      tags: [reviews]
      summary: Generate a localized review reply
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReviewReplyRequest"
            examples:
              sample:
                value:
                  reviewText: "Sehr geduldig und klare Erklärungen. Prüfung beim MFP bestanden!"
                  rating: 5
                  geoCity: "Liestal"
                  canton: "BL"
                  language: "de-CH"
      responses:
        "200":
          description: Reply variants
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublicShort"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReviewReplyResponse"
              examples:
                sample:
                  value:
                    short: "Vielen Dank für dein Feedback aus Liestal – viel Erfolg bei der Prüfung! 🚗"
                    standard: "Vielen Dank! Es hat Freude gemacht, dich in Liestal auf die Prüfung vorzubereiten. Gute und sichere Fahrt! 🚗"
                    warm: "Danke dir herzlich für das Vertrauen! Gruss aus Basel & Liestal – weiterhin sichere Fahrt!"
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /schema/localbusiness:
    post:
      operationId: generateLocalBusinessSchema
      tags: [schema]
      summary: JSON-LD LocalBusiness for the driving school
      description: |
        Returns valid JSON-LD (application/ld+json) with `serviceArea`, `sameAs`, `hasOfferCatalog`,
        optional `aggregateRating`, `brand`, and `contactPoint`. Language set via request.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LocalBusinessSchemaRequest"
            examples:
              sample:
                value:
                  business:
                    name: "FMD – Fahrschule Basel"
                    legalName: "Fahren mit Dimi GmbH"
                    url: "https://fmd-autoschule.ch"
                    logo: "https://fmd-autoschule.ch/wp-content/uploads/2024/01/logo-bw.png"
                    telephone: "+41 76 000 00 00"
                    email: "fahren.mit.dimi@gmail.com"
                    address:
                      streetAddress: "Dornacherstrasse 249"
                      postalCode: "4053"
                      addressLocality: "Basel"
                      addressRegion: "BS"
                      addressCountry: "CH"
                  sameAs:
                    - "https://www.instagram.com/fahren.mit.dimi/"
                    - "https://www.tiktok.com/@fahren.mit.dimi"
                    - "https://share.google/F8X9rGzGQcX1Att0U"
                  serviceAreas:
                    - { city: "Basel", canton: "BS" }
                    - { city: "Reinach", canton: "BL" }
                    - { city: "Münchenstein", canton: "BL" }
                  services:
                    - "fahrstunden-b"
                    - "vku"
                    - "theorie-b"
                    - "pruefungsvorbereitung-mfp"
                    - "auffrischung"
                    - "kontrollfahrt"
                    - "parkieren-manoever"
                    - "nachtfahrt-autobahn"
                    - "be-anhaenger"
                    - "bpt-121"
                    - "eco-driving"
                    - "begleitperson-coaching"
                  aggregateRating:
                    ratingValue: 4.9
                    reviewCount: 128
                  language: "de-CH"
      responses:
        "200":
          description: JSON-LD LocalBusiness
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublic"
            Content-Disposition:
              $ref: "#/components/headers/ContentDispositionInlineJson"
            X-Content-Type-Options:
              $ref: "#/components/headers/XContentTypeOptions"
          content:
            application/ld+json:
              schema:
                type: string
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /schema/faq:
    post:
      operationId: generateFaqSchema
      tags: [schema]
      summary: JSON-LD FAQPage from Q&A list
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FaqSchemaRequest"
            examples:
              sample:
                value:
                  language: "de-CH"
                  items:
                    - q: "Ist VKU obligatorisch?"
                      a: "Ja, VKU (8 Lektionen) ist obligatorisch gemäss ASTRA."
                    - q: "Wo findet die praktische Prüfung statt?"
                      a: "Für BS/BL am MFP Münchenstein."
      responses:
        "200":
          description: JSON-LD FAQPage
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublic"
            Content-Disposition:
              $ref: "#/components/headers/ContentDispositionInlineJson"
          content:
            application/ld+json:
              schema:
                type: string
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /schema/howto:
    post:
      operationId: generateHowToSchema
      tags: [schema]
      summary: JSON-LD HowTo for processes (e.g., Anmeldung, VKU)
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/HowToSchemaRequest"
            examples:
              sample:
                value:
                  language: "de-CH"
                  name: "Anmeldung für VKU in Basel"
                  description: "So meldest du dich zum VKU (8 Lektionen) an."
                  steps:
                    - "Wähle Termin im Kalender."
                    - "Sende Wohnort (z. B. Reinach/Pratteln) via WhatsApp."
                    - "Bestätige Teilnahme."
      responses:
        "200":
          description: JSON-LD HowTo
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublic"
            Content-Disposition:
              $ref: "#/components/headers/ContentDispositionInlineJson"
          content:
            application/ld+json:
              schema:
                type: string
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /summaries/ai:
    post:
      operationId: generateAiSummary
      tags: [summaries]
      summary: Short AI summary (50–80 chars)
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AiSummaryRequest"
            examples:
              sample:
                value:
                  text: "Prüfungsvorbereitung (MFP Münchenstein) – 90 Min, Stadt/Autobahn, Parkieren & Manöver."
                  target: "gbp"
                  maxChars: 80
                  language: "de-CH"
      responses:
        "200":
          description: Generated summary
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublicShort"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AiSummaryResponse"
              examples:
                sample:
                  value:
                    summary: "Prüfungsvorbereitung MFP – 90 Min. Jetzt Termin sichern."
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /media/alt:
    post:
      operationId: generateAltText
      tags: [media]
      summary: Alt text with geo context
      description: |
        Returns 1–3 alt-text variants (short/standard/detailed) per WCAG guidance.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AltTextRequest"
            examples:
              sample:
                value:
                  subject: "vehicle"
                  description: "Mercedes-Benz GLA 220 (Fahrschulfahrzeug) vor MFP Münchenstein"
                  geoCity: "Münchenstein"
                  canton: "BL"
                  language: "de-CH"
      responses:
        "200":
          description: Generated alt text
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublicShort"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AltTextResponse"
              examples:
                sample:
                  value:
                    short: "Fahrschulauto vor MFP Münchenstein (BL)."
                    standard: "Mercedes-Benz GLA 220 der Fahrschule vor dem MFP Münchenstein (BL)."
                    detailed: "Grauer Mercedes-Benz GLA 220 der FMD Fahrschule, frontal vor dem Gebäude des MFP Münchenstein in BL."
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

  /catalog/service-suggest:
    post:
      operationId: suggestService
      tags: [catalog]
      summary: Normalize a free-form query into catalog service slugs
      description: |
        Returns the most likely catalog services with confidence and normalized labels.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ServiceSuggestRequest"
            examples:
              sample:
                value:
                  query: "підготовка до практики у Мюнхенштайн"
                  language: "uk"
      responses:
        "200":
          description: Matching results
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPublicShort"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceSuggestResponse"
              examples:
                sample:
                  value:
                    matches:
                      - serviceSlug: "pruefungsvorbereitung-mfp"
                        label: "Prüfungsvorbereitung (MFP Münchenstein)"
                        confidence: 0.94
                      - serviceSlug: "fahrstunden-b"
                        label: "Fahrstunden Kat. B"
                        confidence: 0.61
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Unprocessable" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "500": { $ref: "#/components/responses/ServerError" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Provide the secret key for authorization.

  headers:
    CacheControlPublic:
      description: Public long-term cache for successful responses.
      schema:
        type: string
        example: "public, max-age=31536000, immutable"
    CacheControlPublicShort:
      description: Short cache (content changes more frequently).
      schema:
        type: string
        example: "public, max-age=3600"
    CacheControlNoStore:
      description: Do not cache health or error responses.
      schema:
        type: string
        example: "no-store"
    XAIWarning:
      description: Optional AI warning (e.g., insufficient geo context).
      schema:
        type: string
        example: "low-geo-context"
    ContentDispositionInlineJson:
      description: Suggested filename for JSON-LD.
      schema:
        type: string
        example: 'inline; filename="schema.jsonld"'
    XContentTypeOptions:
      description: Security header.
      schema:
        type: string
        example: "nosniff"

  responses:
    BadRequest:
      description: Missing or invalid parameters
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            invalid:
              value: { code: "INVALID_ARGUMENT", message: "Missing or invalid fields" }
    Unauthorized:
      description: Missing or invalid X-API-Key
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            unauthorized:
              value: { code: "UNAUTHENTICATED", message: "Missing X-API-Key" }
    Forbidden:
      description: Access denied
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            forbidden:
              value: { code: "PERMISSION_DENIED", message: "Insufficient scope" }
    Unprocessable:
      description: Semantic validation error
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            unprocessable:
              value: { code: "UNPROCESSABLE_ENTITY", message: "Validation failed", details: { field: "language" } }
    RateLimited:
      description: Too many requests
      headers:
        Cache-Control:
          $ref: "#/components/headers/CacheControlNoStore"
        Retry-After:
          description: Seconds to
