<?php

declare(strict_types=1);

namespace PpPhp\Core\Actions;

use PpPhp\Core\Security;

/**
 * Custom AJAX action endpoints for generated apps (platform feature A6).
 *
 * Lets a project expose small server actions (upvote, accept answer, flag, …)
 * at POST /action/<name>, without hand-writing a whole page. The project ships a
 * map of handlers in `app/actions.php`:
 *
 *   return [
 *     'vote'   => fn() => [...],                       // requires login (default)
 *     'ping'   => ['public' => true, 'handler' => fn() => [...]],   // opt-in anonymous
 *   ];
 *
 * Each request is CSRF-guarded and (unless the handler opts into `public`) blocks
 * anonymous callers with a 401 JSON — never RequireAuth()'s HTML redirect, which
 * would be useless to an AJAX caller. The handler returns an array → JSON.
 *
 * `match()` is pure and unit-tested; `dispatch()` is the thin glue the generated
 * front controller calls. It is inert unless the path is exactly /action/<name>,
 * so it never disturbs normal routing.
 */
final class ActionRouter
{
    /** Extract the action name from a request path, or null if it isn't an action route. */
    public static function match(string $path): ?string
    {
        $path = (string) (parse_url($path, PHP_URL_PATH) ?? $path);
        $path = rtrim($path, '/');
        return preg_match('#^/action/([A-Za-z0-9_-]+)$#', $path, $m) ? $m[1] : null;
    }

    /** Handle POST /action/<name>. Always exits (JSON). Only call after match() succeeds. */
    public static function dispatch(string $name, string $baseDir): void
    {
        if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
            self::json(['success' => false, 'error' => 'Method not allowed.'], 405);
        }

        $file = $baseDir . '/app/actions.php';
        $map = is_file($file) ? require $file : null;
        if (!is_array($map) || !isset($map[$name])) {
            self::json(['success' => false, 'error' => 'Unknown action.'], 404);
        }

        $entry = $map[$name];
        $public = is_array($entry) && !empty($entry['public']);
        $handler = is_array($entry) ? ($entry['handler'] ?? null) : $entry;
        if (!is_callable($handler)) {
            self::json(['success' => false, 'error' => 'Unknown action.'], 404);
        }

        // CSRF on every action (write endpoints). Accepts the field or a header.
        $token = (string) ($_POST['_token'] ?? $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
        if (!Security::CSRF()->Check($token)) {
            self::json(['success' => false, 'error' => 'Invalid or expired security token.'], 400);
        }

        // Auth (unless the handler opted into public). 401 JSON, not an HTML redirect.
        $authOn = defined('PP_AUTH_ENABLED') && (bool) constant('PP_AUTH_ENABLED');
        if (!$public && $authOn && empty(Security::CurrentUserData())) {
            self::json(['success' => false, 'error' => 'Authentication required.'], 401);
        }

        // Rate limit (feature C3): 60 requests / 60s per action per client IP.
        $rlKey = 'action|' . $name . '|' . (string) ($_SERVER['REMOTE_ADDR'] ?? 'cli');
        if (!\PpPhp\Core\Guard\RateLimiter::allow($rlKey, 60, 60, $baseDir . '/storage/ratelimit')) {
            self::json(['success' => false, 'error' => 'Too many requests — please slow down.'], 429);
        }

        try {
            $result = $handler();
        } catch (\Throwable $e) {
            self::json(['success' => false, 'error' => 'The action failed.'], 500);
        }

        if (is_array($result)) {
            self::json($result + ['success' => $result['success'] ?? true], 200);
        }
        self::json(['success' => true], 200);
    }

    /** @param array<string,mixed> $payload */
    private static function json(array $payload, int $status): void
    {
        http_response_code($status);
        header('Content-Type: application/json');
        echo json_encode($payload);
        exit;
    }
}
