<?php

declare(strict_types=1);

namespace Ekdos\Auth;

use Ekdos\Users\UserRepository;
use Redis;

/**
 * Anmeldung gegen die eigene Benutzertabelle.
 *
 * Die Bremse haengt an Benutzername und Quell-IP gemeinsam, damit weder ein
 * verteilter Angriff auf ein Konto noch ein einzelner Client durchprobieren kann.
 */
final readonly class AuthService
{
    public function __construct(
        private UserRepository $users,
        private SessionStore $sessions,
        private Redis $redis,
        private string $prefix,
        private int $maxAttempts,
        private int $window,
    ) {}

    /**
     * @return array{ok: true, session: Session}|array{ok: false, error: string, status: int}
     */
    public function login(string $username, string $password, string $clientIp): array
    {
        $username = trim($username);

        if ($username === '' || $password === '') {
            return ['ok' => false, 'error' => 'Benutzername und Passwort sind erforderlich.', 'status' => 400];
        }

        if ($this->isThrottled($username, $clientIp)) {
            return ['ok' => false, 'error' => 'Zu viele Anmeldeversuche. Bitte in einigen Minuten erneut versuchen.', 'status' => 429];
        }

        $user = $this->users->findByUsernameWithHash($username);

        // Auch bei unbekanntem Benutzer wird gehasht, damit die Antwortzeit nichts verraet.
        $hash = $user?->passwordHash ?? '$argon2id$v=19$m=65536,t=4,p=1$aaaaaaaaaaaaaaaaaaaaaa$aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
        $valid = password_verify($password, $hash);

        if ($user === null || !$valid) {
            $this->recordFailure($username, $clientIp);

            return ['ok' => false, 'error' => 'Benutzer oder Passwort ist nicht korrekt.', 'status' => 401];
        }

        if (!$user->isActive) {
            $this->recordFailure($username, $clientIp);

            return ['ok' => false, 'error' => 'Dieses Konto ist deaktiviert.', 'status' => 403];
        }

        $this->clearFailures($username, $clientIp);

        if (password_needs_rehash($hash, PASSWORD_ARGON2ID)) {
            $this->users->update($user->id, ['password_hash' => self::hash($password)]);
        }

        $this->users->touchLogin($user->id);

        return ['ok' => true, 'session' => $this->sessions->create($user)];
    }

    public function logout(string $sessionId): void
    {
        if ($sessionId !== '') {
            $this->sessions->destroy($sessionId);
        }
    }

    public static function hash(string $password): string
    {
        return password_hash($password, PASSWORD_ARGON2ID);
    }

    /** Mindestanforderung an ein Passwort. Bewusst schlicht: Laenge schlaegt Zeichenklassen. */
    public static function rejectWeakPassword(string $password): ?string
    {
        if (mb_strlen($password) < 12) {
            return 'Das Passwort muss mindestens 12 Zeichen lang sein.';
        }

        if (mb_strlen($password) > 200) {
            return 'Das Passwort darf höchstens 200 Zeichen lang sein.';
        }

        return null;
    }

    private function isThrottled(string $username, string $clientIp): bool
    {
        return (int) ($this->redis->get($this->throttleKey($username, $clientIp)) ?: 0) >= $this->maxAttempts;
    }

    private function recordFailure(string $username, string $clientIp): void
    {
        $key = $this->throttleKey($username, $clientIp);

        if ((int) $this->redis->incr($key) === 1) {
            $this->redis->expire($key, $this->window);
        }
    }

    private function clearFailures(string $username, string $clientIp): void
    {
        $this->redis->del($this->throttleKey($username, $clientIp));
    }

    private function throttleKey(string $username, string $clientIp): string
    {
        return $this->prefix . 'login:' . sha1(strtolower($username) . '|' . $clientIp);
    }
}
