<?php

declare(strict_types=1);

namespace PrettyLinks\Pro\PrettyBar\Templates;

use PrettyLinks\Options\Store as LiteOptionsStore;

/**
 * Common helpers shared by every Bar template — document header emission,
 * string truncation, hex colour sanitization, and the
 * CSS-variable / inner-block builders used by every modern template.
 * Templates extend this and only implement the markup that differentiates
 * them (surrounding `<body class="…">` wrapper, iframe placement, etc.).
 */
abstract class AbstractTemplate implements BarTemplate
{
    /**
     * Shared default key set common to every modern template. Per-template
     * defaults() methods layer template-specific extras (height, position)
     * on top. Char limits seed from v3 site-wide options so an admin's
     * existing numeric tuning carries forward on upgrade.
     *
     * @return array<string, mixed>
     */
    protected static function sharedDefaults(): array
    {
        // V3 stored these character caps on `prli_options` (Lite store).
        // Reading them here seeds each v4 template's defaults from the
        // admin's v3-era values on first render — so an existing site's
        // tuning carries forward without a migration step.
        $lite = new LiteOptionsStore();
        return [
            'bgType'       => 'solid', // Solid | gradient.
            'bgColor'      => '#ffffff',
            'bgColor2'     => '#7c3aed',
            'bgAngle'      => 135,
            'bgOpacity'    => 100,           // 0..100 — turn down to combine with blur for frosted glass
            'textColor'    => '#111827',
            'accentColor'  => '#01aae9',
            'ctaTextColor' => '#ffffff',
            'ctaText'      => __('Share', 'pretty-link-pro'),
            'borderRadius' => 0,
            'shadow'       => 'soft',        // None | soft | medium | strong.
            'logoHeight'   => 32,            // 16..56 px
            'showTitle'    => true,
            'showDesc'     => true,
            'showUrl'      => true,
            'showShare'    => false,
            'showAttrib'   => true,
            'showDismiss'  => false,
            'titleLimit'   => (int) $lite->get('prettybar_title_limit', 60),
            'descLimit'    => (int) $lite->get('prettybar_desc_limit', 140),
            'urlLimit'     => (int) $lite->get('prettybar_link_limit', 60),
        ];
    }

    /**
     * Returns the template's unique identifier.
     *
     * @return string
     */
    abstract public function id(): string;

    /**
     * Returns the template's human-readable label.
     *
     * @return string
     */
    abstract public function label(): string;

    /**
     * Returns the resolved settings for this template.
     *
     * @return array<string, mixed>
     */
    abstract public function settings(): array;

    /**
     * Renders the bar document for the given link and target.
     *
     * @param array<string, mixed> $link     Link row.
     * @param string               $target   Resolved outbound URL.
     * @param array<string, mixed> $settings Resolved template settings.
     *
     * @return void
     */
    abstract public function render(array $link, string $target, array $settings): void;

    /**
     * Truncate a display string to `$limit` characters, appending an ellipsis.
     * `$limit <= 0` returns the trimmed input unchanged.
     *
     * @param string  $value The string to truncate.
     * @param integer $limit The maximum number of characters.
     *
     * @return string The truncated string.
     */
    protected function truncate(string $value, int $limit): string
    {
        $value = trim($value);
        if ($value === '' || $limit <= 0) {
            return $value;
        }
        if (mb_strlen($value) <= $limit) {
            return $value;
        }
        return mb_substr($value, 0, $limit) . '…';
    }

    /**
     * Validates a hex color string, returning a fallback when malformed.
     *
     * @param string $value    The candidate hex color.
     * @param string $fallback The fallback color used when invalid.
     *
     * @return string The sanitized hex color.
     */
    protected function sanitizeHex(string $value, string $fallback): string
    {
        $value = trim($value);
        if ($value === '' || preg_match('/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $value) !== 1) {
            return $fallback;
        }
        return $value;
    }

    /**
     * Emit response headers + opening <head> for a cloaked render. The
     * caller is responsible for closing </head> and writing the <body>.
     *
     * @param array<string, mixed> $link      Link row.
     * @param string               $title     Document title.
     * @param string               $inlineCss Optional inline CSS appended in a style tag.
     *
     * @return void
     */
    protected function emitHead(array $link, string $title, string $inlineCss = ''): void
    {
        nocache_headers();
        if (!headers_sent()) {
            header('Content-Type: text/html; charset=utf-8');
            header('X-Pretty-Link: prettybar:' . $this->id());
        }

        $baseUrl    = defined('PRLI_URL') ? (string) constant('PRLI_URL') : '';
        $stylesheet = rtrim($baseUrl, '/') . '/assets/css/pretty-bar.css';

        echo '<!doctype html><html lang="en"><head>';
        echo '<meta charset="utf-8">';
        echo '<meta name="robots" content="noindex,nofollow">';
        echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
        echo '<title>' . esc_html($title) . '</title>';
        // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet -- Pretty Bar renders a standalone HTML shell; wp_enqueue_style is unavailable here.
        echo '<link rel="stylesheet" href="' . esc_url($stylesheet) . '">';
        if ($inlineCss !== '') {
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $inlineCss is built internally from template-owned CSS strings; no user input.
            echo '<style>' . $inlineCss . '</style>';
        }
        do_action('prli-pretty-bar-head', $link);
        echo '</head>';
    }

    /**
     * Run the rendered "Powered by Pretty Links" attribution markup through
     * v3's `prli-display-attrib-link` filter and return what should be
     * emitted. The `prettybar_hide_attrib_link` option (`$hideAttrib`)
     * decides the default: when hidden, the markup passed in is the empty
     * string. The filter receives the (possibly empty) HTML string and its
     * return value is rendered verbatim — returning '' hides the credit,
     * matching v3 behavior.
     *
     * @param string               $html       Pre-escaped attribution markup, or '' when hidden by option.
     * @param boolean              $hideAttrib Whether the attribution link is hidden by option.
     * @param array<string, mixed> $link       Link row.
     *
     * @return string The attribution markup to emit.
     */
    protected function attributionHtml(string $html, bool $hideAttrib, array $link): string
    {
        if ($hideAttrib) {
            $html = '';
        }

        /**
         * Filtered attribution markup.
         *
         * @var string $html
         */
        $html = apply_filters('prli-display-attrib-link', $html, $link);
        return (string) $html;
    }

    /**
     * Normalizes the attribution URL, falling back to the default site.
     *
     * @param string $url The configured attribution URL.
     *
     * @return string The resolved attribution URL.
     */
    protected function attributionUrl(string $url): string
    {
        $url = trim($url);
        return $url === '' ? 'https://prettylinks.com' : $url;
    }

    // ─────────────────────────────────────────────────────────────────
    // Shared render helpers used by every modern template.
    // Each emits a CSS-class-only block so pretty-bar.css owns the look
    // and the inline `:root { --pb-* }` vars (built via rootCss below)
    // carry all per-template customization.
    // ─────────────────────────────────────────────────────────────────

    /**
     * Inline `:root { --pb-* }` declarations for a template's current
     * settings. Values are sanitized here rather than echoed directly in
     * render() so a typo in an admin-saved hex can't break the page.
     *
     * @param array<string, mixed> $settings Resolved template settings.
     *
     * @return string The inline `:root` CSS declaration block.
     */
    protected function rootCss(array $settings): string
    {
        $bgColor  = $this->sanitizeHex((string) ($settings['bgColor']  ?? '#ffffff'), '#ffffff');
        $bgColor2 = $this->sanitizeHex((string) ($settings['bgColor2'] ?? '#7c3aed'), '#7c3aed');
        $text     = $this->sanitizeHex((string) ($settings['textColor']    ?? '#111827'), '#111827');
        $accent   = $this->sanitizeHex((string) ($settings['accentColor']  ?? '#01aae9'), '#01aae9');
        $ctaText  = $this->sanitizeHex((string) ($settings['ctaTextColor'] ?? '#ffffff'), '#ffffff');

        $angle   = max(0, min(359, (int) ($settings['bgAngle']   ?? 135)));
        $opacity = max(0, min(100, (int) ($settings['bgOpacity'] ?? 100))) / 100;

        // Apply opacity to the color(s) so `backdrop-filter: blur()` has
        // something to blur through. Opaque (100%) emits the hex as-is
        // so the CSS is as clean as possible in the common case.
        $stopA = $opacity >= 1 ? $bgColor  : self::hexToRgba($bgColor, $opacity);
        $stopB = $opacity >= 1 ? $bgColor2 : self::hexToRgba($bgColor2, $opacity);

        $bg = ($settings['bgType'] ?? 'solid') === 'gradient'
            ? 'linear-gradient(' . $angle . 'deg,' . $stopA . ' 0%,' . $stopB . ' 100%)'
            : $stopA;

        $shadow = self::shadowValue((string) ($settings['shadow'] ?? 'soft'));
        $radius = max(0, min(999, (int) ($settings['borderRadius'] ?? 0))) . 'px';
        $logoH  = max(12, min(72, (int) ($settings['logoHeight']   ?? 32))) . 'px';

        $pieces = [
            '--pb-bg:' . $bg,
            '--pb-text:' . $text,
            '--pb-accent:' . $accent,
            '--pb-cta-text:' . $ctaText,
            '--pb-shadow:' . $shadow,
            '--pb-radius:' . $radius,
            '--pb-logo-h:' . $logoH,
        ];
        return ':root{' . implode(';', $pieces) . ';}';
    }

    /**
     * Convert #RGB or #RRGGBB to `rgba(r,g,b,alpha)`. Used to bake
     * admin-picked opacity into the emitted background color so the
     * stylesheet can stay simple (no separate opacity custom prop).
     *
     * @param string $hex   The hex color (#RGB or #RRGGBB).
     * @param float  $alpha The alpha value between 0 and 1.
     *
     * @return string The rgba() color string.
     */
    protected static function hexToRgba(string $hex, float $alpha): string
    {
        $hex = ltrim($hex, '#');
        if (strlen($hex) === 3) {
            $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
        }
        if (strlen($hex) !== 6 || !ctype_xdigit($hex)) {
            return 'rgba(255,255,255,' . $alpha . ')';
        }
        $r = hexdec(substr($hex, 0, 2));
        $g = hexdec(substr($hex, 2, 2));
        $b = hexdec(substr($hex, 4, 2));
        // Round to 2 decimals to keep the emitted CSS short.
        return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . round($alpha, 2) . ')';
    }

    /**
     * Maps a shadow preset key to its CSS box-shadow value.
     *
     * @param string $key The shadow preset key.
     *
     * @return string The CSS box-shadow value.
     */
    protected static function shadowValue(string $key): string
    {
        switch ($key) {
            case 'none':
                return 'none';
            case 'medium':
                return '0 8px 24px -6px rgba(0,0,0,0.15)';
            case 'strong':
                return '0 16px 40px -10px rgba(0,0,0,0.25)';
            case 'soft':
            default:
                return '0 2px 8px rgba(0,0,0,0.06)';
        }
    }

    /**
     * `<img>` tag for the shared Pretty Bar logo, or empty string when
     * no logo is configured. Wraps vs. direct based on caller's markup.
     *
     * @param array<string, mixed> $shared  Shared Pretty Bar settings.
     * @param string               $altText Image alt text.
     *
     * @return string The `<img>` markup, or an empty string.
     */
    protected function logoImg(array $shared, string $altText = ''): string
    {
        $url = (string) ($shared['logo_url'] ?? '');
        if ($url === '') {
            return '';
        }
        return '<img src="' . esc_url($url) . '" alt="' . esc_attr($altText) . '">';
    }

    /**
     * Emit the title / note / target-url block. Each row is gated by its
     * `show*` setting so admins can hide anything they don't want.
     *
     * @param array<string, mixed> $link     Link row.
     * @param string               $target   Resolved outbound URL.
     * @param array<string, mixed> $settings Resolved template settings.
     * @param boolean              $isPill   Whether the block is rendered in pill mode.
     *
     * @return string The meta block markup.
     */
    protected function metaBlock(array $link, string $target, array $settings, bool $isPill = false): string
    {
        $title = (string) ($link['name'] ?? '');
        $desc  = (string) ($link['description'] ?? '');
        $out   = '';

        if (!empty($settings['showTitle']) && $title !== '') {
            $out .= '<div class="prli-pb-title">'
                . esc_html($this->truncate($title, (int) ($settings['titleLimit'] ?? 60)))
                . '</div>';
        }
        if (!$isPill && !empty($settings['showDesc']) && $desc !== '') {
            $out .= '<div class="prli-pb-desc">'
                . esc_html($this->truncate($desc, (int) ($settings['descLimit'] ?? 140)))
                . '</div>';
        }
        if (!empty($settings['showUrl'])) {
            $out .= '<a class="prli-pb-target" href="' . esc_url($target) . '" target="_top" rel="noopener">'
                . esc_html($this->truncate($target, (int) ($settings['urlLimit'] ?? 60)))
                . '</a>';
        }
        return $out === '' ? '' : '<div class="prli-pb-meta">' . $out . '</div>';
    }

    /**
     * Emit the actions cluster — share CTA, attribution link, optional
     * dismiss button. Order is fixed so screens and screen readers
     * encounter them in the same sequence regardless of template.
     *
     * @param array<string, mixed> $link     Link row.
     * @param string               $target   Resolved outbound URL.
     * @param array<string, mixed> $settings Resolved template settings.
     * @param array<string, mixed> $shared   Shared Pretty Bar settings.
     *
     * @return string The actions block markup.
     */
    protected function actionsBlock(array $link, string $target, array $settings, array $shared): string
    {
        $parts = [];

        if (!empty($settings['showShare'])) {
            $shareUrl = 'https://x.com/intent/tweet?url=' . rawurlencode($target)
                . '&text=' . rawurlencode((string) ($link['name'] ?? ''));
            $label    = trim((string) ($settings['ctaText'] ?? ''));
            if ($label === '') {
                $label = __('Share', 'pretty-link-pro');
            }
            $parts[] = '<a class="prli-pb-share" href="' . esc_url($shareUrl)
                . '" target="_blank" rel="noopener nofollow">' . esc_html($label) . '</a>';
        }

        if (!empty($settings['showAttrib'])) {
            $attribHtml = '<a class="prli-pb-attrib" href="' . esc_url($this->attributionUrl((string) ($shared['attribution_url'] ?? '')))
                . '" target="_blank" rel="noopener">'
                . esc_html__('Powered by Pretty Links', 'pretty-link-pro') . '</a>';
            $attribHtml = $this->attributionHtml($attribHtml, (bool) ($shared['hide_attribution'] ?? false), $link);
            if ($attribHtml !== '') {
                $parts[] = $attribHtml;
            }
        }

        if (!empty($settings['showDismiss'])) {
            $parts[] = '<button type="button" class="prli-pb-dismiss" aria-label="'
                . esc_attr__('Dismiss', 'pretty-link-pro')
                . '"><svg width="10" height="10" viewBox="0 0 10 10" fill="none" aria-hidden="true">'
                . '<path d="M2 2l6 6M8 2L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>'
                . '</svg></button>';
        }

        return empty($parts) ? '' : '<div class="prli-pb-actions">' . implode('', $parts) . '</div>';
    }

    /**
     * Tiny inline script for the dismiss button — removes the closest
     * .prli-pb-bar or .prli-pb-pill on click. Same across templates,
     * inlined here rather than chunked out into a separate JS file so
     * there's no extra HTTP round-trip on the redirect page.
     */
    protected function dismissScript(): string
    {
        return '<script>document.querySelectorAll(".prli-pb-dismiss").forEach(function(b){'
            . 'b.addEventListener("click",function(){'
            . 'var p=b.closest(".prli-pb-pill,.prli-pb-bar");if(p)p.remove();'
            . '});});</script>';
    }
}
