<?php
/**
 * print.opensprinklershop.de – Druck-Webservice v2.0
 *
 * Endpunkte (alle JSON-Antworten, alle bis auf /v1/health erfordern Bearer-Auth):
 *
 *   GET  /v1/health               → {"ok":true,"version":"…"}
 *   POST /v1/job     multipart    files[]=@pdf meta={"order_id":…,"lang":"de"}
 *                                 → {"ok":true,"job_id":"job-20260430-123456-42","queued":N}
 *   POST /v1/upload  multipart    filename=<name>  file=@<pdf>   (legacy)
 *   POST /v1/items   JSON|form    order_id=<int>   content=<text> (legacy)
 *   POST /v1/print   (leer)       → triggert Verarbeitung         (legacy)
 */

declare(strict_types=1);

const PRINT_DIR        = '/data/print';
const QUEUE_DIR        = '/data/print/queue';
const TRIGGER_FILE     = '/data/print/.trigger';
const ENV_FILE         = '/etc/print-api.env';
const MAX_UPLOAD_BYTES = 50 * 1024 * 1024;
const VERSION          = '2.1.0';
const DEBUG_LOG        = '/var/log/apache2/print-api-debug.log';

function dbg(string $tag, array $data = []): void {
    $line = '[' . date('c') . '] ' . $tag
          . ' ip='   . ($_SERVER['REMOTE_ADDR']  ?? '?')
          . ' path=' . ($_SERVER['REQUEST_URI']  ?? '?')
          . ' '      . json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
          . "\n";
    @file_put_contents(DEBUG_LOG, $line, FILE_APPEND | LOCK_EX);
}

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');

function respond(int $code, array $payload): never {
    http_response_code($code);
    echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function load_token(): string {
    if (!is_readable(ENV_FILE)) respond(500, ['error' => 'server_misconfigured', 'detail' => 'env unreadable']);
    foreach (file(ENV_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
        if (str_starts_with(ltrim($line), '#')) continue;
        if (!str_contains($line, '=')) continue;
        [$k, $v] = explode('=', $line, 2);
        if (trim($k) === 'PRINT_API_TOKEN') return trim($v, " \t\"'");
    }
    respond(500, ['error' => 'server_misconfigured', 'detail' => 'no token']);
}

function require_auth(): void {
    $expected = load_token();
    $hdr = $_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? '';
    if (!preg_match('/^Bearer\s+(\S+)$/', $hdr, $m) || !hash_equals($expected, $m[1])) {
        respond(401, ['error' => 'unauthorized']);
    }
}

function sanitize_filename(string $name): string {
    $name = basename($name);
    if ($name === '' || $name[0] === '.') respond(400, ['error' => 'invalid_filename', 'detail' => 'empty or starts with dot']);
    if (!preg_match('/^[A-Za-z0-9 _.+\-]{1,200}$/u', $name)) {
        respond(400, ['error' => 'invalid_filename', 'detail' => 'illegal characters', 'name' => $name]);
    }
    // Alle PDFs und legacy items-Dateien sind erlaubt – der Zeichenfilter oben sichert bereits ab
    if (preg_match('/\.pdf$/i', $name)) return $name;
    if (preg_match('/^items_\d+$/', $name)) return $name;
    respond(400, ['error' => 'invalid_filename', 'detail' => 'filename not in whitelist', 'name' => $name]);
}

function read_body(): array {
    $ct = $_SERVER['CONTENT_TYPE'] ?? '';
    if (stripos($ct, 'application/json') !== false) {
        $raw  = file_get_contents('php://input') ?: '';
        $data = json_decode($raw, true);
        if (!is_array($data)) respond(400, ['error' => 'invalid_json']);
        return $data;
    }
    return $_POST;
}

// ── Routing ──────────────────────────────────────────────────────────────────
$method = $_SERVER['REQUEST_METHOD'];
$path   = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$path   = rtrim($path, '/') ?: '/';

// ── Health ────────────────────────────────────────────────────────────────────
if ($path === '/v1/health' && $method === 'GET') {
    @mkdir(QUEUE_DIR, 0775, true);
    respond(is_writable(PRINT_DIR) ? 200 : 503, [
        'ok'        => is_writable(PRINT_DIR),
        'version'   => VERSION,
        'print_dir' => PRINT_DIR,
        'queue_dir' => QUEUE_DIR,
        'writable'  => is_writable(PRINT_DIR),
    ]);
}

require_auth();

// ── Docs-Listing (für Dokumentenbibliothek-Import) ────────────────────────────
if ($path === '/v1/docs' && $method === 'GET') {
    $files = [];
    foreach (glob(PRINT_DIR . '/*.pdf') ?: [] as $f) {
        $bn      = basename($f);
        $files[] = [
            'name' => $bn,
            'size' => filesize($f),
            'mtime' => filemtime($f),
        ];
    }
    usort($files, fn($a, $b) => strcmp($a['name'], $b['name']));
    respond(200, ['ok' => true, 'files' => $files]);
}

// ── Einzelnes Dokument herunterladen (für Import) ─────────────────────────────
if (preg_match('#^/v1/docs/(.+\.pdf)$#i', $path, $m) && $method === 'GET') {
    $filename = basename(urldecode($m[1]));
    // Allow safe filenames including Unicode (German umlauts etc.)
    if (strlen($filename) > 200 || $filename[0] === '.' || preg_match('/[\/\\\0]/', $filename)) {
        respond(400, ['error' => 'invalid_filename']);
    }
    $filepath = PRINT_DIR . '/' . $filename;
    if (!file_exists($filepath) || !is_readable($filepath)) {
        respond(404, ['error' => 'not_found', 'file' => $filename]);
    }
    // Override content-type for binary response
    header('Content-Type: application/pdf');
    header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"');
    header('Content-Length: ' . filesize($filepath));
    header('Cache-Control: private, no-store');
    // Remove JSON content-type set at top
    header_remove('Content-Type');
    header('Content-Type: application/pdf');
    readfile($filepath);
    exit;
}


if ($path === '/v1/job' && $method === 'POST') {
    @mkdir(QUEUE_DIR, 0775, true);

    // Metadaten aus 'meta'-Feld (JSON-String) oder POST-Feldern
    $meta_raw = $_POST['meta'] ?? '{}';
    $meta     = is_string($meta_raw) ? (json_decode($meta_raw, true) ?? []) : [];
    $order_id = (int) ($meta['order_id'] ?? $_POST['order_id'] ?? 0);
    $lang     = preg_replace('/[^a-z]/', '', strtolower((string)($meta['lang'] ?? $_POST['lang'] ?? 'de')));

    if (!isset($_FILES['files'])) {
        dbg('job.no_files', ['meta' => $meta]);
        respond(400, ['error' => 'no_files']);
    }

    // Einzelne Datei oder Array normalisieren
    $files_raw = $_FILES['files'];
    $is_multi  = is_array($files_raw['name']);
    if (!$is_multi) {
        // Einzelne Datei als Array wrappen
        $files_raw = array_map(fn($v) => [$v], $files_raw);
    }
    $count = count($files_raw['name']);

    // Eindeutiges Job-Verzeichnis anlegen
    $job_id  = 'job-' . date('Ymd-His') . '-' . mt_rand(1000, 9999);
    $job_dir = QUEUE_DIR . '/' . $job_id;
    if (!mkdir($job_dir, 0775, true)) {
        respond(500, ['error' => 'cannot_create_job_dir']);
    }

    // Metadaten als JSON speichern
    file_put_contents($job_dir . '/meta.json', json_encode([
        'order_id' => $order_id,
        'lang'     => $lang,
        'created'  => date('c'),
    ], JSON_PRETTY_PRINT));

    $stored = [];
    for ($i = 0; $i < $count; $i++) {
        $err  = (int) $files_raw['error'][$i];
        $size = (int) $files_raw['size'][$i];
        $orig = (string) $files_raw['name'][$i];
        $tmp  = (string) $files_raw['tmp_name'][$i];

        if ($err !== UPLOAD_ERR_OK) {
            dbg('job.file_error', ['i' => $i, 'err' => $err, 'name' => $orig]);
            continue;
        }
        if ($size > MAX_UPLOAD_BYTES) {
            dbg('job.file_too_large', ['name' => $orig, 'size' => $size]);
            continue;
        }

        $name   = sanitize_filename($orig);
        $target = $job_dir . '/' . $name;

        if (!move_uploaded_file($tmp, $target)) {
            dbg('job.move_failed', ['name' => $name, 'target' => $target]);
            continue;
        }
        @chmod($target, 0664);
        $stored[] = $name;
    }

    if (empty($stored)) {
        // Job-Verzeichnis wieder entfernen
        @rmdir($job_dir);
        respond(400, ['error' => 'no_valid_files']);
    }

    dbg('job.queued', ['job_id' => $job_id, 'order_id' => $order_id, 'files' => $stored]);

    // Verarbeitung anstoßen: print.sh mit Job-Verzeichnis aufrufen
    $script = PRINT_DIR . '/print.sh';
    if (is_executable($script)) {
        $log = PRINT_DIR . '/archiv/print-' . $job_id . '.log';
        @mkdir(PRINT_DIR . '/archiv', 0775, true);
        exec(escapeshellarg($script) . ' ' . escapeshellarg($job_dir) . ' > ' . escapeshellarg($log) . ' 2>&1 &');
    } else {
        // Fallback: trigger-Datei (für systemd path unit)
        touch(TRIGGER_FILE);
    }

    respond(202, [
        'ok'     => true,
        'job_id' => $job_id,
        'queued' => count($stored),
        'files'  => $stored,
    ]);
}

// ── Legacy: /v1/upload ───────────────────────────────────────────────────────
if ($path === '/v1/upload' && $method === 'POST') {
    dbg('upload.received', ['POST_keys' => array_keys($_POST), 'FILES_keys' => array_keys($_FILES)]);
    $rawName = (string)($_POST['filename'] ?? $_POST['name'] ?? '');
    $name    = sanitize_filename($rawName);
    if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
        respond(400, ['error' => 'no_file', 'upload_err' => $_FILES['file']['error'] ?? null]);
    }
    if ($_FILES['file']['size'] > MAX_UPLOAD_BYTES) respond(413, ['error' => 'file_too_large']);
    $target = PRINT_DIR . '/' . $name;
    if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
        respond(500, ['error' => 'store_failed']);
    }
    @chmod($target, 0664);
    respond(200, ['ok' => true, 'stored' => $name, 'bytes' => filesize($target)]);
}

// ── Legacy: /v1/items ────────────────────────────────────────────────────────
if ($path === '/v1/items' && $method === 'POST') {
    $body    = read_body();
    $orderId = (int)($body['order_id'] ?? 0);
    $content = (string)($body['content'] ?? '');
    if ($orderId <= 0) respond(400, ['error' => 'invalid_order_id']);
    if ($content === '') respond(400, ['error' => 'empty_content']);
    if (strlen($content) > 64 * 1024) respond(413, ['error' => 'content_too_large']);
    $name   = 'items_' . $orderId;
    $target = PRINT_DIR . '/' . $name;
    if (file_put_contents($target, $content, LOCK_EX) === false) respond(500, ['error' => 'write_failed']);
    @chmod($target, 0664);
    respond(200, ['ok' => true, 'stored' => $name, 'bytes' => strlen($content)]);
}

// ── Legacy: /v1/print ────────────────────────────────────────────────────────
if ($path === '/v1/print' && $method === 'POST') {
    if (touch(TRIGGER_FILE) === false) respond(500, ['error' => 'trigger_failed']);
    respond(202, ['ok' => true, 'triggered' => true]);
}

respond(404, ['error' => 'not_found', 'path' => $path, 'method' => $method]);
