<?php

namespace Formatrica\Frontend;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Formatrica\Capability\Capabilities;
use Formatrica\Capability\Feature_Gate;
use Formatrica\Forms\Form_Repository;

final class Shortcode_Renderer
{
    // Modal attribute bounds
    private const TRIGGER_MAX_LENGTH = 100;
    private const AUTO_OPEN_AFTER_MAX = 300;
    private const AUTO_OPEN_SCROLL_MAX = 100;
    private const WIDTH_MIN = 280;
    private const WIDTH_MAX = 1280;
    // Mirrors Pro\Frontend\Modal_Markup_Renderer::WIDTH_DEFAULT. Kept here as a
    // literal so this Free-shipped class never references the Pro modal class
    // outside the capability-gated render branch.
    private const WIDTH_DEFAULT = 680;
    private const AUTO_CLOSE_TIMEOUT_MAX = 30;
    private const AUTO_CLOSE_TIMEOUT_DEFAULT = 3;
    private const FREQUENCIES = ['once-per-session', 'once-per-day', 'every-time'];
    private const THEMES = ['auto', 'light', 'dark'];

    private Form_Repository $forms;
    private Form_Renderer $renderer;

    private static bool $localized = false;

    /** Per-request counter so two modals on one page get unique DOM ids. */
    private static int $instance_counter = 0;

    public function __construct(?Form_Repository $forms = null, ?Form_Renderer $renderer = null)
    {
        $this->forms = $forms ?? new Form_Repository();
        $this->renderer = $renderer ?? new Form_Renderer();
    }

    public function register_shortcode(): void
    {
        add_shortcode('formatrica', [$this, 'render_shortcode']);
    }

    public function render_shortcode(array $atts): string
    {
        $atts = shortcode_atts(
            [
                'id' => 0,
                // Modal placement (Pro forms.modal). Free silently ignores
                // modal=1 and renders inline — no error, no upsell leak.
                'modal' => '0',
                'trigger' => '',
                'theme' => '',
                'width' => self::WIDTH_DEFAULT,
                'show-title' => '0',
                'auto-open' => '0',
                'auto-open-after' => '',
                'auto-open-after-scroll' => '',
                'frequency' => 'once-per-session',
                'auto-close' => '0',
                'auto-close-timeout' => self::AUTO_CLOSE_TIMEOUT_DEFAULT,
            ],
            $atts,
            'formatrica'
        );

        $form_id = (int) $atts['id'];

        if ($form_id <= 0) {
            return '';
        }

        $form = $this->forms->get($form_id);

        if (null === $form) {
            return '';
        }

        // Do not render inactive forms on the frontend
        if (isset($form['active']) && (int) $form['active'] !== 1) {
            return '';
        }

        // Payment-enabled forms FAIL CLOSED without the payment capability
        // (plan §5.3): rendering the form would invite submissions the
        // pipeline rejects (Form_Lookup_Stage throws 503). Show a neutral
        // unavailable notice instead — never a silently-free paid form.
        if (!empty($form['config']['options']['payments']['enabled'])) {
            $gate = Feature_Gate::get_default();
            if ($gate === null || !$gate->has(Capabilities::PAYMENTS_BASIC)) {
                return $this->render_payment_notice(
                    'warning',
                    __('This form is temporarily unavailable. Please try again later.', 'formatrica')
                );
            }
        }

        // Payment return (Pro payments): when the visitor was just bounced back
        // from a COMPLETED payment, show the form's success message in place of
        // the form instead of re-rendering an empty form with no confirmation.
        $completed_message = $this->payment_return_message($form_id, $form, 'completed');
        if ($completed_message !== null) {
            $this->enqueue_assets();
            return $this->render_payment_notice('success', $completed_message);
        }

        $this->enqueue_assets();

        // Pro theme presets (P17): load the overlay stylesheet only when this
        // form actually uses one. Capability-blind — a lapsed licence keeps
        // rendering an authored preset (only authoring is gated).
        $preset = $form['config']['options']['theme']['preset'] ?? 'classic';
        if (is_string($preset) && $preset !== 'classic') {
            // Unconditional enqueue (see enqueue_assets): in FSE the shortcode
            // parses pre-registration; the handle resolves at print time.
            wp_enqueue_style('formatrica-frontend-themes');
        }

        $inline = $this->renderer->render($form);

        // Modal wrap: only when requested AND the Pro capability is available.
        // The gate is read at shortcode-parse time (no stored config, so no
        // save-guard) — a lapsed licence degrades to the inline form.
        if ($atts['modal'] === '1' && $this->is_modal_available()) {
            self::$instance_counter++;
            $output = \Formatrica\Pro\Frontend\Modal_Markup_Renderer::render(
                $inline,
                $form,
                $this->parse_modal_atts($atts, $form_id),
                self::$instance_counter
            );
        } else {
            $output = $inline;
        }

        // Payment return (Pro payments): a CANCELLED payment keeps the form so
        // the visitor can retry, with a warning banner above it.
        $cancelled_message = $this->payment_return_message($form_id, $form, 'cancelled');
        if ($cancelled_message !== null) {
            $output = $this->render_payment_notice('warning', $cancelled_message) . $output;
        }

        return $output;
    }

    /**
     * The authenticated payment-return message for this form, or null. $kind is
     * 'completed' (success) or 'cancelled' (retry). The detector ships in the Pro
     * package only, so the class_exists guard keeps this Free-shipped method
     * fail-closed — Free never mints payment orders, so it always returns null.
     *
     * @param array<string, mixed> $form
     */
    private function payment_return_message(int $form_id, array $form, string $kind): ?string
    {
        if (!class_exists(\Formatrica\Payments\Payment_Return_Detector::class)) {
            return null;
        }

        $detector = new \Formatrica\Payments\Payment_Return_Detector();

        return $kind === 'cancelled'
            ? $detector->cancelled_message_for($form_id, $form)
            : $detector->completed_message_for($form_id, $form);
    }

    /**
     * Wrap a payment-return message in the form's notice markup. $type is a
     * formatrica__notice modifier ('success' | 'warning'). The .formatrica__form
     * wrapper keeps the banner at the form's width rather than full-bleed.
     */
    private function render_payment_notice(string $type, string $message): string
    {
        return sprintf(
            '<div class="formatrica__form"><div class="formatrica__messages" aria-live="polite">'
            . '<div class="formatrica__notice formatrica__notice--%1$s" role="status" tabindex="-1">%2$s</div>'
            . '</div></div>',
            esc_attr($type),
            esc_html($message)
        );
    }

    private function is_modal_available(): bool
    {
        // The Pro modal renderer ships only in the Pro package; the class_exists
        // guard keeps this Free-shipped method fail-closed (inline) even if the
        // capability were somehow granted without the implementation present.
        if (!class_exists(\Formatrica\Pro\Frontend\Modal_Markup_Renderer::class)) {
            return false;
        }

        $gate = Feature_Gate::get_default();
        return $gate !== null && $gate->has(Capabilities::FORMS_MODAL);
    }

    /**
     * Clamp/normalise the modal attributes.
     *
     * @param array<string, mixed> $atts
     * @return array<string, mixed>
     */
    private function parse_modal_atts(array $atts, int $form_id): array
    {
        $trigger = is_string($atts['trigger']) ? $atts['trigger'] : '';
        if (mb_strlen($trigger, 'UTF-8') > self::TRIGGER_MAX_LENGTH) {
            $trigger = '';
        }

        $theme = strtolower(is_string($atts['theme']) ? $atts['theme'] : '');
        if (!in_array($theme, self::THEMES, true)) {
            $theme = 'auto';
        }

        $frequency = is_string($atts['frequency']) ? $atts['frequency'] : '';
        if (!in_array($frequency, self::FREQUENCIES, true)) {
            $frequency = 'once-per-session';
        }

        $auto_open_after = is_numeric($atts['auto-open-after'])
            ? max(0, min(self::AUTO_OPEN_AFTER_MAX, (int) $atts['auto-open-after']))
            : null;
        $auto_open_after_scroll = is_numeric($atts['auto-open-after-scroll'])
            ? max(0, min(self::AUTO_OPEN_SCROLL_MAX, (int) $atts['auto-open-after-scroll']))
            : null;

        return [
            'id' => $form_id,
            'modal' => true,
            'trigger' => $trigger,
            'theme' => $theme,
            'width' => is_numeric($atts['width'])
                ? max(self::WIDTH_MIN, min(self::WIDTH_MAX, (int) $atts['width']))
                : self::WIDTH_DEFAULT,
            'show_title' => $atts['show-title'] === '1',
            'auto_open' => $atts['auto-open'] === '1',
            'auto_open_after' => $auto_open_after,
            'auto_open_after_scroll' => $auto_open_after_scroll,
            'frequency' => $frequency,
            'auto_close' => $atts['auto-close'] === '1',
            'auto_close_timeout' => is_numeric($atts['auto-close-timeout'])
                ? max(0, min(self::AUTO_CLOSE_TIMEOUT_MAX, (int) $atts['auto-close-timeout']))
                : self::AUTO_CLOSE_TIMEOUT_DEFAULT,
        ];
    }

    private function enqueue_assets(): void
    {
        // Localization moved to Manager::register_frontend_assets — at parse
        // time (pre-registration in FSE) wp_localize_script silently fails.
        wp_enqueue_script('formatrica-frontend');

        // No registered-guards here: in block themes (FSE) the shortcode runs
        // BEFORE wp_head fires wp_enqueue_scripts, so nothing is registered
        // yet. WP queues unknown handles and resolves them at print time —
        // exactly how the frontend script already survives this ordering.
        wp_enqueue_style('formatrica-frontend-style');

        wp_enqueue_style('formatrica-frontend-inline');
    }
}
