<?php

declare(strict_types=1);

namespace App\Controllers;

use App\ApiClient\ApiAuthException;
use App\ApiClient\ApiException;
use App\ApiClient\ApiNotFoundException;
use App\ApiClient\ApiValidationException;
use App\Auth\SessionManager;
use App\Auth\UserContext;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Views\Twig;

/**
 * Common helpers for the M10 admin CRUD controllers: session-required
 * gate, parsed-body extraction, RBAC check helpers, and an exception →
 * flash mapper that turns api errors into user-readable messages while
 * still letting the form re-render with field-level details.
 */
trait CrudControllerSupport
{
    abstract protected function sessions(): SessionManager;

    abstract protected function twig(): Twig;

    /**
     * Gate every /app/* admin page on a logged-in user. Returns a 303
     * redirect (or null when the user is logged in).
     */
    protected function requireUser(ServerRequestInterface $request, ResponseInterface $response): ?ResponseInterface
    {
        $user = $this->sessions()->getUser();
        if ($user !== null) {
            return null;
        }

        return $response->withStatus(302)->withHeader('Location', '/login');
    }

    /**
     * The session-cached role + a helper to gate UI render branches on
     * permissions. Treat as cosmetic; the api enforces RBAC.
     */
    protected function userIs(UserContext $user, string ...$allowedRoles): bool
    {
        return in_array($user->role, $allowedRoles, true);
    }

    /**
     * Parse a form-encoded body. CsrfMiddleware guards the request, so
     * the token is already validated by the time we read fields.
     *
     * @return array<string, mixed>
     */
    protected function formBody(ServerRequestInterface $request): array
    {
        $body = $request->getParsedBody();

        return is_array($body) ? $body : [];
    }

    /**
     * Map an api exception to a flash message. Validation errors are
     * surfaced inline by the caller (it stashes details in flash); other
     * exceptions render a single-line flash so the user knows the
     * action failed.
     */
    protected function flashFromException(ApiException $e): void
    {
        if ($e instanceof ApiValidationException) {
            $details = [];
            foreach ($e->details as $field => $msg) {
                $details[] = sprintf('%s: %s', $field, $msg);
            }
            $message = $details === [] ? 'validation failed' : implode(', ', $details);
            $this->sessions()->flash('error', $message);

            return;
        }
        if ($e instanceof ApiAuthException) {
            $this->sessions()->flash('error', 'Forbidden — your role doesn\'t allow this.');

            return;
        }
        if ($e instanceof ApiNotFoundException) {
            $this->sessions()->flash('error', 'Not found.');

            return;
        }
        $this->sessions()->flash('error', 'API error: ' . $e->getMessage());
    }

    protected function parseId(string $raw): ?int
    {
        return preg_match('/^[1-9][0-9]*$/', $raw) === 1 ? (int) $raw : null;
    }

    /**
     * Convert form-typed booleans (`'1'`, `'true'`, `'on'`) to PHP true.
     * Anything else (including absent) is false.
     */
    protected function formBool(mixed $v): bool
    {
        if (is_bool($v)) {
            return $v;
        }
        if (is_string($v)) {
            return in_array(strtolower($v), ['1', 'true', 'on', 'yes'], true);
        }

        return false;
    }
}
