<?php
namespace Incoviba\Service;

use DateTimeInterface;
use DateTimeImmutable;
use DateInterval;
use Exception;
use PDOException;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Incoviba\Repository;
use Incoviba\Model;
use function random_bytes;
use function password_hash;
use function setcookie;

class Login
{
    public function __construct(protected Repository\Login $repository, protected Repository\User $userRepository,
                                protected string $cookie_name,
                                protected int $max_login_time, protected string $domain = '',
                                protected string $path = '', protected string $cookie_separator = ':')
    {
        $this->loadCookie();
    }

    protected string $selector = '';
    protected string $token = '';

    public function isIn(?string $selector = null, ?string $sentToken = null): bool
    {
        try {
            $login = $this->repository->fetchActiveBySelector($selector ?? $this->selector);
            if (!$this->validToken($login, $sentToken)) {
                return false;
            }
            $now = new DateTimeImmutable();
            if ($login->dateTime->add(new DateInterval("PT{$this->max_login_time}H")) > $now) {
                return true;
            }
        } catch (PDOException|EmptyResult) {}
        return false;
    }
    public function getUser(): Model\User
    {
        $login = $this->repository->fetchActiveBySelector($this->selector);
        if (!$this->validToken($login)) {
            throw new Exception('User not found');
        }
        return $login->user;
    }
    public function getToken(): string
    {
        return implode($this->cookie_separator, [$this->selector, $this->token]);
    }
    public function getSeparator(): string
    {
        return $this->cookie_separator;
    }

    public function addUser(array $data): Model\User
    {
        try {
            return $this->userRepository->fetchByName($data['name']);
        } catch (EmptyResult) {
            list($passphrase, $encrypted) = $this->splitPassword($data['password']);
            $password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase);
            $password = password_hash($password, PASSWORD_DEFAULT);
            $user = $this->userRepository->create([
                'name' => $data['name'],
                'password' => $password,
                'enabled' => $data['enabled'] ?? 1
            ]);
            return $this->userRepository->save($user);
        }
    }
    public function validateUser(Model\User $user, string $encryptedPassword): bool
    {
        list($passphrase, $encrypted) = $this->splitPassword($encryptedPassword);
        try {
            $password = $this->cryptoJs_aes_decrypt($encrypted, $passphrase);
        } catch (Exception) {
            return false;
        }
        return $user->validate($password);
    }
    public function login(Model\User $user): bool
    {
        try {
            $login = $this->repository->fetchActiveByUser($user->id);
            $this->logout($login->user);
        } catch (PDOException|EmptyResult) {
        }

        try {
            $now = new DateTimeImmutable();
            $login = $this->repository->create([
                'user_id' => $user->id,
                'time' => $now->format('Y-m-d H:i:s'),
                'status' => 1
            ]);
            list('selector' => $selector, 'token' => $token) = $this->generateToken($login);
            $login->selector = $selector;
            $login->token = password_hash($token, PASSWORD_DEFAULT);
            $this->repository->save($login);
            $this->saveCookie($selector, $token, $login->dateTime->add(new DateInterval("PT{$this->max_login_time}H")));
            return true;
        } catch (PDOException|Exception) {
            return false;
        }
    }
    public function logout(Model\User $user): bool
    {
        $this->removeCookie();
        try {
            $logins = $this->repository->fetchByUser($user->id);
        } catch (PDOException) {
            return true;
        }
        try {
            foreach ($logins as $login) {
                $this->repository->edit($login, ['status' => 0]);
            }
            return true;
        } catch (PDOException) {
            return false;
        }
    }
    public function parseToken(Model\Login $login): string
    {
        return implode($this->cookie_separator, [$login->selector, $login->token]);
    }

    protected function loadCookie(): void
    {
        if (!isset($_COOKIE[$this->cookie_name])) {
            return;
        }
        $cookie = $_COOKIE[$this->cookie_name];
        if (!str_contains($cookie, $this->cookie_separator)) {
            $this->removeCookie();
            return;
        }
        list($this->selector, $this->token) = explode($this->cookie_separator, $cookie);
    }
    protected function saveCookie(string $selector, string $token, DateTimeInterface $expires): void
    {
        setcookie(
            $this->cookie_name,
            implode($this->cookie_separator, [$selector, $token]),
            [
                'expires' => $expires->getTimestamp(),
                'path' => $this->path,
                'domain' => $this->domain,
                'samesite' => 'Strict'
            ]
        );
        $this->selector = $selector;
        $this->token = $token;
    }
    protected function removeCookie(): void
    {
        setcookie(
            $this->cookie_name,
            '',
            (new DateTimeImmutable())->getTimestamp(),
            $this->path,
            $this->domain
        );
    }

    protected function validToken(Model\Login $login, ?string $sentToken = null): bool
    {
        if ($sentToken !== null) {
            return password_verify($sentToken, $login->token);
        }
        return password_verify($this->token, $login->token);
    }
    protected function generateToken(Model\Login $login): array
    {
        $selector = bin2hex(random_bytes(12));
        $token = bin2hex(random_bytes(20));
        return ['selector' => $selector, 'token' => $token];
    }

    protected function splitPassword(string $input): array
    {
        $ini = strpos($input, 'U');
        $passphrase = substr($input, 0, $ini);
        $message = substr($input, $ini);
        return [$passphrase, $message];
    }
    protected function cryptoJs_aes_decrypt($data, $key): string
    {
        $data = base64_decode($data);
        if (!str_starts_with($data, "Salted__")) {
            return false;
        }
        $salt = substr($data, 8, 8);
        $keyAndIV = $this->aes_evpKDF($key, $salt);
        $decrypted = openssl_decrypt(
            substr($data, 16),
            "aes-256-cbc",
            $keyAndIV["key"],
            OPENSSL_RAW_DATA, // base64 was already decoded
            $keyAndIV["iv"]
        );
        if ($decrypted === false) {
            throw new Exception();
        }
        return $decrypted;
    }
    protected function aes_evpKDF($password, $salt, $keySize = 8, $ivSize = 4, $iterations = 1, $hashAlgorithm = "md5"): array
    {
        $targetKeySize = $keySize + $ivSize;
        $derivedBytes = "";
        $numberOfDerivedWords = 0;
        $block = NULL;
        $hasher = hash_init($hashAlgorithm);
        while ($numberOfDerivedWords < $targetKeySize) {
            if ($block != NULL) {
                hash_update($hasher, $block);
            }
            hash_update($hasher, $password);
            hash_update($hasher, $salt);
            $block = hash_final($hasher, TRUE);
            $hasher = hash_init($hashAlgorithm);

            // Iterations
            for ($i = 1; $i < $iterations; $i++) {
                hash_update($hasher, $block);
                $block = hash_final($hasher, TRUE);
                $hasher = hash_init($hashAlgorithm);
            }

            $derivedBytes .= substr($block, 0, min(strlen($block), ($targetKeySize - $numberOfDerivedWords) * 4));

            $numberOfDerivedWords += strlen($block) / 4;
        }
        return array(
            "key" => substr($derivedBytes, 0, $keySize * 4),
            "iv"  => substr($derivedBytes, $keySize * 4, $ivSize * 4)
        );
    }
}
