<?php

declare(strict_types=1);

namespace mirzaev\surikovlib\models;

use pdo;
use exception;

/**
 * Модель регистрации, аутентификации и авторизации
 *
 * @package mirzaev\surikovlib\models
 * @author Arsen Mirzaev Tatyano-Muradovich <arsen@mirzaev.sexy>
 */
final class accounts_model extends core
{
    /**
     * Регистрация
     *
     * @param string $name Входной псевдоним
     * @param string $email Почта
     * @param string $password Пароль (password)
     * @param bool $authentication Автоматическая аутентификация в случае успешной регистрации
     * @param array &$errors Журнал ошибок
     *
     * @return array|bool Аккаунт, если удалось аутентифицироваться
     */
    public static function registration(string $name = null, string $email = null, string $password, array &$errors = []): array
    {
        try {
            if (static::account($errors)) {
                // Аутентифицирован пользователь

                // Запись ошибки
                throw new exception('Уже аутентифицирован');
            }

            if (empty($account = static::read(['name' => $name]) or $account = static::read(['email' => $email]))) {
                // Не удалось найти аккаунт

                if (static::write($name, $email, $password, $errors)) {
                    // Удалось зарегистрироваться

                    return $account;
                }
            } else {
                // Удалось найти аккаунт

                return $account;
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return [];
    }

    /**
     * Аутентификация
     *
     * @param string $login Входной псевдоним
     * @param string $password Пароль (password)
     * @param bool $remember Функция "Запомнить меня" - увеличенное время хранения cookies
     * @param array &$errors Журнал ошибок
     *
     * @return array Аккаунт (если не найден, то пустой массив)
     */
    public static function authentication(string $login, string $password, bool $remember = false, array &$errors = []): array
    {
        try {
            if (static::account($errors)) {
                // Аутентифицирован пользователь

                // Запись ошибки
                throw new exception('Уже аутентифицирован');
            }


            if (empty($account = static::read(['name' => $login]) or $account = static::read(['email' => $login]))) {
                // Не удалось найти аккаунт

                throw new exception('Не удалось найти аккаунт');
            }

            if (password_verify($password, $account['password'])) {
                // Совпадают хеши паролей

                // Инициализация идентификатора сессии
                session_id($account['id']);

                // Инициализация названия сессии
                session_name('id');

                // Инициализация сессии
                session_start();

                // Инициализация времени хранения хеша
                $time = time() + ($remember ? 604800 : 86400);

                // Инициализация хеша
                $hash = static::hash((int) $account['id'], crypt($account['password'], time() . $account['id']), $time, $errors)['hash'];

                // Инициализация cookies
                setcookie("hash", $hash, $time, path: '/', secure: true);

                return $account;
            } else {
                // Не совпадают хеши паролей

                throw new exception('Неправильный пароль');
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return [];
    }

    /**
     * Аутентификация
     *
     * @param array &$errors Журнал ошибок
     *
     * @return bool Удалось ли деаутентифицироваться
     */
    public static function deauthentication(array &$errors = []): bool
    {
        try {
            if ($account = static::account($errors)) {
                // Аутентифицирован пользователь

                // Инициализация запроса
                $request = static::$db->prepare("UPDATE `accounts` SET `hash` = null, `time` = 0 WHERE `id` = :id");

                // Параметры запроса
                $params = [
                    ":id" => $account['id'],
                ];

                // Отправка запроса
                $request->execute($params);

                // Генерация ответа
                $request->fetch(pdo::FETCH_ASSOC);

                // Деинициализация cookies
                setcookie("id", '', 0, path: '/', secure: true);
                setcookie("hash", '', 0, path: '/', secure: true);

                return true;
            } else {
                // Не аутентифицирован пользователь

                // Запись ошибки
                throw new exception('Не аутентифицирован');
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return false;
    }

    /**
     * Прочитать данные аккаунта, если пользователь аутентифицирован
     *
     * Можно использовать как проверку на аутентифицированность
     *
     * @param array &$errors Журнал ошибок
     *
     * @return array Аккаунт (если не найден, то пустой массив)
     *
     * @todo 1. Сделать в static::read() возможность передачи нескольких параметров и перенести туда непосредственно чтение аккаунта с проверкой хеша
     */
    public static function account(array &$errors = []): array
    {
        try {
            if (!empty($_COOKIE['id']) && !empty($_COOKIE['hash'])) {
                // Аутентифицирован аккаунт (найдены cookie и они хранят значения - подразумевается, что не null или пустое)

                if ($_COOKIE['hash'] === static::hash((int) $_COOKIE['id'], errors: $errors)['hash']) {
                    // Совпадает переданный хеш с тем, что хранится в базе данных
                } else {
                    // Не совпадает переданный хеш с тем, что хранится в базе данных

                    // Генерация ошибки
                    throw new exception('Вы аутентифицированы с другого устройства (не совпадают хеши аутентификации)');
                }

                // Инициализация запроса
                $request = static::$db->prepare("SELECT * FROM `accounts` WHERE `id` = :id && `hash` = :hash");

                // Параметры запроса
                $params = [
                    ":id" => $_COOKIE['id'],
                    ":hash" => $_COOKIE['hash'],
                ];

                // Отправка запроса
                $request->execute($params);

                // Генерация ответа
                if (empty($account = $request->fetch(pdo::FETCH_ASSOC))) {
                    // Не найдена связка идентификатора с хешем

                    // Генерация ошибки
                    throw new exception('Не найден пользотватель или время аутентификации истекло');
                }

                // Чтение разрешений
                $account['permissions'] = static::permissions((int) $account['id'], $errors);

                return $account;
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return [];
    }

    /**
     * Прочитать разрешения аккаунта
     *
     * @param int $id Идентификатор аккаунта
     * @param array &$errors Журнал ошибок
     *
     * @return array Разрешения аккаунта, если найдены
     */
    public static function permissions(int $id, array &$errors = []): array
    {
        try {
            // Инициализация запроса
            $request = static::$db->prepare("SELECT * FROM `permissions` WHERE `id` = :id");

            // Параметры запроса
            $params = [
                ":id" => $id
            ];

            // Отправка запроса
            $request->execute($params);

            // Генерация ответа
            if (empty($response = $request->fetch(pdo::FETCH_ASSOC))) {
                // Не найдены разрешения

                // Генерация ошибки
                throw new exception('Не найдены разрешения');
            }

            // Удаление ненужных данных
            unset($response['id']);

            return $response;
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return [];
    }

    /**
     * Проверить разрешение
     *
     * @param string $permission Разрешение
     * @param int|null $id Идентификатор аккаунта
     * @param array &$errors Журнал ошибок
     *
     * @return bool|null Статус разрешения, если оно записано
     */
    public static function access(string $permission, int|null $id = null, array &$errors = []): ?bool
    {
        try {
            // Инициализация аккаунта
            $account = isset($id) ? self::read(['id' => $id], $errors) : self::account($errors);

            return isset($account['permissions'][$permission]) ? (bool) $account['permissions'][$permission] : null;
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return null;
    }

    /**
     * Запись пользователя в базу данных
     *
     * @param string|null $name Имя
     * @param string|null $email Почта
     * @param string|null $password Пароль
     * @param array &$errors Журнал ошибок
     *
     * @return array Аккаунт (если не найден, то пустой массив)
     */
    public static function write(string|null $name = null, string|null $email = null, string|null $password = null, array &$errors = []): array
    {
        try {
            // Инициализация параметров запроса
            $params = [];

            if (isset($name)) {
                try {
                    // Проверка параметра
                    if (iconv_strlen($name) < 3) throw new exception('Длина имени должна быть не менее 3 символов');
                    if (iconv_strlen($name) > 60) throw new exception('Длина имени должна быть не более 60 символов');

                    // Запись в буфер параметров запроса
                    $params[':name'] = $name;
                } catch (exception $e) {
                    // Запись в журнал ошибок
                    $errors[] = [
                        'text' => $e->getMessage(),
                        'file' => $e->getFile(),
                        'line' => $e->getLine()
                    ];

                    goto end;
                }
            }

            if (isset($email)) {
                try {
                    // Проверка параметра
                    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) throw new exception('Не удалось распознать почту');
                    if (iconv_strlen($email) < 3) throw new exception('Длина почты должна быть не менее 3 символов');
                    if (iconv_strlen($email) > 60) throw new exception('Длина почты должна быть не более 80 символов');

                    // Запись в буфер параметров запроса
                    $params[':email'] = $email;
                } catch (exception $e) {
                    // Запись в журнал ошибок
                    $errors[] = [
                        'text' => $e->getMessage(),
                        'file' => $e->getFile(),
                        'line' => $e->getLine()
                    ];

                    goto end;
                }
            }

            if (isset($password)) {
                try {
                    // Проверка параметра
                    if (iconv_strlen($password) < 3) throw new exception('Длина пароля должна быть не менее 3 символов');
                    if (iconv_strlen($password) > 60) throw new exception('Длина пароля должна быть не более 120 символов');

                    // Запись в буфер параметров запроса
                    $params[':password'] = password_hash($password, PASSWORD_BCRYPT);
                } catch (exception $e) {
                    // Запись в журнал ошибок
                    $errors[] = [
                        'text' => $e->getMessage(),
                        'file' => $e->getFile(),
                        'line' => $e->getLine()
                    ];

                    goto end;
                }
            }

            // Инициализация запроса
            $request = static::$db->prepare("INSERT INTO `accounts` (" . (isset($name) ? '`name`' : '') . (isset($name) && isset($email) ? ', ' : '') . (isset($email) ? '`email`' : '') . ((isset($name) || isset($email)) && isset($password) ? ', ' : '') . (isset($password) ? '`password`' : '') . ") VALUES (" . (isset($name) ? ':name' : '') . (isset($name) && isset($email) ? ', ' : '') .  (isset($email) ? ':email' : '') . ((isset($name) || isset($email)) && isset($password) ? ', ' : '') . (isset($password) ? ':password' : '') . ")");

            // Отправка запроса
            $request->execute($params);

            // Генерация ответа
            $request->fetch(pdo::FETCH_ASSOC);

            try {
                if (isset($name)) {
                    // Передано имя аккаунта

                    // Чтение аккаунта
                    $account = static::read(['name' => $name]);
                } else if (isset($email)) {
                    // Передана почта аккаунта

                    // Чтение аккаунта
                    $account = static::read(['email' => $email]);
                } else {
                    // Не передано ни имя, ни почта

                    throw new exception('Не переданны данные для полноценной регистрации аккаунта');
                }

                // Инициализация запроса
                $request = static::$db->prepare("INSERT INTO `permissions` (`id`) VALUES (:id)");

                // Инициализация параметров
                $params = [
                    ':id' => $account['id']
                ];

                // Отправка запроса
                $request->execute($params);

                // Генерация ответа
                $request->fetch(pdo::FETCH_ASSOC);
            } catch (exception $e) {
                // Запись в журнал ошибок
                $errors[] = [
                    'text' => $e->getMessage(),
                    'file' => $e->getFile(),
                    'line' => $e->getLine(),
                    'stack' => $e->getTrace()
                ];
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        // Конец выполнения
        end:

        return isset($account) && $account ? $account : [];
    }

    /**
     * Чтение пользователя из базы данных
     *
     * @param array $search Поиск ('поле' => 'значение'), работает только с одним полем
     * @param array &$errors Журнал ошибок
     *
     * @return array Аккаунт, если найден
     */
    public static function read(array $search, array &$errors = []): array
    {
        try {
            // Инициализация данных для поиска
            $field = array_keys($search)[0] ?? null;
            $value = $search[$field] ?? null;

            if (empty($field)) {
                // Получено пустое значение поля

                // Запись ошибки
                throw new exception('Пустое значение поля для поиска');
            }

            // Инициализация запроса
            $request = static::$db->prepare("SELECT * FROM `accounts` WHERE `$field` = :field LIMIT 1");

            // Параметры запроса
            $params = [
                ":field" => $value,
            ];

            // Отправка запроса
            $request->execute($params);

            // Генерация ответа
            if ($account = $request->fetch(pdo::FETCH_ASSOC)) {
                // Найден аккаунт

                try {
                    if ($permissions = static::permissions((int) $account['id'], $errors)) {
                        // Найдены разрешения

                        // Запись в буфер данных аккаунта
                        $account['permissions'] = $permissions;
                    } else {
                        // Не найдены разрешения

                        throw new exception('Не удалось найти и прочитать разрешения');
                    }
                } catch (exception $e) {
                    // Запись в журнал ошибок
                    $errors[] = [
                        'text' => $e->getMessage(),
                        'file' => $e->getFile(),
                        'line' => $e->getLine()
                    ];
                }
            } else {
                // Не найден аккаунт

                throw new exception('Не удалось найти аккаунт');
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return isset($account) && $account ? $account : [];
    }

    /**
     * Запись или чтение хеша из базы данных
     *
     * @param int $id Идентификатор аккаунта
     * @param int|null $hash Хеш аутентифиакции
     * @param string|null $time Время хранения хеша
     * @param array &$errors Журнал ошибок
     *
     * @return array ['hash' => $hash, 'time' => $time]
     */
    public static function hash(int $id, string|null $hash = null, int|null $time = null, array &$errors = []): array
    {
        try {
            if (isset($hash, $time)) {
                // Переданы хеш и его время хранения

                // Инициализация запроса
                $request = static::$db->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `id` = :id");

                // Параметры запроса
                $params = [
                    ":id" => $id,
                    ":hash" => $hash,
                    ":time" => $time,
                ];

                // Отправка запроса
                $request->execute($params);

                // Генерация ответа
                $request->fetch(pdo::FETCH_ASSOC);
            } else {
                // Не переданы хеш и его время хранения

                // Инициализация запроса
                $request = static::$db->prepare("SELECT `hash`, `time` FROM `accounts` WHERE `id` = :id");

                // Параметры запроса
                $params = [
                    ":id" => $id,
                ];

                // Отправка запроса
                $request->execute($params);

                // Генерация ответа
                extract((array) $request->fetch(pdo::FETCH_ASSOC));

                if (!empty($response['time']) && $response['time'] <= time()) {
                    // Истекло время жизни хеша

                    // Инициализация запроса
                    $request = static::$db->prepare("UPDATE `accounts` SET `hash` = :hash, `time` = :time WHERE `id` = :id");

                    // Параметры запроса
                    $params = [
                        ":id" => $id,
                        ":hash" => null,
                        ":time" => null,
                    ];

                    // Отправка запроса
                    $request->execute($params);

                    // Генерация ответа
                    $response = $request->fetch(pdo::FETCH_ASSOC);

                    // Генерация ошибки
                    throw new exception('Время аутентификации истекло');
                }
            }
        } catch (exception $e) {
            // Запись в журнал ошибок
            $errors[]= [
                'text' => $e->getMessage(),
                'file' => $e->getFile(),
                'line' => $e->getLine(),
                'stack' => $e->getTrace()
            ];
        }

        return ['hash' => $hash, 'time' => $time];
    }
}
