<?php
namespace EasyChat\Controller;

use EasyChat\Library\PresenceService;
use EasyChat\Library\PasskeyService;
use EasyChat\Model\Manager;
use EasyChat\Library\Security\PasswordCredentialService;
use EasyChat\Library\Security\AuthRateLimiter;
use EasyChat\Model\Setting;

class Auth extends Base {

    protected $layouts = 'layoutclean';

    public function index() {
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');
        $session_available = $this->sessionAvailable();
        $cms_handoff_entry = $this->isCmsHandoffEntry();
        if (!$session_available) {
            http_response_code(503);
            header('Retry-After: 60');
        }

         
         
         
         
         
        if ($session_available && $this->hasCommittedManagerSession() && !$cms_handoff_entry) {
            header('Location: index.php?route=index');
            exit;
        }

        $this->vars['title'] = 'Вход в EasyChat';
        $this->vars['session_storage_available'] = $session_available;
        $this->vars['session_storage_error'] = $this->sessionUnavailableMessage();
        $this->template->view('login');
    }

    public function login() {
        $this->jsonHeaders();
        $this->requirePostJson();

        if (!$this->requireSessionJson()) {
            return;
        }

        $cms_source = isset(\EasyChat\Library\RequestContext::post()['cms_source']) ? strtolower(trim((string)\EasyChat\Library\RequestContext::post()['cms_source'])) : '';
        $allowed_cms_sources = array('opencart', 'wordpress', 'webasyst', 'bitrix');
        $cms_source = in_array($cms_source, $allowed_cms_sources, true) ? $cms_source : 'cms';

        $handoff_token = isset(\EasyChat\Library\RequestContext::post()['cms_handoff_token']) ? trim((string)\EasyChat\Library\RequestContext::post()['cms_handoff_token']) : '';
        if ($handoff_token !== '') {
            $this->loginWithCmsHandoff($handoff_token, $cms_source);
            exit;
        }

         
         
         
         
        if ($this->hasCommittedManagerSession()) {
            echo json_encode(['success' => true]);
            exit;
        }

        $username = isset(\EasyChat\Library\RequestContext::post()['username']) ? trim(\EasyChat\Library\RequestContext::post()['username']) : '';
         
        $password = isset(\EasyChat\Library\RequestContext::post()['password']) ? (string)\EasyChat\Library\RequestContext::post()['password'] : '';

        if (!$username || !$password) {
            echo json_encode(['success' => false, 'error' => 'Заполните все поля ввода']);
            exit;
        }

        $model_manager = new Manager($this->registry);
        $manager = $model_manager->getActiveManagerByUsername($username);
        $ip = (string)(\EasyChat\Library\RequestContext::server()['REMOTE_ADDR'] ?? '');
        $limit_manager_id = (int)($manager['id'] ?? 0);
        $limiter = new AuthRateLimiter($this->registry);
        $limit = $limiter->status('web_login', $limit_manager_id, $ip);
        if (empty($limit['allowed'])) {
            http_response_code(429);
            header('Retry-After: ' . max(1, (int)($limit['retry_after'] ?? 60)));
            echo json_encode(['success' => false, 'error' => 'Слишком много попыток. Повторите через несколько минут.']);
            exit;
        }

        if (!$manager) {
            $limiter->recordFailure('web_login', 0, $ip);
            echo json_encode(['success' => false, 'error' => 'Неверное имя пользователя или пароль']);
            exit;
        }

        $credentials = new PasswordCredentialService();
        if (!$credentials->verify($password, (string)$manager['password'])) {
            $limiter->recordFailure('web_login', (int)$manager['id'], $ip);
            echo json_encode(['success' => false, 'error' => 'Неверное имя пользователя или пароль']);
            exit;
        }
        $limiter->clear('web_login', (int)$manager['id'], $ip);

         
         
        if ($credentials->needsRehash((string)$manager['password'])) {
            $manager['password'] = $credentials->hash($password);
            $model_manager->updatePasswordHash((int)$manager['id'], (string)$manager['password']);
        }

        $cms_prefill = isset(\EasyChat\Library\RequestContext::post()['cms_prefill']) && (string)\EasyChat\Library\RequestContext::post()['cms_prefill'] === '1';
        if (!$this->openManagerSession($manager, $model_manager, $cms_prefill, $cms_source, $password)) {
            $this->sendSessionUnavailableJson();
            exit;
        }

        echo json_encode(['success' => true]);
        exit;
    }

    public function passkeyloginoptions() {
        $this->jsonHeaders();
        $this->requirePostJson();
        if (!$this->requireSessionJson()) {
            return;
        }

        try {
            $service = new PasskeyService($this->registry);
            echo json_encode(array('success' => true, 'publicKey' => $service->loginOptions()), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } catch (\Throwable $e) {
            echo json_encode(array('success' => false, 'error' => 'Не удалось начать вход по passkey.'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        exit;
    }

    public function passkeyloginverify() {
        $this->jsonHeaders();
        $this->requirePostJson();
        if (!$this->requireSessionJson()) {
            return;
        }

        try {
            $payload = $this->passkeyPayload();
            $model_manager = new Manager($this->registry);
            $service = new PasskeyService($this->registry);
            $result = $service->verifyLogin($payload);
            $manager = $result['manager'];
            if (!$this->openManagerSession($manager, $model_manager, false, 'passkey', '')) {
                $this->sendSessionUnavailableJson();
                exit;
            }

            echo json_encode(array('success' => true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } catch (\Throwable $e) {
            echo json_encode(array('success' => false, 'error' => $this->safePasskeyError($e)), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        exit;
    }

    public function passkeyregisteroptions() {
        $this->jsonHeaders();
        $this->requirePostJson();
        $manager = $this->requireManagerJson();
        if (!$manager) {
            exit;
        }
        $this->requireCsrfTokenJson();

        try {
            $service = new PasskeyService($this->registry);
            echo json_encode(array('success' => true, 'publicKey' => $service->registrationOptions($manager)), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } catch (\Throwable $e) {
            echo json_encode(array('success' => false, 'error' => 'Не удалось начать привязку passkey.'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        exit;
    }

    public function passkeyregisterverify() {
        $this->jsonHeaders();
        $this->requirePostJson();
        $manager = $this->requireManagerJson();
        if (!$manager) {
            exit;
        }
        $this->requireCsrfTokenJson();

        try {
            $payload = $this->passkeyPayload();
            $device_label = isset(\EasyChat\Library\RequestContext::post()['device_label']) ? (string)\EasyChat\Library\RequestContext::post()['device_label'] : '';
            $service = new PasskeyService($this->registry);
            $service->verifyRegistration($payload, (int)$manager['id'], $device_label);

            echo json_encode(array('success' => true, 'credentials' => $service->listCredentials((int)$manager['id'])), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        } catch (\Throwable $e) {
            echo json_encode(array('success' => false, 'error' => $this->safePasskeyError($e)), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        exit;
    }

    public function passkeylist() {
        $this->jsonHeaders();
        $this->requirePostJson();
        $manager = $this->requireManagerJson();
        if (!$manager) {
            exit;
        }
        $this->requireCsrfTokenJson();

        $service = new PasskeyService($this->registry);
        echo json_encode(array('success' => true, 'credentials' => $service->listCredentials((int)$manager['id'])), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    public function passkeyrevoke() {
        $this->jsonHeaders();
        $this->requirePostJson();
        $manager = $this->requireManagerJson();
        if (!$manager) {
            exit;
        }
        $this->requireCsrfTokenJson();

        $credential_id = isset(\EasyChat\Library\RequestContext::post()['credential_id']) ? (int)\EasyChat\Library\RequestContext::post()['credential_id'] : 0;
        if ($credential_id <= 0) {
            echo json_encode(array('success' => false, 'error' => 'Не выбран passkey.'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            exit;
        }

        $service = new PasskeyService($this->registry);
        $service->revokeCredential((int)$manager['id'], $credential_id);
        echo json_encode(array('success' => true, 'credentials' => $service->listCredentials((int)$manager['id'])), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }

    private function loginWithCmsHandoff($token, $source) {
        if (!$this->sessionAvailable()) {
            $this->sendSessionUnavailableJson();
            return;
        }

        if (!preg_match('/^[a-f0-9]{64}$/', $token)) {
            echo json_encode(['success' => false, 'error' => 'Недействительный одноразовый вход из CMS']);
            return;
        }

        $model_manager = new Manager($this->registry);
        $manager = $model_manager->getManagerById(1);
        if (!$manager || (int)$manager['status'] !== 1 || $manager['role'] !== 'admin') {
            echo json_encode(['success' => false, 'error' => 'Администратор EasyChat недоступен']);
            return;
        }

        $setting = new Setting($this->registry);
        $stored_hash = (string)$setting->get('cms_admin_handoff_hash', '');
        $expires_at = (int)$setting->get('cms_admin_handoff_expires_at', '0');
        $stored_source = strtolower(trim((string)$setting->get('cms_admin_handoff_source', '')));
        $presented_hash = hash('sha256', $token);
        if ($stored_hash === ''
            || $expires_at < time()
            || $stored_source === ''
            || !hash_equals($stored_source, (string)$source)
        ) {
            echo json_encode(['success' => false, 'error' => 'Одноразовый вход из CMS истек или уже использован']);
            return;
        }

        $claim = $this->resumeCmsHandoffClaim($stored_hash, $presented_hash);
        if ($claim === null && hash_equals($stored_hash, $presented_hash)) {
            $claim = $this->beginCmsHandoffClaim($stored_hash, $presented_hash);
        }
        if ($claim === false) {
            $this->sendSessionUnavailableJson();
            return;
        }
        if (!is_array($claim)) {
            echo json_encode(['success' => false, 'error' => 'Одноразовый вход из CMS истек или уже использован']);
            return;
        }

        $claim_marker = (string)$claim['marker'];
        $claim_secret = (string)$claim['secret'];

         
         
        if (hash_equals($stored_hash, $presented_hash)) {
            $db = $this->registry->get('db');
            $prefix = easychat_db_prefix();
            try {
                $db->query("UPDATE `{$prefix}settings`
                    SET `value` = '" . $db->escape($claim_marker) . "'
                    WHERE `key` = 'cms_admin_handoff_hash'
                      AND `value` = '" . $db->escape($presented_hash) . "'
                    LIMIT 1");
                if (!method_exists($db, 'countAffected') || (int)$db->countAffected() !== 1) {
                    \EasyChat\Library\SessionBootstrap::destroy();
                    echo json_encode(['success' => false, 'error' => 'Одноразовый вход из CMS уже использован']);
                    return;
                }
            } catch (\Throwable $e) {
                \EasyChat\Library\SessionBootstrap::destroy();
                $this->sendSessionUnavailableJson();
                return;
            }
        }

        $this->populateManagerSession($manager, $model_manager, true, $source, '', true);
        $expected_session = array(
            'manager_id' => (int)$manager['id'],
            'manager_pass_hash' => md5($manager['password']),
            'cms_handoff_pending_claim' => $claim_secret,
            'cms_handoff_pending_token_hash' => $presented_hash,
        );
        if (!\EasyChat\Library\SessionBootstrap::verifyPersistence($expected_session)) {
             
             
            $this->sendSessionUnavailableJson();
            return;
        }

        $db = $this->registry->get('db');
        $prefix = easychat_db_prefix();
        try {
            $db->query("UPDATE `{$prefix}settings`
                SET `value` = ''
                WHERE `key` = 'cms_admin_handoff_hash'
                  AND `value` = '" . $db->escape($claim_marker) . "'
                LIMIT 1");
            if (!method_exists($db, 'countAffected') || (int)$db->countAffected() !== 1) {
                \EasyChat\Library\SessionBootstrap::destroy();
                echo json_encode(['success' => false, 'error' => 'Одноразовый вход из CMS уже использован']);
                return;
            }
        } catch (\Throwable $e) {
            \EasyChat\Library\SessionBootstrap::destroy();
            $this->sendSessionUnavailableJson();
            return;
        }

        unset(
            \EasyChat\Library\RequestContext::session()['cms_handoff_pending_claim'],
            \EasyChat\Library\RequestContext::session()['cms_handoff_pending_token_hash']
        );
        if (!\EasyChat\Library\SessionBootstrap::verifyPersistence(array(
            'manager_id' => (int)$manager['id'],
            'manager_pass_hash' => md5($manager['password']),
        ), array(
            'cms_handoff_pending_claim',
            'cms_handoff_pending_token_hash',
        ))) {
             
             
             
            $this->sendSessionUnavailableJson();
            return;
        }
        $this->finalizeManagerLogin($manager, $model_manager);

        echo json_encode(['success' => true]);
    }

    private function openManagerSession($manager, Manager $model_manager, $cms_prefill, $cms_source, $password) {
        if (!$this->prepareAuthenticatedSession()) {
            return false;
        }

        $this->populateManagerSession($manager, $model_manager, $cms_prefill, $cms_source, $password, false);
        if (!\EasyChat\Library\SessionBootstrap::verifyPersistence(array(
            'manager_id' => (int)$manager['id'],
            'manager_pass_hash' => md5($manager['password']),
        ))) {
            return false;
        }

        $this->finalizeManagerLogin($manager, $model_manager);
        return true;
    }

    private function populateManagerSession($manager, Manager $model_manager, $cms_prefill, $cms_source, $password, $force_password_suggestion) {
        \EasyChat\Library\RequestContext::session()['manager_id'] = (int)$manager['id'];
        \EasyChat\Library\RequestContext::session()['manager_role'] = $manager['role'];
        \EasyChat\Library\RequestContext::session()['manager_name'] = $manager['name'];
        \EasyChat\Library\RequestContext::session()['manager_img'] = $manager['image'];
        \EasyChat\Library\RequestContext::session()['manager_pass_hash'] = md5($manager['password']);

        if ($cms_prefill) {
            \EasyChat\Library\RequestContext::session()['login_source'] = 'cms_adapter';
            \EasyChat\Library\RequestContext::session()['login_cms_source'] = $cms_source;

            if ($password !== '' && $manager['role'] === 'admin' && !$model_manager->isAdminPasswordCustomized() && $model_manager->isInitialAdminPassword($password, $manager)) {
                \EasyChat\Library\RequestContext::session()['suggest_password_change'] = 1;
            }
        } elseif ($cms_source === 'passkey') {
            \EasyChat\Library\RequestContext::session()['login_source'] = 'passkey';
        }

        if ($force_password_suggestion && !$model_manager->isAdminPasswordCustomized()) {
            \EasyChat\Library\RequestContext::session()['suggest_password_change'] = 1;
        }
    }

    private function finalizeManagerLogin($manager, Manager $model_manager) {
         
         
        try {
            if ($manager['role'] === 'admin') {
                $model_manager->runMigrations();
            }
        } catch (\Throwable $e) {
        }

        try {
            $presence = new PresenceService($this->registry);
            $presence->markManagerActive((int)$manager['id'], 'web_login', true);
        } catch (\Throwable $e) {
        }
    }

    public function logout() {
        if (!\EasyChat\Library\SessionBootstrap::destroy()) {
            http_response_code(503);
            header('Content-Type: text/plain; charset=utf-8');
            header('Cache-Control: no-store, no-cache, must-revalidate');
            header('Retry-After: 5');
            echo 'EasyChat не смог подтвердить безопасный выход. Повторите действие через несколько секунд или проверьте доступ сайта к папке log/sessions.';
            exit;
        }
        header('Location: index.php?route=auth');
        exit;
    }

    private function jsonHeaders() {
        header('Content-Type: application/json; charset=utf-8');
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');
    }

    private function sessionAvailable() {
        return (!defined('EASYCHAT_SESSION_AVAILABLE') || EASYCHAT_SESSION_AVAILABLE)
            && \EasyChat\Library\SessionBootstrap::isAvailable();
    }

    private function hasCommittedManagerSession() {
        $session = \EasyChat\Library\RequestContext::session();
        return !empty($session['manager_id']) && empty($session['cms_handoff_pending_claim']);
    }

    private function isCmsHandoffEntry() {
        $get = \EasyChat\Library\RequestContext::get();
        return isset($get['cms_handoff']) && (string)$get['cms_handoff'] === '1';
    }

    private function prepareAuthenticatedSession() {
        if (!$this->sessionAvailable() || !\EasyChat\Library\SessionBootstrap::regenerateId()) {
            return false;
        }

         
         
        \EasyChat\Library\RequestContext::replaceSession([]);
        return true;
    }

    private function beginCmsHandoffClaim($stored_hash, $presented_hash) {
        if (!hash_equals((string)$stored_hash, (string)$presented_hash) || !$this->prepareAuthenticatedSession()) {
            return false;
        }

        try {
            $claim_secret = bin2hex(random_bytes(32));
        } catch (\Throwable $e) {
            return false;
        }
        $marker = $this->cmsHandoffClaimMarker($claim_secret, $presented_hash);
        \EasyChat\Library\RequestContext::session()['cms_handoff_pending_claim'] = $claim_secret;
        \EasyChat\Library\RequestContext::session()['cms_handoff_pending_token_hash'] = $presented_hash;

        if (!\EasyChat\Library\SessionBootstrap::verifyPersistence(array(
            'cms_handoff_pending_claim' => $claim_secret,
            'cms_handoff_pending_token_hash' => $presented_hash,
        ))) {
            return false;
        }

        return array('secret' => $claim_secret, 'marker' => $marker);
    }

    private function resumeCmsHandoffClaim($stored_hash, $presented_hash) {
        $session = \EasyChat\Library\RequestContext::session();
        $claim_secret = isset($session['cms_handoff_pending_claim']) ? (string)$session['cms_handoff_pending_claim'] : '';
        $pending_token_hash = isset($session['cms_handoff_pending_token_hash']) ? (string)$session['cms_handoff_pending_token_hash'] : '';
        if ($claim_secret === '' || !preg_match('/^[a-f0-9]{64}$/', $claim_secret) || !hash_equals($pending_token_hash, $presented_hash)) {
            return null;
        }

        $marker = $this->cmsHandoffClaimMarker($claim_secret, $presented_hash);
        if (!hash_equals((string)$stored_hash, $marker)) {
            return null;
        }
        if (!\EasyChat\Library\SessionBootstrap::verifyPersistence(array(
            'cms_handoff_pending_claim' => $claim_secret,
            'cms_handoff_pending_token_hash' => $presented_hash,
        ))) {
            return false;
        }

        return array('secret' => $claim_secret, 'marker' => $marker);
    }

    private function cmsHandoffClaimMarker($claim_secret, $presented_hash) {
        return 'pending:' . hash('sha256', session_id() . '|' . (string)$claim_secret . '|' . (string)$presented_hash);
    }

    private function requireSessionJson() {
        if ($this->sessionAvailable()) {
            return true;
        }

        $this->sendSessionUnavailableJson();
        return false;
    }

    private function sendSessionUnavailableJson() {
        http_response_code(503);
        header('Retry-After: 60');
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');
        echo json_encode(['success' => false, 'error' => $this->sessionUnavailableMessage()]);
    }

    private function sessionUnavailableMessage() {
        return 'EasyChat не может подтвердить защищенную сессию. Вернитесь на предыдущую страницу и откройте EasyChat еще раз. Если ошибка повторится, проверьте права EasyChat на запись или обратитесь в поддержку.';
    }

    private function requirePostJson() {
        if (\EasyChat\Library\RequestContext::method() !== 'POST') {
            http_response_code(405);
            echo json_encode(array('success' => false, 'error' => 'method_not_allowed'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            exit;
        }
    }

    private function requireManagerJson() {
        if (!$this->hasCommittedManagerSession()) {
            echo json_encode(array('success' => false, 'error' => 'Требуется вход в EasyChat.'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            return null;
        }

        $session = \EasyChat\Library\RequestContext::session();
        $model_manager = new Manager($this->registry);
        $manager = $model_manager->getManagerById((int)$session['manager_id']);
        $session_hash = (string)($session['manager_pass_hash'] ?? '');
        $session_role = (string)($session['manager_role'] ?? '');
        $manager_hash = $manager ? md5((string)$manager['password']) : '';
        $manager_role = $manager ? (string)$manager['role'] : '';

         
         
         
         
        if (!$manager
            || (int)$manager['status'] !== 1
            || $session_hash === ''
            || $session_role === ''
            || !hash_equals($manager_hash, $session_hash)
            || !hash_equals($manager_role, $session_role)
        ) {
            \EasyChat\Library\SessionBootstrap::destroy();
            echo json_encode(array('success' => false, 'error' => 'Учетная запись недоступна.'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
            return null;
        }

        return $manager;
    }

    private function passkeyPayload(): array {
        $raw = isset(\EasyChat\Library\RequestContext::post()['credential']) ? (string)\EasyChat\Library\RequestContext::post()['credential'] : '';
        $payload = json_decode($raw, true);
        if (!is_array($payload)) {
            throw new \RuntimeException('Браузер вернул некорректный passkey payload.');
        }
        return $payload;
    }

    private function safePasskeyError(\Throwable $e): string {
        $message = trim($e->getMessage());
        return $message !== '' ? $message : 'Не удалось подтвердить passkey.';
    }
}
