<?php

class Agro_Senha
{
    private array $senhaLetrasMinusculas = [
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
        'w', 'x', 'y', 'z'
    ];

    private array $senhaLetrasMaiusculas = [
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
        'W', 'X', 'Y', 'Z'
    ];

    private array $senhasNumeros = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];

    private array $caracteresEspeciais = [
        '!', '@', '#', '$', '%', '&', '*', '(', ')', '-', '_', '+', '=', '|', '{', '}', '[', ']', '^', '~',
        '.', ',', ':', ';'
    ];

    private int $numeroCaracteresMinimoSenha = 12;
    private int $numeroCaracteresMaximoSenha = 20;

    private array $errosSenha = [
        'maiusculo' => [
            'erro' => false,
            'mensagem' => 'Senha deve ter ao menos 1 letra maiúscula.'
        ],
        'minusculo' => [
            'erro' => false,
            'mensagem' => 'Senha deve ter ao menos 1 letra minúscula.'
        ],
        'numero' => [
            'erro' => false,
            'tipo' => 'numero',
            'mensagem' => 'Senha deve ter ao menos 1 número.'
        ],
        'caracteresRepetidos' => [
            'erro' => false,
            'mensagem' => 'Senha não deve ter caracteres repetidos (independente de letra maiúscula ou minúscula).'
        ],
        'sequenciaCaracteres' => [
            'erro' => false,
            'mensagem' => 'Senha não deve ter uma sequência de caracteres (ex.: 123, abc, fed).'
        ],
        'caracterEspecial' => [
            'erro' => false,
            'mensagem' => 'Senha deve ter ao menos 1 caracter especial ( !@#$%&*()-_+=|{}[]^~.,:; ) .'
        ],
        'numeroCaracteres' => [
            'erro' => false,
            'mensagem' => 'Senha deve ter entre 12 e 20 caracteres.'
        ],
        'senhasPreenchidas' => [
            'erro' => false,
            'mensagem' => 'Os 2 campos de senha devem ser preenchidos.'
        ],
        'senhasIguais' => [
            'erro' => false,
            'mensagem' => 'As senhas informadas devem ser iguais.'
        ],
        'senhaUtilizadaAnteriormente' => [
            'erro' => false,
            'mensagem' => 'Não é possível cadastrar uma senha utilizada anteriormente.'
        ],
    ];

    private string $senha = '';
    private string $confirmacaoSenha = '';
    private bool $verificarHistoricoSenhas = false;
    private int $idUsuario;

    public function gerarNovaSenha(): string
    {
        $senhaValida = false;
        $caracteresSenha = null;

        while (!$senhaValida) {
            $caracteresSenha = $this->gerarCombinacaoCaracteres();
            $senhaValida = $this->validarSenha($caracteresSenha, $caracteresSenha);
        }

        return $caracteresSenha;
    }

    private function gerarCombinacaoCaracteres(bool $utilizarCaracteresEspeciais = true): string
    {
        $letrasUsadas = [];
        $senha = [];
        $todosElementos = [];
        $arrays = [
            $this->senhaLetrasMinusculas,
            $this->senhaLetrasMaiusculas,
            $this->senhasNumeros,
        ];

        if ($utilizarCaracteresEspeciais) {
            $arrays[] = $this->caracteresEspeciais;
        }

        foreach ($arrays as $array) {
            $todosElementos = array_merge($todosElementos, $array);
        }

        shuffle($todosElementos);

        foreach ($todosElementos as $elemento) {
            if (count($senha) >= $this->numeroCaracteresMinimoSenha) {
                break;
            }

            if (!in_array(strtolower($elemento), $letrasUsadas)) {
                $senha[] = $elemento;
                $letrasUsadas[] = strtolower($elemento);
            }
        }

        return implode('', $senha);
    }

    public function validarSenha(string $senha, string $confirmacaoSenha): bool
    {
        $this->senha = $senha;
        $this->confirmacaoSenha = $confirmacaoSenha;

        $arraySenha = str_split($senha);

        $this->senhasPreenchidas();
        $this->senhasIguais();
        $this->temNumeroCaracteresExigido($arraySenha);
        $this->temCaracterEspecial($arraySenha);
        $this->temLetraNumero($arraySenha, 'maiusculo');
        $this->temLetraNumero($arraySenha, 'minusculo');
        $this->temLetraNumero($arraySenha, 'numero');
        $this->temCaracterRepetido($arraySenha);
        $this->temSequenciaCaracteres($arraySenha);

        if ($this->verificarHistoricoSenhas) {
            $this->senhaUtilizadaAnteriormente();
        }

        foreach ($this->errosSenha as $erro) {
            if ($erro['erro']) {
                return false;
            }
        }

        return true;
    }

    private function temNumeroCaracteresExigido(array $arraySenha) : void
    {
        $tamanhoSenha = count($arraySenha);

        if ($tamanhoSenha < $this->numeroCaracteresMinimoSenha || $tamanhoSenha > $this->numeroCaracteresMaximoSenha) {
            $this->errosSenha['numeroCaracteres']['erro'] = true;
        }
    }

    private function temCaracterEspecial(array $arraySenha): void
    {
        $temCaracterEspecial = false;

        foreach ($arraySenha as $caracterSenha) {
            if (in_array($caracterSenha, $this->caracteresEspeciais)) {
                $temCaracterEspecial = true;
                break;
            }
        }

        if (!$temCaracterEspecial) {
            $this->errosSenha['caracterEspecial']['erro'] = true;
        }
    }

    private function temLetraNumero(array $arraySenha, string $validacao): void
    {
        $propriedadeValidacao = [
            'maiusculo' => 'senhaLetrasMaiusculas',
            'minusculo' => 'senhaLetrasMinusculas',
            'numero' => 'senhasNumeros',
        ];
        $temLetraNumero = false;
        $this->errosSenha[$validacao]['erro'] = false;

        foreach ($arraySenha as $caracterSenha) {
            if (in_array($caracterSenha, $this->{$propriedadeValidacao[$validacao]})) {
                $temLetraNumero = true;
                break;
            }
        }

        if (!$temLetraNumero) {
            $this->errosSenha[$validacao]['erro'] = true;
        }
    }

    private function temCaracterRepetido(array $arraySenha): void
    {
        $controleCaracteres = [];

        foreach ($arraySenha as $caracter) {
            $caracter = strtolower($caracter);

            if (in_array($caracter, $controleCaracteres)) {
                $this->errosSenha['caracteresRepetidos']['erro'] = true;
                break;
            }

            $controleCaracteres[] = $caracter;
        }
    }

    private function temSequenciaCaracteres(array $arraySenha): void
    {
        if ($this->temSequenciaNumeros($arraySenha) || $this->temSequenciaLetras($arraySenha)) {
            $this->errosSenha['sequenciaCaracteres']['erro'] = true;
        }
    }

    private function buscarArrayCaracter(string $caracter): array
    {
        if (in_array($caracter, $this->senhaLetrasMaiusculas)) {
            return $this->senhaLetrasMaiusculas;
        }

        if (in_array($caracter, $this->senhaLetrasMinusculas)) {
            return $this->senhaLetrasMinusculas;
        }

        return $this->senhasNumeros;
    }

    public function getErrosSenha() : array
    {
        return $this->errosSenha;
    }

    public function gerarHash() : string
    {
        $this->numeroCaracteresMinimoSenha = 27;
        $hash = $this->gerarCombinacaoCaracteres(false);

        return base64_encode($hash);
    }

    private function senhasPreenchidas(): void
    {
        if (empty($this->senha) || empty($this->confirmacaoSenha)) {
            $this->errosSenha['senhasPreenchidas']['erro'] = true;
        }
    }

    private function senhasIguais(): void
    {
        if ($this->senha !== $this->confirmacaoSenha) {
            $this->errosSenha['senhasIguais']['erro'] = true;
        }
    }

    public function verificarHistoricoSenhas(bool $verificarHistoricoSenhas, int $idUsuario) : void
    {
        $this->verificarHistoricoSenhas = $verificarHistoricoSenhas;
        $this->idUsuario = $idUsuario;
    }

    private function senhaUtilizadaAnteriormente(): void
    {
        if (!(new Sistema_Model_UsuarioHistoricoSenha())->podeUsarSenha($this->idUsuario, $this->senha)) {
            $this->errosSenha['senhaUtilizadaAnteriormente']['erro'] = true;
        }
    }

    public function retornarMensagemErros() : string
    {
        $errosSenha = $this->getErrosSenha();
        $mensagem = "Senha inválida. Erro(s) encontrado(s):";

        foreach ($errosSenha as $erro) {
            if ($erro['erro'] === false) {
                continue;
            }

            $mensagem .= "<br>- {$erro['mensagem']}";
        }

        return $mensagem;
    }

    /**
     * @param array $arraySenha
     * @return bool
     */
    private function temSequenciaNumeros(array $arraySenha) : bool
    {
        preg_match_all('/\d{3,}/', implode('', $arraySenha), $matchesSequenciaNumeros);

        foreach ($matchesSequenciaNumeros[0] as $sequenciaNumero) {
            $contadorSequenciaCrescente = $contadorSequenciaDecrescente = 1;
            $tamanhoSequencia = strlen($sequenciaNumero);

            for ($contador = 0; $contador < $tamanhoSequencia; $contador++) {
                $proximoNumeroSequencia = $sequenciaNumero[$contador + 1] ?? null;
                $somaNumeroAtual = (int)$sequenciaNumero[$contador] + 1;
                $subtracaoNumeroAtual = (int)$sequenciaNumero[$contador] - 1;

                if (!is_null($proximoNumeroSequencia)) {
                    $proximoNumeroSequencia = (int)$proximoNumeroSequencia;
                    $contadorSequenciaCrescente++;
                    $contadorSequenciaDecrescente++;

                    if ($proximoNumeroSequencia !== $somaNumeroAtual) {
                        $contadorSequenciaCrescente = 0;
                    }

                    if ($proximoNumeroSequencia !== $subtracaoNumeroAtual) {
                        $contadorSequenciaDecrescente = 0;
                    }
                }

                if ($contadorSequenciaCrescente >= 3 || $contadorSequenciaDecrescente >= 3) {
                    return true;
                }
            }
        }

        return false;
    }

    private function temSequenciaLetras(array $arraySenha) : bool
    {
        preg_match_all('/[a-zA-Z]{3}/', implode('', $arraySenha), $matchesSequenciaLetras);

        return false;
    }
}
