<?php

declare(strict_types=1);

namespace Ekdos\Relay;

use Ekdos\Http\Middleware\SessionMiddleware;
use Ekdos\N8n\CachePolicy;
use Ekdos\Support\Json;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

final readonly class TicketsController extends RelayController
{
    /** Ticketnummern haben die Form 2026-021. */
    private const string TICKET_PATTERN = '/^20\d{2}-\d{3}$/';

    /**
     * Die offene Ticketliste. n8n liefert deutsche Feldnamen, die Oberflaeche
     * erwartet camelCase -- diese Abbildung ist die einzige echte Logik im Relay.
     */
    public function index(Request $request, Response $response): Response
    {
        $reply = $this->cachedGet(CachePolicy::Tickets, $this->n8nUrl->openTickets());

        if (!$reply->ok()) {
            return $this->upstreamError($response, $reply, 'Offene Tickets konnten nicht aus n8n geladen werden.');
        }

        $tickets = [];

        foreach ($reply->items('tickets') as $row) {
            if (!is_array($row)) {
                continue;
            }

            $ticket = self::mapTicket($row);

            // Zeilen ohne Ticketnummer sind fuer die Ansicht wertlos.
            if ($ticket['id'] !== '') {
                $tickets[] = $ticket;
            }
        }

        $payload = ['tickets' => $tickets, 'refreshedAt' => gmdate('c')];

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

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

    /**
     * Wird von Workflow4A nach einer Ticketerstellung aufgerufen.
     *
     * Anders als frueher ist das kein Platzhalter mehr: der Aufruf verwirft den
     * Ticket-Cache, sodass die naechste Abfrage der Oberflaeche garantiert die
     * neuen Daten sieht statt auf den TTL-Ablauf zu warten.
     */
    public function refresh(Request $request, Response $response): Response
    {
        $secret = $this->config->string('N8N_REFRESH_SECRET');

        // Entweder eine angemeldete Sitzung oder das geteilte Geheimnis aus n8n.
        if (SessionMiddleware::of($request) === null) {
            $presented = $request->getHeaderLine('x-ekdos-webhook-secret');

            if ($secret === '' || !hash_equals($secret, $presented)) {
                return Json::error($response, 'Bitte erneut anmelden.', 401);
            }
        }

        $this->cache->invalidate(CachePolicy::Tickets[0]);

        return Json::write($response, ['ok' => true, 'refreshedAt' => gmdate('c')]);
    }

    /** Ergebnis der Kundenruecksprache festhalten. Vormals: nur Svenja. */
    public function consultation(Request $request, Response $response): Response
    {
        $body = $this->body($request);
        $ticketNumber = self::text($body, 'ticketnummer');
        $result = self::text($body, 'ergebnis');

        if ($ticketNumber === '' || $result === '') {
            return Json::error($response, 'Ticketnummer und Ergebnis sind erforderlich.', 400);
        }

        if (mb_strlen($result) > 4000) {
            return Json::error($response, 'Das Ergebnis darf maximal 4.000 Zeichen enthalten.', 400);
        }

        // Vorher pruefen, ob schon ein Ergebnis hinterlegt ist. Bewusst ungecacht:
        // hier zaehlt der aktuelle Stand, nicht ein 15 Sekunden alter.
        $lookup = $this->n8n->get($this->n8nUrl->openTickets());

        foreach ($lookup->items('tickets') as $row) {
            if (!is_array($row)) {
                continue;
            }

            $id = self::text($row, 'ticketnummer') !== '' ? self::text($row, 'ticketnummer') : self::text($row, 'id');

            if ($id === $ticketNumber && self::text($row, 'kundenruecksprache_ergebnis') !== '') {
                return Json::error($response, 'Für dieses Ticket wurde bereits ein Ergebnis gespeichert.', 409);
            }
        }

        $reply = $this->n8n->post($this->n8nUrl->ticketConsultation(), ['ticketnummer' => $ticketNumber, 'ergebnis' => $result]);

        if (!$reply->ok()) {
            return $this->upstreamError($response, $reply, 'Das Ergebnis der Kundenrücksprache konnte nicht in n8n gespeichert werden.');
        }

        $this->cache->invalidate(CachePolicy::Tickets[0]);

        return Json::write($response, ['ok' => true, 'updatedAt' => gmdate('c')]);
    }

    public function digitalFile(Request $request, Response $response): Response
    {
        $ticket = $this->query($request, 'ticket');

        if (preg_match(self::TICKET_PATTERN, $ticket) !== 1) {
            return Json::error($response, 'Die Ticketnummer ist ungültig.', 400);
        }

        $reply = $this->n8n->get($this->n8nUrl->ticketDigitalFile($ticket));

        if (!$reply->ok() || $reply->body === '') {
            return $this->upstreamError($response, $reply, 'Die Digitale Akte ist nicht verfügbar.');
        }

        return $this->pdfResponse($response, $reply->body, 'Digitale_Akte.pdf');
    }

    public function serviceReport(Request $request, Response $response): Response
    {
        $ticket = $this->query($request, 'ticket');

        if (preg_match(self::TICKET_PATTERN, $ticket) !== 1) {
            return Json::error($response, 'Die Ticketnummer ist ungültig.', 400);
        }

        $reply = $this->n8n->get($this->n8nUrl->ticketServiceReport($ticket));

        if (!$reply->ok() || $reply->body === '') {
            return $this->upstreamError($response, $reply, 'Der letzte Servicebericht ist nicht verfügbar.');
        }

        return $this->pdfResponse($response, $reply->body, 'Letzter_Servicebericht.pdf');
    }

    /** @return array<string, mixed> */
    private static function mapTicket(array $row): array
    {
        $continuation = self::text($row, 'fortsetzung');
        $decisionCode = self::text($row, 'entscheidung_code');

        return [
            'id' => self::firstOf($row, ['ticketnummer', 'id'], ''),
            'status' => self::firstOf($row, ['status'], 'offen'),
            'reportStatus' => self::optional($row, 'letzter_servicebericht_status'),
            'customer' => self::firstOf($row, ['kunde', 'customer'], '–'),
            'property' => self::firstOf($row, ['liegenschaft', 'property'], '–'),
            'title' => self::firstOf($row, ['title'], ''),
            'updatedAt' => self::firstOf($row, ['updatedAt', 'updated_at', 'erstellt_am', 'createdAt'], ''),
            'continuation' => $continuation !== '' ? $continuation : null,
            'decisionCode' => $decisionCode !== '' ? $decisionCode : null,
            'requiresCustomerConsultation' => self::needsConsultation($decisionCode, $continuation),
            'consultationResult' => self::optional($row, 'kundenruecksprache_ergebnis'),
            'consultationUpdatedAt' => self::optional($row, 'kundenruecksprache_am'),
            'consultationBy' => self::optional($row, 'kundenruecksprache_von'),
        ];
    }

    /**
     * Ein Ticket braucht Ruecksprache, wenn n8n es entweder ausdruecklich so
     * kennzeichnet oder der Fortsetzungstext des Technikers es so formuliert.
     */
    private static function needsConsultation(string $decisionCode, string $continuation): bool
    {
        if ($decisionCode === 'TEILERLEDIGUNG_RUECKSPRACHE_KUNDE') {
            return true;
        }

        // Der Text kommt aus einem PDF und traegt gelegentlich zusammengesetzte
        // Umlaute, deshalb vor dem Vergleich nach NFKC normalisieren.
        $normalized = class_exists(\Normalizer::class)
            ? (\Normalizer::normalize($continuation, \Normalizer::FORM_KC) ?: $continuation)
            : $continuation;

        return preg_match('/nimmt\s+mit\s+kunden\s+für\s+das\s+weitere\s+vorgehen\s+kontakt\s+auf/iu', $normalized) === 1;
    }

    /** Erstes gesetztes Feld aus der Liste, sonst der Vorgabewert. */
    private static function firstOf(array $row, array $keys, string $fallback): string
    {
        foreach ($keys as $key) {
            if (isset($row[$key]) && (is_string($row[$key]) || is_numeric($row[$key]))) {
                return (string) $row[$key];
            }
        }

        return $fallback;
    }

    /** Leere Felder werden zu null, damit die Oberflaeche sie ueberspringen kann. */
    private static function optional(array $row, string $key): ?string
    {
        $value = self::text($row, $key);

        return $value !== '' ? $value : null;
    }
}
