<?php

namespace App\Domains\Ticket\Services;

use App\Domains\Auth\Models\User;
use App\Domains\Ticket\Models\Ticket;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class ScannerTicketService
{
    /**
     * @param  array{q?: string|null, page?: int, per_page?: int}  $filters
     * @return LengthAwarePaginator<Ticket>
     */
    public function scannedBy(User $scanner, array $filters = []): LengthAwarePaginator
    {
        $search = trim((string) ($filters['q'] ?? ''));

        return $this->baseQuery()
            ->where('tenant_code', $scanner->tenant_codigo)
            ->where('scanner_user_id', $scanner->getKey())
            ->when($search !== '', function (Builder $query) use ($search): void {
                $usedAtDate = $this->parseSearchDate($search);

                $query->where(function (Builder $searchQuery) use ($search, $usedAtDate): void {
                    $searchQuery->where('ticket', 'like', "%{$search}%");

                    if (ctype_digit($search)) {
                        $searchQuery->orWhere('id', (int) $search);
                    }

                    if ($usedAtDate !== null) {
                        $searchQuery->orWhereDate('used_at', $usedAtDate);
                    }
                });
            })
            ->orderByDesc('used_at')
            ->orderByDesc('id')
            ->paginateFromRequest()
            ->withQueryString();
    }

    private function parseSearchDate(string $search): ?string
    {
        if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $search, $matches) === 1) {
            [$year, $month, $day] = array_map('intval', array_slice($matches, 1));

            if (checkdate($month, $day, $year)) {
                return sprintf('%04d-%02d-%02d', $year, $month, $day);
            }
        }

        if (preg_match('/^(\d{2})\/(\d{2})\/(\d{2}|\d{4})$/', $search, $matches) === 1) {
            $day = (int) $matches[1];
            $month = (int) $matches[2];
            $year = (int) $matches[3];
            $year = strlen($matches[3]) === 2 ? 2000 + $year : $year;

            if (checkdate($month, $day, $year)) {
                return sprintf('%04d-%02d-%02d', $year, $month, $day);
            }
        }

        return null;
    }

    public function detail(User $scanner, string $ticketUuid): Ticket
    {
        $query = $this->baseQuery()
            ->where('tenant_code', $scanner->tenant_codigo)
            ->where('ticket', $ticketUuid);

        if ($scanner->tenant()->firstOrFail()->requiresScannerCategoryValidation()) {
            $categoryIds = $this->scannerCategoryIds($scanner);

            $query->where(function (Builder $query) use ($scanner, $categoryIds): void {
                $query
                    ->where('scanner_user_id', $scanner->getKey())
                    ->orWhereHas(
                        'sourceCatalogItem',
                        fn (Builder $catalogItemQuery): Builder => $catalogItemQuery
                            ->whereIn('category_id', $categoryIds)
                    );
            });
        }

        return $query->firstOrFail();
    }

    public function scan(User $scanner, string $ticketUuid): Ticket
    {
        return DB::transaction(function () use ($scanner, $ticketUuid): Ticket {
            $ticket = $this->baseQuery()
                ->where('tenant_code', $scanner->tenant_codigo)
                ->where('ticket', $ticketUuid)
                ->lockForUpdate()
                ->firstOrFail();

            if (! $this->scannerCanScan($scanner, $ticket)) {
                throw ValidationException::withMessages([
                    'ticket' => __('api.ticket.scanner_category_forbidden'),
                ]);
            }

            if ($ticket->is_used) {
                throw ValidationException::withMessages([
                    'ticket' => __('api.ticket.already_scanned'),
                ]);
            }

            if (! $ticket->is_valid) {
                throw ValidationException::withMessages([
                    'ticket' => $ticket->is_expired
                        ? __('api.ticket.expired_for_scan')
                        : __('api.ticket.not_valid_for_scan'),
                ]);
            }

            $ticket->forceFill([
                'used_at' => now(),
                'scanner_user_id' => $scanner->getKey(),
            ])->save();

            return $ticket->refresh()->load($this->relations());
        });
    }

    /** @return Builder<Ticket> */
    private function baseQuery(): Builder
    {
        return Ticket::query()->with($this->relations());
    }

    /** @return array<int, string> */
    private function relations(): array
    {
        return [
            ...TicketValidityResolver::RELATIONS,
            ...TicketPresentationResolver::RELATIONS,
            'sourceCatalogItem.category',
            'sourceVariant.eventDate',
            'sourceVariant.catalogItem',
            'user',
        ];
    }

    /** @return array<int, int> */
    private function scannerCategoryIds(User $scanner): array
    {
        return $scanner->scanCategories()
            ->pluck('categorias.id')
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
    }

    private function scannerCanScan(User $scanner, Ticket $ticket): bool
    {
        if (! $scanner->tenant()->firstOrFail()->requiresScannerCategoryValidation()) {
            return true;
        }

        $categoryId = $ticket->sourceCatalogItem?->category_id;

        return $categoryId !== null
            && $scanner->scanCategories()
                ->where('categorias.id', $categoryId)
                ->exists();
    }
}
