-- =============================================================================
-- DC_BACKEND_PKG
-- PL/SQL-Schnittstelle zum Quarkus dc-backend (Kubernetes Service dc-backend:8090)
--
-- Voraussetzungen:
--   - APEX_WEB_SERVICE-Berechtigung für das Schema:
--       GRANT EXECUTE ON APEX_240100.APEX_WEB_SERVICE TO <schema>;  -- APEX-Version anpassen
--   - ACL für ausgehende HTTP-Verbindung im selben Namespace:
--       DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
--         host => 'dc-backend', lower_port => 8090, upper_port => 8090,
--         ace  => xs$ace_type(privilege_list => xs$name_list('connect','resolve'),
--                             principal_name => '<schema>', principal_type => xs_acl.ptype_db));
--
-- Deployment:
--   sqlplus user/pass@db @"Scripts/DC_BACKEND_PKG.sql"
-- =============================================================================


-- =============================================================================
-- Package Spec
-- =============================================================================
CREATE OR REPLACE PACKAGE DC_BACKEND_PKG AS

    -- Basis-URL des dc-backend Kubernetes Service
    c_base_url  CONSTANT VARCHAR2(200) := 'http://dc-backend:8090';

    -- API-Key (X-API-KEY Header).
    -- Leer = kein Auth (Dev-Modus).
    -- Zur Laufzeit setzen: DC_BACKEND_PKG.g_api_key := 'mein-key';
    g_api_key   VARCHAR2(500) := '';

    -- -------------------------------------------------------------------------
    -- Startet die KI-gestützte Dokumentenprüfung für ein Projekt (asynchron).
    -- Das Projekt muss im Status PENDING sein.
    -- POST /check/{p_project_id} → 202 Accepted
    --
    -- p_project_id   ID des dc_projects-Eintrags
    -- p_status_code  HTTP-Statuscode der Antwort (202 = OK, 409 = nicht PENDING)
    -- p_response     JSON-Antwort des Backend
    -- -------------------------------------------------------------------------
    PROCEDURE start_check(
        p_project_id   IN  NUMBER,
        p_status_code  OUT NUMBER,
        p_response     OUT VARCHAR2
    );

    -- -------------------------------------------------------------------------
    -- Health-Check: prüft ob der dc-backend Service erreichbar ist.
    -- GET /check/health → 200 UP
    --
    -- Rückgabe: TRUE = Service läuft, FALSE = nicht erreichbar
    -- -------------------------------------------------------------------------
    FUNCTION health_check RETURN BOOLEAN;

    -- -------------------------------------------------------------------------
    -- Setzt Projekt auf PENDING zurück – eigene Transaktion (AUTONOMOUS COMMIT).
    -- dc_projects.status → PENDING, report_markdown → NULL
    -- dc_project_documents.original_text/translated_text → NULL, progress → 0
    -- dc_results löschen
    -- -------------------------------------------------------------------------
    PROCEDURE reset_project(
        p_project_id IN NUMBER
    );

    -- -------------------------------------------------------------------------
    -- Startet die Prüfung und wirft eine Exception wenn nicht 202 zurückkommt.
    -- Convenience-Wrapper für direkte APEX-Button-Aufrufe.
    --
    -- p_reset  TRUE = Projekt vor dem Start zurücksetzen (eigener COMMIT):
    --          dc_projects.status → PENDING, report_markdown → NULL
    --          dc_project_documents.original_text/translated_text → NULL, progress → 0
    --          dc_results löschen
    -- -------------------------------------------------------------------------
    PROCEDURE start_check_or_raise(
        p_project_id IN NUMBER,
        p_reset      IN BOOLEAN DEFAULT FALSE
    );

    -- -------------------------------------------------------------------------
    -- Fortschritts-Record
    -- -------------------------------------------------------------------------
    TYPE t_project_status IS RECORD (
        status               dc_projects.status%TYPE,
        processing_progress  dc_projects.progress%TYPE,
        is_completed         BOOLEAN,
        is_running           BOOLEAN,
        is_pending           BOOLEAN
    );

    -- -------------------------------------------------------------------------
    -- Liest Status und Fortschritt eines Projekts direkt aus der DB.
    -- Kein REST-Aufruf – sofort und transaktionssicher.
    --
    -- Beispiel:
    --   l_s := DC_BACKEND_PKG.get_status(1);
    --   DBMS_OUTPUT.PUT_LINE(l_s.status || ' ' || l_s.processing_progress || '%');
    -- -------------------------------------------------------------------------
    FUNCTION get_status(
        p_project_id IN NUMBER
    ) RETURN t_project_status;

    -- -------------------------------------------------------------------------
    -- Wartet (polling) bis das Projekt COMPLETED oder ein Timeout erreicht ist.
    -- Wirft eine Exception bei Timeout oder wenn das Projekt nicht gefunden wird.
    --
    -- p_timeout_sec   Max. Wartezeit in Sekunden (Default: 1800 = 30 Min.)
    -- p_interval_sec  Polling-Intervall in Sekunden (Default: 5)
    -- -------------------------------------------------------------------------
    PROCEDURE wait_for_completion(
        p_project_id    IN NUMBER,
        p_timeout_sec   IN NUMBER DEFAULT 1800,
        p_interval_sec  IN NUMBER DEFAULT 5
    );

    -- -------------------------------------------------------------------------
    -- Konvertiert einen Markdown-CLOB zu DOCX oder PDF.
    -- POST /export/markdown → BLOB (application/pdf oder .docx)
    --
    -- p_markdown  Markdown-Text (z.B. dc_projects.report_markdown)
    -- p_format    'PDF' (Standard) oder 'DOCX'
    -- Rückgabe    BLOB mit dem fertigen Dokument
    -- -------------------------------------------------------------------------
    FUNCTION convert_markdown(
        p_markdown IN CLOB,
        p_format   IN VARCHAR2 DEFAULT 'PDF'
    ) RETURN BLOB;

    -- -------------------------------------------------------------------------
    -- Convenience: liest report_markdown des Projekts und konvertiert zu PDF/DOCX.
    --
    -- Beispiel:
    --   v_blob := DC_BACKEND_PKG.project_report(1, 'PDF');
    -- -------------------------------------------------------------------------
    FUNCTION project_report(
        p_project_id IN NUMBER,
        p_format     IN VARCHAR2 DEFAULT 'PDF'
    ) RETURN BLOB;

    -- -------------------------------------------------------------------------
    -- Generiert den Markdown-Prüfbericht für ein Projekt neu und speichert ihn
    -- in dc_projects.report_markdown. Ruft intern DC_UTILS_PKG.generate_report auf.
    --
    -- Beispiel:
    --   DC_BACKEND_PKG.regenerate_report(1);
    -- -------------------------------------------------------------------------
    PROCEDURE regenerate_report(
        p_project_id IN NUMBER
    );

    -- -------------------------------------------------------------------------
    -- Startet die asynchrone Katalog-Generierung aus einem Regelwerk-Dokument.
    -- Gibt sofort zurück (202 Accepted-Muster) – die KI läuft im Hintergrund.
    --
    -- p_file             BLOB des Regelwerks (PDF, DOCX, ODT, TXT)
    -- p_mime_type        MIME-Typ, z.B. 'application/pdf'
    -- p_filename         Dateiname (wird im multipart-Part übergeben)
    -- p_catalog_name     Name des anzulegenden Katalogs (z.B. 'ADSp 2017')
    -- p_document_type_id ID des Dokumenttyps (dc_document_types.id)
    -- p_description      Optionale Beschreibung
    -- Rückgabe           ID des Katalog-Placeholders (Status: GENERATING)
    --
    -- Beispiel (mit Warten auf Abschluss):
    --   DECLARE
    --     l_blob BLOB;
    --     l_id   NUMBER;
    --   BEGIN
    --     SELECT file_blob INTO l_blob FROM dc_reference_documents WHERE id = 1;
    --     l_id := DC_BACKEND_PKG.generate_catalog(
    --                p_file             => l_blob,
    --                p_mime_type        => 'application/pdf',
    --                p_filename         => 'ADSp-2017.pdf',
    --                p_catalog_name     => 'ADSp 2017',
    --                p_document_type_id => 1,
    --                p_description      => 'Allgemeine Deutsche Spediteurbedingungen 2017');
    --     DBMS_OUTPUT.PUT_LINE('Katalog ID: ' || l_id || ' (läuft im Hintergrund)');
    --     DC_BACKEND_PKG.wait_for_catalog(l_id);   -- optional, wartet bis READY
    --     DBMS_OUTPUT.PUT_LINE('Katalog fertig!');
    --   END;
    -- -------------------------------------------------------------------------
    FUNCTION generate_catalog(
        p_file             IN BLOB,
        p_mime_type        IN VARCHAR2 DEFAULT 'application/pdf',
        p_filename         IN VARCHAR2 DEFAULT 'dokument.pdf',
        p_catalog_name     IN VARCHAR2,
        p_document_type_id IN NUMBER,
        p_description      IN VARCHAR2 DEFAULT NULL
    ) RETURN NUMBER;

    -- -------------------------------------------------------------------------
    -- Liest den Generierungsstatus direkt aus der DB (kein REST-Aufruf).
    -- Rückgabe: 'GENERATING', 'READY' oder 'ERROR'
    -- -------------------------------------------------------------------------
    FUNCTION get_catalog_status(
        p_catalog_id IN NUMBER
    ) RETURN VARCHAR2;

    -- -------------------------------------------------------------------------
    -- Wartet (polling) bis der Katalog den Status READY erreicht hat.
    -- Wirft eine Exception bei Timeout oder wenn der Katalog ERROR meldet.
    --
    -- p_timeout_sec   Max. Wartezeit in Sekunden (Default: 1800 = 30 Min.)
    -- p_interval_sec  Polling-Intervall in Sekunden (Default: 10)
    -- -------------------------------------------------------------------------
    PROCEDURE wait_for_catalog(
        p_catalog_id    IN NUMBER,
        p_timeout_sec   IN NUMBER DEFAULT 1800,
        p_interval_sec  IN NUMBER DEFAULT 10
    );

END DC_BACKEND_PKG;
/


-- =============================================================================
-- Package Body
-- =============================================================================
CREATE OR REPLACE PACKAGE BODY DC_BACKEND_PKG AS

    -- -------------------------------------------------------------------------
    -- Setzt Projekt auf PENDING zurück – eigene Transaktion (AUTONOMOUS).
    -- -------------------------------------------------------------------------
    PROCEDURE reset_project(p_project_id IN NUMBER) IS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        UPDATE dc_projects
           SET status          = 'PENDING',
               report_markdown = NULL
         WHERE id = p_project_id;

        UPDATE dc_project_documents
           SET original_text   = NULL,
               translated_text = NULL,
               progress        = 0,
               status          = 'PENDING'
         WHERE project_id = p_project_id;

        DELETE FROM dc_results
         WHERE project_id = p_project_id;

        COMMIT;
    END reset_project;


    -- -------------------------------------------------------------------------
    -- Setzt gemeinsame Request-Header (Content-Type + optional API-Key)
    -- -------------------------------------------------------------------------
    PROCEDURE set_headers IS
        l_idx PLS_INTEGER := 1;
    BEGIN
        APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;

        APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).NAME  := 'Content-Type';
        APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).VALUE := 'application/json';
        l_idx := l_idx + 1;

        IF g_api_key IS NOT NULL AND LENGTH(TRIM(g_api_key)) > 0 THEN
            APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).NAME  := 'X-API-KEY';
            APEX_WEB_SERVICE.G_REQUEST_HEADERS(l_idx).VALUE := g_api_key;
        END IF;
    END set_headers;


    -- -------------------------------------------------------------------------
    PROCEDURE start_check(
        p_project_id   IN  NUMBER,
        p_status_code  OUT NUMBER,
        p_response     OUT VARCHAR2
    ) IS
        l_url      VARCHAR2(500);
        l_response CLOB;
    BEGIN
        l_url := c_base_url || '/check/' || TO_CHAR(p_project_id);

        set_headers();

        l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
            p_url         => l_url,
            p_http_method => 'POST',
            p_body        => '{}'   -- leerer Body, projectId ist im Pfad
        );

        p_status_code := APEX_WEB_SERVICE.G_STATUS_CODE;
        p_response    := SUBSTR(l_response, 1, 4000);

    EXCEPTION
        WHEN OTHERS THEN
            p_status_code := -1;
            p_response    := 'Verbindungsfehler: ' || SQLERRM;
    END start_check;


    -- -------------------------------------------------------------------------
    FUNCTION health_check RETURN BOOLEAN IS
        l_url      VARCHAR2(500);
        l_response CLOB;
        l_status   NUMBER;
    BEGIN
        l_url := c_base_url || '/check/health';

        set_headers();

        l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
            p_url         => l_url,
            p_http_method => 'GET'
        );

        l_status := APEX_WEB_SERVICE.G_STATUS_CODE;
        RETURN l_status = 200;

    EXCEPTION
        WHEN OTHERS THEN
            RETURN FALSE;
    END health_check;


    -- -------------------------------------------------------------------------
    PROCEDURE start_check_or_raise(
        p_project_id IN NUMBER,
        p_reset      IN BOOLEAN DEFAULT FALSE
    ) IS
        l_status   NUMBER;
        l_response VARCHAR2(4000);
    BEGIN
        IF p_reset THEN
            reset_project(p_project_id);
        END IF;

        start_check(
            p_project_id  => p_project_id,
            p_status_code => l_status,
            p_response    => l_response
        );

        IF l_status = 202 THEN
            -- OK: Verarbeitung wurde gestartet
            NULL;
        ELSIF l_status = 409 THEN
            RAISE_APPLICATION_ERROR(-20100,
                'Projekt ' || p_project_id || ' ist nicht im Status PENDING.');
        ELSIF l_status = 401 THEN
            RAISE_APPLICATION_ERROR(-20101,
                'Ungültiger API-Key für dc-backend.');
        ELSIF l_status = -1 THEN
            RAISE_APPLICATION_ERROR(-20102,
                'dc-backend nicht erreichbar: ' || l_response);
        ELSE
            RAISE_APPLICATION_ERROR(-20103,
                'Unerwarteter HTTP-Status ' || l_status || ': ' || l_response);
        END IF;
    END start_check_or_raise;


    -- -------------------------------------------------------------------------
    FUNCTION get_status(
        p_project_id IN NUMBER
    ) RETURN t_project_status IS
        l_rec t_project_status;
    BEGIN
        SELECT status, progress
          INTO l_rec.status, l_rec.processing_progress
          FROM dc_projects
         WHERE id = p_project_id;

        l_rec.is_completed := l_rec.status = 'COMPLETED';
        l_rec.is_running   := l_rec.status = 'IN_PROGRESS';
        l_rec.is_pending   := l_rec.status = 'PENDING';
        RETURN l_rec;

    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20110,
                'Projekt ' || p_project_id || ' nicht gefunden.');
    END get_status;


    -- -------------------------------------------------------------------------
    PROCEDURE wait_for_completion(
        p_project_id    IN NUMBER,
        p_timeout_sec   IN NUMBER DEFAULT 1800,
        p_interval_sec  IN NUMBER DEFAULT 5
    ) IS
        l_start  TIMESTAMP := SYSTIMESTAMP;
        l_status t_project_status;
    BEGIN
        LOOP
            l_status := get_status(p_project_id);

            EXIT WHEN l_status.is_completed;

            IF EXTRACT(SECOND FROM (SYSTIMESTAMP - l_start))
             + EXTRACT(MINUTE FROM (SYSTIMESTAMP - l_start)) * 60
             + EXTRACT(HOUR   FROM (SYSTIMESTAMP - l_start)) * 3600
               > p_timeout_sec
            THEN
                RAISE_APPLICATION_ERROR(-20111,
                    'Timeout nach ' || p_timeout_sec || 's – Projekt ' ||
                    p_project_id || ' noch im Status ' || l_status.status ||
                    ' (' || l_status.processing_progress || '%).');
            END IF;

            DBMS_SESSION.SLEEP(p_interval_sec);
        END LOOP;
    END wait_for_completion;


    -- -------------------------------------------------------------------------
    FUNCTION convert_markdown(
        p_markdown IN CLOB,
        p_format   IN VARCHAR2 DEFAULT 'PDF'
    ) RETURN BLOB IS
        l_url      VARCHAR2(500);
        l_body     CLOB;
        l_result   BLOB;
        l_status   NUMBER;
    BEGIN
        l_url := c_base_url || '/export/markdown';

        -- JSON-Body mit CLOB-Inhalt: APEX_JSON unterstützt CLOBs beliebiger Größe
        APEX_JSON.INITIALIZE_CLOB_OUTPUT;
        APEX_JSON.OPEN_OBJECT;
        APEX_JSON.WRITE('content', p_markdown);
        APEX_JSON.WRITE('format',  UPPER(p_format));
        APEX_JSON.CLOSE_OBJECT;
        l_body := APEX_JSON.GET_CLOB_OUTPUT;
        APEX_JSON.FREE_OUTPUT;

        set_headers();

        l_result := APEX_WEB_SERVICE.MAKE_REST_REQUEST_B(
            p_url         => l_url,
            p_http_method => 'POST',
            p_body        => l_body
        );

        l_status := APEX_WEB_SERVICE.G_STATUS_CODE;

        IF l_status NOT IN (200, 201) THEN
            RAISE_APPLICATION_ERROR(-20200,
                'Export-Service Fehler: HTTP ' || l_status ||
                ' (format=' || p_format || ')');
        END IF;

        RETURN l_result;

    EXCEPTION
        WHEN OTHERS THEN
            IF SQLCODE = -20200 THEN RAISE; END IF;
            RAISE_APPLICATION_ERROR(-20201,
                'Verbindungsfehler beim Export: ' || SQLERRM);
    END convert_markdown;


    -- -------------------------------------------------------------------------
    FUNCTION project_report(
        p_project_id IN NUMBER,
        p_format     IN VARCHAR2 DEFAULT 'PDF'
    ) RETURN BLOB IS
        l_markdown CLOB;
    BEGIN
        SELECT report_markdown
          INTO l_markdown
          FROM dc_projects
         WHERE id = p_project_id;

        IF l_markdown IS NULL OR DBMS_LOB.GETLENGTH(l_markdown) = 0 THEN
            RAISE_APPLICATION_ERROR(-20202,
                'Projekt ' || p_project_id || ' hat noch keinen Markdown-Bericht. '
                || 'Bitte zuerst POST /api/dc/projects/' || p_project_id || '/report aufrufen.');
        END IF;

        RETURN convert_markdown(l_markdown, p_format);

    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20110,
                'Projekt ' || p_project_id || ' nicht gefunden.');
    END project_report;


    -- -------------------------------------------------------------------------
    PROCEDURE regenerate_report(
        p_project_id IN NUMBER
    ) IS
    BEGIN
        DC_UTILS_PKG.generate_report(p_project_id => p_project_id);
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20110,
                'Projekt ' || p_project_id || ' nicht gefunden.');
    END regenerate_report;


    -- -------------------------------------------------------------------------
    -- Interne Hilfsfunktion: schreibt einen VARCHAR2-Text als UTF8-Bytes in ein BLOB.
    -- -------------------------------------------------------------------------
    PROCEDURE append_text_to_blob(p_dest IN OUT BLOB, p_text IN VARCHAR2) IS
        l_raw RAW(32767);
    BEGIN
        l_raw := UTL_RAW.CAST_TO_RAW(p_text);
        DBMS_LOB.WRITEAPPEND(p_dest, UTL_RAW.LENGTH(l_raw), l_raw);
    END append_text_to_blob;


    -- -------------------------------------------------------------------------
    FUNCTION get_catalog_status(
        p_catalog_id IN NUMBER
    ) RETURN VARCHAR2 IS
        l_status VARCHAR2(20);
    BEGIN
        SELECT generation_status
          INTO l_status
          FROM dc_question_catalogs
         WHERE id = p_catalog_id;
        RETURN l_status;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE_APPLICATION_ERROR(-20310,
                'Katalog ' || p_catalog_id || ' nicht gefunden.');
    END get_catalog_status;


    -- -------------------------------------------------------------------------
    PROCEDURE wait_for_catalog(
        p_catalog_id    IN NUMBER,
        p_timeout_sec   IN NUMBER DEFAULT 1800,
        p_interval_sec  IN NUMBER DEFAULT 10
    ) IS
        l_start   TIMESTAMP := SYSTIMESTAMP;
        l_status  VARCHAR2(20);
        l_elapsed NUMBER;
    BEGIN
        LOOP
            l_status := get_catalog_status(p_catalog_id);

            IF l_status = 'READY' THEN
                RETURN;
            END IF;

            IF l_status = 'ERROR' THEN
                DECLARE
                    l_error VARCHAR2(4000);
                BEGIN
                    SELECT generation_error
                      INTO l_error
                      FROM dc_question_catalogs
                     WHERE id = p_catalog_id;
                    RAISE_APPLICATION_ERROR(-20311,
                        'Katalog ' || p_catalog_id || ' Generierung fehlgeschlagen: '
                        || NVL(l_error, 'Unbekannter Fehler'));
                END;
            END IF;

            -- Timeout prüfen
            l_elapsed :=
                EXTRACT(SECOND FROM (SYSTIMESTAMP - l_start))
              + EXTRACT(MINUTE FROM (SYSTIMESTAMP - l_start)) * 60
              + EXTRACT(HOUR   FROM (SYSTIMESTAMP - l_start)) * 3600;

            IF l_elapsed > p_timeout_sec THEN
                RAISE_APPLICATION_ERROR(-20312,
                    'Timeout nach ' || p_timeout_sec || 's – Katalog ' ||
                    p_catalog_id || ' noch im Status ' || l_status || '.');
            END IF;

            DBMS_SESSION.SLEEP(p_interval_sec);
        END LOOP;
    END wait_for_catalog;


    -- -------------------------------------------------------------------------
    FUNCTION generate_catalog(
        p_file             IN BLOB,
        p_mime_type        IN VARCHAR2 DEFAULT 'application/pdf',
        p_filename         IN VARCHAR2 DEFAULT 'dokument.pdf',
        p_catalog_name     IN VARCHAR2,
        p_document_type_id IN NUMBER,
        p_description      IN VARCHAR2 DEFAULT NULL
    ) RETURN NUMBER IS

        c_crlf      CONSTANT VARCHAR2(2)   := CHR(13) || CHR(10);
        l_boundary  VARCHAR2(50) := 'dc-boundary-' || TO_CHAR(SYSDATE, 'YYYYMMDDHHMI')
                                    || DBMS_RANDOM.STRING('X', 8);
        l_url       VARCHAR2(500);
        l_body      BLOB;
        l_response  CLOB;
        l_status    NUMBER;
        l_catalog_id NUMBER;

        -- Hilfsfunktion: multipart-Textfeld anfügen
        PROCEDURE add_field(p_name VARCHAR2, p_value VARCHAR2) IS
        BEGIN
            IF p_value IS NULL THEN RETURN; END IF;
            append_text_to_blob(l_body,
                '--' || l_boundary || c_crlf
                || 'Content-Disposition: form-data; name="' || p_name || '"' || c_crlf
                || c_crlf
                || p_value || c_crlf);
        END add_field;

    BEGIN
        l_url := c_base_url || '/catalog/generate';

        -- ---- multipart-Body aufbauen ----
        DBMS_LOB.CREATETEMPORARY(l_body, TRUE);

        -- Textfelder
        add_field('catalog_name',    p_catalog_name);
        add_field('document_type_id', TO_CHAR(p_document_type_id));
        add_field('description',     p_description);

        -- Datei-Part: Header
        append_text_to_blob(l_body,
            '--' || l_boundary || c_crlf
            || 'Content-Disposition: form-data; name="file"; filename="'
                || p_filename || '"' || c_crlf
            || 'Content-Type: ' || p_mime_type || c_crlf
            || c_crlf);

        -- Datei-Part: BLOB-Inhalt anhängen
        DBMS_LOB.APPEND(l_body, p_file);

        -- Datei-Part: Abschluss-CRLF
        append_text_to_blob(l_body, c_crlf);

        -- Abschließende Boundary
        append_text_to_blob(l_body, '--' || l_boundary || '--' || c_crlf);

        -- ---- HTTP-Request senden ----
        APEX_WEB_SERVICE.G_REQUEST_HEADERS.DELETE;
        APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).NAME  := 'Content-Type';
        APEX_WEB_SERVICE.G_REQUEST_HEADERS(1).VALUE :=
            'multipart/form-data; boundary=' || l_boundary;

        IF g_api_key IS NOT NULL AND LENGTH(TRIM(g_api_key)) > 0 THEN
            APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).NAME  := 'X-API-KEY';
            APEX_WEB_SERVICE.G_REQUEST_HEADERS(2).VALUE := g_api_key;
        END IF;

        l_response := APEX_WEB_SERVICE.MAKE_REST_REQUEST(
            p_url         => l_url,
            p_http_method => 'POST',
            p_body_blob   => l_body
        );
        DBMS_LOB.FREETEMPORARY(l_body);

        l_status := APEX_WEB_SERVICE.G_STATUS_CODE;

        -- 202 Accepted = asynchrone Verarbeitung gestartet
        IF l_status NOT IN (200, 201, 202) THEN
            RAISE_APPLICATION_ERROR(-20300,
                'Katalog-Generierung fehlgeschlagen: HTTP ' || l_status
                || ' – ' || SUBSTR(l_response, 1, 500));
        END IF;

        -- catalog_id aus JSON-Antwort lesen:
        -- {"catalog_id": 42, "catalog_name": "...", "generation_status": "GENERATING"}
        l_catalog_id := TO_NUMBER(JSON_VALUE(l_response, '$.catalog_id'));

        IF l_catalog_id IS NULL THEN
            RAISE_APPLICATION_ERROR(-20301,
                'Antwort enthält keine catalog_id: ' || SUBSTR(l_response, 1, 500));
        END IF;

        RETURN l_catalog_id;

    EXCEPTION
        WHEN OTHERS THEN
            IF DBMS_LOB.ISTEMPORARY(l_body) = 1 THEN
                DBMS_LOB.FREETEMPORARY(l_body);
            END IF;
            IF SQLCODE IN (-20300, -20301) THEN RAISE; END IF;
            RAISE_APPLICATION_ERROR(-20302,
                'Verbindungsfehler bei Katalog-Generierung: ' || SQLERRM);
    END generate_catalog;


END DC_BACKEND_PKG;
/


-- =============================================================================
-- Schnelltest (optional, auskommentiert)
-- =============================================================================
/*
-- Health-Check
BEGIN
    IF DC_BACKEND_PKG.health_check() THEN
        DBMS_OUTPUT.PUT_LINE('dc-backend: UP');
    ELSE
        DBMS_OUTPUT.PUT_LINE('dc-backend: NICHT ERREICHBAR');
    END IF;
END;
/

-- Prüfung starten (Projekt-ID anpassen)
DECLARE
    l_status   NUMBER;
    l_response VARCHAR2(4000);
BEGIN
    DC_BACKEND_PKG.start_check(
        p_project_id  => 1,
        p_status_code => l_status,
        p_response    => l_response
    );
    DBMS_OUTPUT.PUT_LINE('Status: ' || l_status);
    DBMS_OUTPUT.PUT_LINE('Response: ' || l_response);
END;
/

-- Bericht als PDF exportieren
DECLARE
    l_blob BLOB;
BEGIN
    DC_BACKEND_PKG.g_api_key := 'mein-api-key';
    l_blob := DC_BACKEND_PKG.project_report(p_project_id => 1, p_format => 'PDF');
    DBMS_OUTPUT.PUT_LINE('PDF-Größe: ' || DBMS_LOB.GETLENGTH(l_blob) || ' Bytes');
END;
/

-- Bericht als DOCX exportieren
DECLARE
    l_blob BLOB;
BEGIN
    DC_BACKEND_PKG.g_api_key := 'mein-api-key';
    l_blob := DC_BACKEND_PKG.project_report(p_project_id => 1, p_format => 'DOCX');
    DBMS_OUTPUT.PUT_LINE('DOCX-Größe: ' || DBMS_LOB.GETLENGTH(l_blob) || ' Bytes');
END;
/
*/
