<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\Rest;

use PrettyLinks\Helpers\LinkUrl;
use PrettyLinks\Pro\Qr\QrService;
use PrettyLinks\Redirect\Engine;
use PrettyLinks\Rest\Controllers\BaseController;
use WP_REST_Request;
use WP_REST_Server;

/**
 * Live QR preview for the options panel and the per-link QR tab. Returns
 * a rendered PNG for a URL determined by the optional `slug` param, with
 * logo/size/background supplied via query params.
 *
 * With no `slug` (the options-page preview), this always encodes the
 * same fixed sample URL — it's a visual check of the site-wide overlay
 * defaults, not a general-purpose QR generator, and doubles as an
 * easter egg. With `slug` (the per-link QR tab), it encodes that link's
 * pretty URL via LinkUrl::build() — never an arbitrary external URL,
 * since the slug is only ever appended to this site's own base URL.
 * Admin-only (permission_callback gates on the standard capability).
 */
class QrPreviewController extends BaseController
{
    /**
     * Sample URL encoded into the preview QR when no `slug` is given
     * (the options-page preview). Fixed on the server, both to keep the
     * options-page check independent of any specific link and to
     * preserve the easter egg.
     */
    private const SAMPLE_URL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ';

    /**
     * Register the QR preview REST route.
     *
     * @return void
     */
    public function register(): void
    {
        // No `default`s / type coercion here: the client passes 0 / empty
        // to mean "inherit site default" and we need to detect that
        // explicitly rather than have REST fill in a synthetic value.
        register_rest_route($this->namespace(), '/pro/qr/preview', [
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => [$this, 'render'],
            'permission_callback' => $this->permission(),
        ]);
    }

    /**
     * Render and emit the preview QR PNG, then exit.
     *
     * @param WP_REST_Request $request Incoming REST request.
     *
     * @return void
     */
    public function render(WP_REST_Request $request): void
    {
        $proOptions = (array) (get_option('prlipro_options', []) ?: []);

        // Each param falls back to the site-wide default when the client
        // sends an empty / zero value. Lets the same endpoint serve both
        // the options-page preview (always explicit) and the link-form
        // preview (where unset fields should show the inherited render).
        $logoId = (int) $request->get_param('logo_id');
        if ($logoId <= 0) {
            $logoId = (int) ($proOptions['qr_logo_attachment_id'] ?? 0);
        }

        $size = (int) $request->get_param('size');
        if ($size <= 0) {
            $size = (int) ($proOptions['qr_logo_size_percent'] ?? 20);
        }
        $size = max(10, min(30, $size));

        $whiteBgParam = $request->get_param('white_bg');
        if ($whiteBgParam === null || $whiteBgParam === '') {
            $whiteBg = (bool) ($proOptions['qr_logo_white_background'] ?? true);
        } else {
            $whiteBg = $whiteBgParam === '1' || $whiteBgParam === 1 || $whiteBgParam === true;
        }

        $data = $this->resolveData($request);

        (new QrService())->emitPreview($data, $logoId, $size, $whiteBg);
        exit;
    }

    /**
     * Resolves the URL to encode: the requesting link's pretty URL when a
     * `slug` param is present (per-link QR tab), otherwise the fixed
     * sample URL (options-page preview).
     *
     * `slug` is sanitized to the same safe charset as stored slugs before
     * being handed to LinkUrl::build() — it's never passed through
     * un-sanitized, and it only ever resolves to a path under this
     * site's own base URL, so this can't be used to encode an arbitrary
     * external URL.
     *
     * Once a `slug` param is present at all, this never falls back to
     * SAMPLE_URL — even if it sanitizes down to `''` (e.g. whitespace or
     * punctuation-only input from an unsaved link's in-progress slug
     * field). Presence of the param means the caller is the per-link
     * tab, whose UI copy tells the admin this preview encodes their
     * link's URL; silently swapping in the sample URL there would make
     * that claim false. LinkUrl::build('') degrades to the bare site
     * base URL instead, which stays honest.
     *
     * @param  WP_REST_Request $request Incoming REST request.
     * @return string
     */
    private function resolveData(WP_REST_Request $request): string
    {
        $slugParam = $request->get_param('slug');
        if (!is_string($slugParam) || $slugParam === '') {
            return self::SAMPLE_URL;
        }

        return LinkUrl::build($this->sanitizeSlug($slugParam));
    }

    /**
     * Same safe charset as Links::sanitizeSlug(), sharing the single source
     * of truth in Engine::SLUG_CHAR_CLASS so a QR preview URL matches the
     * slug that is actually stored and resolved (including `.`, `=`, `~`, `+`).
     * Kept local since Links::sanitizeSlug() is private and this endpoint only
     * needs the charset rule, not persistence-side slug generation.
     *
     * @param  string $slug Raw slug from the request.
     * @return string
     */
    private function sanitizeSlug(string $slug): string
    {
        $slug = trim($slug);
        $slug = (string) preg_replace('/\s+/', '-', $slug);
        $slug = (string) preg_replace('#[^' . Engine::SLUG_CHAR_CLASS . ']#', '', $slug);
        $slug = (string) preg_replace('#/+#', '/', $slug);
        $slug = (string) preg_replace('/-+/', '-', $slug);
        return trim($slug, '-/');
    }
}
