{
  "openapi": "3.1.0",
  "info": {
    "title": "Pup-PDF API",
    "version": "2.0.0",
    "description": "URL'leri PDF'e dönüştürüp DigitalOcean Spaces / S3'e yükleyen API. Persistent Chrome + bounded concurrency queue. Idempotency-Key, async mode, structured log + Prometheus metrics destekler.",
    "contact": {
      "name": "Pup-PDF",
      "url": "https://github.com/mtahirsen/Pup-PDF"
    },
    "license": {
      "name": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://pdf.dinamikkatalog.com",
      "description": "Production"
    }
  ],
  "tags": [
    { "name": "pdf",     "description": "PDF üretim endpoint'leri" },
    { "name": "jobs",    "description": "Async iş yönetimi" },
    { "name": "system",  "description": "Health, queue, metrics" },
    { "name": "legacy",  "description": "Geriye dönük uyumluluk endpoint'leri" }
  ],
  "paths": {
    "/v1/pdf": {
      "post": {
        "tags": ["pdf"],
        "summary": "URL'i PDF'e dönüştür",
        "description": "Verilen URL'in render edilmiş PDF'ini üretir, S3/Spaces'e yükler ve genel erişimli URL'i döner.\n\n**Idempotency-Key** header'ı ile aynı işlem tekrar tetiklendiğinde aynı yanıt döner (15 dk TTL).\n\n**Async mod** için `Prefer: respond-async` header'ı ekle veya `?async=1` query parametresi kullan — anında `job_id` döner, durumu `GET /v1/jobs/:id` ile sorgulanır.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string" },
            "example": "rapor-2026-01-15-acme",
            "description": "Aynı işlemi tekrar tetiklerken aynı sonucun dönmesini garanti eder"
          },
          {
            "name": "Prefer",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "enum": ["respond-async"] },
            "description": "respond-async → 202 + job_id döner, sync render'ı atla"
          },
          {
            "name": "async",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["1", "true"] },
            "description": "Prefer header'ının query alternatifi"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RenderRequest" },
              "examples": {
                "minimal": {
                  "summary": "En basit kullanım (env'deki default CDN profili)",
                  "value": {
                    "url": "https://example.com/rapor",
                    "rename_to": "rapor-2026-01.pdf"
                  }
                },
                "with_options": {
                  "summary": "Tüm seçeneklerle",
                  "value": {
                    "url": "https://example.com/katalog",
                    "rename_to": "katalog-tr.pdf",
                    "paper_size": "A4",
                    "margins": "normal",
                    "scale": 100,
                    "background_graphics": true,
                    "landscape": false,
                    "emulate_media": "screen",
                    "timeout_ms": 120000
                  }
                },
                "with_named_profile": {
                  "summary": "Env'de tanımlı multi-tenant CDN profile (önerilir)",
                  "value": {
                    "url": "https://example.com/fatura",
                    "rename_to": "fatura-acme-001.pdf",
                    "cdn_profile": "acme"
                  }
                },
                "with_custom_folder": {
                  "summary": "Sadece klasörü override et (credentials env'den)",
                  "value": {
                    "url": "https://example.com/rapor",
                    "rename_to": "rapor.pdf",
                    "cdn": { "folder": "musteri/2026/raporlar" }
                  }
                },
                "legacy_full_cdn": {
                  "summary": "Eski PHP plugin formatı (geriye dönük uyumluluk)",
                  "value": {
                    "url": "https://example.com/page",
                    "rename_to": "doc.pdf",
                    "cdn": {
                      "access_key": "DO00...",
                      "secret_key": "...",
                      "bucket": "my-bucket",
                      "region": "fra1",
                      "folder": "pdfs/2026"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PDF başarıyla üretildi ve CDN'e yüklendi",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/RenderResponse" },
                "example": {
                  "success": true,
                  "filename": "rapor-2026-01.pdf",
                  "file_size": 245678,
                  "duration_ms": 2840,
                  "cdn_url": "https://my-bucket.fra1.cdn.digitaloceanspaces.com/pdfs/rapor-2026-01.pdf",
                  "bucket": "my-bucket",
                  "key": "pdfs/rapor-2026-01.pdf"
                }
              }
            }
          },
          "202": {
            "description": "Async kabul — job kuyruğa alındı",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AsyncAcceptResponse" }
              }
            }
          },
          "400": {
            "description": "İstek geçersiz (URL eksik/hatalı, body schema'ya uymuyor)",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "429": {
            "description": "Kuyruk dolu — Retry-After header'ı içerir",
            "headers": {
              "Retry-After": {
                "schema": { "type": "integer" },
                "description": "Saniye cinsinden tekrar deneme süresi"
              }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "499": {
            "description": "Client erkenden bağlantıyı kesti — render iptal edildi"
          },
          "500": {
            "description": "Sunucu hatası (browser crash, timeout, vs.)",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/api/pdf": {
      "post": {
        "tags": ["legacy"],
        "summary": "Legacy endpoint — pup-pdf-plugin uyumluluğu",
        "deprecated": true,
        "description": "Eski PHP plugin'leri ve mevcut entegrasyonlar için. `/v1/pdf` ile aynı işi yapar; cevapta eski şema alanları (`message`, `duration`, `download_url`, `view_url`) ek olarak döner.\n\n**Yeni entegrasyonlar `/v1/pdf` kullanmalı.**",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/RenderRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Eski cevap şeması (yeni alanlar dahil)",
            "headers": {
              "Deprecation": { "schema": { "type": "string" }, "example": "true" },
              "Sunset": { "schema": { "type": "string" } },
              "Link": { "schema": { "type": "string" }, "example": "</v1/pdf>; rel=\"successor-version\"" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/LegacyRenderResponse" },
                "example": {
                  "success": true,
                  "message": "PDF başarıyla oluşturuldu ve CDN'e yüklendi",
                  "filename": "doc.pdf",
                  "file_size": 245678,
                  "duration": "2.84",
                  "cdn_url": "https://my-bucket.fra1.cdn.digitaloceanspaces.com/pdfs/doc.pdf",
                  "download_url": "https://my-bucket.fra1.cdn.digitaloceanspaces.com/pdfs/doc.pdf",
                  "view_url": "https://my-bucket.fra1.cdn.digitaloceanspaces.com/pdfs/doc.pdf"
                }
              }
            }
          }
        }
      }
    },
    "/v1/jobs/{id}": {
      "get": {
        "tags": ["jobs"],
        "summary": "Async job durumunu sorgula",
        "parameters": [{
          "name": "id", "in": "path", "required": true,
          "schema": { "type": "string", "format": "uuid" }
        }],
        "responses": {
          "200": {
            "description": "Job kaydı",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "job": { "$ref": "#/components/schemas/JobRecord" }
                  }
                }
              }
            }
          },
          "404": { "description": "Job bulunamadı (TTL süresi doldu veya yanlış ID)" }
        }
      }
    },
    "/v1/queue": {
      "get": {
        "tags": ["system"],
        "summary": "Anlık kuyruk istatistiği",
        "responses": {
          "200": {
            "description": "Kuyruk durumu",
            "content": {
              "application/json": {
                "example": { "size": 0, "pending": 0, "concurrency": 2, "queueMax": 20 }
              }
            }
          }
        }
      }
    },
    "/health": {
      "get": {
        "tags": ["system"],
        "summary": "Liveness probe — process ayakta mı?",
        "responses": {
          "200": {
            "description": "Process ayakta",
            "content": {
              "application/json": {
                "example": { "status": "ok", "uptime_s": 1234, "timestamp": "2026-05-05T10:00:00.000Z" }
              }
            }
          }
        }
      }
    },
    "/ready": {
      "get": {
        "tags": ["system"],
        "summary": "Readiness probe — trafik kabul edebilir mi?",
        "description": "Browser hazır mı + kuyruk durumu. 503 dönüyorsa load balancer trafiği geri çekmelidir.",
        "responses": {
          "200": { "description": "Hazır", "content": { "application/json": { "example": { "status": "ready", "browser": "ok", "queue": { "size": 0, "pending": 0, "concurrency": 2, "queueMax": 20 } } } } },
          "503": { "description": "Hazır değil (browser yeniden başlatılıyor vs.)" }
        }
      }
    },
    "/metrics": {
      "get": {
        "tags": ["system"],
        "summary": "Prometheus metrics",
        "description": "Standart Prometheus exposition formatında. `pup_pdf_renders_total`, `pup_pdf_render_duration_seconds`, `pup_pdf_queue_depth`, `pup_pdf_browser_restarts_total` gibi sayaçlar.",
        "responses": {
          "200": {
            "description": "text/plain; version=0.0.4",
            "content": { "text/plain": { "example": "# HELP pup_pdf_renders_total Toplam PDF render sayısı\n# TYPE pup_pdf_renders_total counter\npup_pdf_renders_total{status=\"success\"} 1547\n..." } }
          }
        }
      }
    },
    "/download/{filename}": {
      "get": {
        "tags": ["legacy"],
        "summary": "CDN'e 302 redirect (legacy)",
        "deprecated": true,
        "description": "Eski client'lar için — CDN URL'ine yönlendirir. Doğrudan `cdn_url`'i kullanmak daha verimlidir.",
        "parameters": [{ "name": "filename", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "302": { "description": "Location header CDN URL'ini içerir" },
          "400": { "description": "Geçersiz dosya adı (path traversal koruması)" }
        }
      }
    },
    "/view/{filename}": {
      "get": {
        "tags": ["legacy"],
        "summary": "CDN'e 302 redirect (legacy, /download ile aynı)",
        "deprecated": true,
        "parameters": [{ "name": "filename", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "302": { "description": "CDN URL'ine yönlendirme" } }
      }
    }
  },
  "components": {
    "schemas": {
      "RenderRequest": {
        "type": "object",
        "required": ["url"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "PDF'e çevrilecek sayfa. http/https dışı reddedilir. Production'da private IP/localhost SSRF koruması aktiftir.",
            "example": "https://example.com/rapor"
          },
          "rename_to": {
            "type": "string",
            "description": "PDF dosyasının adı. Verilmezse `pdf_<timestamp>_<rand>.pdf` üretilir. Tehlikeli karakterler temizlenir; .pdf uzantısı yoksa eklenir; max 200 karakter.",
            "example": "rapor-2026-01.pdf"
          },
          "paper_size": {
            "type": "string",
            "enum": ["A4", "A3", "A5", "A0", "A1", "A2", "A6", "Letter", "Legal", "Tabloid", "Ledger"],
            "default": "A4"
          },
          "margins": {
            "type": "string",
            "enum": ["none", "minimum", "normal", "wide"],
            "default": "none",
            "description": "Sırasıyla 0 / 6.35 / 12.7 / 25.4 mm"
          },
          "scale": {
            "type": "number",
            "minimum": 10,
            "maximum": 200,
            "default": 100,
            "description": "Yüzde cinsinden — 100 = orijinal boyut"
          },
          "background_graphics": {
            "type": "boolean",
            "default": true,
            "description": "Arka plan renklerini ve görsellerini PDF'e dahil et"
          },
          "landscape": {
            "type": "boolean",
            "default": false
          },
          "emulate_media": {
            "type": "string",
            "enum": ["screen", "print"],
            "description": "CSS @media tipi. print → print-only stil; screen → varsayılan ekran görünümü."
          },
          "timeout_ms": {
            "type": "integer",
            "minimum": 5000,
            "maximum": 600000,
            "default": 300000,
            "description": "Tek istek için timeout (ms). Sunucu üst sınırı RENDER_TIMEOUT_MAX_MS env'inden gelir."
          },
          "cdn_profile": {
            "type": "string",
            "description": "Env'de `CDN_PROFILE__<ad>__*` ile tanımlı named profile. Multi-tenant senaryolarda credential body'de yollamaktan kaçınmak için tercih edilen yol.",
            "example": "acme"
          },
          "cdn": {
            "$ref": "#/components/schemas/CdnOverride",
            "description": "Tek istek için CDN ayarlarını override et. Tüm alanlar opsiyonel — eksik alanlar env'deki default profile'dan tamamlanır. Örn. sadece `folder` göndererek müşteri-bazlı klasörlemeyi yapabilirsin."
          }
        }
      },
      "CdnOverride": {
        "type": "object",
        "description": "Hepsi opsiyonel — partial override mümkün. Eksik kalan zorunlu alanlar (access_key, secret_key, bucket) env'deki CDN_* default'undan alınır.",
        "properties": {
          "access_key":      { "type": "string" },
          "secret_key":      { "type": "string" },
          "bucket":          { "type": "string" },
          "region":          { "type": "string", "example": "fra1" },
          "endpoint":        { "type": "string", "format": "uri", "example": "https://fra1.digitaloceanspaces.com" },
          "folder":          { "type": "string", "example": "musteri/2026/raporlar" },
          "custom_domain":   { "type": "string", "example": "cdn.dinamikkatalog.com" },
          "force_path_style":{ "type": "boolean", "default": false },
          "keep_local":      { "type": "boolean", "deprecated": true, "description": "Artık kullanılmıyor — lokal disk yazımı yok. Yutuluyor." }
        }
      },
      "RenderResponse": {
        "type": "object",
        "required": ["success", "filename", "file_size", "duration_ms", "cdn_url"],
        "properties": {
          "success":     { "type": "boolean", "const": true },
          "filename":    { "type": "string" },
          "file_size":   { "type": "integer", "description": "byte" },
          "duration_ms": { "type": "integer" },
          "cdn_url":     { "type": "string", "format": "uri" },
          "bucket":      { "type": "string" },
          "key":         { "type": "string", "description": "S3 object key — folder dahil" }
        }
      },
      "LegacyRenderResponse": {
        "allOf": [
          { "$ref": "#/components/schemas/RenderResponse" },
          {
            "type": "object",
            "properties": {
              "message":      { "type": "string", "example": "PDF başarıyla oluşturuldu ve CDN'e yüklendi" },
              "duration":     { "type": "string", "description": "saniye, eski sürümle uyumlu format", "example": "2.84" },
              "download_url": { "type": "string", "format": "uri", "description": "cdn_url ile aynı — eski clientlar bu alana bakıyordu" },
              "view_url":     { "type": "string", "format": "uri", "description": "cdn_url ile aynı" }
            }
          }
        ]
      },
      "AsyncAcceptResponse": {
        "type": "object",
        "properties": {
          "success":  { "type": "boolean", "const": true },
          "async":    { "type": "boolean", "const": true },
          "job_id":   { "type": "string", "format": "uuid" },
          "status":   { "type": "string", "example": "queued" },
          "poll_url": { "type": "string", "example": "/v1/jobs/<uuid>" }
        }
      },
      "JobRecord": {
        "type": "object",
        "properties": {
          "id":        { "type": "string", "format": "uuid" },
          "status":    { "type": "string", "enum": ["queued", "running", "completed", "failed"] },
          "createdAt": { "type": "integer", "description": "epoch ms" },
          "updatedAt": { "type": "integer" },
          "result": {
            "type": "object",
            "nullable": true,
            "properties": {
              "filename":   { "type": "string" },
              "fileSize":   { "type": "integer" },
              "durationMs": { "type": "integer" },
              "cdnUrl":     { "type": "string", "format": "uri" },
              "bucket":     { "type": "string" },
              "key":        { "type": "string" }
            }
          },
          "error": {
            "type": "object",
            "nullable": true,
            "properties": {
              "code":    { "type": "string" },
              "message": { "type": "string" }
            }
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "success": { "type": "boolean", "const": false },
          "error": {
            "type": "object",
            "properties": {
              "code":    { "type": "string", "example": "BAD_REQUEST" },
              "message": { "type": "string" },
              "details": { "description": "İsteğe bağlı — schema validation hatasında zod issues array'i" }
            }
          }
        }
      }
    }
  }
}
