<?php
declare(strict_types=1);

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

/**
 * Icon element renderer (shared between frontend and editor).
 *
 * Phase 1, step 5 of the renderer-unification refactor. The pre-extraction
 * icon was the WORST-asymmetric block in the codebase — the editor
 * carried roughly fifty extra lines of container/SVG/data-attr logic the
 * frontend never gained. Halo, SVG-URL inline reading, the outer
 * alignment wrapper, the 4-sided padding container, the data-view
 * attribute and the continuous-effect class all only worked in the
 * editor. The user explicitly asked for full unification — "frontend
 * should render exactly the same as the editor" — so this function
 * adopts the editor's structure as canonical and the frontend gains
 * every missing feature.
 *
 * Pre-extraction differences and resolutions:
 *
 *   - Halo emission: BOTH renderers computed halo, but the user reported
 *     it only rendered in the editor in practice. Cause: editor passed
 *     `$_iconHaloText` through esc_attr inside the style string (which
 *     is harmless — '#' and ',' and ':' don't get encoded by esc_attr).
 *     Frontend concatenated raw $_iconHaloCssText into the style string
 *     and then esc_attr'd the whole string. Both should produce the
 *     same output, but unification removes the divergence by sharing
 *     one code path.
 *     → Unified: literal halo CSS in both contexts.
 *
 *   - Outer alignment wrapper: editor wrapped the card in an extra div
 *     with `text-align:$align`. Frontend didn't — it set text-align on
 *     the card itself and used flex justify on the wrapper inside.
 *     → Unified to editor's structure: outer wrapper + card. The
 *     wrapper is the parent flex/grid child; the card sits inside,
 *     centered by text-align.
 *
 *   - Container styling: editor read container_bg, padding_top/right/
 *     bottom/left, container_radius, container_width, container_height,
 *     valign. Frontend ignored all of these.
 *     → Unified: both honour them. Existing pages with these unset
 *     (which is the default) render identically.
 *
 *   - SVG URL handling: editor detected URL/path icons and read the
 *     SVG file inline (via file_get_contents) so currentColor would
 *     apply. Frontend passed the URL straight to impulse_lucide_svg,
 *     which returned empty string (not a lucide name) — so icons
 *     referenced via SVG URLs rendered EMPTY on the frontend. Silent
 *     production bug.
 *     → Unified: both inline-read the SVG when the icon value is a
 *     URL or path.
 *
 *   - data-view attribute: editor emits `data-view="$view"` on the
 *     icon-wrap span. Frontend doesn't.
 *     → Unified to always emit (harmless when unused).
 *
 *   - Continuous-effect class: editor adds `impulse-effect-$contEffect`
 *     when continuous_effect is set. Frontend has the same effects but
 *     never read the setting — class never emitted.
 *     → Unified to honour the setting in both.
 *
 *   - Hover transition: editor `transition:all`. Frontend `transition:color`.
 *     → Unified to `transition:all` (smoother for hover-effects that
 *     change shadow/transform).
 *
 *   - Tag for the icon: frontend used <a>|<span> for the wrap when
 *     link is set, and an outer <div> for the card. Editor used <span>
 *     for the wrap and wrapped both wrap + content in an <a> inside the
 *     card.
 *     → Unified to editor's structure (link wraps both icon + content;
 *     clicking the title also navigates).
 *
 *   - Title margin-bottom: both used 4px when text present, 0 when not.
 *     Frontend additionally added `min-height:0` to collapse empty
 *     placeholders. Editor didn't.
 *     → Unified to emit min-height:0 on empty title/text divs.
 *
 *   - CSS variables (editor) vs literals (frontend): preserved
 *     per-context for `color` and `size` so the panel's slider drags
 *     trigger live updates in the editor without iframe refresh.
 *
 *   - Parallax JS global: frontend primes
 *     $GLOBALS['impulse_needs_feature_card_parallax'] so the asset
 *     enqueue downstream loads the script. Editor's iframe always
 *     loads it.
 *     → Preserved per-context.
 *
 * Dead code: the pre-extraction editor file had a SECOND `case 'icon_card':`
 * at line 2605 — never reached (PHP switch executes only the first
 * match). The duplicate is removed when this case is replaced with a
 * single-line call to this function. The duplicates for 'divider',
 * 'code', and 'unfold' are also dead and were also removed below.
 *
 * @param array|object $el       Element node.
 * @param array        $settings Resolved settings (editor) or raw (frontend).
 * @param string       $context  'frontend' or 'editor'.
 *
 * @return string Inner HTML.
 *
 * @sideeffect Frontend primes $GLOBALS['impulse_needs_feature_card_parallax']
 *             when hover_effect = 'parallax'; emits inline CSS on the
 *             instinctor-page-inline handle for hover/parallax animations
 *             in both contexts.
 */
function impulse_element_render_icon_card($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $elId     = preg_replace('/[^A-Za-z0-9_-]/', '', (string) ($el['id'] ?? '0'));
    $isEditor = ($context === 'editor');

    // ── Settings ──────────────────────────────────────────────────
    $icon            = (string) ($settings['icon'] ?? 'star');
    $size            = intval($settings['size'] ?? 48);
    // Mobile rule (Sergei 2026-07-02): icon-card icons default to 24px on mobile,
    // overridable by an explicit size_mobile/_tablet (default via gating, not a
    // cascade lock). Editor mobile preview here; frontend @media near return.
    $_iconMobDefault = (($settings['size_mobile'] ?? '') === '') && (($settings['size_tablet'] ?? '') === '');
    // When the editor previews mobile and no explicit size override exists, the
    // 24px default must be emitted as a HARD value — not the var(--impulse-size)
    // fallback — because the editor's live-preview injects --impulse-size from the
    // device-unaware desktop 'size' (48) on select, which would otherwise win over
    // a fallback and snap the icon back to 48px on click. Sergei 2026-07-02.
    $_iconMobHard = false;
    if ($isEditor && ($GLOBALS['impulse_preview_device'] ?? 'desktop') === 'mobile' && $_iconMobDefault) {
        $size = 24;
        $_iconMobHard = true;
    }
    // In the hard-mobile case the size is a plain px value everywhere (no var), so
    // the live-preview var cannot override it. Elsewhere the editor keeps the var.
    $_useSizeVar = $isEditor && !$_iconMobHard;
    $color           = (string) ($settings['color'] ?? '#666666');
    $align           = (string) ($settings['align'] ?? 'center');
    $link            = (string) ($settings['link'] ?? '');
    $newTab          = !empty($settings['new_tab']);
    $view            = (string) ($settings['view'] ?? 'default');
    $padding         = intval($settings['padding'] ?? 16);
    $borderRadius    = intval($settings['border_radius'] ?? 50);
    $borderWidth     = intval($settings['border_width'] ?? 0);
    $hoverTransition = intval($settings['hover_transition'] ?? 300);
    $hasImage        = !empty($settings['icon_image']);

    // Title/text — title shares text_color (no separate title swatch).
    $titleContent = (string) ($settings['title_content'] ?? '');
    $titleSize    = intval($settings['title_size'] ?? 18);
    $textContent  = (string) ($settings['text_content'] ?? '');
    $textSize     = intval($settings['text_size'] ?? 14);
    $textColor    = (string) ($settings['text_color'] ?? '#666666');
    $titleColor   = $textColor;
    $textPosition = (string) ($settings['text_position'] ?? 'below');
    $textSpacing  = intval($settings['text_spacing'] ?? 12);
    // Mobile rule (Sergei 2026-07-02): tighten the icon->title->text gap on phones so
    // the stacked feature cards stay compact. Overridable by text_spacing_mobile/_tablet.
    $_spaceMobDefault = (($settings['text_spacing_mobile'] ?? '') === '') && (($settings['text_spacing_tablet'] ?? '') === '');
    if ($isEditor && ($GLOBALS['impulse_preview_device'] ?? 'desktop') === 'mobile' && $_spaceMobDefault && $textSpacing > 8) {
        $textSpacing = 8;
    }

    // Container box (editor parity).
    $valign          = (string) ($settings['valign'] ?? 'center');
    $containerBg     = (string) ($settings['container_bg'] ?? 'transparent');
    $paddingTop      = intval($settings['padding_top'] ?? 0);
    $paddingRight    = intval($settings['padding_right'] ?? 0);
    $paddingBottom   = intval($settings['padding_bottom'] ?? 0);
    $paddingLeft     = intval($settings['padding_left'] ?? 0);
    $containerRadius = intval($settings['container_radius'] ?? 0);
    $containerWidth  = intval($settings['container_width'] ?? 0);
    $containerHeight = intval($settings['container_height'] ?? 0);

    $contEffect = (string) ($settings['continuous_effect'] ?? 'none');

    // SVG-URL detection: icon value is a path/URL rather than a Lucide name.
    $isSvg = (strpos($icon, 'http') === 0 || strpos($icon, '/') === 0 || strpos($icon, '.svg') !== false);

    // Stable id used by hover/parallax CSS and (in editor) by live-update.
    $iconId = 'impulse-icon-' . $elId;

    // Halo — slider 0..100, same semantics as Text/Button. drop-shadow
    // around the SVG strokes, text-shadow around the title + body spans.
    $_iconHalo     = max(0, min(100, intval($settings['text_halo'] ?? 0)));
    $_iconHaloIcon = ($_iconHalo > 0 && function_exists('impulse_drop_shadow_halo_css'))
                       ? impulse_drop_shadow_halo_css($_iconHalo, $color) : '';
    $_iconHaloText = ($_iconHalo > 0 && function_exists('impulse_text_halo_css'))
                       ? impulse_text_halo_css($_iconHalo, $textColor) : '';

    // ── Icon-wrap style ───────────────────────────────────────────
    // Editor uses CSS vars for color + size so panel-driven live updates
    // mutate without iframe refresh; frontend emits literals.
    $iconWrapStyle  = 'display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;';
    $iconWrapStyle .= 'transition:all ' . $hoverTransition . 'ms ease-in-out;';
    if ($isEditor) {
        $iconWrapStyle .= 'color:var(--impulse-color,' . esc_attr($color) . ');';
    } else {
        $iconWrapStyle .= 'color:' . esc_attr($color) . ';';
    }
    if ($_iconHaloIcon) $iconWrapStyle .= $_iconHaloIcon;

    // Font-size only applies to icon-font glyphs (Font Awesome) — not SVGs.
    if (!$isSvg) {
        if ($_useSizeVar) {
            $iconWrapStyle .= 'font-size:var(--impulse-size,' . $size . 'px);';
        } else {
            $iconWrapStyle .= 'font-size:' . $size . 'px;';
        }
    }

    // View modes — border (framed), square, default.
    $hasBorder    = $borderWidth > 0;
    $isCircleView = $hasBorder; // legacy: stroke > 0 implies circular framed view
    $isSquareView = ($view === 'square');
    if ($hasBorder) {
        if ($hasImage) {
            $sizeVar = $_useSizeVar ? 'var(--impulse-size,' . $size . 'px)' : ($size . 'px');
            $iconWrapStyle .= 'width:' . $sizeVar . ';height:' . $sizeVar . ';';
        } else {
            $iconWrapStyle .= 'padding:' . $padding . 'px;';
        }
        $iconWrapStyle .= 'border:' . $borderWidth . 'px solid ' . esc_attr($color) . ';';
        $iconWrapStyle .= 'border-radius:' . $borderRadius . '%;overflow:hidden;';
    } elseif ($isSquareView) {
        if ($hasImage) {
            $sizeVar = $_useSizeVar ? 'var(--impulse-size,' . $size . 'px)' : ($size . 'px');
            $iconWrapStyle .= 'width:' . $sizeVar . ';height:' . $sizeVar . ';';
        }
        $iconWrapStyle .= 'border-radius:4px;overflow:hidden;';
    } else {
        // Default view — pin background to transparent to defeat any
        // wrapper-level --impulse-bg-color leak (editor live-update).
        $iconWrapStyle .= 'background:transparent;';
    }

    // ── Hover effect (parallax / lift / none) ─────────────────────
    // Pre-2.1.229 this case emitted ~14 lines of inline CSS per icon
    // card via wp_add_inline_style with #impulse-icon-N-card selectors.
    // The rules are now consolidated as class+data-attr selectors in
    // frontend.css under "Icon hover effects" — ONE rule covers every
    // icon on every page, regardless of how many cards render. The
    // renderer just emits the `data-hover-effect` attribute below so
    // the CSS knows which mode applies. Same for the SVG fill rule
    // (now `.impulse-icon-wrap[data-svg="1"] svg{fill:currentColor}`).
    $hoverEffect = (string) ($settings['hover_effect'] ?? 'parallax');
    if ($hoverEffect === 'parallax' && !$isEditor) {
        $GLOBALS['impulse_needs_feature_card_parallax'] = true;
    }

    // ── Layout (container + content) ──────────────────────────────
    $isHorizontal = in_array($textPosition, ['left', 'right'], true);
    $isReversed   = in_array($textPosition, ['above', 'left'], true);
    $hasContent   = ($titleContent !== '' || $textContent !== '');
    $valignMap    = ['top' => 'flex-start', 'center' => 'center', 'bottom' => 'flex-end'];
    $alignItems   = $valignMap[$valign] ?? 'center';

    $containerStyle = 'display:inline-flex;';
    if ($hasContent) {
        $containerStyle .= 'gap:' . $textSpacing . 'px;';
        if ($isHorizontal) {
            $containerStyle .= 'flex-direction:' . ($isReversed ? 'row-reverse' : 'row') . ';align-items:' . $alignItems . ';';
        } else {
            // Stacked (icon above/below text): honour the card's `align` so the
            // icon left/center/right-aligns with the title+text instead of always
            // centering over a content-width container (which drifts per text
            // length). Default center keeps existing pages unchanged.
            $_colAlign = ['left' => 'flex-start', 'center' => 'center', 'right' => 'flex-end'][$align] ?? 'center';
            $containerStyle .= 'flex-direction:' . ($isReversed ? 'column-reverse' : 'column') . ';align-items:' . $_colAlign . ';';
        }
    } else {
        $containerStyle .= 'flex-direction:column;align-items:center;';
    }
    if ($containerBg !== '' && $containerBg !== 'transparent') {
        $containerStyle .= 'background:' . esc_attr($containerBg) . ';';
    }
    if ($paddingTop > 0 || $paddingRight > 0 || $paddingBottom > 0 || $paddingLeft > 0) {
        $containerStyle .= 'padding:' . $paddingTop . 'px ' . $paddingRight . 'px ' . $paddingBottom . 'px ' . $paddingLeft . 'px;';
    }
    if ($containerRadius > 0) {
        $containerStyle .= 'border-radius:' . $containerRadius . 'px;';
    }
    if ($containerWidth > 0) {
        $containerStyle .= 'width:' . $containerWidth . 'px;justify-content:center;';
    }
    if ($containerHeight > 0) {
        $containerStyle .= 'min-height:' . $containerHeight . 'px;justify-content:center;';
    }

    // Continuous effect class — applied to the card (so the whole icon
    // group pulses/floats/wobbles together).
    $hoverClass = '';
    if ($contEffect !== '' && $contEffect !== 'none') {
        $hoverClass = ' impulse-effect-' . esc_attr($contEffect);
    }

    // ── Icon HTML (img / inline SVG / lucide) ─────────────────────
    $iconImage = (string) ($settings['icon_image'] ?? '');
    if ($iconImage !== '') {
        if ($isCircleView || $isSquareView) {
            $imgStyle = 'width:100%;height:100%;object-fit:cover;display:block;';
        } else {
            $sizeVar  = $_useSizeVar ? 'var(--impulse-size,' . $size . 'px)' : ($size . 'px');
            $imgStyle = 'width:' . $sizeVar . ';height:auto;object-fit:contain;display:block;';
        }
        $iconInner = '<img src="' . esc_url($iconImage) . '" alt="" class="impulse-icon-image" style="' . esc_attr($imgStyle) . '">';
    } elseif ($isSvg) {
        // URL → filesystem-path resolution.  WP-aware (wp_upload_dir +
        // WP_CONTENT_DIR) with a standalone fallback that uses
        // INSTINCTOR_PLUGIN_DIR / Storage::path.
        $svgContent = '';
        $svgPath    = $icon;
        if (function_exists('wp_upload_dir')) {
            $_uploadDir = wp_upload_dir();
            if (!empty($_uploadDir['baseurl']) && strpos($icon, $_uploadDir['baseurl']) === 0) {
                $svgPath = str_replace($_uploadDir['baseurl'], $_uploadDir['basedir'], $icon);
            }
        }
        if ($svgPath === $icon && function_exists('content_url') && defined('WP_CONTENT_DIR')) {
            $contentBase = content_url();
            if (strpos($icon, $contentBase) === 0) {
                $svgPath = str_replace($contentBase, WP_CONTENT_DIR, $icon);
            }
        }
        // (Removed the home_url()->ABSPATH fallback: mapping the site URL to the
        // filesystem root via ABSPATH breaks on subdirectory / mapped-domain
        // installs. The uploads, content_url, and WP_CONTENT_URL branches cover
        // every real icon location; anything else falls back to an <img> tag
        // below. WP.org review Jun 2026.)
        if ($svgPath === $icon && defined('WP_CONTENT_URL') && defined('WP_CONTENT_DIR')) {
            // Root-relative content URL → filesystem path. Derive the content
            // path prefix from WP_CONTENT_URL so a renamed/relocated
            // wp-content directory still resolves correctly.
            $_cBasePath = (string) wp_parse_url(WP_CONTENT_URL, PHP_URL_PATH);
            if ($_cBasePath !== '' && strpos($icon, rtrim($_cBasePath, '/') . '/') === 0) {
                $svgPath = WP_CONTENT_DIR . substr($icon, strlen(rtrim($_cBasePath, '/')));
            }
        }
        if (file_exists($svgPath)) {
            $svgContent = (string) file_get_contents($svgPath);
            $svgContent = preg_replace('/<svg/', '<svg width="' . $size . '" height="' . $size . '"', $svgContent, 1);
            $svgContent = preg_replace('/fill="[^"]*"/', '', $svgContent);
            $svgContent = preg_replace('/<svg/', '<svg fill="currentColor"', $svgContent, 1);
            // Defense-in-depth: custom-icon SVG is read from an uploaded file; strip
            // the only XSS-bearing constructs before output (drawing markup untouched).
            $svgContent = preg_replace('#<script\b[^>]*>.*?</script\s*>#is', '', (string) $svgContent);
            $svgContent = preg_replace('#<foreignObject\b[^>]*>.*?</foreignObject\s*>#is', '', (string) $svgContent);
            $svgContent = preg_replace('#\son[a-z]+\s*=\s*("[^"]*"|\'[^\']*\'|[^\s>]+)#i', '', (string) $svgContent);
            $svgContent = (string) preg_replace('#(href|xlink:href)\s*=\s*(["\']?)\s*javascript:[^"\'>\s]*\2#i', '', (string) $svgContent);
            $iconInner = $svgContent;
        } else {
            $iconInner = '<img src="' . esc_url($icon) . '" alt="" style="width:' . $size . 'px;height:' . $size . 'px">';
        }
    } else {
        // Lucide name.
        $iconInner = function_exists('impulse_lucide_svg')
            ? impulse_lucide_svg($icon, $size, 'currentColor')
            : '';
    }

    // ── Title/text style ─────────────────────────────────────────
    $titleStyle  = 'font-size:' . $titleSize . 'px;font-weight:600;color:' . esc_attr($titleColor) . ';';
    $titleStyle .= 'margin-bottom:' . ($textContent !== '' ? '4' : '0') . 'px;';
    if ($titleContent === '') $titleStyle .= 'min-height:0;';
    $titleStyle .= $_iconHaloText;

    $bodyStyle  = 'font-size:' . $textSize . 'px;color:' . esc_attr($textColor) . ';white-space:pre-wrap;';
    if ($textContent === '') $bodyStyle .= 'min-height:0;';
    $bodyStyle .= $_iconHaloText;

    // ── Compose HTML ─────────────────────────────────────────────
    $wrapperStyle = 'text-align:' . esc_attr($align) . ';';

    ob_start();
    echo '<div style="' . esc_attr($wrapperStyle) . '">';
    // data-hover-effect drives the consolidated CSS rules in frontend.css
    // (replaces the per-element-ID parallax/lift inline CSS we used to emit).
    echo '<div id="' . esc_attr($iconId) . '-card" class="impulse-icon' . esc_attr($hoverClass) . '" data-hover-effect="' . esc_attr($hoverEffect) . '" style="' . esc_attr($containerStyle) . '">';
    if ($link !== '') {
        $linkStyle = 'text-decoration:none;color:inherit;';
        if ($hasContent) {
            $linkStyle .= 'display:flex;align-items:' . $alignItems . ';gap:' . $textSpacing . 'px;';
            $linkStyle .= $isHorizontal
                ? 'flex-direction:' . ($isReversed ? 'row-reverse' : 'row') . ';'
                : 'flex-direction:' . ($isReversed ? 'column-reverse' : 'column') . ';';
        }
        echo '<a href="' . esc_url($link) . '"' . ($newTab ? ' target="_blank" rel="noopener"' : '') . ' style="' . esc_attr($linkStyle) . '">';
    }
    // data-svg="1" triggers `.impulse-icon-wrap[data-svg="1"] svg{fill:currentColor}`
    // in frontend.css — replaces the per-element SVG-fill inline CSS.
    echo '<span id="' . esc_attr($iconId) . '" class="impulse-icon-wrap" data-view="' . esc_attr($view) . '"' . ($isSvg ? ' data-svg="1"' : '') . ' style="' . esc_attr($iconWrapStyle) . '">';
    echo wp_kses($iconInner, impulse_svg_allowed());
    echo '</span>';
    // Title + text divs are ALWAYS rendered (even empty) so live CSS-
    // content injection has a target on first keystroke. Empty divs
    // collapse via min-height:0 above.
    echo '<div class="impulse-icon-content" style="text-align:' . esc_attr($align) . ';' . ($isHorizontal ? 'flex:1;min-width:0;' : '') . '">';
    echo '<div class="impulse-icon-title" style="' . esc_attr($titleStyle) . '">' . esc_html($titleContent) . '</div>';
    echo '<div class="impulse-icon-text" style="' . esc_attr($bodyStyle) . '">' . esc_html($textContent) . '</div>';
    echo '</div>';
    if ($link !== '') echo '</a>';
    echo '</div>'; // close .impulse-icon
    echo '</div>'; // close alignment wrapper
    // Mobile rule: default the icon to 24px on mobile when no explicit mobile size.
    if (!$isEditor && $_iconMobDefault && function_exists('wp_add_inline_style')) {
        wp_add_inline_style('instinctor-page-inline', '@media(max-width:767px){#' . $iconId . '{width:24px!important;height:24px!important;font-size:24px!important}}');
    }
    // Mobile rule: tighten the icon->title->text gap on phones (default 12 -> 8).
    if (!$isEditor && $_spaceMobDefault && $textSpacing > 8 && function_exists('wp_add_inline_style')) {
        wp_add_inline_style('instinctor-page-inline', '@media(max-width:767px){#' . $iconId . '-card,#' . $iconId . '-card > a{gap:8px!important}}');
    }
    return ob_get_clean();
}
