<?php

declare(strict_types=1);

namespace Ekdos\Relay;

use Ekdos\Auth\Session;
use Ekdos\Http\Middleware\SessionMiddleware;
use Ekdos\N8n\Cache;
use Ekdos\N8n\Client;
use Ekdos\N8n\Endpoints;
use Ekdos\N8n\Reply;
use Ekdos\Support\Config;
use Ekdos\Support\Json;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

/**
 * Gemeinsame Basis aller n8n-Weiterleitungen.
 *
 * Jeder abgeleitete Controller bleibt damit auf dem, was ihn ausmacht: welche
 * Adresse, welche Gruppe, welche Meldung im Fehlerfall.
 */
abstract readonly class RelayController
{
    public function __construct(
        protected Client $n8n,
        protected Endpoints $n8nUrl,
        protected Cache $cache,
        protected Config $config,
    ) {}

    /**
     * Holt eine Liste durch den Cache.
     *
     * @param array{0: string, 1: int, 2: int} $policy siehe CachePolicy
     */
    protected function cachedGet(array $policy, string $url, array $headers = []): Reply
    {
        [$group, $ttl, $stale] = $policy;

        return $this->cache->remember(
            $group,
            $url . '|' . json_encode($headers, JSON_THROW_ON_ERROR),
            $ttl,
            $stale,
            fn (): Reply => $this->n8n->get($url, $headers),
        );
    }

    protected function session(Request $request): Session
    {
        // RequireAuth laeuft vor jedem Controller, deshalb ist die Sitzung hier gesetzt.
        return SessionMiddleware::of($request) ?? throw new \LogicException('Route ohne RequireAuth erreicht.');
    }

    protected function body(Request $request): array
    {
        $parsed = $request->getParsedBody();

        if (is_array($parsed)) {
            return $parsed;
        }

        return Json::decode((string) $request->getBody());
    }

    protected function query(Request $request, string $key, string $fallback = ''): string
    {
        $value = $request->getQueryParams()[$key] ?? null;

        return is_string($value) ? trim($value) : $fallback;
    }

    /** Nimmt Zeichenketten und Zahlen an und liefert immer eine getrimmte Zeichenkette. */
    protected static function text(array $source, string $key): string
    {
        $value = $source[$key] ?? null;

        return is_string($value) || is_int($value) || is_float($value) ? trim((string) $value) : '';
    }

    /**
     * Einheitliche Fehlerantwort fuer einen fehlgeschlagenen n8n-Aufruf.
     * Ein Verbindungsfehler (Status 0) wird zu 502.
     */
    protected function upstreamError(Response $response, Reply $reply, string $message): Response
    {
        return Json::error($response, $message, $reply->statusOr(502));
    }

    /** Antwortet mit einer Liste und markiert, wenn sie aus dem Rueckfall-Cache stammt. */
    protected function listResponse(Response $response, Reply $reply, string $key, string $as): Response
    {
        $payload = [$as => $reply->items($key), 'refreshedAt' => gmdate('c')];

        if ($reply->stale) {
            $payload['stale'] = true;
            $payload['notice'] = 'n8n ist gerade nicht erreichbar. Angezeigt werden die zuletzt bekannten Daten.';
        }

        return Json::write($response, $payload);
    }

    /** Reicht eine Binaerantwort (PDF) unveraendert durch. */
    protected function pdfResponse(Response $response, string $bytes, string $filename): Response
    {
        $response->getBody()->write($bytes);

        return $response->withHeader('Content-Type', 'application/pdf')
            ->withHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
            ->withHeader('Cache-Control', 'no-store');
    }

    /** Geteiltes Geheimnis fuer die geschuetzten n8n-Workflows. */
    protected function invoiceSyncSecret(): string
    {
        return $this->config->string('N8N_INVOICE_SYNC_SECRET');
    }
}
