<?php
namespace WalletUpPro\Core\Onboarding;

use WalletUpPro\Core\PaymentRegistry;
use WalletUpPro\Core\Config\PaymentMethodsConfig;
use WalletUpPro\Services\QRCodeService;

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

/**
 * Phase D — builds the "your payment page is ready" artifact for the final
 * onboarding step, and creates a draft WordPress page with the right block
 * pre-placed when the user clicks "Add to a page."
 *
 * Endpoints (both nonce + manage_options gated):
 *   wallet_up_get_artifact         → JSON { url, qrDataUri, label, shortcode, blockMarkup, ... }
 *   wallet_up_generate_payment_page → JSON { pageId, editUrl, previewUrl }
 */
class PaymentPageGenerator
{
    public static function init(): void
    {
        add_action('wp_ajax_wallet_up_get_artifact', [__CLASS__, 'handleGetArtifact']);
        add_action('wp_ajax_wallet_up_generate_payment_page', [__CLASS__, 'handleGeneratePage']);
        add_action('wp_ajax_wallet_up_generate_shortcode_page', [__CLASS__, 'handleGenerateShortcodePage']);
    }

    // -----------------------------------------------------------------------
    // AJAX handlers
    // -----------------------------------------------------------------------

    public static function handleGetArtifact(): void
    {
        try {
            self::assertAllowed();
            wp_send_json_success(self::buildArtifact());
        } catch (\Throwable $e) {
            wp_send_json_error(['message' => $e->getMessage()]);
        }
    }

    public static function handleGeneratePage(): void
    {
        try {
            self::assertAllowed();
            // Optional methodId — when present, page contains only that method's
            // block (single-install). When absent, page contains every configured
            // method (full payment page).
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in assertAllowed()
            $onlyMethodId = isset($_POST['methodId']) ? sanitize_key(wp_unslash($_POST['methodId'])) : '';

            $artifact = self::buildArtifact();
            $title    = self::pageTitleFor($artifact, $onlyMethodId);
            $content  = self::buildPageContent($artifact, $title, $onlyMethodId);
            $pageId   = self::createDraftPage($title, $content);

            if (!$pageId) {
                throw new \Exception(esc_html__('Could not create page', 'walletup'));
            }
            wp_send_json_success([
                'pageId'     => $pageId,
                'editUrl'    => esc_url_raw(get_edit_post_link($pageId, 'raw')),
                'previewUrl' => esc_url_raw(add_query_arg('preview', 'true', get_permalink($pageId))),
                'title'      => $title,
                'scope'      => $onlyMethodId !== '' ? 'single' : 'all',
            ]);
        } catch (\Throwable $e) {
            wp_send_json_error(['message' => $e->getMessage()]);
        }
    }

    /**
     * Builds the page content:
     *   - $onlyMethodId set → single block for that one method
     *   - donations goal     → single Tip Jar block (Tip Jar is itself multi-amount)
     *   - otherwise          → one block per configured method
     */
    private static function buildPageContent(array $artifact, string $title, string $onlyMethodId = ''): string
    {
        $heading = '<!-- wp:heading -->' . "\n"
            . '<h2 class="wp-block-heading">' . esc_html($title) . '</h2>' . "\n"
            . '<!-- /wp:heading -->';

        if (empty($artifact['methods'])) {
            return $heading;
        }

        if ($onlyMethodId !== '') {
            $picked = self::findMethod($artifact, $onlyMethodId);
            if ($picked === null) {
                // Asked-for method isn't configured — bail to the heading rather
                // than silently rendering a different method.
                return $heading;
            }
            return $heading . "\n\n" . $picked['blockMarkup'];
        }

        // Donations: one Tip Jar per configured method, each labeled with a
        // small subheading so a multi-method donation page reads cleanly
        // ("Cash App", "PayPal", "Bitcoin" — each with its own Tip Jar below).
        // Single-method donations skip the subheading (it would duplicate the
        // method label already shown inside the Tip Jar block itself).
        if ($artifact['goal'] === 'donations') {
            $methods = $artifact['methods'];
            if (count($methods) === 1) {
                return $heading . "\n\n" . $methods[0]['blockMarkup'];
            }
            $blocks = [];
            foreach ($methods as $m) {
                $blocks[] = '<!-- wp:heading {"level":3} -->' . "\n"
                    . '<h3 class="wp-block-heading">' . esc_html((string) ($m['label'] ?? '')) . '</h3>' . "\n"
                    . '<!-- /wp:heading -->';
                $blocks[] = $m['blockMarkup'];
            }
            return $heading . "\n\n" . implode("\n\n", $blocks);
        }

        $blocks = [];
        foreach ($artifact['methods'] as $m) {
            $blocks[] = $m['blockMarkup'];
        }
        return $heading . "\n\n" . implode("\n\n", $blocks);
    }

    private static function findMethod(array $artifact, string $methodId): ?array
    {
        foreach ($artifact['methods'] as $m) {
            if (($m['id'] ?? '') === $methodId) {
                return $m;
            }
        }
        return null;
    }

    /**
     * Drop a user-built shortcode (from the Shortcode Builder) onto a fresh
     * draft page. Wrapped in a wp:shortcode block so the editor recognises it.
     * The title is inferred from the shortcode's tag.
     */
    public static function handleGenerateShortcodePage(): void
    {
        try {
            self::assertAllowed();

            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in assertAllowed()
            $raw = isset($_POST['shortcode']) ? (string) wp_unslash($_POST['shortcode']) : '';
            $shortcode = trim($raw);

            // Shape check: must start with [<tag> and end with ]
            if ($shortcode === ''
                || !preg_match('/^\[\s*[a-z_][a-z0-9_]*/i', $shortcode)
                || substr($shortcode, -1) !== ']'
            ) {
                throw new \Exception(esc_html__('Invalid shortcode', 'walletup'));
            }

            // Strip any HTML; shortcode contents are plain text
            $shortcode = wp_kses($shortcode, []);

            $title = self::shortcodeToTitle($shortcode);

            $heading = '<!-- wp:heading -->' . "\n"
                . '<h2 class="wp-block-heading">' . esc_html($title) . '</h2>' . "\n"
                . '<!-- /wp:heading -->';

            $shortcodeBlock = '<!-- wp:shortcode -->' . "\n" . $shortcode . "\n" . '<!-- /wp:shortcode -->';
            $content = $heading . "\n\n" . $shortcodeBlock;

            $pageId = self::createDraftPage($title, $content);
            if (!$pageId) {
                throw new \Exception(esc_html__('Could not create page', 'walletup'));
            }

            wp_send_json_success([
                'pageId'  => $pageId,
                'editUrl' => esc_url_raw(get_edit_post_link($pageId, 'raw')),
                'title'   => $title,
            ]);
        } catch (\Throwable $e) {
            wp_send_json_error(['message' => $e->getMessage()]);
        }
    }

    /**
     * Derive a sensible page title from the shortcode tag.
     * Payment method tags map to "Pay with {Name}". QR / donation / shortener
     * tags get their own labels. Everything else falls back to a generic title.
     */
    private static function shortcodeToTitle(string $shortcode): string
    {
        if (!preg_match('/^\[\s*([a-z_][a-z0-9_]*)/i', $shortcode, $matches)) {
            return __('Wallet Up payment', 'walletup');
        }
        $tag = strtolower($matches[1]);

        // Direct payment-method shortcodes
        $methodTags = ['cashapp', 'paypal', 'venmo', 'zelle', 'fbpay', 'waluppay',
                       'bitcoin', 'ethereum', 'usdt', 'usdc', 'bnb', 'dogecoin'];
        if (in_array($tag, $methodTags, true)) {
            return sprintf(
                /* translators: %s: payment method name (e.g. Cash App) */
                __('Pay with %s', 'walletup'),
                self::methodLabel($tag)
            );
        }

        if ($tag === 'walletup_qrcode' || $tag === 'walletup_qr') {
            return __('Scan to pay', 'walletup');
        }
        if (strpos($tag, 'donation') !== false) {
            return __('Support us', 'walletup');
        }
        if (strpos($tag, 'url_shortener') !== false
            || strpos($tag, 'integrated_shortener') !== false
            || strpos($tag, 'click_tracker') !== false
        ) {
            return __('Short link', 'walletup');
        }

        return __('Wallet Up payment', 'walletup');
    }

    private static function assertAllowed(): void
    {
        if (!check_ajax_referer('wallet_up_admin_nonce', 'nonce', false)) {
            throw new \Exception(esc_html__('Invalid security token', 'walletup'));
        }
        if (!current_user_can('manage_options')) {
            throw new \Exception(esc_html__('Permission denied', 'walletup'));
        }
    }

    // -----------------------------------------------------------------------
    // Artifact builder
    // -----------------------------------------------------------------------

    /**
     * Returns:
     *   methods         — array of { id, label, holder, url, qrDataUri, shortcode, blockMarkup, label }
     *                     for EVERY enabled+configured method. The dashboard shows them all;
     *                     the singular fields below are the FIRST entry for single-method callers
     *                     (onboarding artifact step) and become empty when methods=[].
     *   methodCount     — count of configured methods
     *   defaultMode     — user's QR / Button / Link preference, used by page generator
     *   pageTitle       — derived title for the auto-generated page
     *   configured      — true if at least one method has a holder
     *
     * Plus singular pass-throughs (first method for back-compat with onboarding):
     *   method, methodLabel, holder, amount, goal, eventName, presets,
     *   url, qrDataUri, shortcode, blockMarkup, label, linkSmsBody
     */
    public static function buildArtifact(): array
    {
        $accounts        = (array) get_option('wallet_up_transfer_call', []);
        $enabled         = (array) get_option('wallet_up_enabled_methods', []);
        $amount          = (string) get_option('wallet_up_default_amount', '');
        $goal            = (string) get_option('wallet_up_onboarding_goal', 'services');
        $eventName       = (string) get_option('wallet_up_event_name', '');
        $presets         = (array)  get_option('wallet_up_tipjar_presets', [3, 5, 10]);
        $displaySettings = (array) get_option('wallet_up_display_settings', []);
        $defaultMode     = (string) ($displaySettings['defaultMode'] ?? 'qr');
        if (!in_array($defaultMode, ['qr', 'button', 'link'], true)) {
            $defaultMode = 'qr';
        }

        // Build one entry per ENABLED + CONFIGURED + REGISTERED method. This
        // is the source of truth for the dashboard's multi-row display and for
        // the page generator (one block per method).
        $available = PaymentMethodsConfig::getAvailableMethodIds();
        $methods = [];
        foreach (self::orderedEnabledIds($enabled) as $methodId) {
            $holder = trim((string) ($accounts[$methodId . '_holder'] ?? ''));
            if ($holder === '') continue;
            // A method counts as configured when this build supports it and a
            // holder is filled in. Don't gate on shortcode_exists(): the shortcode
            // may not be registered yet during an admin-ajax request, which would
            // drop every method and leave a configured site stuck on "not set up".
            if (!in_array($methodId, $available, true)) continue;

            $url = self::buildPaymentUrl($methodId, $holder, $amount);
            $methods[] = [
                'id'          => $methodId,
                'label'       => self::methodLabel($methodId),
                'holder'      => $holder,
                'url'         => $url,
                'qrDataUri'   => $url !== '' ? self::generateQrDataUri($url) : '',
                'shortcode'   => self::buildShortcode($methodId, $amount, $goal, $presets),
                'blockMarkup' => self::buildBlockMarkup($methodId, $amount, $goal, $presets, $defaultMode),
                'displayName' => self::userLabel($methodId, $amount, $goal, $eventName),
            ];
        }

        $primary = !empty($methods) ? $methods[0] : null;

        return [
            // Multi-method
            'methods'      => $methods,
            'methodCount'  => count($methods),
            'configured'   => !empty($methods),

            // Display & goal state used by the generator and the UI
            'amount'       => $amount,
            'goal'         => $goal,
            'eventName'    => $eventName,
            'presets'      => array_values(array_filter(array_map('floatval', $presets), function ($v) { return $v > 0; })),
            'defaultMode'  => $defaultMode,

            // Singular pass-through — first configured method, or empty.
            // Onboarding artifact (single-method context) reads these.
            'method'       => $primary['id'] ?? '',
            'methodLabel'  => $primary['label'] ?? '',
            'holder'       => $primary['holder'] ?? '',
            'url'          => $primary['url'] ?? '',
            'qrDataUri'    => $primary['qrDataUri'] ?? '',
            'shortcode'    => $primary['shortcode'] ?? '',
            'blockMarkup'  => $primary['blockMarkup'] ?? '',
            'label'        => $primary['displayName'] ?? '',
            'linkSmsBody'  => $primary ? self::buildSmsBody($primary['displayName'], $primary['url']) : '',
        ];
    }

    /**
     * Deterministic order for enabled methods: the user's primary (set during
     * onboarding) first, then the free triplet, then everything else by config
     * order. Prevents arbitrary "first key wins" surprises like Bitcoin
     * jumping to the top because of insertion order.
     */
    private static function orderedEnabledIds(array $enabled): array
    {
        $primary = (string) get_option('wallet_up_primary_method', '');
        $freeFirst = ['cashapp', 'paypal', 'venmo'];

        $allEnabled = array_keys(array_filter($enabled));

        $ordered = [];
        if ($primary !== '' && in_array($primary, $allEnabled, true)) {
            $ordered[] = $primary;
        }
        foreach ($freeFirst as $id) {
            if (in_array($id, $allEnabled, true) && !in_array($id, $ordered, true)) {
                $ordered[] = $id;
            }
        }
        foreach ($allEnabled as $id) {
            if (!in_array($id, $ordered, true)) {
                $ordered[] = $id;
            }
        }
        return $ordered;
    }

    // -----------------------------------------------------------------------
    // URL building (direct deeplinks)
    // -----------------------------------------------------------------------

    public static function buildPaymentUrl(string $method, string $holder, string $amount = ''): string
    {
        if ($holder === '') return '';

        // Prefer the registered PaymentMethod instance (handles Pro methods too)
        try {
            if (class_exists(PaymentRegistry::class)) {
                $registry = PaymentRegistry::getInstance();
                if (method_exists($registry, 'getMethod')) {
                    $instance = $registry->getMethod($method);
                    if ($instance && method_exists($instance, 'getPaymentUrl')) {
                        return $instance->getPaymentUrl($amount, '');
                    }
                }
            }
        } catch (\Throwable $e) {
            // fall through to manual
        }

        // Manual builders for the 3 free methods (covers the worst case where
        // the registry doesn't return an instance, e.g. method not yet enabled)
        $amount = $amount !== '' && is_numeric($amount) ? (string) (float) $amount : '';
        switch ($method) {
            case 'cashapp':
                $tag = ltrim(trim($holder), '$');
                $url = 'https://cash.app/$' . rawurlencode($tag);
                if ($amount !== '') $url .= '/' . $amount;
                return $url;
            case 'paypal':
                $user = trim($holder);
                $url = 'https://paypal.me/' . rawurlencode($user);
                if ($amount !== '') $url .= '/' . $amount;
                return $url;
            case 'venmo':
                $user = ltrim(trim($holder), '@');
                $url = 'https://venmo.com/' . rawurlencode($user);
                if ($amount !== '') $url .= '?txn=pay&amount=' . $amount;
                return $url;
            default:
                return '';
        }
    }

    // -----------------------------------------------------------------------
    // QR generation (direct via endroid; no caching/tracking — fast preview)
    // -----------------------------------------------------------------------

    public static function generateQrDataUri(string $data): string
    {
        if ($data === '') {
            return '';
        }
        // Use the same QR service the front-end display uses, so the dashboard
        // preview matches the live QR and stays compatible with whatever Endroid
        // version ships. No 'payment_method' is passed, so the preview encodes
        // the direct payment URL (no scan-tracking redirect).
        try {
            if (class_exists(QRCodeService::class)) {
                $uri = QRCodeService::getInstance()->generateQR($data);
                if (is_string($uri) && $uri !== '') {
                    return $uri;
                }
            }
        } catch (\Throwable $e) {
            // fall through
        }
        return '';
    }

    // -----------------------------------------------------------------------
    // Shortcode + block markup builders
    // -----------------------------------------------------------------------

    private static function buildShortcode(string $method, string $amount, string $goal, array $presets): string
    {
        if ($goal === 'donations') {
            // No dedicated donations shortcode in Free — we recommend the block.
            return '';
        }
        $scan = $goal === 'qr' ? 'yes' : 'no';
        $amt = $amount !== '' ? sprintf(' amount="%s"', esc_attr($amount)) : '';
        return sprintf('[%s scan="%s"%s]', $method, $scan, $amt);
    }

    /**
     * Block selection rules:
     *   goal=donations     → tip-jar (always)
     *   goal=qr            → payment-qr (explicit user intent overrides display setting)
     *   else               → user's wallet_up_display_settings.defaultMode
     *                        (qr → payment-qr, button → payment-button, link → pay-link)
     */
    private static function buildBlockMarkup(string $method, string $amount, string $goal, array $presets, string $defaultMode = 'qr'): string
    {
        if ($goal === 'donations') {
            $clean = array_values(array_filter(array_map('floatval', $presets), function ($v) { return $v > 0; }));
            if (empty($clean)) $clean = [3.0, 5.0, 10.0];
            $attrs = wp_json_encode([
                'method'      => $method,
                'presets'     => $clean,
                'allowCustom' => true,
            ]);
            return sprintf('<!-- wp:walletup/tip-jar %s /-->', $attrs);
        }

        $blockMap = [
            'qr'     => 'walletup/payment-qr',
            'button' => 'walletup/payment-button',
            'link'   => 'walletup/pay-link',
        ];
        $blockName = $blockMap[$defaultMode] ?? $blockMap['qr'];

        // Explicit "qr" goal overrides display preference (the user picked QR as their goal)
        if ($goal === 'qr') {
            $blockName = 'walletup/payment-qr';
        }

        $attrs = ['method' => $method];
        if ($amount !== '') {
            $attrs['amount'] = $amount;
        }
        return sprintf('<!-- wp:%s %s /-->', $blockName, wp_json_encode($attrs));
    }

    // -----------------------------------------------------------------------
    // Labels + page title
    // -----------------------------------------------------------------------

    public static function methodLabel(string $methodId): string
    {
        if (class_exists(PaymentMethodsConfig::class) && defined(PaymentMethodsConfig::class . '::PAYMENT_METHODS')) {
            $cfg = PaymentMethodsConfig::PAYMENT_METHODS[$methodId] ?? null;
            if (is_array($cfg) && !empty($cfg['name'])) {
                return (string) $cfg['name'];
            }
        }
        return ucfirst($methodId);
    }

    private static function userLabel(string $method, string $amount, string $goal, string $eventName): string
    {
        $methodName = self::methodLabel($method);
        if ($goal === 'event' && $eventName !== '') {
            return sprintf(
                /* translators: 1: event name, 2: price, 3: payment method */
                __('%1$s — $%2$s via %3$s', 'walletup'),
                $eventName,
                $amount !== '' ? $amount : '?',
                $methodName
            );
        }
        if ($goal === 'donations') {
            return sprintf(__('Tip Jar — %s', 'walletup'), $methodName);
        }
        if ($amount !== '') {
            return sprintf(__('Pay $%1$s with %2$s', 'walletup'), $amount, $methodName);
        }
        return sprintf(__('Pay with %s', 'walletup'), $methodName);
    }

    private static function pageTitleFor(array $artifact, string $onlyMethodId = ''): string
    {
        // Single-method install: weave the method name into the title so the
        // user (and search engines) get a clearer page identity than "Pay me".
        if ($onlyMethodId !== '') {
            $picked = self::findMethod($artifact, $onlyMethodId);
            if ($picked !== null) {
                $methodName = $picked['label'];
                switch ($artifact['goal']) {
                    case 'event':
                        return $artifact['eventName'] !== ''
                            ? sprintf(
                                /* translators: 1: event name, 2: payment method name */
                                __('%1$s — %2$s', 'walletup'),
                                $artifact['eventName'],
                                $methodName
                            )
                            : sprintf(
                                /* translators: %s: payment method name */
                                __('Pay with %s', 'walletup'),
                                $methodName
                            );
                    case 'donations':
                        return sprintf(
                            /* translators: %s: payment method name */
                            __('Tip jar — %s', 'walletup'),
                            $methodName
                        );
                    case 'qr':
                        return sprintf(
                            /* translators: %s: payment method name */
                            __('Scan to pay with %s', 'walletup'),
                            $methodName
                        );
                    default:
                        return sprintf(
                            /* translators: %s: payment method name */
                            __('Pay with %s', 'walletup'),
                            $methodName
                        );
                }
            }
        }

        switch ($artifact['goal']) {
            case 'event':
                return $artifact['eventName'] !== ''
                    ? $artifact['eventName']
                    : __('Tickets', 'walletup');
            case 'donations':
                return __('Support us', 'walletup');
            case 'qr':
                return __('Scan to pay', 'walletup');
            case 'services':
                return __('Pay me', 'walletup');
            default:
                return __('Wallet Up payment', 'walletup');
        }
    }

    private static function buildSmsBody(string $label, string $url): string
    {
        if ($url === '') return '';
        return sprintf("%s\n%s", $label, $url);
    }

    // -----------------------------------------------------------------------
    // Draft page creation
    // -----------------------------------------------------------------------

    private static function createDraftPage(string $title, string $content): int
    {
        $pageId = wp_insert_post([
            'post_title'   => $title,
            'post_content' => $content,
            'post_status'  => 'draft',
            'post_type'    => 'page',
            'post_author'  => get_current_user_id(),
            'meta_input'   => [
                '_wallet_up_generated' => time(),
            ],
        ], true);

        return is_wp_error($pageId) ? 0 : (int) $pageId;
    }
}
