<?php
namespace Misuzu\Users;

use InvalidArgumentException;
use RuntimeException;
use Index\DateTime;
use Index\Data\DbStatementCache;
use Index\Data\DbTools;
use Index\Data\IDbConnection;
use Misuzu\Pagination;

class Bans {
    public const SEVERITY_MAX = 10;
    public const SEVERITY_MIN = -10;
    public const SEVERITY_DEFAULT = 0;

    private IDbConnection $dbConn;
    private DbStatementCache $cache;

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

    public function countBans(
        UserInfo|string|null $userInfo = null,
        ?bool $activeOnly = null
    ): int {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $hasUserInfo = $userInfo !== null;
        $hasActiveOnly = $activeOnly !== null;

        $args = 0;
        $query = 'SELECT COUNT(*) FROM msz_users_bans';
        if($hasActiveOnly) {
            ++$args;
            if($activeOnly)
                $query .= ' WHERE ban_expires IS NULL OR ban_expires > NOW()';
            else
                $query .= ' WHERE ban_expires <= NOW()';
        }
        if($hasUserInfo)
            $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');

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

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

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

        return $count;
    }

    public function getBans(
        UserInfo|string|null $userInfo = null,
        ?bool $activeOnly = null,
        ?bool $activeFirst = null,
        ?Pagination $pagination = null
    ): array {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        $hasUserInfo = $userInfo !== null;
        $hasActiveOnly = $activeOnly !== null;
        $hasActiveFirst = $activeFirst !== null;
        $hasPagination = $pagination !== null;

        $args = 0;
        $query = 'SELECT ban_id, user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, UNIX_TIMESTAMP(ban_created), UNIX_TIMESTAMP(ban_expires) FROM msz_users_bans';
        if($hasActiveOnly) {
            ++$args;
            if($activeOnly)
                $query .= ' WHERE ban_expires IS NULL OR ban_expires > NOW()';
            else
                $query .= ' WHERE ban_expires <= NOW()';
        }
        if($hasUserInfo)
            $query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
        $query .= ' ORDER BY ';
        if($hasActiveFirst)
            $query .= sprintf('ban_expires IS NULL %1$s, ban_expires %1$s', $activeFirst ? 'DESC' : 'ASC');
        else $query .= 'ban_created DESC';
        if($hasPagination)
            $query .= ' LIMIT ? OFFSET ?';

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

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

        while($result->next())
            $bans[] = new BanInfo($result);

        return $bans;
    }

    public function getBan(string $banId): BanInfo {
        $stmt = $this->cache->get('SELECT ban_id, user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, UNIX_TIMESTAMP(ban_created), UNIX_TIMESTAMP(ban_expires) FROM msz_users_bans WHERE ban_id = ?');
        $stmt->addParameter(1, $banId);
        $stmt->execute();

        $result = $stmt->getResult();
        if(!$result->next())
            throw new RuntimeException('No ban with ID $banId found.');

        return new BanInfo($result);
    }

    public function countActiveBans(
        UserInfo|string $userInfo,
        int $minimumSeverity = self::SEVERITY_MIN
    ): int {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        // orders by ban_expires descending with NULLs (permanent) first
        $stmt = $this->cache->get('SELECT COUNT(*) FROM msz_users_bans WHERE user_id = ? AND ban_severity >= ? AND (ban_expires IS NULL OR ban_expires > NOW()) ORDER BY ban_expires IS NULL DESC, ban_expires DESC');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $minimumSeverity);
        $stmt->execute();

        $result = $stmt->getResult();
        return $result->next() ? $result->getInteger(0) : 0;
    }

    public function tryGetActiveBan(
        UserInfo|string $userInfo,
        int $minimumSeverity = self::SEVERITY_MIN
    ): ?BanInfo {
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();

        // orders by ban_expires descending with NULLs (permanent) first
        $stmt = $this->cache->get('SELECT ban_id, user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, UNIX_TIMESTAMP(ban_created), UNIX_TIMESTAMP(ban_expires) FROM msz_users_bans WHERE user_id = ? AND ban_severity >= ? AND (ban_expires IS NULL OR ban_expires > NOW()) ORDER BY ban_expires IS NULL DESC, ban_expires DESC');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $minimumSeverity);
        $stmt->execute();

        $result = $stmt->getResult();
        return $result->next() ? new BanInfo($result) : null;
    }

    public function createBan(
        UserInfo|string $userInfo,
        DateTime|int|null $expires,
        string $publicReason,
        string $privateReason,
        int $severity = self::SEVERITY_DEFAULT,
        UserInfo|string|null $modInfo = null
    ): BanInfo {
        if($severity < self::SEVERITY_MIN || $severity > self::SEVERITY_MAX)
            throw new InvalidArgumentException('$severity may not be less than -10 or more than 10.');
        if($userInfo instanceof UserInfo)
            $userInfo = $userInfo->getId();
        if($modInfo instanceof UserInfo)
            $modInfo = $modInfo->getId();
        if($expires instanceof DateTime)
            $expires = $expires->getUnixTimeSeconds();

        $stmt = $this->cache->get('INSERT INTO msz_users_bans (user_id, mod_id, ban_severity, ban_reason_public, ban_reason_private, ban_expires) VALUES (?, ?, ?, ?, ?, FROM_UNIXTIME(?))');
        $stmt->addParameter(1, $userInfo);
        $stmt->addParameter(2, $modInfo);
        $stmt->addParameter(3, $severity);
        $stmt->addParameter(4, $publicReason);
        $stmt->addParameter(5, $privateReason);
        $stmt->addParameter(6, $expires);
        $stmt->execute();

        return $this->getBan((string)$this->dbConn->getLastInsertId());
    }

    public function deleteBans(BanInfo|string|array $banInfos): void {
        if(!is_array($banInfos))
            $banInfos = [$banInfos];
        elseif(empty($banInfos))
            return;

        $stmt = $this->cache->get(sprintf(
            'DELETE FROM msz_users_bans WHERE ban_id IN (%s)',
            DbTools::prepareListString($banInfos)
        ));

        $args = 0;
        foreach($banInfos as $banInfo) {
            if($banInfo instanceof BanInfo)
                $banInfo = $banInfo->getId();
            elseif(!is_string($banInfo))
                throw new InvalidArgumentException('$banInfos must be strings of instances of BanInfo.');

            $stmt->addParameter(++$args, $banInfo);
        }

        $stmt->execute();
    }
}
