<?php

namespace Components\Resources;

use Components\Base;
use Components\Button;
use Components\Form\Engine;

class ResourceEditForm
{
    /**
     * Convert a variety of date/time inputs to a UNIX timestamp or null on failure.
     */
    private static function toTimestamp($v): ?int
    {
        if ($v === '' || $v === null) return null;
        if (is_int($v)) return $v;
        $s = is_string($v) ? trim($v) : '';
        if ($s === '') return null;
        // Accept already-normalized formats first
        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $s)) {
            $ts = strtotime($s . ' 00:00:00');
            if ($ts !== false) return $ts;
        }
        if (preg_match('/^\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}/', $s)) {
            $ts = strtotime(str_replace('T', ' ', substr($s, 0, 16)) . ':00');
            if ($ts !== false) return $ts;
        }
        $ts = strtotime($s);
        return ($ts !== false) ? $ts : null;
    }

    /**
     * Render hidden inputs to persist control parameters across submissions.
     */
    private static function renderPersistInputs(array $persist): string
    {
        if (empty($persist)) {
            return '';
        }
        ob_start();
        if (isset($persist['return'])) {
            $valEsc = \Components\Base::h($persist['return']);
            echo <<<HTML
<input type="hidden" name="return" value="{$valEsc}">
HTML;
        }
        if (isset($persist['drawer'])) {
            echo <<<HTML
<input type="hidden" name="drawer" value="1">
HTML;
        }
        if (isset($persist['keys'])) {
            $valEsc = \Components\Base::h($persist['keys']);
            echo <<<HTML
<input type="hidden" name="keys" value="{$valEsc}">
HTML;
        }
        if (isset($persist['autoclose'])) {
            $valEsc = \Components\Base::h($persist['autoclose']);
            echo <<<HTML
<input type="hidden" name="autoclose" value="{$valEsc}">
HTML;
        }
        if (isset($persist['fields'])) {
            $valEsc = \Components\Base::h($persist['fields']);
            echo <<<HTML
<input type="hidden" name="fields" value="{$valEsc}">
HTML;
        }
        // Persist event routing hints so the POST can emit the intended HX-Trigger
        // Scope: when editing from an expandable flow element we pass event_suffix=expandable:refresh
        // and we must carry that through the form submission; otherwise CrudController
        // will fall back to the default ":updated" event which refreshes the whole row.
        if (isset($persist['event_prefix'])) {
            $valEsc = \Components\Base::h($persist['event_prefix']);
            echo <<<HTML
<input type="hidden" name="event_prefix" value="{$valEsc}">
HTML;
        }
        if (isset($persist['event'])) {
            $valEsc = \Components\Base::h($persist['event']);
            echo <<<HTML
<input type="hidden" name="event" value="{$valEsc}">
HTML;
        }
        if (isset($persist['event_suffix'])) {
            $valEsc = \Components\Base::h($persist['event_suffix']);
            echo <<<HTML
<input type="hidden" name="event_suffix" value="{$valEsc}">
HTML;
        }
        if (isset($persist['ui_event_id'])) {
            $valEsc = \Components\Base::h($persist['ui_event_id']);
            echo <<<HTML
<input type="hidden" name="ui_event_id" value="{$valEsc}">
HTML;
        }
        if (isset($persist['panel_id'])) {
            $valEsc = \Components\Base::h($persist['panel_id']);
            echo <<<HTML
<input type="hidden" name="panel_id" value="{$valEsc}">
HTML;
        }
        // Generic pass-through: render any additional request flags provided
        // under the 'carry' map (name => value) as hidden inputs so that
        // they survive the GET->POST roundtrip in modal forms.
        if (isset($persist['carry']) && is_array($persist['carry'])) {
            foreach ($persist['carry'] as $name => $value) {
                $n = (string)$name;
                if ($n === '') { continue; }
                // Only allow safe key names
                if (!preg_match('/^[A-Za-z0-9_\-:]+$/', $n)) { continue; }
                $nameEsc = \Components\Base::h($n);
                $valEsc = \Components\Base::h((string)$value);
                echo <<<HTML
<input type="hidden" name="{$nameEsc}" value="{$valEsc}">
HTML;
            }
        }
        return ob_get_clean();
    }


    /**
     * Ensure a target selector has a leading '#' if it's a simple ID and not a keyword like 'this'.
     */
    private static function formatTarget(?string $target): string
    {
        $t = trim((string)$target);
        if ($t === '' || $t === '#') return '';
        if ($t === 'this' || $t === 'body' || strpos($t, '#') === 0 || strpos($t, '.') === 0 || strpos($t, 'closest') === 0 || strpos($t, 'next') === 0 || strpos($t, 'previous') === 0 || strpos($t, 'find') === 0) {
            return $t;
        }
        return '#' . $t;
    }

    /**
     * Render a generic resource edit form based on a Form Engine blueprint.
     *
     * Options:
     * - engine: Engine instance (required)
     * - fields: array of field defs (required)
     * - groups: array of groups (required)
     * - ungrouped: array of field names
     * - submitted: array of current/submitted values
     * - errors: array of validation errors per field
     * - hx_post: string URL for hx-post submission (required)
     * - persist_params: array of hidden inputs to persist (drawer, return, keys, autoclose)
     * - allowed_set: associative array of allowed keys for subset validation (optional)
     * - drawer: int 0/1 context (optional; deprecated in favor of panel_type/panel_id)
     * - panel_type: 'modal'|'drawer' (optional; default inferred from drawer flag)
     * - panel_id: string id of the panel element (e.g., 'config-modal' or 'config-drawer')
     * - panel_target_id: string id of the body container in the panel (e.g., 'config-modal-body')
     * - autoclose: string raw autoclose flag (optional)
     *
     * Returns: HTML string
     */
    public static function render(array $options): string
    {
        /** @var Engine $engine */
        $engine = $options['engine'];
        $fields = $options['fields'] ?? [];
        $groups = $options['groups'] ?? [];
        $ungrouped = $options['ungrouped'] ?? [];
        $submitted = $options['submitted'] ?? [];
        // Normalize initial values for Alpine form state so x-model does not overwrite
        // HTML5 date/datetime-local inputs with incompatible formats (e.g., MM/DD/YYYY).
        // We inspect field types from the provided $fields and coerce values accordingly.
        if (is_array($submitted) && is_array($fields)) {
            $norm = $submitted;
            foreach ($fields as $fname => $def) {
                if (!is_array($def)) {
                    continue;
                }
                if (!array_key_exists($fname, $norm)) {
                    continue;
                }
                $raw = $norm[$fname];
                $type = strtolower((string)($def['type'] ?? ''));
                if ($raw === null || $raw === '') {
                    continue;
                }
                if ($type === 'date') {
                    $ts = self::toTimestamp($raw);
                    if ($ts !== null) {
                        $norm[$fname] = date('Y-m-d', $ts);
                    }
                } elseif ($type === 'date_time' || $type === 'datetime' || $type === 'date-time') {
                    $ts = self::toTimestamp($raw);
                    if ($ts !== null) {
                        $norm[$fname] = date('Y-m-d\TH:i', $ts);
                    }
                }
            }
            $submitted = $norm;
        }
        $errors = $options['errors'] ?? [];
        $hxPost = (string)($options['hx_post'] ?? '');
        $persist = $options['persist_params'] ?? [];
        $allowedSet = $options['allowed_set'] ?? null;
        // When rendering a subset form, ensure POST carries a 'keys' hint so the server
        // recognizes this as a partial save (avoids setting CONF_SAVED_ on single-field saves).
        if (is_array($allowedSet) && !isset($persist['keys'])) {
            $persist['keys'] = implode(',', array_keys($allowedSet));
        } elseif (!isset($persist['keys']) && isset($persist['fields']) && is_string($persist['fields']) && trim($persist['fields']) !== '') {
            $persist['keys'] = (string)$persist['fields'];
        }
        // Determine panel ID: prefer explicit option, otherwise resolve dynamically from context/HTMX
        $drawer = (int)($options['drawer'] ?? 0);
        $panelIdResolved = \Components\Support\Htmx::resolvePanelId('', $drawer);
        $panelId = (string)($options['panel_id'] ?? ($panelIdResolved ?: ($drawer ? 'config-drawer' : 'config-modal')));
        $isPanel = ($options['panel_id'] ?? $panelIdResolved) !== '';
        $panelType = (string)($options['panel_type'] ?? ($isPanel ? ($drawer ? 'drawer' : 'modal') : 'inline'));
        $panelTargetId = (string)($options['panel_target_id'] ?? ($isPanel ? ($panelId . '-body') : \Components\Support\Htmx::getTarget()));
        $hxSwap = (string)($options['hx_swap'] ?? 'innerHTML');
        $autoCloseRaw = (string)($options['autoclose'] ?? '');
        $saved = (bool)($options['saved'] ?? false);
        if (!$engine instanceof Engine) {
            return '';
        }
        if ($allowedSet && is_array($allowedSet)) {
            $clientSchema = array_intersect_key($engine->getClientValidationSchema(), $allowedSet);
        } else {
            $clientSchema = $engine->getClientValidationSchema();
        }
        ob_start();
        ?>
        <style>[x-cloak] {
                display: none !important;
            }</style>
        <?php echo \Components\Common\ClientValidation::script(); ?>
        <script>
            function moduleConfig(initial) {
                return {
                    form: Object.assign({}, initial || {}),
                    visible: function (field, cond) {
                        try {
                            // Support accessing form[field] directly
                            let v = (this.form && this.form[field] !== undefined) ? this.form[field] : '';
                            if (v === undefined || v === null) v = '';

                            const toBoolean = function (value) {
                                if (typeof value === 'boolean') return value;
                                if (typeof value === 'number') return value !== 0;
                                if (typeof value === 'string') {
                                    const s = value.trim().toLowerCase();
                                    return s === '1' || s === 'true' || s === 'on' || s === 'yes' || s === 'y';
                                }
                                return false;
                            };

                            const hasValue = function (value) {
                                if (Array.isArray(value)) {
                                    for (let i = 0; i < value.length; i++) {
                                        if (hasValue(value[i])) return true;
                                    }
                                    return false;
                                }
                                if (value === null || value === undefined) return false;
                                if (typeof value === 'boolean') return value;
                                if (typeof value === 'object') {
                                    const keys = Object.keys(value || {});
                                    for (let i = 0; i < keys.length; i++) {
                                        if (hasValue(value[keys[i]])) return true;
                                    }
                                    return false;
                                }
                                return String(value).trim() !== '';
                            };

                            const usesBoolean = (typeof cond.eq === 'boolean') || (Array.isArray(cond.any_of) && cond.any_of.some(x => typeof x === 'boolean'));

                            if (usesBoolean) {
                                // Normalize the form value to boolean semantics used by toggles/checkboxes
                                const vb = toBoolean(v);

                                if (cond.any_of && Array.isArray(cond.any_of)) {
                                    if (cond.any_of.includes(vb)) return true;
                                    // also fallback to string compares if provided as strings
                                    const sset = cond.any_of.filter(x => typeof x !== 'boolean').map(String);
                                    if (sset.length && sset.includes(String(v))) return true;
                                }
                                if (Object.prototype.hasOwnProperty.call(cond, 'eq')) {
                                    if (typeof cond.eq === 'boolean') {
                                        if (vb === cond.eq) return true;
                                    } else {
                                        if (String(v) === String(cond.eq)) return true;
                                    }
                                }
                                if (Object.prototype.hasOwnProperty.call(cond, 'neq')) {
                                    if (typeof cond.neq === 'boolean') {
                                        if (vb !== cond.neq) return true;
                                    } else {
                                        if (String(v) !== String(cond.neq)) return true;
                                    }
                                }
                            } else {
                                // Legacy string/number based comparison
                                if (v === true) v = 'on';
                                if (v === false) v = '';
                                if (cond.any_of && Array.isArray(cond.any_of)) {
                                    const s = String(v);
                                    if (cond.any_of.map(String).includes(s)) return true;
                                }
                                if (Object.prototype.hasOwnProperty.call(cond, 'eq')) {
                                    if (String(v) === String(cond.eq)) return true;
                                }
                                if (Object.prototype.hasOwnProperty.call(cond, 'neq')) {
                                    if (String(v) !== String(cond.neq)) return true;
                                }
                            }

                            if (Object.prototype.hasOwnProperty.call(cond, 'has_value') || Object.prototype.hasOwnProperty.call(cond, 'not_empty')) {
                                const expect = Object.prototype.hasOwnProperty.call(cond, 'has_value')
                                    ? toBoolean(cond.has_value)
                                    : toBoolean(cond.not_empty);
                                const actual = hasValue(v);
                                if (actual === expect) return true;
                            }

                            const num = (v !== '' && !isNaN(v)) ? parseFloat(v) : null;
                            if (num !== null && Object.prototype.hasOwnProperty.call(cond, 'gt') && !isNaN(cond.gt)) {
                                if (num > parseFloat(cond.gt)) return true;
                            }
                            if (num !== null && Object.prototype.hasOwnProperty.call(cond, 'gte') && !isNaN(cond.gte)) {
                                if (num >= parseFloat(cond.gte)) return true;
                            }
                            return false;
                        } catch (e) {
                            return false;
                        }
                    },
                    isSourceApplicable: function (applicability) {
                        try {
                            if (!applicability || !Array.isArray(applicability)) return true;
                            const current = this.form.newsletter_list_applicability;
                            if (!current) return true;
                            return applicability.includes(String(current));
                        } catch (e) {
                            return true;
                        }
                    }
                }
            }

            function buildValidator(schema) {
                const self = this;

                function deriveSchemaFromDom(root) {
                    try {
                        const s = {};
                        const elRoot = root || document;
                        elRoot.querySelectorAll('input[name][data-validate]').forEach(function (el) {
                            try {
                                const n = el.getAttribute('name');
                                const dv = el.getAttribute('data-validate') || '{}';
                                const r = JSON.parse(dv);
                                if (n && r && Object.keys(r).length) {
                                    s[n] = r;
                                }
                            } catch (e) { /* no-op */
                            }
                        });
                        return s;
                    } catch (e) {
                        return {};
                    }
                }

                function computeDependents(s) {
                    const deps = {};
                    try {
                        Object.keys(s || {}).forEach(function (fname) {
                            const rule = s[fname] || {};
                            const cmps = Array.isArray(rule.compare) ? rule.compare : [];
                            cmps.forEach(function (c) {
                                const of = c && c.field ? String(c.field) : '';
                                if (!of) return;
                                if (!deps[of]) deps[of] = [];
                                if (!deps[of].includes(fname)) deps[of].push(fname);
                            });
                        });
                    } catch (e) { /* no-op */
                    }
                    return deps;
                }

                const root = (self && self.$root) ? self.$root : document;
                const effectiveSchema = (schema && Object.keys(schema || {}).length) ? schema : deriveSchemaFromDom(root);
                const v = window.MBZValidator.create({
                    schema: effectiveSchema,
                    getValue: function (name) {
                        let v = (self && self.form) ? self.form[name] : '';
                        if (v === undefined || v === null) return '';
                        if (typeof v === 'boolean') return v ? 'on' : '';
                        return v;
                    },
                    getElement: function (name) {
                        const root = (self && self.$root) ? self.$root : document;
                        return root.querySelector('[name="' + (window.CSS && CSS.escape ? CSS.escape(name) : name) + '"]');
                    },
                    isVisible: function (name) {
                        return true;
                    }
                });
                v.schema = effectiveSchema;
                v.dependents = computeDependents(effectiveSchema);
                return v;
            }

            function configForm(initial, schema, savedInitial) {
                // Ensure all fields from schema are initialized in form state to ensure reactivity
                const initialNormalized = Object.assign({}, initial || {});
                if (schema && typeof schema === 'object') {
                    Object.keys(schema).forEach(function (name) {
                        if (initialNormalized[name] === undefined) {
                            initialNormalized[name] = '';
                        }
                    });
                }
                const base = moduleConfig(initialNormalized);
                const v = buildValidator.call(base, schema);
                const data = Object.assign(base, v, {
                    schema: v.schema || (schema || {}),
                    clientErrors: v.clientErrors,
                    initialState: JSON.stringify(initial || {}),
                    isDirty: false,
                    loading: false,
                    saved: savedInitial || false,
                    isVisible: savedInitial || false,
                    _saveTimeout: null,
                    _savedResetTimeout: null,
                    init() {
                        window.form = this.form;
                        this.$watch('form', (value) => {
                            this.isDirty = JSON.stringify(value) !== this.initialState;
                            if (this.isDirty) {
                                this.saved = false;
                                this.isVisible = true;
                                if (this._saveTimeout) clearTimeout(this._saveTimeout);
                                if (this._savedResetTimeout) clearTimeout(this._savedResetTimeout);
                            } else if (!this.saved) {
                                this.isVisible = false;
                            }
                        }, { deep: true });

                        this.$watch('form.newsletter_list_applicability', (newApp) => {
                            this.$nextTick(() => {
                                const currentSource = this.form.newsletter_list_source;
                                if (!currentSource) return;

                                // Find the selected radio input in the DOM to check if it has been disabled or hidden
                                // Note: we search within $el which is the <form>
                                const selectedInp = this.$el.querySelector('input[name="newsletter_list_source"][value="' + currentSource + '"]');
                                if (selectedInp) {
                                    // Check if it is hidden (via x-show) or disabled (via :disabled)
                                    // x-show usually sets display: none
                                    const isHidden = selectedInp.closest('label').style.display === 'none';
                                    const isDisabled = selectedInp.disabled;

                                    if (isHidden || isDisabled) {
                                        // Current selection is no longer applicable, reset to first available
                                        const allInps = Array.from(this.$el.querySelectorAll('input[name="newsletter_list_source"]'));
                                        const firstApplicable = allInps.find(inp => {
                                            const label = inp.closest('label');
                                            return label.style.display !== 'none' && !inp.disabled;
                                        });
                                        if (firstApplicable) {
                                            this.form.newsletter_list_source = firstApplicable.value;
                                            // Also trigger a change event to ensure any other listeners are notified
                                            firstApplicable.checked = true;
                                            firstApplicable.dispatchEvent(new Event('change', { bubbles: true }));
                                        }
                                    }
                                }
                            });
                        });

                        if (this.$el && typeof this.$el.addEventListener === 'function') {
                            this.$el.addEventListener('mb-field-sync', (event) => {
                                try {
                                    const detail = (event && event.detail && typeof event.detail === 'object') ? event.detail : {};
                                    const rawName = (typeof detail.name === 'string') ? detail.name : '';
                                    const name = (rawName.endsWith('[]') ? rawName.slice(0, -2) : rawName).trim();
                                    if (!name) return;

                                    let value = Object.prototype.hasOwnProperty.call(detail, 'value') ? detail.value : '';
                                    if (Array.isArray(value)) {
                                        value = value
                                            .map(v => String(v))
                                            .filter(v => v.trim() !== '');
                                    } else if (value === null || value === undefined) {
                                        value = '';
                                    }

                                    this.form[name] = value;
                                    this.validateField(name);
                                } catch (e) {
                                }
                            });
                        }

                        if (this.saved) {
                            this._saveTimeout = setTimeout(() => {
                                this.isVisible = false;
                                this._savedResetTimeout = setTimeout(() => {
                                    this.saved = false;
                                }, 400);
                            }, 1500);
                        }
                    }
                });
                data.validateField = function (name) {
                    const ok = v.validateField(name);
                    try {
                        const deps = (v.dependents && v.dependents[name]) ? v.dependents[name] : [];
                        if (Array.isArray(deps)) {
                            const visited = {};
                            deps.forEach(function (dn) {
                                if (!visited[dn]) {
                                    visited[dn] = true;
                                    v.validateField(dn);
                                }
                            });
                        }
                        this.clientErrors = Object.assign({}, v.clientErrors);
                    } catch (e) {
                    }
                    return ok;
                };
                data.validateAll = function () {
                    const ok = v.validateAll();
                    try {
                        this.clientErrors = Object.assign({}, v.clientErrors);
                    } catch (e) {
                    }
                    return ok;
                };
                data._timers = {};
                data.queueValidate = function (name, delay) {
                    const ms = (typeof delay === 'number' && delay >= 0) ? delay : 600;
                    try {
                        if (this._timers[name]) {
                            clearTimeout(this._timers[name]);
                        }
                    } catch (e) {
                    }
                    const self = this;
                    this._timers[name] = setTimeout(function () {
                        self.validateField(name);
                    }, ms);
                };
                return data;
            }
        </script>
        <form class="space-y-6 <?php echo $panelType === 'inline' ? 'pb-10' : ''; ?>"
              data-no-local-loader="true"
              data-no-global-loader="true"
              x-data="configForm(<?php echo htmlspecialchars(json_encode($submitted) ?: '{}', ENT_QUOTES); ?>, <?php echo htmlspecialchars(json_encode($clientSchema) ?: '{}', ENT_QUOTES); ?>, <?php echo $saved ? 'true' : 'false'; ?>)"
              hx-post="<?php echo $hxPost; ?>"
              hx-push-url="false"
              hx-target="<?php echo \Components\Base::h(self::formatTarget($panelTargetId)); ?>"
              hx-swap="<?php echo \Components\Base::h($hxSwap); ?>"
              @htmx:before-request="if ($event.detail.elt !== $el) return; if (!validateAll()) { $event.preventDefault(); } else { loading = true; }"
              @htmx:after-request="loading = false">
            <!-- Hidden sentinel to ensure POST always contains at least one non-control field.
                 This allows forms consisting solely of toggles (all turned off) to submit and
                 be processed server-side, so unchecked toggles can be saved as off (''). -->
            <input type="hidden" name="__form_submit" value="1">
            <?php echo MH_POST_TOKEN; ?>
            <?php echo self::renderPersistInputs($persist); ?>
            <?php
            $renderGroup = function ($titleLabel, $names) use ($fields, $engine, $submitted, $errors) {
                if (empty($names)) return;
                $hasPermitted = false;
                $conds = [];
                foreach ($names as $nameOrRow) {
                    $rowNames = is_array($nameOrRow) ? $nameOrRow : [$nameOrRow];
                    foreach ($rowNames as $k => $name) {
                        if (!is_int($k)) continue;
                        $def = $fields[$name] ?? null;
                        if (!$def) continue;
                        if (!$engine->isFieldPermitted($def)) {
                            continue;
                        }
                        // Only enforce mode-based visibility on the server.
                        $mode = (string)($submitted['__mode'] ?? 'edit');
                        if (!\Components\Common\FieldVisibility::isVisibleByMode($def, $mode)) {
                            continue;
                        }
                        $hasPermitted = true;
                        $vis = $def['visible_if'] ?? null;
                        if ($vis && is_array($vis)) {
                            $expr = Engine::buildVisibleIfExpr($vis);
                            $conds[] = ($expr !== null) ? $expr : 'false';
                        } else {
                            $conds[] = 'true';
                        }
                    }
                }
                if (!$hasPermitted) return;
                $xShow = !empty($conds) ? htmlspecialchars('(' . implode(' || ', $conds) . ')', ENT_QUOTES) : 'true';
                ob_start();
                ?>
                <div x-cloak x-show="<?= $xShow ?>">
                    <div class="flex flex-col gap-6 rounded-2xl border border-divider bg-white p-4 dark:border-white/10 dark:bg-slate-900">
                        <?php foreach ($names as $nameOrRow):
                            if (is_array($nameOrRow)): ?>
                                <?php
                                $rowFields = array_filter($nameOrRow, 'is_int', ARRAY_FILTER_USE_KEY);
                                $rowOptions = $nameOrRow['_row_options'] ?? [];
                                $cols = count($rowFields);
                                $rowLayout = (string)($rowOptions['layout'] ?? '');

                                $gridClass = 'grid grid-cols-1 md:grid-cols-' . min(3, max(1, $cols)) . ' gap-4';
                                $itemSpans = [];

                                if ($cols === 2) {
                                    if ($rowLayout === '2/3-1/3' || $rowLayout === 'label-2/3' || $rowLayout === 'label-wide') {
                                        $gridClass = 'grid grid-cols-1 md:grid-cols-3 gap-4';
                                        $itemSpans = ['md:col-span-2', 'md:col-span-1'];
                                    } elseif ($rowLayout === '1/3-2/3' || $rowLayout === 'label-1/3') {
                                        $gridClass = 'grid grid-cols-1 md:grid-cols-3 gap-4';
                                        $itemSpans = ['md:col-span-1', 'md:col-span-2'];
                                    } elseif ($rowLayout === '3/4-1/4' || $rowLayout === 'label-3/4') {
                                        $gridClass = 'grid grid-cols-1 md:grid-cols-4 gap-4';
                                        $itemSpans = ['md:col-span-3', 'md:col-span-1'];
                                    } elseif ($rowLayout === '1/4-3/4' || $rowLayout === 'label-1/4') {
                                        $gridClass = 'grid grid-cols-1 md:grid-cols-4 gap-4';
                                        $itemSpans = ['md:col-span-1', 'md:col-span-3'];
                                    }
                                }
                                ?>
                                <div class="<?= $gridClass ?>">
                                    <?php
                                    $idx = 0;
                                    foreach ($rowFields as $name):
                                        $def = $fields[$name] ?? null;
                                        if (!$def) continue;
                                        if (!$engine->isFieldPermitted($def)) continue;
                                        $mode = (string)($submitted['__mode'] ?? 'edit');
                                        if (!\Components\Common\FieldVisibility::isVisibleByMode($def, $mode)) continue;
                                        $val = $submitted[$name] ?? '';
                                        $itemSpan = $itemSpans[$idx] ?? '';
                                        $idx++;
                                        echo Engine::renderField($name, $def, $val, $errors[$name] ?? [], array_merge($submitted, ['in_row' => true, 'col_span' => $itemSpan]), $fields);
                                    endforeach; ?>
                                </div>
                            <?php else:
                                $name = $nameOrRow;
                                $def = $fields[$name] ?? null;
                                if (!$def) continue;
                                if (!$engine->isFieldPermitted($def)) continue;
                                $mode = (string)($submitted['__mode'] ?? 'edit');
                                if (!\Components\Common\FieldVisibility::isVisibleByMode($def, $mode)) continue;
                                $val = $submitted[$name] ?? '';
                                echo Engine::renderField($name, $def, $val, $errors[$name] ?? [], $submitted, $fields);
                            endif;
                        endforeach; ?>
                    </div>
                </div>
                <?php
                echo ob_get_clean();
            };
            foreach ($groups as $g) {
                $label = $g['label'] ?? ($g['title'] ?? ($g['id'] ?? ''));
                $names = $g['fields'] ?? [];
                $renderGroup($label, $names);
            }
            if (!empty($ungrouped)) {
                $renderGroup(\mh_lng('UI_OTHER', 'Other'), $ungrouped);
            }

            if (($options['show_uninstall'] ?? false) && empty($allowedSet) && empty($persist['keys']) && empty($persist['fields'])) {
                $module = $options['module'] ?? '';
                ?>
                <div class="mt-8 pt-6 border-t border-divider">
                    <div class="flex justify-center">
                        <?php echo Button::render(\mh_lng('UI_CLICK_TO_UNINSTALL_THIS_MODULE', 'Uninstall'), [
                            'variant' => 'ghost-danger',
                            'size' => 'md',
                            'icon' => Base::SVG_TRASH,
                            'iconPosition' => 'front',
                            'attrs' => [
                                'command' => 'show-modal',
                                'commandfor' => 'confirm-modal',
                                'data-title' => \mh_lng('UI_CONFIRM_UNINSTALL', 'Are you sure you want to uninstall this module?'),
                                'data-type' => 'alert',
                                'hx-get' => \Components\Router::url('module_uninstall_confirm', array_merge(['module' => $module, 'partial' => 1], $persist)),
                                'hx-target' => '#confirm-modal-body',
                                'hx-swap' => 'innerHTML',
                            ],
                        ]); ?>
                    </div>
                </div>
                <?php
            }
            ?>
            <?php
            $successMessageHtml = '
                <div class="flex items-center gap-2 text-sm text-green-600 h-10">
                    <span class="text-green-600"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /></svg></span>
                    <span class="font-medium">' . \mh_lng('UI_SAVED_SUCCESS', 'Saved successfully.') . '</span>
                </div>';

            $cancelAttrs = $options['cancel_attrs'] ?? null;
            $cancelLabel = (string)($options['cancel_label'] ?? \mh_lng('UI_CANCEL', 'Cancel'));
            $cancelBaseAttrs = [
                ':disabled' => 'loading',
                ':class' => "{ 'opacity-50 pointer-events-none': loading }",
            ];
            $cancelButtonHtml = '';
            if ($panelType === 'inline') {
                $hxGet = $options['hx_get'] ?? '';
                if ($hxGet !== '') {
                    $cancelButtonHtml = Button::render($cancelLabel, [
                        'variant' => 'secondary',
                        'size' => 'md',
                        'attrs' => array_merge($cancelBaseAttrs, [
                            'hx-get' => $hxGet,
                            'hx-target' => self::formatTarget($options['hx_target'] ?? $panelTargetId),
                            'hx-swap' => $hxSwap,
                        ], is_array($cancelAttrs) ? $cancelAttrs : []),
                    ]);
                }
            } else {
                if (is_array($cancelAttrs)) {
                    $cancelButtonHtml = Button::render($cancelLabel, [
                        'variant' => 'secondary',
                        'size' => 'md',
                        'attrs' => array_merge($cancelBaseAttrs, $cancelAttrs),
                    ]);
                } else {
                    $cancelButtonHtml = Button::render($cancelLabel, [
                        'variant' => 'secondary',
                        'size' => 'md',
                        'type' => 'button',
                        'attrs' => array_merge($cancelBaseAttrs, [
                            'command' => 'close',
                            'commandfor' => $panelId,
                            'aria-expanded' => 'true',
                        ]),
                    ]);
                }
            }
            $submitLabel = (string)($options['submit_label'] ?? \mh_lng('UI_SAVE', 'Save'));
            $submitIcon = (string)($options['submit_icon'] ?? '');
            $saveButtonHtml = Button::render($submitLabel, [
                'variant' => 'primary',
                'size' => 'md',
                'type' => 'button',
                'icon' => $submitIcon . '<span x-show="loading" class="ml-2">' . \Components\Base::SVG_SPINNER . '</span>',
                'attrs' => [
                    '@click' => '$root.requestSubmit()',
                    ':class' => "{ 'opacity-50 pointer-events-none': loading || Object.values(clientErrors).some(e => e && e.length) }",
                    ':disabled' => 'loading',
                ],
            ]);
            $splitFooterActions = (bool)($options['split_footer_actions'] ?? false) && $cancelButtonHtml !== '';
            $actionsContainerClass = $splitFooterActions ? 'flex items-center gap-3 w-full' : 'flex items-center gap-3';
            $actionButtonsRowClass = $splitFooterActions ? 'flex items-center gap-3 h-9 w-full' : 'flex items-center gap-3 h-9';
            ?>
            <?php if ($panelType === 'drawer' || $panelType === 'modal'): ?>
            <template x-teleport="#<?= \Components\Base::h($panelId) ?>-footer">
                <div x-init="$nextTick(() => { const f = document.getElementById('<?= \Components\Base::h($panelId) ?>-footer'); if(f) f.classList.remove('hidden'); }); htmx.process($el);"
                     class="flex shrink-0 justify-between items-center w-full">
                    <div class="text-sm text-gray-600 dark:text-gray-400">
                        <div x-show="loading" class="flex items-center gap-2">
                            <span class="text-brand"><?= \Components\Base::SVG_SPINNER ?></span>
                            <span><?= \mh_lng('UI_SAVING_CHANGES', 'Saving changes...') ?></span>
                        </div>
                    </div>
                    <div class="<?= \Components\Base::h($actionsContainerClass) ?>">
<?php elseif ($panelType === 'inline'): ?>
            <template x-teleport="#app-shell-main-notification-area">
                <div x-init="htmx.process($el)"
                     x-show="isVisible"
                     x-transition:enter="transition ease-out duration-300"
                     x-transition:enter-start="translate-y-full opacity-0"
                     x-transition:enter-end="translate-y-0 opacity-100"
                     x-transition:leave="transition ease-in duration-200"
                     x-transition:leave-start="translate-y-0 opacity-100"
                     x-transition:leave-end="translate-y-full opacity-0"
                     class="pointer-events-auto absolute inset-x-0 bottom-0 bg-white/90 backdrop-blur-sm border-t border-divider p-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] dark:bg-slate-900/90 dark:border-white/10"
                >
                    <div class="flex items-center justify-between gap-6 max-w-full">
                        <div class="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-2">
                            <div x-show="loading" class="flex items-center gap-2">
                                <span class="text-brand"><?= \Components\Base::SVG_SPINNER ?></span>
                                <span><?= \mh_lng('UI_SAVING_CHANGES', 'Saving changes...') ?></span>
                            </div>
                            <div x-show="isDirty && !loading" class="flex items-center gap-2">
                                <span class="text-brand"><?= \Components\Base::SVG_INFORMATION_CIRCLE ?></span>
                                <span><?= \mh_lng('UI_UNSAVED_CHANGES', 'You have unsaved changes.') ?></span>
                            </div>
                        </div>
                        <div class="<?= \Components\Base::h($actionsContainerClass) ?>">
            <?php else: ?>
                <div class="flex shrink-0 justify-end gap-3 px-0 py-4">
            <?php endif; ?>

                <div x-show="isDirty || !saved" class="<?= \Components\Base::h($actionButtonsRowClass) ?>">
                    <?php if ($splitFooterActions): ?>
                    <?= $cancelButtonHtml ?>
                    <div class="flex-1"></div>
                    <?= $saveButtonHtml ?>
                    <?php else: ?>
                    <?= $cancelButtonHtml ?>
                    <?= $saveButtonHtml ?>
                    <?php endif; ?>
                </div>
                <div x-show="!isDirty && saved">
                    <?= $successMessageHtml ?>
                </div>
            </div>

            <?php if ($panelType === 'drawer' || $panelType === 'modal'): ?>
                </div>
            </template>
            <?php elseif ($panelType === 'inline'): ?>
                </div>
                </div>
            </template>
            <?php endif; ?>
        </form>
        <?php
        return ob_get_clean();
    }
}
