<?php

namespace GriffinForms\Frontend\Html\Forms\Fields;

final class InputPassword extends \GriffinForms\Frontend\Html\Forms\Fields\Format
{
    private $password_settings = array();
    private $password_visible_class;
    private $password_confirm_class;
    private $password_strength_id;

    protected function setFieldSettings()
    {
        parent::setFieldSettings();

        $defaults = array(
            'display_strength_meter' => 1,
            'show_password_toggle' => 1,
            'autocomplete_mode' => 'new-password',
            'require_confirmation' => 0,
            'confirmation_label' => $this->lang->getText('password_confirmation_label_default'),
            'confirmation_message' => $this->lang->getText('password_confirmation_message_default'),
            'encrypt_passwords' => 0,
            'encryption_key' => ''
        );

        $this->password_settings = wp_parse_args($this->field_settings, $defaults);
    }

    protected function setHtmlIds()
    {
        parent::setHtmlIds();
        $this->password_visible_class = 'griffinforms-password-input-' . $this->id;
        $this->password_confirm_class = 'griffinforms-password-confirm-' . $this->id;
        $this->password_strength_id = 'griffinforms-password-strength-' . $this->id;
    }

    protected function formControl($iteration = 0)
    {
        $hidden_class = $this->html_ids['formcontrol'];
        $visible_class = $this->password_visible_class;
        $confirm_class = $this->password_confirm_class;
        $strength_id = $this->password_strength_id;

        $html = '<div class="griffinforms-password-wrapper">';
        $html .= '<div class="input-group mb-2 griffinforms-password-input-group">';
        $toggle_button_id = 'griffinforms-password-toggle-' . $this->id;

        $html .= '<input type="password" class="' . esc_attr($visible_class) . ' form-control"';
        $html .= $this->requiredAttr();
        $html .= $this->placeholderAttr();
        $html .= $this->passwordAutocompleteAttr();
        $html .= $this->readOnlyAttr();
        $html .= $this->disabledAttr();
        if ($this->isShowPasswordToggle()) {
            $html .= ' aria-describedby="' . esc_attr($toggle_button_id) . '"';
        }
        $html .= ' data-gf-type="password">';

        if ($this->isShowPasswordToggle()) {
            $html .= '<button type="button" class="btn btn-light border griffinforms-password-toggle"';
            $html .= ' id="' . esc_attr($toggle_button_id) . '" aria-label="' . esc_attr__('Toggle password visibility', 'griffinforms-form-builder') . '">';
            $html .= '<span class="griffinforms-password-toggle-label">' . esc_html__('Show', 'griffinforms-form-builder') . '</span>';
            $html .= '</button>';
        }

        $html .= '</div>';

        if ($this->isDisplayStrengthMeter()) {
            $html .= '<div id="' . esc_attr($strength_id) . '" class="griffinforms-password-strength mb-2 small visually-hidden" aria-live="polite">';
            $html .= '<div class="progress" role="presentation" style="height:6px;"><div class="progress-bar bg-danger" style="width:0%"></div></div>';
            $html .= '<div class="griffinforms-password-strength-text mt-1 text-danger"></div>';
            $html .= '</div>';
        }

        if ($this->isRequireConfirmation()) {
            $html .= '<label class="form-label d-block">' . esc_html($this->password_settings['confirmation_label']) . '</label>';
            $html .= '<input type="password" class="' . esc_attr($confirm_class) . ' form-control mb-2" data-gf-type="password-confirm"';
            $html .= $this->readOnlyAttr();
            $html .= $this->disabledAttr();
            $html .= '>';
        }

        $html .= '<input type="hidden" class="' . esc_attr($hidden_class) . '" data-gf-formpage="' . absint($this->data['formpage']) . '">';
        $html .= '</div>';

        return $html;
    }

    protected function setErrors()
    {
        $this->errors = array(
            'required',
            'min',
            'max',
            'allowedSets',
            'requireUppercase',
            'requireLowercase',
            'requireNumber',
            'requireSymbol',
            'disallowedChars',
            'strengthRequirement',
            'confirmation'
        );
    }

    protected function valueAttr()
    {
        return ' value=""';
    }

    protected function placeholderAttr()
    {
        $placeholder = $this->item->getProp('placeholder');
        return !empty($placeholder) ? ' placeholder="' . esc_attr($placeholder) . '"' : '';
    }

    protected function isAllowedSets()
    {
        return $this->isCustomValidations() && !empty($this->validations['allowed_sets']);
    }

    protected function isRequireUppercase()
    {
        return $this->hasRequiredSet('uppercase');
    }

    protected function isRequireLowercase()
    {
        return $this->hasRequiredSet('lowercase');
    }

    protected function isRequireNumber()
    {
        return $this->hasRequiredSet('numbers');
    }

    protected function isRequireSymbol()
    {
        return $this->hasRequiredSet('symbols');
    }

    protected function isDisallowedChars()
    {
        return $this->isCustomValidations() && !empty($this->validations['disallowed_chars']);
    }

    protected function isStrengthRequirement()
    {
        if (!$this->isCustomValidations()) {
            return false;
        }

        return isset($this->validations['min_strength']) && $this->validations['min_strength'] !== 'weak';
    }

    protected function isMin()
    {
        return $this->isCustomValidations()
            && isset($this->validations['min'])
            && is_numeric($this->validations['min'])
            && (int) $this->validations['min'] > 0;
    }

    protected function isMax()
    {
        return $this->isCustomValidations()
            && isset($this->validations['max'])
            && is_numeric($this->validations['max'])
            && (int) $this->validations['max'] > 0;
    }

    protected function isConfirmation()
    {
        return $this->isRequireConfirmation();
    }

    protected function hasRequiredSet($set)
    {
        if (!$this->isCustomValidations()) {
            return false;
        }

        if (!isset($this->validations['required_sets']) || !is_array($this->validations['required_sets'])) {
            return false;
        }

        return in_array($set, $this->validations['required_sets'], true);
    }

    protected function isShowPasswordToggle()
    {
        return !empty($this->password_settings['show_password_toggle']);
    }

    protected function isDisplayStrengthMeter()
    {
        return !empty($this->password_settings['display_strength_meter']);
    }

    protected function isRequireConfirmation()
    {
        return !empty($this->password_settings['require_confirmation']);
    }

    protected function passwordAutocompleteAttr()
    {
        if (!$this->isAutoComplete()) {
            return ' autocomplete="off"';
        }

        $mode = isset($this->password_settings['autocomplete_mode']) ? $this->password_settings['autocomplete_mode'] : 'new-password';
        return ' autocomplete="' . esc_attr($mode) . '"';
    }

    protected function subItemJs()
    {
        wp_enqueue_script('password-strength-meter');

        $visible_selector = '.' . $this->password_visible_class;
        $confirm_selector = '.' . $this->password_confirm_class;
        $hidden_selector = '.' . $this->html_ids['formcontrol'];
        $strength_selector = '#' . $this->password_strength_id;

        $allowed_sets = isset($this->validations['allowed_sets']) && is_array($this->validations['allowed_sets']) ? $this->validations['allowed_sets'] : array('uppercase', 'lowercase', 'numbers', 'symbols');
        $required_sets = isset($this->validations['required_sets']) && is_array($this->validations['required_sets']) ? $this->validations['required_sets'] : array();
        $disallowed_chars = isset($this->validations['disallowed_chars']) ? $this->validations['disallowed_chars'] : '';
        $min_strength = isset($this->validations['min_strength']) ? $this->validations['min_strength'] : 'weak';
        $min_length = isset($this->validations['min']) ? absint($this->validations['min']) : 0;

        $strength_labels = array(
            'weak' => $this->lang->getText('password_strength_label_weak'),
            'medium' => $this->lang->getText('password_strength_label_medium'),
            'strong' => $this->lang->getText('password_strength_label_strong'),
            'very_strong' => $this->lang->getText('password_strength_label_very_strong')
        );

        echo '
            const passwordInput = "' . esc_js($visible_selector) . '";
            const confirmInput = "' . esc_js($confirm_selector) . '";
            const hiddenInput = "' . esc_js($hidden_selector) . '";
            const strengthWrap = "' . esc_js($strength_selector) . '";
            const requireConfirmation = ' . ($this->isRequireConfirmation() ? 'true' : 'false') . ';
            const displayStrength = ' . ($this->isDisplayStrengthMeter() ? 'true' : 'false') . ';
            const allowedSets = ' . wp_json_encode($allowed_sets) . ';
            const requiredSets = ' . wp_json_encode($required_sets) . ';
            const disallowedChars = ' . wp_json_encode($disallowed_chars) . ';
            const minStrength = ' . wp_json_encode($min_strength) . ';
            const minLengthRule = ' . $min_length . ';
            const strengthLabels = ' . wp_json_encode($strength_labels) . ';

            const charSets = {
                uppercase: /[A-Z]/,
                lowercase: /[a-z]/,
                numbers: /[0-9]/,
                symbols: /[^A-Za-z0-9\s]/,
                spaces: /\\s/
            };

            function updateHiddenPasswordValue() {
                const passwordVal = $(passwordInput).val() || "";
                const confirmationVal = requireConfirmation ? ($(confirmInput).val() || "") : "";
                const payload = {
                    password: passwordVal,
                    confirmation: confirmationVal
                };
                $(hiddenInput).val(JSON.stringify(payload));
            }

            function updateStrengthUI() {
                if (!displayStrength) {
                    return;
                }

                const value = $(passwordInput).val() || "";
                const meetsMinForStrength = !minLengthRule || value.length >= minLengthRule;
                const strength = (!meetsMinForStrength || value === "") ? null : evaluateStrength(value);

            const percent = strength ? mapStrengthToPercent(strength) : 0;

            const meter = $(strengthWrap).find(".progress-bar");
            if (meter.length) {
                meter.css("width", percent + "%");
                meter.removeClass("bg-danger bg-warning bg-success");
                if (!strength) {
                    meter.addClass("bg-danger");
                } else if (strength === "weak") {
                    meter.addClass("bg-danger");
                } else if (strength === "medium") {
                    meter.addClass("bg-warning");
                } else {
                    meter.addClass("bg-success");
                }
            }

            const textWrap = $(strengthWrap).find(".griffinforms-password-strength-text");
            if (textWrap.length) {
                textWrap.removeClass("text-danger text-warning text-success");
                if (!strength) {
                    textWrap.addClass("text-danger");
                    textWrap.text("");
                } else {
                    textWrap.text(strengthLabels[strength] || strengthLabels["weak"]);
                    if (strength === "weak") {
                        textWrap.addClass("text-danger");
                    } else if (strength === "medium") {
                        textWrap.addClass("text-warning");
                    } else {
                        textWrap.addClass("text-success");
                    }
                }
            }

            const wrap = $(strengthWrap);
            if (wrap.length) {
                if (!strength) {
                    wrap.addClass("visually-hidden");
                } else {
                    wrap.removeClass("visually-hidden");
                }
            }
            }

            function mapStrengthToPercent(strength) {
                switch (strength) {
                    case "very_strong":
                        return 100;
                    case "strong":
                        return 75;
                    case "medium":
                        return 50;
                    default:
                        return 25;
                }
            }

            function toggleVisibility(button) {
                const input = $(passwordInput);
                if (!input.length) {
                    return;
                }
                const isPassword = input.attr("type") === "password";
                input.attr("type", isPassword ? "text" : "password");
                if (requireConfirmation) {
                    const confirmField = $(confirmInput);
                    confirmField.attr("type", isPassword ? "text" : "password");
                }
                $(button).find(".griffinforms-password-toggle-label").text(isPassword ? "' . esc_js(__('Hide', 'griffinforms-form-builder')) . '" : "' . esc_js(__('Show', 'griffinforms-form-builder')) . '");
            }

            function hasDisallowedCharacter(value) {
                if (!allowedSets.length) {
                    return false;
                }
                for (let char of value) {
                    let allowed = false;
                    for (let set of allowedSets) {
                        if (charSets[set] && charSets[set].test(char)) {
                            allowed = true;
                            break;
                        }
                    }
                    if (!allowed) {
                        return true;
                    }
                }
                return false;
            }

            function requiresSet(setName) {
                return requiredSets.includes(setName);
            }

            function containsSet(value, setName) {
                if (!charSets[setName]) {
                    return false;
                }
                return charSets[setName].test(value);
            }

            function passwordHasDisallowedChars(value) {
                if (!disallowedChars) {
                    return false;
                }
                const pattern = new RegExp("[" + disallowedChars.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\\\$&") + "]");
                return pattern.test(value);
            }

            function meetsStrength(value) {
                if (minStrength === "weak") {
                    return true;
                }
                if (minLengthRule && value.length < minLengthRule) {
                    return false;
                }
                const strength = evaluateStrength(value);
                const order = ["weak", "medium", "strong", "very_strong"];
                const currentIndex = Math.max(order.indexOf(strength), 0);
                const requiredIndex = Math.max(order.indexOf(minStrength), 0);
                return currentIndex >= requiredIndex;
            }

            function syncPasswordAlerts(hasError, errorType, target) {
                if (hasError) {
                    addAlert($(hiddenInput), errorType);
                } else {
                    removeAlert($(hiddenInput), errorType);
                }
                toggleVisibleErrorState();
            }

            function evaluateStrength(value) {
                const length = value.length;
                if (!length) {
                    return "weak";
                }

                const hasUpper = charSets.uppercase.test(value);
                const hasLower = charSets.lowercase.test(value);
                const hasNumber = charSets.numbers.test(value);
                const hasSymbol = charSets.symbols.test(value) || charSets.spaces.test(value);

                let classCount = 0;
                classCount += hasUpper ? 1 : 0;
                classCount += hasLower ? 1 : 0;
                classCount += hasNumber ? 1 : 0;
                classCount += hasSymbol ? 1 : 0;

                if (length < 8 || classCount === 0) {
                    return "weak";
                }

                if (classCount >= 3 && length >= 12) {
                    return "very_strong";
                }

                if ((classCount >= 3 && length >= 10) || (classCount >= 2 && length >= 14)) {
                    return "strong";
                }

                if (classCount >= 2 && length >= 8) {
                    return "medium";
                }

                return "weak";
            }

            function toggleVisibleErrorState() {
                const alerts = JSON.parse(sessionStorage.getItem(alertsObjName)) || {};
                let hasErrors = false;
                Object.keys(alerts).forEach(function(key) {
                    if (alerts[key] && Object.keys(alerts[key]).length) {
                        hasErrors = true;
                    }
                });
                const inputEl = $(passwordInput);
                if (hasErrors) {
                    inputEl.addClass("border-danger bg-danger bg-opacity-10");
                    inputEl.addClass("gf-validation-invalid");
                } else {
                    inputEl.removeClass("border-danger bg-danger bg-opacity-10");
                    inputEl.removeClass("gf-validation-invalid");
                }
            }

            $(passwordInput).on("input", function() {
                updateHiddenPasswordValue();
                updateStrengthUI();
            });

            if (requireConfirmation) {
                $(confirmInput).on("input", updateHiddenPasswordValue);
            }

            $(document).on("click", ".griffinforms-password-toggle", function(e) {
                e.preventDefault();
                toggleVisibility(this);
            });

            updateHiddenPasswordValue();
            updateStrengthUI();
        ';
    }

    protected function minJs($error)
    {
        if (!isset($this->validations['min']) || !is_numeric($this->validations['min'])) {
            return;
        }

        echo '
            function checkPasswordMin() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                if (value.length < ' . absint($this->validations['min']) . ') {
                    syncPasswordAlerts(true, "' . esc_js($error) . '", passwordInput);
                } else {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                }
            }

            $(passwordInput).on("input blur", checkPasswordMin);
            $(actionBtn).on("mouseenter", checkPasswordMin);
        ';
    }

    protected function maxJs($error)
    {
        if (!isset($this->validations['max']) || !is_numeric($this->validations['max'])) {
            return;
        }

        echo '
            function checkPasswordMax() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                if (value.length > ' . absint($this->validations['max']) . ') {
                    syncPasswordAlerts(true, "' . esc_js($error) . '", passwordInput);
                } else {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                }
            }

            $(passwordInput).on("input blur", checkPasswordMax);
            $(actionBtn).on("mouseenter", checkPasswordMax);
        ';
    }

    protected function allowedSetsJs($error)
    {
        echo '
            function checkAllowedSets() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const hasInvalid = hasDisallowedCharacter(value);
                syncPasswordAlerts(hasInvalid, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", checkAllowedSets);
            $(actionBtn).on("mouseenter", checkAllowedSets);
        ';
    }

    protected function requireUppercaseJs($error)
    {
        echo '
            function checkRequireUppercase() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const missing = !containsSet(value, "uppercase");
                syncPasswordAlerts(missing, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", checkRequireUppercase);
            $(actionBtn).on("mouseenter", checkRequireUppercase);
        ';
    }

    protected function requireLowercaseJs($error)
    {
        echo '
            function checkRequireLowercase() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const missing = !containsSet(value, "lowercase");
                syncPasswordAlerts(missing, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", checkRequireLowercase);
            $(actionBtn).on("mouseenter", checkRequireLowercase);
        ';
    }

    protected function requireNumberJs($error)
    {
        echo '
            function checkRequireNumber() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const missing = !containsSet(value, "numbers");
                syncPasswordAlerts(missing, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", checkRequireNumber);
            $(actionBtn).on("mouseenter", checkRequireNumber);
        ';
    }

    protected function requireSymbolJs($error)
    {
        echo '
            function checkRequireSymbol() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const missing = !containsSet(value, "symbols");
                syncPasswordAlerts(missing, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", checkRequireSymbol);
            $(actionBtn).on("mouseenter", checkRequireSymbol);
        ';
    }

    protected function disallowedCharsJs($error)
    {
        echo '
            function checkDisallowedChars() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const hasBlocked = passwordHasDisallowedChars(value);
                syncPasswordAlerts(hasBlocked, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", checkDisallowedChars);
            $(actionBtn).on("mouseenter", checkDisallowedChars);
        ';
    }

    protected function strengthRequirementJs($error)
    {
        echo '
            function checkStrengthRequirement() {
                const value = $(passwordInput).val() || "";
                if (!value) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const meets = meetsStrength(value);
                syncPasswordAlerts(!meets, "' . esc_js($error) . '", passwordInput);
            }

            $(passwordInput).on("input blur", function() {
                checkStrengthRequirement();
                updateStrengthUI();
            });
            $(actionBtn).on("mouseenter", checkStrengthRequirement);
        ';
    }

    protected function confirmationJs($error)
    {
        if (!$this->isRequireConfirmation()) {
            return;
        }

        echo '
            function checkPasswordConfirmation() {
                const passwordVal = $(passwordInput).val() || "";
                const confirmVal = $(confirmInput).val() || "";
                if (!requireConfirmation) {
                    return;
                }
                if (!passwordVal && !confirmVal) {
                    syncPasswordAlerts(false, "' . esc_js($error) . '", passwordInput);
                    return;
                }
                const mismatch = passwordVal !== confirmVal;
                if (mismatch) {
                    addAlert($(hiddenInput), "' . esc_js($error) . '");
                    $(confirmInput).addClass("border-danger bg-danger bg-opacity-10");
                } else {
                    removeAlert($(hiddenInput), "' . esc_js($error) . '");
                    $(confirmInput).removeClass("border-danger bg-danger bg-opacity-10");
                }
                toggleVisibleErrorState();
            }

            $(passwordInput).on("input blur", checkPasswordConfirmation);
            $(confirmInput).on("input blur", checkPasswordConfirmation);
            $(actionBtn).on("mouseenter", checkPasswordConfirmation);
        ';
    }
}
