<?php

namespace App\FiveM\Adapters;

use App\FiveM\Contracts\FrameworkAdapter;
use App\FiveM\DTOs\CharacterData;
use App\FiveM\Models\QBCore\House;
use App\FiveM\Models\QBCore\Player;
use App\FiveM\Models\QBCore\Vehicle;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use RuntimeException;

/**
 * Targets the standard qb-core (and, by extension, qbx_core/Qbox — see
 * QboxAdapter) `players` table shape documented in
 * docs/database-analysis.md. If a server has renamed columns, override
 * them here rather than forking the class.
 */
class QBCoreAdapter implements FrameworkAdapter
{
    public function findByCitizenId(string $citizenId): ?CharacterData
    {
        $player = Player::query()->find($citizenId);

        return $player ? $this->toCharacterData($player) : null;
    }

    public function findByLicense(string $license): ?CharacterData
    {
        $player = Player::query()->where('license', $license)->first();

        return $player ? $this->toCharacterData($player) : null;
    }

    public function findAllByLicense(string $license): Collection
    {
        return Player::query()->where('license', $license)->get()
            ->map(fn (Player $player) => $this->toCharacterData($player));
    }

    public function paginateCharacters(int $perPage = 25, ?string $search = null): LengthAwarePaginator
    {
        return Player::query()
            ->when($search, fn ($query) => $query->where('name', 'like', "%{$search}%")
                ->orWhere('citizenid', 'like', "%{$search}%")
                ->orWhere('license', 'like', "%{$search}%"))
            ->orderBy('name')
            ->paginate($perPage)
            ->through(fn (Player $player) => $this->toCharacterData($player));
    }

    public function vehiclesForCitizen(string $citizenId): Collection
    {
        return Vehicle::query()->where('citizenid', $citizenId)->get()
            ->map(fn (Vehicle $vehicle) => [
                'id' => $vehicle->id,
                'model' => $vehicle->vehicle,
                'plate' => $vehicle->plate,
                'garage' => $vehicle->garage,
                'state' => (int) $vehicle->state,
            ]);
    }

    public function housesForCitizen(string $citizenId): Collection
    {
        return House::query()->where('citizenid', $citizenId)->get()
            ->map(fn (House $house) => [
                'id' => $house->id,
                'label' => $house->house,
                'garage' => $house->garage,
            ]);
    }

    public function allJobs(): Collection
    {
        return Player::query()
            ->get(['job'])
            ->pluck('job')
            ->filter()
            ->unique(fn (array $job) => $job['name'] ?? null)
            ->mapWithKeys(fn (array $job) => [
                ($job['name'] ?? 'unemployed') => [
                    'label' => $job['label'] ?? 'Unemployed',
                    'grades' => [],
                ],
            ]);
    }

    public function allGangs(): Collection
    {
        return Player::query()
            ->get(['gang'])
            ->pluck('gang')
            ->filter()
            ->unique(fn (?array $gang) => $gang['name'] ?? null)
            ->mapWithKeys(fn (array $gang) => [
                ($gang['name'] ?? 'none') => [
                    'label' => $gang['label'] ?? 'None',
                    'grades' => [],
                ],
            ]);
    }

    public function topByMoney(string $account = 'bank', int $limit = 20): Collection
    {
        abort_unless(in_array($account, ['cash', 'bank', 'crypto'], true), 400, 'Unsupported money account.');

        return Player::query()
            ->orderByRaw("CAST(JSON_UNQUOTE(JSON_EXTRACT(money, '$.\"{$account}\"')) AS UNSIGNED) DESC")
            ->limit($limit)
            ->get()
            ->map(fn (Player $player) => $this->toCharacterData($player));
    }

    public function adjustMoney(string $citizenId, string $account, int $delta): void
    {
        /** @var Player|null $player */
        $player = Player::query()->where('citizenid', $citizenId)->lockForUpdate()->first();

        if (! $player) {
            throw new RuntimeException("No player found for citizen ID [{$citizenId}].");
        }

        $money = $player->money ?? [];
        $current = (int) ($money[$account] ?? 0);
        $next = max(0, $current + $delta);

        if ($next === 0 && $current + $delta < 0) {
            throw new RuntimeException('Insufficient funds for this operation.');
        }

        $money[$account] = $next;
        $player->money = $money;
        $player->save();
    }

    public function paginateVehicles(int $perPage = 25, ?string $search = null): LengthAwarePaginator
    {
        return Vehicle::query()
            ->when($search, fn ($query) => $query->where('plate', 'like', "%{$search}%")
                ->orWhere('vehicle', 'like', "%{$search}%")
                ->orWhere('citizenid', 'like', "%{$search}%"))
            ->orderByDesc('id')
            ->paginate($perPage)
            ->through(fn (Vehicle $vehicle) => [
                'id' => $vehicle->id,
                'citizen_id' => $vehicle->citizenid,
                'model' => $vehicle->vehicle,
                'plate' => $vehicle->plate,
                'garage' => $vehicle->garage,
            ]);
    }

    public function deleteVehicle(int|string $vehicleId): void
    {
        Vehicle::query()->whereKey($vehicleId)->delete();
    }

    public function giveItem(string $citizenId, string $itemName, int $amount): void
    {
        /** @var Player|null $player */
        $player = Player::query()->where('citizenid', $citizenId)->lockForUpdate()->first();

        if (! $player) {
            throw new RuntimeException("No player found for citizen ID [{$citizenId}].");
        }

        $metadata = $player->metadata ?? [];
        $inventory = $metadata['inventory'] ?? [];

        $existingIndex = collect($inventory)->search(fn (array $item) => ($item['name'] ?? null) === $itemName);

        if ($existingIndex !== false) {
            $inventory[$existingIndex]['amount'] = (int) ($inventory[$existingIndex]['amount'] ?? 0) + $amount;
        } else {
            $inventory[] = [
                'name' => $itemName,
                'label' => $itemName,
                'amount' => $amount,
                'slot' => count($inventory) + 1,
            ];
        }

        $metadata['inventory'] = $inventory;
        $player->metadata = $metadata;
        $player->save();
    }

    protected function toCharacterData(Player $player): CharacterData
    {
        $charinfo = $player->charinfo ?? [];
        $job = $player->job ?? [];
        $gang = $player->gang ?? null;

        return new CharacterData(
            citizenId: $player->citizenid,
            license: $player->license,
            name: trim(($charinfo['firstname'] ?? '').' '.($charinfo['lastname'] ?? '')) ?: $player->name,
            money: $player->money ?? [],
            job: [
                'name' => $job['name'] ?? 'unemployed',
                'label' => $job['label'] ?? 'Unemployed',
                'grade' => (int) ($job['grade']['level'] ?? 0),
                'grade_label' => $job['grade']['name'] ?? '',
            ],
            gang: $gang ? [
                'name' => $gang['name'] ?? 'none',
                'label' => $gang['label'] ?? 'None',
                'grade' => (int) ($gang['grade']['level'] ?? 0),
                'grade_label' => $gang['grade']['name'] ?? '',
            ] : null,
            inventory: $this->normalizeInventory($player->metadata['inventory'] ?? []),
            metadata: $player->metadata ?? [],
        );
    }

    protected function normalizeInventory(array $rawInventory): array
    {
        return collect($rawInventory)
            ->filter()
            ->map(fn (array $item, $slot) => [
                'name' => $item['name'] ?? 'unknown',
                'label' => $item['label'] ?? $item['name'] ?? 'Unknown item',
                'amount' => (int) ($item['amount'] ?? 1),
                'slot' => (int) ($item['slot'] ?? $slot),
            ])
            ->values()
            ->all();
    }
}
