<?php
namespace Misuzu\Auth;

use Index\Data\DbStatementCache;
use Index\Data\IDbConnection;
use Index\Net\IPAddress;
use Misuzu\ClientInfo;
use Misuzu\Pagination;
use Misuzu\Users\User;

class LoginAttempts {
    public const REMAINING_MAX = 5;
    public const REMAINING_WINDOW = 60 * 60;

    private DbStatementCache $cache;

    public function __construct(IDbConnection $dbConn) {
        $this->cache = new DbStatementCache($dbConn);
    }

    public function countAttempts(
        ?bool $success = null,
        User|string|null $userInfo = null,
        IPAddress|string|null $remoteAddr = null,
        TimeSpan|int|null $timeRange = null
    ): int {
        if($userInfo instanceof User)
            $userInfo = (string)$userInfo->getId();
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;
        if($timeRange instanceof TimeSpan)
            $timeRange = (int)$timeRange->totalSeconds();

        $hasSuccess = $success !== null;
        $hasUserInfo = $userInfo !== null;
        $hasRemoteAddr = $remoteAddr !== null;
        $hasTimeRange = $timeRange !== null;

        $args = 0;
        $query = 'SELECT COUNT(*) FROM msz_login_attempts';
        if($hasSuccess) {
            ++$args;
            $query .= sprintf(' WHERE attempt_success %s 0', $success ? '<>' : '=');
        }
        if($hasUserInfo)
            $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasRemoteAddr)
            $query .= sprintf(' %s attempt_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasTimeRange)
            $query .= sprintf(' %s attempt_created > NOW() - INTERVAL ? SECOND', ++$args > 1 ? 'AND' : 'WHERE');

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasRemoteAddr)
            $stmt->addParameter(++$args, $remoteAddr);
        if($hasTimeRange)
            $stmt->addParameter(++$args, $timeRange);
        $stmt->execute();

        $result = $stmt->getResult();
        $count = 0;

        if($result->next())
            $count = $result->getInteger(0);

        return $count;
    }

    public function countRemainingAttempts(IPAddress|string $remoteAddr): int {
        return self::REMAINING_MAX - $this->countAttempts(
            success: false,
            timeRange: self::REMAINING_WINDOW,
            remoteAddr: $remoteAddr
        );
    }

    public function getAttempts(
        ?bool $success = null,
        User|string|null $userInfo = null,
        IPAddress|string|null $remoteAddr = null,
        TimeSpan|int|null $timeRange = null,
        ?Pagination $pagination = null
    ): array {
        if($userInfo instanceof User)
            $userInfo = (string)$userInfo->getId();
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;
        if($timeRange instanceof TimeSpan)
            $timeRange = (int)$timeRange->totalSeconds();

        $hasSuccess = $success !== null;
        $hasUserInfo = $userInfo !== null;
        $hasRemoteAddr = $remoteAddr !== null;
        $hasTimeRange = $timeRange !== null;
        $hasPagination = $pagination !== null;

        $args = 0;
        $query = 'SELECT user_id, attempt_success, INET6_NTOA(attempt_ip), attempt_country, UNIX_TIMESTAMP(attempt_created), attempt_user_agent, attempt_client_info FROM msz_login_attempts';
        if($hasSuccess) {
            ++$args;
            $query .= sprintf(' WHERE attempt_success %s 0', $success ? '<>' : '=');
        }
        if($hasUserInfo)
            $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasRemoteAddr)
            $query .= sprintf(' %s attempt_ip = INET6_ATON(?)', ++$args > 1 ? 'AND' : 'WHERE');
        if($hasTimeRange)
            $query .= sprintf(' %s attempt_created > NOW() - INTERVAL ? SECOND', ++$args > 1 ? 'AND' : 'WHERE');
        $query .= ' ORDER BY attempt_created DESC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';

        $args = 0;
        $stmt = $this->cache->get($query);
        if($hasUserInfo)
            $stmt->addParameter(++$args, $userInfo);
        if($hasRemoteAddr)
            $stmt->addParameter(++$args, $remoteAddr);
        if($hasTimeRange)
            $stmt->addParameter(++$args, $timeRange);
        if($hasPagination) {
            $stmt->addParameter(++$args, $pagination->getRange());
            $stmt->addParameter(++$args, $pagination->getOffset());
        }
        $stmt->execute();

        $result = $stmt->getResult();
        $attempts = [];

        while($result->next())
            $attempts[] = new LoginAttemptInfo($result);

        return $attempts;
    }

    public function recordAttempt(
        bool $success,
        IPAddress|string $remoteAddr,
        string $countryCode,
        string $userAgentString,
        ?ClientInfo $clientInfo = null,
        User|string|null $userInfo = null
    ): void {
        if($remoteAddr instanceof IPAddress)
            $remoteAddr = (string)$remoteAddr;
        if($userInfo instanceof User)
            $userInfo = (string)$userInfo->getId();

        $hasUserInfo = $userInfo !== null;
        $clientInfo = json_encode($clientInfo ?? ClientInfo::parse($userAgentString));

        $stmt = $this->cache->get('INSERT INTO msz_login_attempts (user_id, attempt_success, attempt_ip, attempt_country, attempt_user_agent, attempt_client_info) VALUES (?, ?, INET6_ATON(?), ?, ?, ?)');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $success ? 1 : 0);
        $stmt->addParameter(3, $remoteAddr);
        $stmt->addParameter(4, $countryCode);
        $stmt->addParameter(5, $userAgentString);
        $stmt->addParameter(6, $clientInfo);
        $stmt->execute();
    }
}
