<?php

namespace Components\Support;

class Htmx
{
    private static array $queuedTriggers = [];
    /**
     * Versatile panel ID resolution.
     * Detects panel ID from HX-Target header or falls back to default.
     */
    public static function resolvePanelId(?string $default = null, bool $drawer = false): string
    {
        $target = self::getTarget();
        if ($target && preg_match('/^#?([a-z0-9_-]+)-body$/i', $target, $matches)) {
            return $matches[1];
        }
        return $default ?? ($drawer ? 'config-drawer' : 'config-modal');
    }

    /**
     * Return an inline <script> tag that closes a <dialog> (modal/drawer) by id.
     * Safe no-op if the element does not exist or has no .close() method.
     */
    public static function closePanelScript(string $panelId): string
    {
        // keep JS minimal & compatible with existing inline snippets
        return '<script>(function(){try{var m=document.getElementById(' . '"' . addslashes($panelId) . '"' . ');if(m&&typeof m.close===' . '"function"' . '){m.close();}}catch(e){}})();</script>';
    }

    /** Echo the closePanelScript for convenience. */
    public static function closePanel(string $panelId): void
    {
        echo self::closePanelScript($panelId);
    }
    /**
     * Cross-frame HTMX event trigger helper for legacy iframes (Modal/Drawer).
     *
     * Usage (event-only):
     *   Htmx::parentRefresh('newsletter_item:expandable:refresh', '{"id":"3"}');
     *
     * Dispatches an HTMX event on document.body in the parent window using htmx.trigger(...),
     * allowing hx-trigger="event[detail.id == '3'] from:body" to react.
     *
     * Note: Legacy selector-based refresh via mb:refresh has been removed. Pass an event name
     * and optional JSON-encoded detail string instead.
     */
    public static function parentRefresh(string $targetSelector, ?string $url = null, ?string $swap = null): void
    {
        // Always treat the first argument as an event name; build a detail object from the second (optional)
        $detail = null;
        if ($url !== null && $url !== '') {
            // Try to decode JSON detail; if it fails, attempt very small key=value parsing as fallback
            $decoded = json_decode($url, true);
            if (is_array($decoded)) {
                $detail = $decoded;
            } else {
                // Fallback: parse strings like "id=3"
                if (strpos($url, '=') !== false && strpos($url, '&') === false) {
                    [$k,$v] = array_pad(explode('=', $url, 2), 2, '');
                    $detail = [$k => $v];
                }
            }
        }
        if ($detail === null) { $detail = []; }

        $payload = [
            'type' => 'mb:trigger',
            'event' => (string)$targetSelector,
            'detail' => $detail,
        ];

        $json = json_encode($payload, JSON_UNESCAPED_SLASHES);
        if ($json === false) { $json = '{}'; }

        // Capture inline HTML via HEREDOC per rendering conventions
        //
        // Important behaviour notes
        // -------------------------
        //
        // The BeezUI shell (app.php) installs a single window-level
        // "message" listener which reacts to objects of the form:
        //   { type: 'mb:trigger', event: '...', detail: {...} }
        // and then dispatches the corresponding HTMX / CustomEvent on
        // document.body.
        //
        // In the mixed legacy/BeezUI setup we can have several nesting
        // scenarios:
        //   A) app.php is the top window.
        //   B) app.php is loaded inside a legacy wrapper iframe.
        //   C) legacy visual editor iframe is a *child* of app.php
        //      (EmailEditor.php case).
        //
        // When save_meta_htmx is called from module.inc.php running in the
        // main legacy context while BeezUI lives in an iframe, the previous
        // implementation picked window.parent as the single postMessage
        // target. That meant the message was delivered to the *wrapper*
        // instead of to the BeezUI iframe where the EmailEditor listener
        // actually lives, so email_editor:refresh never fired.
        //
        // To make this robust we now:
        //   - Always postMessage to the current window (so app.php receives
        //     its own events when it is the top context or when scripts run
        //     directly inside it), and
        //   - Additionally postMessage to window.parent *when it differs
        //     from window*, preserving existing cross-frame behaviour where
        //     the parent shell owns the dispatcher.
        //
        // Both windows ignore unknown messages, so sending to one or both
        // is safe and keeps the routing logic centralised in app.php.
        $html = <<<HTML
<script>
(function(){
  try {
    var msg = $json;

    function safePost(target) {
      try {
        if (target && target.postMessage) {
          target.postMessage(msg, "*");
          return true;
        }
      } catch (e) {}
      return false;
    }

    var posted = false;

    // 1) Always try to notify the current window context. This ensures
    //    that app.php (BeezUI shell) receives the trigger even when it
    //    is itself running in an iframe or as the top document.
    posted = safePost(window);

    // 2) Additionally notify the parent frame when different. This keeps
    //    legacy flows working where the parent owns the mb:trigger
    //    handler and intermediates between iframes.
    if (window.parent && window.parent !== window) {
      if (safePost(window.parent)) {
        posted = true;
      }
    }

    // 3) Fallback: if postMessage is unavailable or blocked but we are
    //    still in a window with htmx available, trigger locally.
    if (!posted) {
      try {
        if (window.htmx && window.htmx.trigger) {
          window.htmx.trigger(document.body, msg.event, msg.detail || {});
        } else if (document && document.body) {
          document.body.dispatchEvent(new CustomEvent(msg.event, { detail: msg.detail || {} }));
        }
      } catch (e3) {}
    }
  } catch (_e) {}
})();
</script>
HTML;
        echo $html;
    }

    /**
     * Request the parent window (or current window) to close a modal/drawer dialog.
     * If $panelId is provided, attempts to close that id, otherwise tries common ids.
     * Works for legacy iframe content by posting a message to the parent or using direct DOM if same-origin.
     */
    public static function parentClose(?string $panelId = null): void
    {
        $idJson = json_encode($panelId, JSON_UNESCAPED_SLASHES);
        if ($idJson === false) { $idJson = 'null'; }
        // Capture inline HTML via HEREDOC per rendering conventions
        $html = <<<HTML
<script>
(function(){ try {
  var id = $idJson;

  function tryClose(root, id) {
    if (!root) return false;
    try {
      if (id) {
        var e = root.getElementById(String(id));
        if (e && typeof e.close === "function") { e.close(); return true; }
      } else {
        var candidates = ["config-modal", "config2-modal", "confirm-modal", "ai-modal", "email-preview", "config-drawer", "editor-inline-modal"];
        for (var i = 0; i < candidates.length; i++) {
          var el = root.getElementById(candidates[i]);
          if (el && typeof el.close === "function") { el.close(); return true; }
        }
      }
    } catch (e) {}
    return false;
  }

  // 1) Try locally first. This is critical for "pure BeezUI" flows or when BeezUI is itself in an iframe
  // and needs to close its own local modals.
  if (tryClose(document, id)) return;

  // 2) Try parent window (legacy iframe scenario)
  var p = (window.parent && window.parent !== window) ? window.parent : null;
  if (p) {
    // Try same-origin direct DOM close in parent
    var directDone = false;
    try {
      if (p.document && tryClose(p.document, id)) directDone = true;
    } catch (e1) {}

    // Fallback: cross-origin message to parent
    if (!directDone) {
      try { p.postMessage({ type: "mb:close", id: id }, "*"); } catch (e2) {}
    }
  }
} catch (_e) {} })();
</script>
HTML;
        echo $html;
    }

    // --- Header Input Helpers ------------------------------------------------
    
    /**
     * Return the "All changes saved" string with the current timestamp.
     */
    public static function getSavedStatusMessage(): string
    {
        $label =  mh_lng('MAILBEEZ_AUTOSAVE_ALL_SAVED', 'All changes saved');
        return $label; // . ' (' . date('H:i:s') . ')';
    }

    private static function headerBool(string $key): bool
    {
        $v = $_SERVER[$key] ?? null;
        return $v === 'true' || $v === '1';
    }

    public static function isRequest(): bool
    {
        return self::headerBool('HTTP_HX_REQUEST');
    }

    public static function isBoosted(): bool
    {
        return self::headerBool('HTTP_HX_BOOSTED');
    }

    public static function isHistoryRestore(): bool
    {
        return self::headerBool('HTTP_HX_HISTORY_RESTORE_REQUEST');
    }

    public static function getTrigger(): ?string
    {
        return $_SERVER['HTTP_HX_TRIGGER'] ?? null;
    }

    public static function getTriggerName(): ?string
    {
        return $_SERVER['HTTP_HX_TRIGGER_NAME'] ?? null;
    }

    public static function getTarget(): ?string
    {
        return $_SERVER['HTTP_HX_TARGET'] ?? null;
    }

    public static function getCurrentUrl(): ?string
    {
        return $_SERVER['HTTP_HX_CURRENT_URL'] ?? null;
    }

    public static function getPrompt(): ?string
    {
        return $_SERVER['HTTP_HX_PROMPT'] ?? null;
    }

    public static function getMethod(): string
    {
        return $_SERVER['REQUEST_METHOD'] ?? 'GET';
    }

    // Convenience:
    public static function isGet(): bool { return self::getMethod() === 'GET'; }
    public static function isPost(): bool { return self::getMethod() === 'POST'; }
    public static function isPut(): bool { return self::getMethod() === 'PUT'; }
    public static function isDelete(): bool { return self::getMethod() === 'DELETE'; }

    // --- Rendering Logic -----------------------------------------------------

    /**
     * Should we return *only* the HTML fragment (no layout wrapper)?
     * Useful for view renderers.
     */
    public static function wantsFragment(): bool
    {
        return self::isRequest() && !self::isHistoryRestore();
    }

    // --- Response Header Output ----------------------------------------------

    /**
     * Queue HX-Trigger events (multiple triggers safe).
     */
    public static function trigger(string $event, array $data = []): void
    {
        if (isset(self::$queuedTriggers[$event]) && is_array(self::$queuedTriggers[$event])) {
            self::$queuedTriggers[$event] = array_merge(self::$queuedTriggers[$event], $data);
        } else {
            self::$queuedTriggers[$event] = $data;
        }
    }

    /**
     * Apply queued triggers now (call once before sending output).
     */
    public static function flushTriggers(): void
    {
        if (!empty(self::$queuedTriggers)) {
            header('HX-Trigger: ' . json_encode(self::$queuedTriggers));
            self::$queuedTriggers = [];
        }
    }

    public static function triggerAfterSwap(string $event, array $data = []): void
    {
        header('HX-Trigger-After-Swap: ' . json_encode([$event => $data]));
    }

    public static function triggerAfterSettle(string $event, array $data = []): void
    {
        header('HX-Trigger-After-Settle: ' . json_encode([$event => $data]));
    }

    public static function pushUrl(string $url): void
    {
        header('HX-Push-Url: ' . $url);
    }

    public static function replaceUrl(string $url): void
    {
        header('HX-Replace-Url: ' . $url);
    }

    public static function redirect(string $url): void
    {
        header('HX-Redirect: ' . $url);
    }

    public static function refresh(): void
    {
        header('HX-Refresh: true');
    }

    public static function reswap(string $value): void
    {
        header('HX-Reswap: ' . $value);
    }

    public static function retarget(string $selector): void
    {
        header('HX-Retarget: ' . $selector);
    }

    public static function reselect(string $selector): void
    {
        header('HX-Reselect: ' . $selector);
    }

    public static function location(array $data): void
    {
        header('HX-Location: ' . json_encode($data));
    }

    public static function stopPolling(): void
    {
        header('HX-Stop-Polling: true');
    }

    public static function noContent(): void
    {
        http_response_code(204);
    }

    // Generic fallback
    public static function setHeader(string $name, string $value): void
    {
        header($name . ': ' . $value);
    }
}