<?php

namespace GriffinForms\Frontend\Security\FieldValidations;

class InputPassword extends Format
{
    protected function sanitizeValue($value)
    {
        if (is_string($value)) {
            $decoded = json_decode($value, true);
        } elseif (is_array($value)) {
            $decoded = $value;
        } else {
            $decoded = array();
        }

        if (is_array($decoded) && array_key_exists('password', $decoded)) {
            $password = (string) $decoded['password'];
            $confirmation = isset($decoded['confirmation']) ? (string) $decoded['confirmation'] : '';
        } else {
            $password = is_string($value) ? $value : '';
            $confirmation = '';
        }

        return array(
            'password' => $password,
            'confirmation' => $confirmation
        );
    }

    protected function customValidate($value, $iteration_index)
    {
        $password = isset($value['password']) ? (string) $value['password'] : '';
        $confirmation = isset($value['confirmation']) ? (string) $value['confirmation'] : '';

        if ($password === '') {
            return;
        }

        $validations = $this->field->getProp('validations');
        $validations = is_array($validations) ? $validations : array();

        if (!empty($validations['min']) && is_numeric($validations['min']) && mb_strlen($password) < (int) $validations['min']) {
            $this->addError($this->getValidationMessage('min', __('Your password is too short.', 'griffinforms-form-builder'), $validations), $iteration_index);
        }

        if (!empty($validations['max']) && is_numeric($validations['max']) && mb_strlen($password) > (int) $validations['max']) {
            $this->addError($this->getValidationMessage('max', __('Your password is too long.', 'griffinforms-form-builder'), $validations), $iteration_index);
        }

        if (!empty($validations['allowed_sets']) && is_array($validations['allowed_sets'])) {
            if ($this->hasDisallowedCharacters($password, $validations['allowed_sets'])) {
                $this->addError($this->getValidationMessage('allowed_sets', __('Remove characters that are not allowed.', 'griffinforms-form-builder'), $validations), $iteration_index);
            }
        }

        if (!empty($validations['required_sets']) && is_array($validations['required_sets'])) {
            foreach ($validations['required_sets'] as $set) {
                if (!$this->containsCharacterSet($password, $set)) {
                    $key = $this->mapRequirementKey($set);
                    $this->addError($this->getValidationMessage($key, __('Your password is missing a required character type.', 'griffinforms-form-builder'), $validations), $iteration_index);
                }
            }
        }

        if (!empty($validations['disallowed_chars']) && $this->containsBlockedCharacters($password, $validations['disallowed_chars'])) {
            $this->addError($this->getValidationMessage('disallowed_chars', __('Remove the blocked characters from your password.', 'griffinforms-form-builder'), $validations), $iteration_index);
        }

        if (!empty($validations['min_strength']) && !$this->meetsStrengthRequirement($password, $validations['min_strength'])) {
            $this->addError($this->getValidationMessage('min_strength', __('Your password is not strong enough.', 'griffinforms-form-builder'), $validations), $iteration_index);
        }

        $settings = $this->field->getProp('field_settings');
        if (!empty($settings['require_confirmation']) && $password !== $confirmation) {
            $message = isset($settings['confirmation_message']) && $settings['confirmation_message'] !== ''
                ? sanitize_text_field($settings['confirmation_message'])
                : __('Your passwords do not match.', 'griffinforms-form-builder');
            $this->addError($message, $iteration_index);
        }
    }

    public function transformSanitizedValues(array $values)
    {
        $results = array();
        foreach ($values as $iteration => $value) {
            $password = isset($value['password']) ? (string) $value['password'] : '';
            $results[$iteration] = $this->prepareStoredPassword($password);
        }
        return $results;
    }

    private function prepareStoredPassword($password)
    {
        $settings = $this->field->getProp('field_settings');
        $settings = is_array($settings) ? $settings : array();

        $encrypt = !empty($settings['encrypt_passwords']) && !empty($settings['encryption_key']);
        if (!$encrypt) {
            return array(
                'encrypted' => false,
                'value' => $password
            );
        }

        $key = $this->deriveEncryptionKey($settings['encryption_key']);
        if (strlen($key) !== 32) {
            $this->log->add('error', 'submission', 0, get_current_user_id(), 'Password field encryption key is invalid length.', 'security');
            return array(
                'encrypted' => false,
                'value' => $password
            );
        }

        try {
            $iv = random_bytes(16);
        } catch (\Exception $e) {
            $iv = openssl_random_pseudo_bytes(16);
        }

        $cipher = openssl_encrypt($password, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
        if ($cipher === false) {
            $this->log->add('error', 'submission', 0, get_current_user_id(), 'Failed to encrypt password field value.', 'security');
            return array(
                'encrypted' => false,
                'value' => $password
            );
        }

        return array(
            'encrypted' => true,
            'method' => 'AES-256-CBC',
            'iv' => base64_encode($iv),
            'ciphertext' => base64_encode($cipher)
        );
    }

    private function deriveEncryptionKey($raw_key)
    {
        $raw_key = trim((string) $raw_key);
        if ($raw_key === '') {
            return '';
        }

        $decoded = base64_decode($raw_key, true);
        if ($decoded !== false && strlen($decoded) === 32) {
            return $decoded;
        }

        return hash('sha256', $raw_key, true);
    }

    private function hasDisallowedCharacters($password, array $allowed_sets)
    {
        $allowed_sets = array_unique($allowed_sets);
        $length = mb_strlen($password);
        for ($i = 0; $i < $length; $i++) {
            $char = mb_substr($password, $i, 1);
            if (!$this->charAllowed($char, $allowed_sets)) {
                return true;
            }
        }
        return false;
    }

    private function charAllowed($char, array $allowed_sets)
    {
        $sets = array(
            'uppercase' => '/[A-Z]/u',
            'lowercase' => '/[a-z]/u',
            'numbers' => '/[0-9]/u',
            'symbols' => '/[^A-Za-z0-9\s]/u',
            'spaces' => '/\s/u'
        );

        foreach ($allowed_sets as $set) {
            if (isset($sets[$set]) && preg_match($sets[$set], $char)) {
                return true;
            }
        }
        return false;
    }

    private function containsCharacterSet($password, $set)
    {
        switch ($set) {
            case 'uppercase':
                return preg_match('/[A-Z]/u', $password);
            case 'lowercase':
                return preg_match('/[a-z]/u', $password);
            case 'numbers':
                return preg_match('/[0-9]/u', $password);
            case 'symbols':
                return preg_match('/[^A-Za-z0-9\s]/u', $password);
            case 'spaces':
                return preg_match('/\s/u', $password);
            default:
                return false;
        }
    }

    private function mapRequirementKey($set)
    {
        switch ($set) {
            case 'uppercase':
                return 'require_uppercase';
            case 'lowercase':
                return 'require_lowercase';
            case 'numbers':
                return 'require_number';
            case 'symbols':
                return 'require_symbol';
            default:
                return 'require_' . $set;
        }
    }

    private function containsBlockedCharacters($password, $blocked)
    {
        $pattern = '/[' . preg_quote($blocked, '/') . ']/u';
        return preg_match($pattern, $password);
    }

    private function meetsStrengthRequirement($password, $required_strength)
    {
        $strength_order = array('weak', 'medium', 'strong', 'very_strong');
        $current = $this->evaluateStrength($password);
        if (!in_array($required_strength, $strength_order, true)) {
            $required_strength = 'weak';
        }
        if (!in_array($current, $strength_order, true)) {
            $current = 'weak';
        }

        return array_search($current, $strength_order, true) >= array_search($required_strength, $strength_order, true);
    }

    private function evaluateStrength($password)
    {
        $length = mb_strlen($password);
        if ($length === 0) {
            return 'weak';
        }

        $has_upper = (bool) preg_match('/[A-Z]/u', $password);
        $has_lower = (bool) preg_match('/[a-z]/u', $password);
        $has_digit = (bool) preg_match('/[0-9]/u', $password);
        $has_symbol = (bool) preg_match('/[^A-Za-z0-9\s]/u', $password);
        $has_space = (bool) preg_match('/\s/u', $password);

        $class_count = 0;
        $class_count += $has_upper ? 1 : 0;
        $class_count += $has_lower ? 1 : 0;
        $class_count += $has_digit ? 1 : 0;
        $class_count += ($has_symbol || $has_space) ? 1 : 0;

        if ($length < 8 || $class_count === 0) {
            return 'weak';
        }

        if ($class_count >= 3 && $length >= 12) {
            return 'very_strong';
        }

        if (($class_count >= 3 && $length >= 10) || ($class_count >= 2 && $length >= 14)) {
            return 'strong';
        }

        if ($class_count >= 2 && $length >= 8) {
            return 'medium';
        }

        return 'weak';
    }

    private function getValidationMessage($key, $fallback, $validations)
    {
        if (isset($validations['messages'][$key]) && $validations['messages'][$key] !== '') {
            return $this->replaceTokens($validations['messages'][$key], $validations);
        }
        return $fallback;
    }

    private function replaceTokens($message, $validations)
    {
        $tokens = array(
            '{{min}}' => isset($validations['min']) ? absint($validations['min']) : '',
            '{{max}}' => isset($validations['max']) ? absint($validations['max']) : '',
            '{{strength}}' => isset($validations['min_strength']) ? $validations['min_strength'] : ''
        );

        $message = strtr($message, $tokens);

        if (strpos($message, '{{strength}}') !== false) {
            $labels = array(
                'very_strong' => __('very strong', 'griffinforms-form-builder'),
                'strong' => __('strong', 'griffinforms-form-builder'),
                'medium' => __('medium', 'griffinforms-form-builder'),
                'weak' => __('weak', 'griffinforms-form-builder')
            );
            $strength = isset($validations['min_strength']) ? $validations['min_strength'] : 'weak';
            $message = str_replace('{{strength}}', isset($labels[$strength]) ? $labels[$strength] : $labels['weak'], $message);
        }

        return $message;
    }
}
