<?php

namespace App\Services;

use App\Models\GameAccountUser;
use App\Support\WowData;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class AzerothCoreAccountService
{
    public const ACCOUNT_TAB_PLAYERS = 'players';

    public const ACCOUNT_TAB_BOTS = 'bots';

    public function __construct(private readonly AzerothCoreSrpService $srp)
    {
    }

    public function findUserById(int $accountId): ?GameAccountUser
    {
        $record = $this->authConnection()
            ->table('account')
            ->selectRaw('id, username, email, reg_mail, locked, last_login, joindate, HEX(salt) as salt_hex, HEX(verifier) as verifier_hex')
            ->where('id', $accountId)
            ->first();

        if (! $record) {
            return null;
        }

        $gmLevel = $this->accessLevelsFor([$accountId])[$accountId] ?? 0;

        return $this->mapUser($record, $gmLevel);
    }

    public function findUserByUsername(string $username): ?GameAccountUser
    {
        $record = $this->authConnection()
            ->table('account')
            ->selectRaw('id, username, email, reg_mail, locked, last_login, joindate, HEX(salt) as salt_hex, HEX(verifier) as verifier_hex')
            ->where('username', $this->srp->normalizeUsername($username))
            ->first();

        if (! $record) {
            return null;
        }

        $gmLevel = $this->accessLevelsFor([(int) $record->id])[(int) $record->id] ?? 0;

        return $this->mapUser($record, $gmLevel);
    }

    public function validatePassword(GameAccountUser $user, string $password): bool
    {
        return $this->srp->credentialsMatch($user->username, $password, $user->saltHex, $user->verifierHex);
    }

    public function updatePassword(GameAccountUser $user, string $newPassword): void
    {
        [$saltHex, $verifierHex] = $this->srp->makeRegistrationData($user->username, $newPassword);

        $this->authConnection()->update(
            'UPDATE account SET salt = UNHEX(?), verifier = UNHEX(?), session_key = NULL, failed_logins = 0 WHERE id = ?',
            [$saltHex, $verifierHex, $user->id],
        );
    }

    /**
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    public function listAccounts(?string $search = null, string $accountTab = self::ACCOUNT_TAB_PLAYERS): Collection
    {
        $query = $this->baseAccountsQuery($search);

        $this->applyAccountTabFilter($query, $accountTab);

        $accounts = $query->get();
        $accountIds = $accounts->pluck('id')->map(fn ($id): int => (int) $id)->all();
        $accessLevels = $this->accessLevelsFor($accountIds);
        $characters = $this->charactersForAccountIds($accountIds);

        return $accounts->map(function (object $account) use ($accessLevels, $characters): array {
            $accountId = (int) $account->id;
            $gmLevel = $accessLevels[$accountId] ?? 0;

            return [
                'id' => $accountId,
                'username' => $account->username,
                'email' => $account->email,
                'reg_mail' => $account->reg_mail,
                'gm_level' => $gmLevel,
                'access_label' => WowData::securityLevelName($gmLevel),
                'locked' => (bool) $account->locked,
                'online' => (bool) $account->online,
                'joined_at' => $this->formatDate($account->joindate),
                'last_login_at' => $this->formatDate($account->last_login),
                'characters' => $characters->get($accountId, collect()),
            ];
        });
    }

    /**
     * @return array{players: int, bots: int}
     */
    public function accountTypeStats(?string $search = null): array
    {
        $playersQuery = $this->baseAccountsQuery($search);
        $botsQuery = $this->baseAccountsQuery($search);

        $this->applyAccountTabFilter($playersQuery, self::ACCOUNT_TAB_PLAYERS);
        $this->applyAccountTabFilter($botsQuery, self::ACCOUNT_TAB_BOTS);

        return [
            self::ACCOUNT_TAB_PLAYERS => $playersQuery->count(),
            self::ACCOUNT_TAB_BOTS => $botsQuery->count(),
        ];
    }

    /**
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    public function charactersForAccount(int $accountId): Collection
    {
        return $this->charactersForAccountIds([$accountId])->get($accountId, collect());
    }

    public function updateAccessLevel(int $accountId, int $gmLevel): void
    {
        $realmId = $this->targetRealmId();

        $this->authConnection()->statement(
            'INSERT INTO account_access (id, gmlevel, RealmID, comment) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE gmlevel = VALUES(gmlevel), comment = VALUES(comment)',
            [
                $accountId,
                $gmLevel,
                $realmId,
                config('moonwell.portal.access_comment'),
            ],
        );
    }

    /**
     * @param  array<int, int>  $accountIds
     * @return \Illuminate\Support\Collection<int, \Illuminate\Support\Collection<int, array<string, mixed>>>
     */
    private function charactersForAccountIds(array $accountIds): Collection
    {
        if ($accountIds === []) {
            return collect();
        }

        return $this->charactersConnection()
            ->table('characters')
            ->select('guid', 'account', 'name', 'race', 'class', 'gender', 'level', 'online', 'money', 'totaltime')
            ->whereIn('account', $accountIds)
            ->orderBy('account')
            ->orderByDesc('level')
            ->orderBy('name')
            ->get()
            ->map(function (object $character): array {
                return [
                    'guid' => (int) $character->guid,
                    'account' => (int) $character->account,
                    'name' => $character->name,
                    'level' => (int) $character->level,
                    'race' => WowData::raceName((int) $character->race),
                    'class' => WowData::className((int) $character->class),
                    'gender' => WowData::genderName((int) $character->gender),
                    'online' => (bool) $character->online,
                    'money' => WowData::formatMoney((int) $character->money),
                    'played' => WowData::formatPlayedTime((int) $character->totaltime),
                ];
            })
            ->groupBy('account')
            ->map(fn (Collection $group): Collection => $group->values());
    }

    /**
     * @param  array<int, int>  $accountIds
     * @return array<int, int>
     */
    private function accessLevelsFor(array $accountIds): array
    {
        if ($accountIds === []) {
            return [];
        }

        $targetRealmId = $this->targetRealmId();
        $realmIds = array_values(array_unique([$targetRealmId, -1]));

        return $this->authConnection()
            ->table('account_access')
            ->select('id', 'gmlevel', 'RealmID')
            ->whereIn('id', $accountIds)
            ->whereIn('RealmID', $realmIds)
            ->get()
            ->groupBy('id')
            ->map(function (Collection $rows) use ($targetRealmId): int {
                $exactMatch = $rows->firstWhere('RealmID', $targetRealmId);

                return (int) ($exactMatch->gmlevel ?? $rows->firstWhere('RealmID', -1)?->gmlevel ?? 0);
            })
            ->all();
    }

    private function mapUser(object $record, int $gmLevel): GameAccountUser
    {
        return new GameAccountUser(
            id: (int) $record->id,
            username: $record->username,
            email: $record->email,
            regMail: $record->reg_mail,
            gmLevel: $gmLevel,
            locked: (bool) $record->locked,
            lastLoginAt: $record->last_login,
            joinedAt: $record->joindate,
            saltHex: strtoupper($record->salt_hex),
            verifierHex: strtoupper($record->verifier_hex),
        );
    }

    private function formatDate(?string $value): string
    {
        return $value ? Carbon::parse($value)->format('d.m.Y H:i') : '—';
    }

    private function baseAccountsQuery(?string $search = null): Builder
    {
        $query = $this->authConnection()
            ->table('account')
            ->select('id', 'username', 'email', 'reg_mail', 'locked', 'last_login', 'joindate', 'online')
            ->orderByDesc('id');

        if ($search !== null && trim($search) !== '') {
            $normalizedSearch = $this->srp->normalizeUsername($search);

            $query->where(function ($builder) use ($normalizedSearch): void {
                $builder
                    ->where('username', 'like', "%{$normalizedSearch}%")
                    ->orWhere('email', 'like', "%{$normalizedSearch}%")
                    ->orWhere('reg_mail', 'like', "%{$normalizedSearch}%");
            });
        }

        return $query;
    }

    private function applyAccountTabFilter(Builder $query, string $accountTab): void
    {
        $botPrefix = $this->botAccountPrefix();

        if ($botPrefix === '') {
            return;
        }

        $pattern = $botPrefix.'%';

        if ($accountTab === self::ACCOUNT_TAB_BOTS) {
            $query->where('username', 'like', $pattern);

            return;
        }

        $query->where('username', 'not like', $pattern);
    }

    private function botAccountPrefix(): string
    {
        return strtoupper(trim((string) config('moonwell.portal.bot_account_prefix', 'RNDBOT')));
    }

    private function targetRealmId(): int
    {
        return (int) config('moonwell.registration.realm_id', -1);
    }

    public function onlinePlayerCount(): ?int
    {
        try {
            return (int) $this->charactersConnection()
                ->table('characters')
                ->where('online', 1)
                ->count();
        } catch (\Throwable $exception) {
            report($exception);

            return null;
        }
    }

    private function authConnection()
    {
        return DB::connection(config('moonwell.auth_connection'));
    }

    private function charactersConnection()
    {
        return DB::connection(config('moonwell.characters_connection'));
    }
}
