<?php
declare(strict_types=1);

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

/**
 * Video element renderer (shared).
 *
 * Phase 1, step 16. Handles three sources transparently:
 *
 *   - YouTube (any /watch?v= / /embed/ / /shorts/ / youtu.be/ URL)
 *   - Vimeo   (vimeo.com/<id> URL)
 *   - Self-hosted MP4 (direct URL)
 *
 * Plus optional SVG mask (clip the video to a shape — viewBox sets
 * the aspect ratio when active), poster image, autoplay/loop/muted,
 * box-shadow preset, alignment, max-width cap.
 *
 * Pre-extraction notes:
 *
 *   - Sizing technique drift: editor used the legacy
 *     `padding-bottom: aspect%` trick; frontend used modern
 *     `aspect-ratio`. Output looked the same but the editor's
 *     approach broke when the wrapper was inside flex containers
 *     with `align-items: stretch`. Unified to `aspect-ratio` (modern
 *     canonical).
 *
 *   - Poster field drift: frontend reads `$settings['poster_url']`
 *     (string URL); editor reads `$settings['poster_id']` (int → resolved
 *     via impulse_resolve_image_url). Whichever name the panel saves
 *     would only work in ONE renderer.
 *     → Both keys now accepted, with `poster_id` resolved when present
 *     and `poster_url` used as fallback. Future panel saves should
 *     prefer poster_id.
 *
 *   - Shadow preset: editor honoured `shadow=small|medium|large` to
 *     emit a box-shadow on the wrapper. Frontend dropped this. Now
 *     emits in both.
 *
 *   - Frontend has `align` setting (left/center/right outer flex
 *     alignment); editor centred always. Editor now honours `align`
 *     too (was a silent miss).
 *
 *   - Frontend autoplay used `data-auto-inview="1"` so an
 *     IntersectionObserver plays only when the video scrolls into
 *     view; editor used the bare `autoplay` attribute. The IO path
 *     is friendlier to page-load performance. Preserved frontend
 *     behaviour for frontend; editor uses bare autoplay (the editor
 *     iframe is always "in view").
 *
 *   - Empty-state placeholder: both render the same play-glyph SVG
 *     in a grey square when no video URL is set yet. Centralised.
 */
function impulse_element_render_video($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $isEditor = ($context === 'editor');
    $device   = $isEditor ? ($GLOBALS['impulse_preview_device'] ?? 'desktop') : 'desktop';

    $read = function (string $key, $default) use ($settings, $isEditor, $device) {
        if ($isEditor && function_exists('impulse_device_val')) {
            return impulse_device_val($settings, $key, $device, $default);
        }
        return $settings[$key] ?? $default;
    };

    // Sources: video_url (canonical) or legacy video_file_url / video_id fallback.
    $videoUrl    = (string) ($settings['video_url'] ?? ($settings['video_file_url'] ?? ($settings['video_id'] ?? '')));
    $autoplay    = !empty($settings['autoplay']);
    $loop        = !empty($settings['loop']) || $autoplay;
    $muted       = !empty($settings['muted']) || $autoplay;
    $controls    = (!isset($settings['controls']) || $settings['controls'] === true || $settings['controls'] === 'true' || $settings['controls'] === 1 || $settings['controls'] === '1');
    $aspectRatio = (string) $read('aspect_ratio', '16:9');
    $borderRadius = intval($settings['border_radius'] ?? 0);
    $maxWidth    = intval($settings['max_width']    ?? 0);
    $width       = intval($read('width', 30));
    $align       = (string) ($settings['align']     ?? 'center');
    // Legacy SVG mask catalog retired in v2.1.1289 — replaced by the
    // unified crop engine (closed-loop Catmull-Rom with handles). The
    // `mask_url` / `mask_viewbox` settings are no longer read; existing
    // videos with a saved mask render as straight rectangles until the
    // user opens the crop overlay and shapes them.

    // Poster: editor saves `poster_id` (int), frontend used to save
    // `poster_url` (string). Accept both.
    $posterUrl = (string) ($settings['poster_url'] ?? '');
    if ($posterUrl === '' && !empty($settings['poster_id']) && function_exists('impulse_resolve_image_url')) {
        $posterUrl = impulse_resolve_image_url(intval($settings['poster_id']));
    }

    // ── Aspect ratio resolution ──────────────────────────────────
    $ratioMap = ['16:9'=>[16,9],'4:3'=>[4,3],'21:9'=>[21,9],'1:1'=>[1,1],'9:16'=>[9,16]];
    [$rw, $rh] = $ratioMap[$aspectRatio] ?? [16, 9];

    // Mask-driven aspect override retired with the mask catalog in v2.1.1289.

    // ── Outer + wrapper styles ───────────────────────────────────
    $justifyMap = ['left' => 'flex-start', 'center' => 'center', 'right' => 'flex-end'];
    $justify    = $justifyMap[$align] ?? 'center';
    $outerStyle = 'display:flex;align-items:center;justify-content:' . $justify . ';width:100%;';

    $wrapStyle  = 'position:relative;width:' . $width . '%;aspect-ratio:' . $rw . '/' . $rh
                . ';max-height:100dvh;max-width:100%;overflow:hidden';

    // Per-corner radius (v2.1.1289) — same data shape as image block, set
    // by the crop engine's canvas radius rings. Falls back to the legacy
    // scalar `border_radius` field if no per-corner values are present.
    $_rTL = max(0, intval($settings['video_radius_tl'] ?? 0));
    $_rTR = max(0, intval($settings['video_radius_tr'] ?? 0));
    $_rBR = max(0, intval($settings['video_radius_br'] ?? 0));
    $_rBL = max(0, intval($settings['video_radius_bl'] ?? 0));
    if ($_rTL || $_rTR || $_rBR || $_rBL) {
        $wrapStyle .= ';border-radius:' . $_rTL . 'px ' . $_rTR . 'px ' . $_rBR . 'px ' . $_rBL . 'px';
    } elseif ($borderRadius > 0) {
        $wrapStyle .= ';border-radius:' . $borderRadius . 'px';
    }
    // Auto margins on a flex item WIN over the outer flex's justify-content, so
    // an unconditional `margin:0 auto` centered the video even when align was
    // left/right (align was silently dead whenever max_width constrained it).
    // Only auto-center when align actually is 'center'; otherwise let the outer
    // justify-content place it.
    $wrapStyle .= ($align === 'center') ? ';margin:0 auto;' : ';margin:0;';
    if ($maxWidth > 0) $wrapStyle .= 'max-width:min(100%,' . $maxWidth . 'px);';

    // Crop silhouette (v2.1.1289) — shared engine with image. Reads
    // `video_crop_*` keys via the same Helpers\CropPath helper that the
    // image renderer uses. Returns CSS for the wrap + an inline SVG that
    // holds the clipPath element.
    $_videoCrop = \Instinctor\Helpers\CropPath::build($settings, intval($el['id'] ?? 0), 'video_crop_');
    if ($_videoCrop['css']) {
        $wrapStyle .= $_videoCrop['css'];
    }

    // Directional shadow was tried in v2.1.1305-1307 (Helpers\ShadowFilter
    // with 'video_shadow_' prefix) and dropped in v2.1.1308: the per-side
    // feMerge composite stacks alpha at corners (~0.64 where 2 sides
    // overlap), which reads as visible black corner artifacts on a flat
    // opaque video rectangle. Image keeps the effect because varied
    // content + rounded corners mask the corner doubling.

    // Legacy SVG-mask CSS retired in v2.1.1289 (replaced by the unified
    // crop engine — see $_videoCrop above).

    // ── Source detection + embed URL ─────────────────────────────
    $embedUrl = '';
    $ytId = '';
    $isYouTube = preg_match('/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/|v\/)|youtu\.be\/)([a-zA-Z0-9_-]+)/', $videoUrl, $m);
    if ($isYouTube) $ytId = $m[1];
    // Frontend autoplay embeds do NOT bake autoplay into the URL - the
    // IntersectionObserver (impulse_render_video_autoplay_script) appends it
    // when the video scrolls into view, so playback starts on screen, not on
    // page load (Sergei 2026-07-24). Editor keeps immediate autoplay (the
    // canvas is always in view).
    $embedAutoNow = $autoplay && $isEditor;
    $embedInview  = $autoplay && !$isEditor;
    if ($isYouTube) {
        $embedUrl = 'https://www.youtube.com/embed/' . $m[1] . '?rel=0';
        if ($embedAutoNow) { $embedUrl .= '&autoplay=1&mute=1'; }
        if ($loop)     { $embedUrl .= '&loop=1&playlist=' . $m[1]; }
        if (!$controls) $embedUrl .= '&controls=0';
    } elseif (preg_match('/vimeo\.com\/(?:video\/)?(\d+)/', $videoUrl, $m)) {
        $embedUrl = 'https://player.vimeo.com/video/' . $m[1] . '?dnt=1';
        if ($embedAutoNow) $embedUrl .= '&autoplay=1&muted=1';
        if ($loop)     $embedUrl .= '&loop=1';
        if (!$controls) $embedUrl .= '&controls=0';
    }

    // ── Compose HTML ─────────────────────────────────────────────
    ob_start();
    echo '<div class="impulse-video-outer" style="' . esc_attr($outerStyle) . '">';
    echo '<div class="impulse-video" style="' . esc_attr($wrapStyle) . '">';
    // Crop SVG (clipPath element). Must live inside the wrap so the
    // editor's wrapper-swap guard doesn't orphan it on refresh.
    if (!empty($_videoCrop['svg'])) {
        // Computed clip-path SVG from CropPath::build() (numeric crop geometry,
        // no user markup). clipPathUnits isn't in impulse_svg_allowed(), so run
        // it through wp_kses with a bespoke clip-path allowlist that preserves
        // every tag/attribute present — keeps output byte-identical.
        echo wp_kses($_videoCrop['svg'], [
            'svg'      => ['width' => true, 'height' => true, 'style' => true, 'aria-hidden' => true],
            'clippath' => ['id' => true, 'clippathunits' => true],
            'path'     => ['d' => true],
        ]);
    }

    // Click-to-play facade (frontend, non-autoplay YouTube). The real embed is
    // ~1MB of player JS, and Chromium's lazy-load threshold on slow connections
    // is large enough that even a below-fold iframe loads it: measured 6 points
    // of mobile PSI per page across the 280 tutorial embeds (2026-08-01). The
    // facade is one thumbnail; the iframe exists only after a click, and then
    // with autoplay so the click means play, not load-then-click-again.
    // Editor keeps the live iframe; autoplay embeds keep the in-view observer.
    $facade = ($embedUrl !== '' && !$isEditor && !$embedAutoNow && !$embedInview && $ytId !== '');
    if ($facade) {
        $thumb = $posterUrl !== '' ? $posterUrl : 'https://i.ytimg.com/vi/' . $ytId . '/hqdefault.jpg';
        echo '<div class="impulse-video-facade" role="button" tabindex="0" aria-label="Play video"'
           . ' data-embed="' . esc_url($embedUrl . '&autoplay=1') . '"'
           . ' style="position:absolute;inset:0;cursor:pointer;background:#000">'
           . '<img src="' . esc_url($thumb) . '" alt="" loading="lazy"'
           . ' style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0.85">'
           . '<span style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:64px;height:64px;border-radius:50%;border:3px solid #fff;background:rgba(0,0,0,0.35);display:flex;align-items:center;justify-content:center;transition:transform 0.2s ease-in-out">'
           . '<svg width="28" height="28" viewBox="0 0 24 24" fill="#fff" stroke="none" aria-hidden="true"><polygon points="9,6 18,12 9,18"/></svg>'
           . '</span></div>';
        static $facadeScript = false;
        if (!$facadeScript) {
            $facadeScript = true;
            echo '<script>(function(){function play(f){var i=document.createElement("iframe");i.src=f.getAttribute("data-embed");i.setAttribute("allow","accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture");i.setAttribute("allowfullscreen","");i.style.cssText="position:absolute;top:0;left:0;width:100%;height:100%;border:none";f.parentNode.replaceChild(i,f);}document.addEventListener("click",function(e){var f=e.target.closest?e.target.closest(".impulse-video-facade"):null;if(f){e.preventDefault();play(f);}},true);document.addEventListener("keydown",function(e){if(e.key!=="Enter"&&e.key!==" ")return;var f=e.target.closest?e.target.closest(".impulse-video-facade"):null;if(f){e.preventDefault();play(f);}},true);})();</script>';
        }
    } elseif ($embedUrl !== '') {
        echo '<iframe src="' . esc_url($embedUrl) . '"' . (!empty($embedInview) ? ' data-auto-embed="1"' : '') . ' style="position:absolute;top:0;left:0;width:100%;height:100%;border:none" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen loading="lazy"></iframe>';
        if ($isEditor) {
            // Editor overlay swallows clicks so the user can drag/select the block
            // without accidentally interacting with the iframe.
            echo '<div class="impulse-video-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;cursor:pointer;z-index:10"></div>';
        }
    } elseif ($videoUrl !== '') {
        // <video>/<source> are form/media controls (not SVG-allowlist tags), so
        // the tag skeleton is emitted as fixed literals and only the dynamic
        // attribute values (boolean flags are literals; src/poster are URLs) are
        // escaped inline — esc_url() on the URLs, keeping output byte-identical.
        $vidFlags   = '';        // leading boolean attribute tokens (no quotes)
        $vidInview  = false;     // frontend-only data-auto-inview marker
        $vidTail    = '';        // trailing boolean tokens after the inview marker
        if ($isEditor) {
            // Editor: bare autoplay (no IntersectionObserver — the iframe is always
            // visible from the runtime's perspective).
            if ($autoplay) $vidFlags .= ' autoplay muted loop';
            elseif ($muted) $vidFlags .= ' muted';
            if ($loop && !$autoplay) $vidFlags .= ' loop';
        } else {
            // Frontend: autoplay gated by IntersectionObserver via data-attr
            // (impulse_render_video_autoplay_script wires it up).
            if ($autoplay) { $vidFlags .= ' muted loop'; $vidInview = true; }
            elseif ($muted) $vidFlags .= ' muted';
            if ($loop && !$autoplay) $vidFlags .= ' loop';
        }
        if ($controls) $vidTail .= ' controls';
        echo '<video class="impulse-video-media" style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover" playsinline';
        echo esc_attr($vidFlags);
        if ($vidInview) echo ' data-auto-inview="1"';
        echo esc_attr($vidTail);
        if ($posterUrl !== '') echo ' poster="' . esc_url($posterUrl) . '"';
        echo ' preload="metadata"><source src="' . esc_url($videoUrl) . '" type="video/mp4"></video>';
    } else {
        // Empty-state placeholder.
        echo '<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#f5f5f5"><div style="width:64px;height:64px;border-radius:50%;border:3px solid #fff;display:flex;align-items:center;justify-content:center"><svg width="28" height="28" viewBox="0 0 24 24" fill="#fff" stroke="none"><polygon points="9,6 18,12 9,18"/></svg></div></div>';
    }

    echo '</div></div>';

    // Frontend autoplay observer JS (only when actually needed).
    if (!$isEditor && $autoplay && $videoUrl !== '' && function_exists('impulse_render_video_autoplay_script')) {
        // Enqueues its inline observer script via wp_add_inline_script and always
        // returns '' (no direct output); esc_html keeps that empty return escaped.
        echo esc_html(impulse_render_video_autoplay_script());
    }

    return ob_get_clean();
}
