<?php

namespace Components\Resources;

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'])) {
            echo '<input type="hidden" name="return" value="' . \Components\Base::h($persist['return']) . '">';
        }
        if (isset($persist['drawer'])) {
            echo '<input type="hidden" name="drawer" value="1">';
        }
        if (isset($persist['keys'])) {
            echo '<input type="hidden" name="keys" value="' . \Components\Base::h($persist['keys']) . '">';
        }
        if (isset($persist['autoclose'])) {
            echo '<input type="hidden" name="autoclose" value="' . \Components\Base::h($persist['autoclose']) . '">';
        }
        if (isset($persist['fields'])) {
            echo '<input type="hidden" name="fields" value="' . \Components\Base::h($persist['fields']) . '">';
        }
        // 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'])) {
            echo '<input type="hidden" name="event_prefix" value="' . \Components\Base::h($persist['event_prefix']) . '">';
        }
        if (isset($persist['event'])) {
            echo '<input type="hidden" name="event" value="' . \Components\Base::h($persist['event']) . '">';
        }
        if (isset($persist['event_suffix'])) {
            echo '<input type="hidden" name="event_suffix" value="' . \Components\Base::h($persist['event_suffix']) . '">';
        }
        return ob_get_clean();
    }

    /**
     * Render a group of fields with visibility logic.
     */
    private static function renderGroup($titleLabel, array $names, array $fields, Engine $engine, array $submitted, array $errors): string
    {
        if (empty($names)) return '';
        $permitted = [];
        $conds = [];
        foreach ($names as $name) {
            $def = $fields[$name] ?? null;
            if (!$def) continue;
            if (!$engine->isFieldPermitted($def)) {
                continue;
            }
            // Only enforce mode-based visibility on the server.
            // Do NOT evaluate dynamic visible_if here, so dependent fields are rendered
            // and controlled client-side via <template x-if="visible(...)">.
            $mode = (string)($submitted['__mode'] ?? 'edit');
            if (!\Components\Common\FieldVisibility::isVisibleByMode($def, $mode)) {
                continue;
            }
            $permitted[] = $name;
            $vis = $def['visible_if'] ?? null;
            if ($vis && is_array($vis)) {
                $field = (string)($vis['field'] ?? '');
                if ($field !== '') {
                    $cond = [];
                    if (!empty($vis['any_of']) && is_array($vis['any_of'])) {
                        // Preserve boolean entries if present for toggle-aware visibility
                        $any = array_values($vis['any_of']);
                        $hasBool = array_reduce($any, function ($c, $v) {
                            return $c || is_bool($v);
                        }, false);
                        $cond['any_of'] = $hasBool ? $any : array_values(array_map('strval', $any));
                    } elseif (!empty($vis['in']) && is_array($vis['in'])) {
                        $any = array_values($vis['in']);
                        $hasBool = array_reduce($any, function ($c, $v) {
                            return $c || is_bool($v);
                        }, false);
                        $cond['any_of'] = $hasBool ? $any : array_values(array_map('strval', $any));
                    }
                    if (array_key_exists('eq', $vis)) {
                        $cond['eq'] = $vis['eq'];
                    }
                    if (isset($vis['gt']) && $vis['gt'] !== '') {
                        $cond['gt'] = is_numeric($vis['gt']) ? 0 + $vis['gt'] : (string)$vis['gt'];
                    }
                    if (isset($vis['gte']) && $vis['gte'] !== '') {
                        $cond['gte'] = is_numeric($vis['gte']) ? 0 + $vis['gte'] : (string)$vis['gte'];
                    }
                    $fieldJs = addslashes($field);
                    $condJson = json_encode($cond);
                    $conds[] = "visible('{$fieldJs}', {$condJson})";
                } else {
                    $conds[] = 'false';
                }
            } else {
                $conds[] = 'true';
            }
        }
        if (empty($permitted)) 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-lg border border-divider bg-white p-4 dark:border-white/10 dark:bg-slate-900">
                <?php foreach ($permitted as $name): $def = $fields[$name] ?? null;
                    if (!$def) continue;
                    $val = $submitted[$name] ?? ''; ?>
                    <?php echo Engine::renderField($name, $def, $val, $errors[$name] ?? [], $submitted); ?>
                <?php endforeach; ?>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

    /**
     * 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;
        // Legacy flag still accepted but superseded by panel_type/panel_id
        $drawer = (int)($options['drawer'] ?? 0);
        $panelType = (string)($options['panel_type'] ?? ($drawer ? 'drawer' : 'modal'));
        $panelId = (string)($options['panel_id'] ?? ($panelType === 'drawer' ? 'config-drawer' : 'config-modal'));
        $panelTargetId = (string)($options['panel_target_id'] ?? ($panelId . '-body'));
        $autoCloseRaw = (string)($options['autoclose'] ?? '');
        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 {
                            let v = this.form[field];
                            if (v === undefined || v === null) v = '';

                            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
                                let vb;
                                if (typeof v === 'boolean') {
                                    vb = v;
                                } else if (typeof v === 'number') {
                                    vb = (v !== 0);
                                } else if (typeof v === 'string') {
                                    const s = v.trim().toLowerCase();
                                    vb = (s === 'on' || s === '1' || s === 'true' || s === 'yes' || s === 'y');
                                } else {
                                    vb = false;
                                }

                                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;
                                    }
                                }
                            } 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;
                                }
                            }

                            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;
                        }
                    }
                }
            }

            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) {
                const base = moduleConfig(initial);
                const v = buildValidator.call(base, schema);
                const data = Object.assign(base, v, {schema: v.schema || (schema || {}), clientErrors: v.clientErrors});
                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"
              x-data="configForm(<?php echo htmlspecialchars(json_encode($submitted), ENT_QUOTES); ?>, <?php echo htmlspecialchars(json_encode($clientSchema), ENT_QUOTES); ?>)"
              hx-post="<?php echo $hxPost; ?>"
              hx-target="#<?php echo \Components\Base::h($panelTargetId); ?>"
              hx-on="htmx:beforeRequest: if (!validateAll()) { event.preventDefault(); }">
            <!-- 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 self::renderPersistInputs($persist); ?>
            <?php
            $renderGroup = function ($titleLabel, $names) use ($fields, $engine, $submitted, $errors) {
                if (empty($names)) return;
                $permitted = [];
                $conds = [];
                foreach ($names as $name) {
                    $def = $fields[$name] ?? null;
                    if (!$def) continue;
                    if (!$engine->isFieldPermitted($def)) {
                        continue;
                    }
                    // Only enforce mode-based visibility on the server.
                    // Do NOT evaluate dynamic visible_if here, so dependent fields are rendered
                    // and controlled client-side via <template x-if="visible(...)">.
                    $mode = (string)($submitted['__mode'] ?? 'edit');
                    if (!\Components\Common\FieldVisibility::isVisibleByMode($def, $mode)) {
                        continue;
                    }
                    $permitted[] = $name;
                    $vis = $def['visible_if'] ?? null;
                    if ($vis && is_array($vis)) {
                        $field = (string)($vis['field'] ?? '');
                        if ($field !== '') {
                            $cond = [];
                            if (!empty($vis['any_of']) && is_array($vis['any_of'])) {
                                // Preserve boolean entries if present for toggle-aware visibility
                                $any = array_values($vis['any_of']);
                                $hasBool = array_reduce($any, function ($c, $v) {
                                    return $c || is_bool($v);
                                }, false);
                                $cond['any_of'] = $hasBool ? $any : array_values(array_map('strval', $any));
                            } elseif (!empty($vis['in']) && is_array($vis['in'])) {
                                $any = array_values($vis['in']);
                                $hasBool = array_reduce($any, function ($c, $v) {
                                    return $c || is_bool($v);
                                }, false);
                                $cond['any_of'] = $hasBool ? $any : array_values(array_map('strval', $any));
                            }
                            if (array_key_exists('eq', $vis)) {
                                $cond['eq'] = $vis['eq'];
                            }
                            if (isset($vis['gt']) && $vis['gt'] !== '') {
                                $cond['gt'] = is_numeric($vis['gt']) ? 0 + $vis['gt'] : (string)$vis['gt'];
                            }
                            if (isset($vis['gte']) && $vis['gte'] !== '') {
                                $cond['gte'] = is_numeric($vis['gte']) ? 0 + $vis['gte'] : (string)$vis['gte'];
                            }
                            $fieldJs = addslashes($field);
                            $condJson = json_encode($cond);
                            $conds[] = "visible('{$fieldJs}', {$condJson})";
                        } else {
                            $conds[] = 'false';
                        }
                    } else {
                        $conds[] = 'true';
                    }
                }
                if (empty($permitted)) return;
                $xShow = !empty($conds) ? htmlspecialchars('(' . implode(' || ', $conds) . ')', ENT_QUOTES) : 'true';
                ob_start();
                ?>
                <div x-cloak x-show="<?= $xShow ?>">
                    <!--                <div class="mb-2 text-sm font-semibold text-gray-900 dark:text-gray-100">-->
                    <?php //echo \Components\Base::h((string)$titleLabel) ?><!--</div>-->
                    <!--
                  <div class="flex items-center mb-2">
                      <div aria-hidden="true" class="w-full border-t border-divider dark:border-divider"></div>
                      <div class="relative flex justify-start">
                          <span class="text-sm pl-3 font-semibold text-secondary text-nowrap"><?php echo \Components\Base::h((string)$titleLabel) ?></span>
                      </div>
                    </div>
-->


                    <!--
                      Use flex + gap instead of space-y-* utilities to avoid unwanted
                      bottom margin when some siblings are <template> nodes (e.g. Alpine x-if)
                      which cause :not(:last-child) selectors to mis-detect the last visible element.
                    -->
                    <div class="flex flex-col gap-6 rounded-lg border border-divider bg-white p-4 dark:border-white/10 dark:bg-slate-900">
                        <?php foreach ($permitted as $name): $def = $fields[$name] ?? null;
                            if (!$def) continue;
                            $val = $submitted[$name] ?? ''; ?>
                            <?php echo Engine::renderField($name, $def, $val, $errors[$name] ?? [], $submitted); ?>
                        <?php 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);
            }
            ?>
            <div class="flex shrink-0 justify-end px-0 py-4">
                <?php
                // Allow caller to override Cancel behavior via cancel_attrs
                $cancelAttrs = $options['cancel_attrs'] ?? null;
                if (is_array($cancelAttrs)) {
                    echo Button::render(\mh_lng('UI_CANCEL', 'Cancel'), [
                            'variant' => 'secondary',
                            'size' => 'md',
                            'attrs' => $cancelAttrs,
                    ]);
                } else {
                    // Default: close whichever panel we are in (modal or drawer)
                    echo Button::render(\mh_lng('UI_CANCEL', 'Cancel'), [
                            'variant' => 'secondary',
                            'size' => 'md',
                            'type' => 'button',
                            'attrs' => [
                                    'command' => 'close',
                                    'commandfor' => $panelId,
                            ],
                    ]);
                }
                ?>
                <?php echo Button::render(\mh_lng('UI_SAVE', 'Save'), [
                        'variant' => 'primary',
                        'size' => 'md',
                        'type' => 'submit',
                        'class' => 'ml-4',
                        'attrs' => [
                                ':class' => "{ 'opacity-50 pointer-events-none': Object.values(clientErrors).some(e => e && e.length) }",
                        ],
                ]); ?>
            </div>
        </form>
        <?php
        return ob_get_clean();
    }
}
