<?php

declare(strict_types=1);

namespace App\Controllers;

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

/**
 * `/app/policies` — list, edit (threshold matrix), create, delete.
 *
 * Threshold matrix editing happens on the edit page: rows = categories,
 * columns = ["threshold"]. The page POSTs a flat form of slug→value
 * pairs; we resolve those into the api's `thresholds: {slug: number}`
 * shape. Empty values mean "remove from policy".
 *
 * Live preview is driven by an Alpine component on the edit page
 * that fetches `/app/policies/{id}/preview-proxy`, which forwards
 * to the api with the user's session impersonation. The component
 * runs on `x-init` so the preview auto-refreshes after a save
 * (post-save redirect reloads the page); a Refresh button stays
 * available for ad-hoc refetches.
 *
 * RBAC: list/show/preview ⇒ Viewer; write ⇒ Admin.
 */
final class PoliciesController
{
    use CrudControllerSupport;

    public function __construct(
        private readonly Twig $twigEngine,
        private readonly SessionManager $sessionManager,
        private readonly AdminClient $admin,
    ) {
    }

    protected function twig(): Twig
    {
        return $this->twigEngine;
    }

    protected function sessions(): SessionManager
    {
        return $this->sessionManager;
    }

    /**
     * The api gates `GET /api/v1/admin/policies/{id}/preview` and
     * `…/score-distribution` to Viewer-or-higher (i.e. not `none` /
     * unmapped). SEC_REVIEW F42: enforce the same expectation locally
     * so a UI session that drifts (e.g. the api role mapping changes
     * to deny these endpoints to Viewer) doesn't silently forward the
     * call until the api returns 403 — and so a `none`-role session
     * that somehow reached the protected route group cannot use the
     * proxy as a probe channel.
     */
    private const PROXY_ALLOWED_ROLES = ['viewer', 'operator', 'admin'];

    /**
     * Browser-side preview proxy: the policy edit page's Alpine
     * component fetches `/app/policies/{id}/preview-proxy`; this
     * controller forwards to the api with the user's session
     * impersonation. Returns the api's preview JSON verbatim.
     *
     * @param array{id: string} $args
     */
    public function previewProxy(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $user = $this->sessionManager->getUser();
        if ($user === null) {
            $response->getBody()->write((string) json_encode(['error' => 'unauthenticated']));

            return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
        }
        if (!$this->userIs($user, ...self::PROXY_ALLOWED_ROLES)) {
            $response->getBody()->write((string) json_encode(['error' => 'forbidden']));

            return $response->withStatus(403)->withHeader('Content-Type', 'application/json');
        }
        $id = $this->parseId($args['id']);
        if ($id === null) {
            $response->getBody()->write((string) json_encode(['error' => 'not_found']));

            return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
        }
        try {
            $payload = $this->admin->previewPolicy($user->userId, $id);
        } catch (ApiException $e) {
            $response->getBody()->write((string) json_encode(['error' => $e->getMessage()]));

            return $response->withStatus(502)->withHeader('Content-Type', 'application/json');
        }
        $response->getBody()->write((string) json_encode($payload));

        return $response->withStatus(200)->withHeader('Content-Type', 'application/json');
    }

    /**
     * Browser-side distribution proxy: fetched by the policy edit page's
     * Alpine component to render the score-histogram chart with the
     * policy's thresholds overlaid as vertical lines.
     *
     * @param array{id: string} $args
     */
    public function scoreDistributionProxy(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $user = $this->sessionManager->getUser();
        if ($user === null) {
            $response->getBody()->write((string) json_encode(['error' => 'unauthenticated']));

            return $response->withStatus(401)->withHeader('Content-Type', 'application/json');
        }
        if (!$this->userIs($user, ...self::PROXY_ALLOWED_ROLES)) {
            $response->getBody()->write((string) json_encode(['error' => 'forbidden']));

            return $response->withStatus(403)->withHeader('Content-Type', 'application/json');
        }
        $id = $this->parseId($args['id']);
        if ($id === null) {
            $response->getBody()->write((string) json_encode(['error' => 'not_found']));

            return $response->withStatus(404)->withHeader('Content-Type', 'application/json');
        }
        try {
            $payload = $this->admin->policyScoreDistribution($user->userId, $id);
        } catch (ApiException $e) {
            $response->getBody()->write((string) json_encode(['error' => $e->getMessage()]));

            return $response->withStatus(502)->withHeader('Content-Type', 'application/json');
        }
        $response->getBody()->write((string) json_encode($payload));

        return $response->withStatus(200)->withHeader('Content-Type', 'application/json');
    }

    public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $redirect = $this->requireUser($request, $response);
        if ($redirect !== null) {
            return $redirect;
        }
        $user = $this->sessionManager->getUser();
        \assert($user !== null);

        try {
            $list = $this->admin->listPolicies($user->userId);
        } catch (ApiException $e) {
            $list = ['items' => [], 'total' => 0];
            $this->flashFromException($e);
        }

        return $this->twigEngine->render($response, 'pages/policies/index.twig', [
            'active_section' => 'policies',
            'list' => $list,
            'can_write' => $this->userIs($user, 'admin'),
        ]);
    }

    /**
     * @param array{id: string} $args
     */
    public function edit(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $redirect = $this->requireUser($request, $response);
        if ($redirect !== null) {
            return $redirect;
        }
        $user = $this->sessionManager->getUser();
        \assert($user !== null);

        $id = $this->parseId($args['id']);
        if ($id === null) {
            return $this->twigEngine->render($response->withStatus(404), 'pages/error.twig', [
                'status' => 404, 'is_client_error' => true, 'message' => 'Policy not found',
            ]);
        }

        try {
            $policy = $this->admin->getPolicy($user->userId, $id);
            $categories = $this->admin->listCategories($user->userId);
        } catch (ApiNotFoundException) {
            return $this->twigEngine->render($response->withStatus(404), 'pages/error.twig', [
                'status' => 404, 'is_client_error' => true, 'message' => 'Policy not found',
            ]);
        } catch (ApiException $e) {
            $this->flashFromException($e);

            return $response->withStatus(303)->withHeader('Location', '/app/policies');
        }

        return $this->twigEngine->render($response, 'pages/policies/edit.twig', [
            'active_section' => 'policies',
            'policy' => $policy,
            'categories' => $categories['items'] ?? [],
            'can_write' => $this->userIs($user, 'admin'),
        ]);
    }

    public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $redirect = $this->requireUser($request, $response);
        if ($redirect !== null) {
            return $redirect;
        }
        $user = $this->sessionManager->getUser();
        \assert($user !== null);

        $body = $this->formBody($request);
        $payload = $this->payloadFromForm($body);

        try {
            $created = $this->admin->createPolicy($user->userId, $payload);
            $this->sessionManager->flash('success', 'Policy created.');
            $newId = (int) ($created['id'] ?? 0);

            return $response->withStatus(303)->withHeader('Location', $newId > 0 ? '/app/policies/' . $newId : '/app/policies');
        } catch (ApiException $e) {
            $this->flashFromException($e);
        }

        return $response->withStatus(303)->withHeader('Location', '/app/policies');
    }

    /**
     * @param array{id: string} $args
     */
    public function update(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $redirect = $this->requireUser($request, $response);
        if ($redirect !== null) {
            return $redirect;
        }
        $user = $this->sessionManager->getUser();
        \assert($user !== null);

        $id = $this->parseId($args['id']);
        if ($id === null) {
            return $response->withStatus(303)->withHeader('Location', '/app/policies');
        }

        $body = $this->formBody($request);
        $payload = $this->payloadFromForm($body);

        try {
            $this->admin->updatePolicy($user->userId, $id, $payload);
            $this->sessionManager->flash('success', 'Policy saved.');
        } catch (ApiException $e) {
            $this->flashFromException($e);
        }

        return $response->withStatus(303)->withHeader('Location', '/app/policies/' . $id);
    }

    /**
     * @param array{id: string} $args
     */
    public function delete(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface
    {
        $redirect = $this->requireUser($request, $response);
        if ($redirect !== null) {
            return $redirect;
        }
        $user = $this->sessionManager->getUser();
        \assert($user !== null);

        $id = $this->parseId($args['id']);
        if ($id === null) {
            return $response->withStatus(303)->withHeader('Location', '/app/policies');
        }
        try {
            $this->admin->deletePolicy($user->userId, $id);
            $this->sessionManager->flash('success', 'Policy deleted.');
        } catch (ApiException $e) {
            $this->flashFromException($e);
        }

        return $response->withStatus(303)->withHeader('Location', '/app/policies');
    }

    /**
     * Translate the edit-form payload to the api's body shape:
     *   thresholds[<slug>]=number → {<slug>: number}
     *   include_manual_blocks="on" → true (checkbox)
     *
     * @param array<string, mixed> $body
     * @return array<string, mixed>
     */
    private function payloadFromForm(array $body): array
    {
        $payload = [];
        if (isset($body['name']) && is_string($body['name'])) {
            $payload['name'] = trim($body['name']);
        }
        if (isset($body['description']) && is_string($body['description'])) {
            $payload['description'] = trim($body['description']) === '' ? null : trim($body['description']);
        }
        $payload['include_manual_blocks'] = $this->formBool($body['include_manual_blocks'] ?? null);

        $thresholds = [];
        if (isset($body['thresholds']) && is_array($body['thresholds'])) {
            foreach ($body['thresholds'] as $slug => $value) {
                if (!is_string($slug) || $slug === '') {
                    continue;
                }
                if (is_string($value)) {
                    $value = trim($value);
                }
                if ($value === '' || $value === null) {
                    continue; // empty = "not in policy"
                }
                if (is_numeric($value)) {
                    $thresholds[$slug] = (float) $value;
                }
            }
        }
        if ($thresholds !== []) {
            $payload['thresholds'] = $thresholds;
        }

        return $payload;
    }
}
