{
  "openapi": "3.1.0",
  "info": {
    "title": "AlpineChat API",
    "version": "1.0.0",
    "summary": "Public API für AlpineChat-Widgets, Leads, Eskalationen und Backoffice-Zugriff.",
    "description": "REST + SSE-API für die AlpineChat-Plattform. Die meisten Endpunkte sind pro Chatbot geschützt und setzen einen Bearer-API-Key voraus, der unter `/admin/api-keys` vom Tenant-Admin erzeugt wird. Widget-Config und Health-Probe sind öffentlich.\n\nDaten-Hosting: Österreich (Tirol). KI-Inferenz via OpenAI (DPA-konform) oder Azure OpenAI EU-Region.\n\nRate-Limits: Chat 200 Req/h pro IP × Chatbot (Global-Cap) plus konfigurierbares Session-Limit. Lead 5 Req/h pro IP × Chatbot. Escalation 3 Req/h pro IP × Chatbot.",
    "contact": {
      "name": "AlpineChat Support",
      "email": "support@web-crossing.com",
      "url": "https://alpinechat.io"
    },
    "license": {
      "name": "Proprietär"
    }
  },
  "servers": [
    {
      "url": "https://alpinechat.io",
      "description": "Produktion"
    }
  ],
  "tags": [
    { "name": "Widget",        "description": "Public-Read-Endpunkte für das Chat-Widget beim Boot." },
    { "name": "Chat",          "description": "SSE-Streaming-Chat. Kern-Endpoint." },
    { "name": "Leads",         "description": "Lead-Capture aus dem Widget." },
    { "name": "Escalations",   "description": "Eskalation an einen menschlichen Mitarbeiter." },
    { "name": "Feedback",      "description": "Up-/Down-Rating auf Assistant-Nachrichten." },
    { "name": "Conversations", "description": "Read-API für bestehende Conversations (Backoffice)." },
    { "name": "Telemetry",     "description": "Proactive-Trigger fire/convert-Beacons." },
    { "name": "System",        "description": "Health + Ping." }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API-Key mit passendem Scope. Erstellung unter `/admin/api-keys`. Einmal kopieren, danach nur der SHA-256-Hash in der DB. Scopes: `chat`, `leads.read`, `leads.write`, `escalations.read`, `escalations.write`, `conversations.read`, oder `*` (alles)."
      }
    },
    "parameters": {
      "slug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Eindeutiger Slug des Chatbots (z.B. `tvb-osttirol-bot`).",
        "example": "tvb-osttirol-bot"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string", "example": "unauthorized" }
        }
      },
      "ValidationError": {
        "type": "object",
        "properties": {
          "message": { "type": "string", "example": "The given data was invalid." },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": { "type": "string" }
            },
            "example": { "email": ["The email must be a valid email address."] }
          }
        }
      },
      "HealthStatus": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["ok", "degraded"] },
          "errors": { "type": "array", "items": { "type": "string" } },
          "ts": { "type": "string", "format": "date-time" }
        },
        "required": ["status", "errors", "ts"]
      },
      "WidgetConfig": {
        "type": "object",
        "description": "Enthält keine Secrets — rein darstellungs-relevante Felder.",
        "properties": {
          "slug":       { "type": "string" },
          "title":      { "type": "string" },
          "greeting":   { "type": "string", "description": "Erste Begrüßungsnachricht." },
          "language":   { "type": "string", "enum": ["de", "en", "it"] },
          "colors":     { "type": "object", "additionalProperties": true },
          "widget":     { "type": "object", "additionalProperties": true, "description": "Platzierung, Titel, Lead-Capture-Flag usw." },
          "proactive":  { "type": "array", "items": { "type": "object" } },
          "escalation": { "type": "object", "properties": { "enabled": { "type": "boolean" } } },
          "features":   { "type": "object", "properties": { "page_context": { "type": "object", "properties": { "enabled": { "type": "boolean" } } } } },
          "theme":      { "type": "object", "description": "Aufgelöste Theme-Tokens (light + dark) — Shadow-DOM-CSS-Variablen." }
        }
      },
      "ChatRequest": {
        "type": "object",
        "required": ["session_id", "message"],
        "properties": {
          "session_id":   { "type": "string", "minLength": 4, "maxLength": 128, "description": "Stabile Session-ID pro Browser-Tab. Dedupliziert Conversations." },
          "message":      { "type": "string", "minLength": 1, "maxLength": 4000 },
          "page_url":     { "type": "string", "maxLength": 2048, "description": "URL der Seite, von der aus der User schreibt." },
          "language":     { "type": "string", "enum": ["de", "en", "it"] },
          "page_context": {
            "type": "object",
            "description": "Optional — DOM-Kontext, hilft dem Bot seitenspezifisch zu antworten.",
            "properties": {
              "url":     { "type": "string", "format": "uri", "maxLength": 2048 },
              "title":   { "type": "string", "maxLength": 500 },
              "excerpt": { "type": "string", "maxLength": 800 }
            }
          }
        }
      },
      "ChatStreamEvent": {
        "oneOf": [
          { "$ref": "#/components/schemas/ChatRagEvent" },
          { "$ref": "#/components/schemas/ChatTextEvent" },
          { "$ref": "#/components/schemas/ChatCardEvent" },
          { "$ref": "#/components/schemas/ChatUsageEvent" },
          { "$ref": "#/components/schemas/ChatFinishEvent" },
          { "$ref": "#/components/schemas/ChatErrorEvent" },
          { "$ref": "#/components/schemas/ChatDoneEvent" }
        ],
        "discriminator": { "propertyName": "type" }
      },
      "ChatRagEvent": {
        "type": "object",
        "properties": {
          "type":    { "type": "string", "const": "rag" },
          "sources": {
            "type": "array",
            "items": { "type": "object", "properties": { "title": { "type": "string" }, "url": { "type": "string" } } }
          }
        },
        "required": ["type", "sources"]
      },
      "ChatTextEvent": {
        "type": "object",
        "properties": {
          "type":    { "type": "string", "const": "text" },
          "content": { "type": "string", "description": "Text-Chunk. Kommt wiederholt — aneinanderhängen." }
        },
        "required": ["type", "content"]
      },
      "ChatCardEvent": {
        "type": "object",
        "description": "Tool-Output als strukturierte UI-Karte (Touren, Wetter, Karte, Buchung). Widget rendert dedizierte Komponente.",
        "properties": {
          "type": { "type": "string", "const": "card" },
          "card": {
            "type": "object",
            "properties": {
              "type":  { "type": "string", "enum": ["tour_list", "weather", "map", "booking", "tour_detail"] },
              "query": { "type": "string" },
              "tours": { "type": "array", "items": { "type": "object" } }
            },
            "additionalProperties": true
          }
        },
        "required": ["type", "card"]
      },
      "ChatUsageEvent": {
        "type": "object",
        "properties": {
          "type":              { "type": "string", "const": "usage" },
          "prompt_tokens":     { "type": "integer" },
          "completion_tokens": { "type": "integer" },
          "cost_usd":          { "type": "number", "format": "float" }
        },
        "required": ["type", "prompt_tokens", "completion_tokens", "cost_usd"]
      },
      "ChatFinishEvent": {
        "type": "object",
        "properties": {
          "type":   { "type": "string", "const": "finish" },
          "reason": { "type": "string", "enum": ["stop", "length", "blocked", "budget_exceeded", "error"] }
        },
        "required": ["type", "reason"]
      },
      "ChatErrorEvent": {
        "type": "object",
        "properties": {
          "type":    { "type": "string", "const": "error" },
          "message": { "type": "string", "description": "Endkunden-geeignete Meldung. Beispiele: 'Rate limit exceeded, retry in 120s', 'Monatliches Budget erreicht. Bitte wende dich an den Administrator.'" }
        },
        "required": ["type", "message"]
      },
      "ChatDoneEvent": {
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "done" }
        },
        "required": ["type"]
      },
      "LeadRequest": {
        "type": "object",
        "required": ["email"],
        "properties": {
          "email":      { "type": "string", "format": "email", "maxLength": 190 },
          "name":       { "type": "string", "maxLength": 120 },
          "phone":      { "type": "string", "maxLength": 40 },
          "company":    { "type": "string", "maxLength": 120 },
          "session_id": { "type": "string", "maxLength": 128, "description": "Wenn gesetzt, wird der Lead an die laufende Conversation angehängt." },
          "page_url":   { "type": "string", "maxLength": 2048 }
        }
      },
      "LeadResponse": {
        "type": "object",
        "properties": {
          "id":         { "type": "string", "format": "uuid" },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "EscalationRequest": {
        "type": "object",
        "required": ["email", "message"],
        "properties": {
          "email":           { "type": "string", "format": "email", "maxLength": 190 },
          "message":         { "type": "string", "minLength": 5, "maxLength": 4000, "description": "Kundenanliegen." },
          "name":            { "type": "string", "maxLength": 120 },
          "phone":           { "type": "string", "maxLength": 40 },
          "topic":           { "type": "string", "maxLength": 120 },
          "session_id":      { "type": "string", "maxLength": 128 },
          "conversation_id": { "type": "string", "format": "uuid", "description": "Wenn gesetzt und tenant-gültig, wird die Eskalation verknüpft." },
          "page_url":        { "type": "string", "maxLength": 2048 }
        }
      },
      "EscalationResponse": {
        "type": "object",
        "properties": {
          "id":      { "type": "string", "format": "uuid" },
          "status":  { "type": "string", "enum": ["open", "in_progress", "resolved"] },
          "message": { "type": "string", "example": "Danke — wir melden uns bei dir." }
        }
      },
      "FeedbackRequest": {
        "type": "object",
        "required": ["rating", "conversation_id"],
        "properties": {
          "rating":          { "type": "integer", "enum": [-1, 1], "description": "+1 = gut, -1 = schlecht" },
          "conversation_id": { "type": "string", "format": "uuid" },
          "message_id":      { "type": "string", "format": "uuid", "description": "Wenn leer: Rating gilt der letzten Assistant-Nachricht." },
          "comment":         { "type": "string", "maxLength": 2000 }
        }
      },
      "ConversationSummary": {
        "type": "object",
        "properties": {
          "id":         { "type": "string", "format": "uuid" },
          "chatbot_id": { "type": "string", "format": "uuid" },
          "session_id": { "type": "string" },
          "language":   { "type": "string" },
          "page_url":   { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "messages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id":         { "type": "string", "format": "uuid" },
                "role":       { "type": "string", "enum": ["user", "assistant", "system", "tool"] },
                "content":    { "type": "string" },
                "created_at": { "type": "string", "format": "date-time" }
              }
            }
          }
        }
      },
      "ConversationList": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/ConversationSummary" } }
        }
      },
      "ProactiveTelemetry": {
        "type": "object",
        "required": ["kind", "trigger_key"],
        "properties": {
          "kind":        { "type": "string", "enum": ["fire", "convert"] },
          "trigger_key": { "type": "string", "maxLength": 120 },
          "session_id":  { "type": "string", "maxLength": 128 },
          "page_url":    { "type": "string", "maxLength": 2048 },
          "metadata":    { "type": "object", "additionalProperties": true }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Kein oder ungültiger API-Key bzw. fehlender Scope.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "API-Key gehört nicht zu diesem Chatbot-Tenant.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Chatbot-Slug existiert nicht oder ist inaktiv.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Zu viele Requests — Limit überschritten.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "ValidationFailed": {
        "description": "Validierungsfehler — siehe `errors`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ValidationError" } } }
      }
    }
  },
  "paths": {
    "/api/healthz": {
      "get": {
        "tags": ["System"],
        "summary": "Health-Probe",
        "description": "Prüft DB + Redis. Für UptimeRobot / externe Monitore. Keine Authentifizierung.",
        "security": [],
        "responses": {
          "200": { "description": "System gesund.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthStatus" } } } },
          "503": { "description": "System degraded — mindestens eine Dependency down.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HealthStatus" } } } }
        }
      }
    },
    "/api/ping": {
      "get": {
        "tags": ["System"],
        "summary": "Authentifizierter Ping",
        "description": "Gibt `{ok, tenant, scopes}` zurück. Nützlich zum Testen, ob ein API-Key aktiv und mit den erwarteten Scopes versehen ist.",
        "security": [{ "bearerAuth": [] }],
        "responses": {
          "200": {
            "description": "Auth ok.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok":     { "type": "boolean" },
                    "tenant": { "type": "string" },
                    "scopes": { "type": "array", "items": { "type": "string" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/widget-config/{slug}": {
      "get": {
        "tags": ["Widget"],
        "summary": "Public Widget-Konfiguration",
        "description": "Keine Authentifizierung. Liefert Theme + Labels + Feature-Flags, die das Widget beim Boot braucht. Cache-Control: 60s.",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/slug" }],
        "responses": {
          "200": { "description": "OK.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WidgetConfig" } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/chat/{slug}": {
      "post": {
        "tags": ["Chat"],
        "summary": "Streaming Chat-Turn (SSE)",
        "description": "Ein POST = ein User-Turn. Response ist ein `text/event-stream`, das inkrementell konsumiert wird.\n\n**Event-Reihenfolge (typisch):**\n1. `rag` — Quellen aus RAG-Suche (optional).\n2. `text` — ein oder mehrere Text-Chunks. **Zusammenbauen** für die volle Antwort.\n3. `card` — strukturierte UI-Karten aus Tool-Calls (optional).\n4. `usage` — Token-Counts + Kosten.\n5. `finish` — mit `reason` (stop, length, blocked, budget_exceeded, error).\n6. `done` — Terminator, Stream kann geschlossen werden.\n\nEin `error` darf jederzeit kommen (Rate-Limit, Budget, Upstream).\n\n**Rate-Limit:** 200 Req/h pro IP × Chatbot (Global-Cap). Zusätzlich pro-Session-Cap (chatbot-konfigurierbar).",
        "security": [{ "bearerAuth": ["chat"] }],
        "parameters": [{ "$ref": "#/components/parameters/slug" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ChatRequest" } } }
        },
        "responses": {
          "200": {
            "description": "SSE-Stream. Jede `data:`-Zeile ist ein JSON-Event gemäß `ChatStreamEvent`.",
            "content": {
              "text/event-stream": {
                "schema": { "$ref": "#/components/schemas/ChatStreamEvent" },
                "examples": {
                  "typical": {
                    "summary": "Typischer Ablauf",
                    "value": "data: {\"type\":\"rag\",\"sources\":[{\"title\":\"Wandern in Osttirol\",\"url\":\"https://...\"}]}\n\ndata: {\"type\":\"text\",\"content\":\"In Osttirol gibt es \"}\n\ndata: {\"type\":\"text\",\"content\":\"viele anfängerfreundliche Touren.\"}\n\ndata: {\"type\":\"usage\",\"prompt_tokens\":1823,\"completion_tokens\":124,\"cost_usd\":0.00045}\n\ndata: {\"type\":\"finish\",\"reason\":\"stop\"}\n\ndata: {\"type\":\"done\"}\n"
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" }
        }
      }
    },
    "/api/chat/{slug}/telemetry/proactive": {
      "post": {
        "tags": ["Telemetry"],
        "summary": "Proactive-Trigger fire/convert Beacon",
        "description": "Widget schickt `kind=fire`, wenn ein proaktives Trigger ausgelöst wurde, `kind=convert`, wenn daraus eine Conversation wurde. Keine Auth nötig (Public Widget-Endpoint mit Chatbot-CORS).",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/slug" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProactiveTelemetry" } } }
        },
        "responses": {
          "200": { "description": "Beacon gespeichert." },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/escalate/{slug}": {
      "post": {
        "tags": ["Escalations"],
        "summary": "Eskalation an menschlichen Mitarbeiter",
        "description": "Erstellt eine Escalation mit `status=open`, schickt eine E-Mail an den Tenant-Admin und feuert `escalation.created`-Webhook (falls konfiguriert). Rate-Limit 3/h pro IP × Chatbot.",
        "security": [{ "bearerAuth": ["escalations.write"] }],
        "parameters": [{ "$ref": "#/components/parameters/slug" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EscalationRequest" } } }
        },
        "responses": {
          "201": { "description": "Eskalation angelegt.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EscalationResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/escalations/{slug}": {
      "get": {
        "tags": ["Escalations"],
        "summary": "Liste der Eskalationen",
        "security": [{ "bearerAuth": ["escalations.read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/slug" },
          { "name": "status", "in": "query", "schema": { "type": "string", "enum": ["open", "in_progress", "resolved"] }, "description": "Optional filter." },
          { "name": "limit",  "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Liste.",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object" } } } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/lead/{slug}": {
      "post": {
        "tags": ["Leads"],
        "summary": "Lead speichern",
        "description": "Erstellt einen Lead. Bedingt `widget.lead_capture=true` in der Chatbot-Config. Triggert Admin-Benachrichtigung + `lead.created`-Webhook. Rate-Limit 5/h pro IP × Chatbot.",
        "security": [{ "bearerAuth": ["leads.write"] }],
        "parameters": [{ "$ref": "#/components/parameters/slug" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadRequest" } } }
        },
        "responses": {
          "201": { "description": "Lead angelegt.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LeadResponse" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Lead-Capture ist für diesen Chatbot deaktiviert.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/leads/{slug}": {
      "get": {
        "tags": ["Leads"],
        "summary": "Liste der Leads",
        "security": [{ "bearerAuth": ["leads.read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/slug" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 50 } }
        ],
        "responses": {
          "200": {
            "description": "Paginated list.",
            "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/feedback/{slug}": {
      "post": {
        "tags": ["Feedback"],
        "summary": "Thumb-up/down auf Assistant-Nachricht",
        "security": [],
        "parameters": [{ "$ref": "#/components/parameters/slug" }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FeedbackRequest" } } }
        },
        "responses": {
          "200": { "description": "Rating gespeichert." },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationFailed" }
        }
      }
    },
    "/api/conversations/{slug}": {
      "get": {
        "tags": ["Conversations"],
        "summary": "Liste der Conversations",
        "description": "Liefert Conversation-Summaries inkl. eingebetteter Nachrichten (vereinfacht). Reihenfolge: neueste zuerst.",
        "security": [{ "bearerAuth": ["conversations.read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/slug" },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 200, "default": 20 } }
        ],
        "responses": {
          "200": {
            "description": "Liste.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ConversationList" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/conversations/{slug}/{conversation_id}/messages": {
      "get": {
        "tags": ["Conversations"],
        "summary": "Nachrichten einer Conversation",
        "security": [{ "bearerAuth": ["conversations.read"] }],
        "parameters": [
          { "$ref": "#/components/parameters/slug" },
          { "name": "conversation_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Messages.",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "type": "object" } } } } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  }
}
