<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\PublicLinks;

defined('ABSPATH') || exit;

// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// phpcs:disable WordPress.Security.NonceVerification.Missing
// $_SERVER values (REMOTE_ADDR, HTTP_USER_AGENT, REQUEST_URI, etc.) are read for
// click tracking / targeting / UI rendering, not form-submission input. The
// REST route at pro/src/Rest/PublicLinksController.php verifies the form's
// nonce on submit before acting on the data.
use PrettyLinks\Pro\Options\Store as OptionsStore;
use PrettyLinks\Pro\Rest\PublicLinksController;
use PrettyLinks\Pro\SocialButtons\Service as SocialButtonsService;
use PrettyLinks\Repositories\Links as LinksRepository;


/**
 * Public (anonymous-visitor) link creation — shortcode + REST plumbing.
 *
 * Preserves v3 Pro's drop-in shortcode surface so existing pages using
 * `[prli_create_form]` / `[prli_public_link_*]` keep rendering:
 *
 *   - `[prli_create_form]`                 → form posting to the REST route.
 *   - `[prli_create_display]`              → anchor to the new pretty URL.
 *   - `[prli_public_link_url]`             → alias of create_display.
 *   - `[prli_public_link_title]`           → the page title fetched from the target URL at creation time.
 *   - `[prli_public_link_target_url]`      → the new link's target URL.
 *   - `[prli_public_link_social_buttons]`  → the social share bar.
 *
 * Submission target: `POST /wp-json/pretty-links/v1/pro/public-links`
 * (handled in `pro/src/Rest/PublicLinksController.php`). The v3
 * `wp_ajax_plp-create-public-link` admin-ajax endpoint was retired in
 * 4.0 — the form's `action` attribute now points at the REST URL.
 *
 * On success the REST callback redirects back to the configured display
 * page (`public_link_display_page` option) with `?slug=…`; if no display
 * page is configured the visitor is bounced back to the form page.
 */
class Service
{
    /**
     * The links repository.
     *
     * @var LinksRepository
     */
    private LinksRepository $links;

    /**
     * Store the links repository dependency.
     *
     * @param LinksRepository $links The links repository.
     */
    public function __construct(LinksRepository $links)
    {
        $this->links = $links;
    }

    /**
     * Register the public-link shortcodes.
     *
     * @return void
     */
    public function registerShortcodes(): void
    {
        add_shortcode('prli_create_form', [$this, 'renderForm']);
        add_shortcode('prli_create_display', [$this, 'renderDisplay']);
        add_shortcode('prli_public_link_url', [$this, 'renderDisplay']);
        add_shortcode('prli_public_link_title', [$this, 'renderTitle']);
        add_shortcode('prli_public_link_target_url', [$this, 'renderTargetUrl']);
        add_shortcode('prli_public_link_social_buttons', [$this, 'renderSocialButtons']);
    }

    /**
     * Enqueue the public-link front-end stylesheets when enabled.
     *
     * @return void
     */
    public function enqueueAssets(): void
    {
        if (!$this->isEnabled()) {
            return;
        }
        $basePath = defined('PRLI_PATH') ? (string) PRLI_PATH : '';
        $baseUrl  = defined('PRLI_URL') ? (string) PRLI_URL : '';
        $rel      = 'assets/css/public-links.css';
        if ($basePath !== '' && is_file($basePath . $rel)) {
            wp_enqueue_style(
                'prli-public-links',
                $baseUrl . $rel,
                [],
                \PrettyLinks\Bootstrap::version()
            );
        }
        // The [prli_public_link_social_buttons] shortcode renders the same
        // .prli-social-buttons markup as SocialButtons\Service, but the
        // global "social buttons" feature toggle is independent — a site
        // can have public link creation on with social buttons off. Load
        // the shared stylesheet here so the bar isn't unstyled.
        $socialRel = 'assets/css/social-buttons.css';
        if ($basePath !== '' && is_file($basePath . $socialRel)) {
            wp_enqueue_style(
                'prli-social-buttons',
                $baseUrl . $socialRel,
                [],
                \PrettyLinks\Bootstrap::version()
            );
        }
    }

    /**
     * Render the public link creation form shortcode.
     *
     * @param array<string, mixed>|string $atts The shortcode attributes.
     *
     * @return string The rendered form HTML, or empty string when disabled.
     */
    public function renderForm($atts): string
    {
        if (!$this->isEnabled()) {
            return '';
        }
        $atts = shortcode_atts(
            [
                'label'         => __('Enter a URL:', 'pretty-link-pro'),
                'button'        => __('Shrink', 'pretty-link-pro'),
                'redirect_type' => '-1',
                'track'         => '-1',
                'category'      => '-1',
            ],
            is_array($atts) ? $atts : [],
            'prli_create_form'
        );

        $targetUrl = isset($_GET['url']) && is_string($_GET['url'])
            ? esc_url_raw(trim(wp_unslash((string) $_GET['url'])))
            : '';
        $errors    = $this->errorsFromQuery();

        // Capture the URL of the page hosting this form so the REST
        // controller can bounce the visitor back here on validation
        // errors with `?errors=…&url=…` query args.
        $host        = isset($_SERVER['HTTP_HOST']) ? (string) $_SERVER['HTTP_HOST'] : '';
        $uri         = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
        $scheme      = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $referralUrl = $host !== '' ? esc_url_raw($scheme . '://' . $host . $uri) : home_url('/');

        $action = esc_url_raw(rest_url('pretty-links/v1/pro/public-links'));

        $out  = '<div class="prli-public-form">';
        $out .= '<form method="post" action="' . esc_url($action) . '">';
        $out .= '<input type="hidden" name="referral-url" value="' . esc_attr($referralUrl) . '" />';
        $out .= '<input type="hidden" name="redirect_type" value="' . esc_attr((string) $atts['redirect_type']) . '" />';
        $out .= '<input type="hidden" name="track" value="' . esc_attr((string) $atts['track']) . '" />';
        $out .= '<input type="hidden" name="category" value="' . esc_attr((string) $atts['category']) . '" />';
        // Nonce field MUST NOT be named `_wpnonce`: WP REST's cookie-auth
        // check (`rest_cookie_check_errors`) reads `_wpnonce` from the
        // request body for any logged-in caller and validates it against
        // the `wp_rest` action — fails before our route runs and returns
        // `rest_cookie_invalid_nonce`. Use a custom field name; the
        // controller looks for the same name when verifying.
        $out .= wp_nonce_field(PublicLinksController::NONCE_ACTION, PublicLinksController::NONCE_FIELD, true, false);

        if ($errors) {
            $out .= '<ul class="prli-public-form__errors" role="alert">';
            foreach ($errors as $err) {
                $out .= '<li>' . esc_html($err) . '</li>';
            }
            $out .= '</ul>';
        }

        $out .= '<p class="prli-public-form__label">' . esc_html((string) $atts['label']) . '</p>';
        $out .= '<p class="prli-public-form__row">';
        $out .= '<input type="url" class="prli-public-form__url" name="url" required value="' . esc_attr($targetUrl) . '" />';
        if ((string) $atts['button'] !== '') {
            $out .= '<input type="submit" class="prli-public-form__submit" value="' . esc_attr((string) $atts['button']) . '" />';
        }
        $out .= '</p>';

        $out .= '</form></div>';
        return $out;
    }

    /**
     * Render the pretty-link URL shortcode for the queried slug.
     *
     * @return string The rendered link HTML, or empty string when not found.
     */
    public function renderDisplay(): string
    {
        $link = $this->linkFromSlugQuery();
        if (!$link) {
            return '';
        }
        $url = trim((string) ($link['pretty_url'] ?? ''));
        if ($url === '') {
            return '';
        }
        return '<p><a href="' . esc_url($url) . '">' . esc_html($url) . '</a></p>';
    }

    /**
     * Render the link title shortcode for the queried slug.
     *
     * @return string The rendered title HTML, or empty string when not found.
     */
    public function renderTitle(): string
    {
        $link = $this->linkFromSlugQuery();
        if (!$link) {
            return '';
        }
        $name = trim((string) ($link['name'] ?? ''));
        if ($name === '') {
            return '';
        }
        return '<p>' . esc_html($name) . '</p>';
    }

    /**
     * Render the link target URL shortcode for the queried slug.
     *
     * @return string The rendered target URL HTML, or empty string when not found.
     */
    public function renderTargetUrl(): string
    {
        $link = $this->linkFromSlugQuery();
        if (!$link) {
            return '';
        }
        $url = trim((string) ($link['url'] ?? ''));
        if ($url === '') {
            return '';
        }
        return '<p>' . esc_url($url) . '</p>';
    }

    /**
     * Render the social buttons shortcode for the queried slug.
     *
     * @return string The rendered social buttons HTML, or empty string when not found.
     */
    public function renderSocialButtons(): string
    {
        $link = $this->linkFromSlugQuery();
        if (!$link) {
            return '';
        }
        // Delegate to the shared SocialButtons service so the bar's config
        // (enabled networks, styling) matches the site's global settings.
        $social = new SocialButtonsService($this->links);
        // SocialButtons keys its render off post context (pretty-link meta).
        // For public-link display pages, we don't have a post — render
        // directly for this link's id via a light wrapper post.
        $postId = (int) get_the_ID();
        if ($postId <= 0) {
            return '';
        }
        // Temporarily associate the meta so the Service resolves to this
        // link; restore immediately so we don't alter post storage.
        $prev = get_post_meta($postId, '_pretty-link', true);
        update_post_meta($postId, '_pretty-link', (int) $link['id']);
        $html = $social->renderForPost($postId);
        if ($prev === '' || $prev === false) {
            delete_post_meta($postId, '_pretty-link');
        } else {
            update_post_meta($postId, '_pretty-link', $prev);
        }
        return $html;
    }

    /**
     * Resolve the link row for the `slug` query argument.
     *
     * @return array<string, mixed>|null The link row, or null when not found.
     */
    private function linkFromSlugQuery(): ?array
    {
        $slug = isset($_GET['slug']) && is_string($_GET['slug'])
            ? sanitize_text_field(wp_unslash((string) $_GET['slug']))
            : '';
        if ($slug === '') {
            return null;
        }
        $link = $this->links->findBySlug($slug);
        return is_array($link) ? $link : null;
    }

    /**
     * Collect sanitized validation error strings from the query.
     *
     * @return string[] The list of error messages.
     */
    private function errorsFromQuery(): array
    {
        if (!isset($_GET['errors']) || !is_array($_GET['errors'])) {
            return [];
        }
        $raw = wp_unslash($_GET['errors']);
        $out = [];
        foreach ($raw as $err) {
            if (is_string($err) && trim($err) !== '') {
                $out[] = sanitize_text_field($err);
            }
        }
        return $out;
    }

    /**
     * Whether public link creation is enabled in the options.
     *
     * @return boolean True when public link creation is allowed.
     */
    private function isEnabled(): bool
    {
        return (bool) (new OptionsStore())->get('allow_public_link_creation', false);
    }
}
