<?php

namespace App\Domains\Cart\Services;

use App\Domains\Auth\Models\User;
use App\Domains\Cart\Models\Cart;
use App\Domains\Catalog\Exceptions\StockReservationExpiredException;
use App\Domains\Catalog\Services\ExpireStockReservationsService;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class CartService
{
    public function show(Tenant $tenant, Request $request): Cart
    {
        $resolvedIdentity = $this->resolveIdentity($request);

        if ($resolvedIdentity === null) {
            return $this->makeEmptyCart($tenant);
        }

        $cart = $this->resolveCart($tenant, $resolvedIdentity['identity']);

        if ($cart === null) {
            return $this->makeEmptyCart($tenant);
        }

        return $this->loadCart($cart, $tenant);
    }

    /**
     * @return array{cart: Cart, guest_token: ?string}
     */
    public function addItem(
        Tenant $tenant,
        Request $request,
        int $catalogItemId,
        ?int $variantId,
        int $quantity,
    ): array {
        $resolvedIdentity = $this->resolveIdentity($request, true);
        $identity = $resolvedIdentity['identity'];
        $cart = $this->findOrCreateCart($tenant, $identity);
        $cart->addItem($catalogItemId, $variantId, $quantity);

        return [
            'cart' => $this->loadCart($cart, $tenant),
            'guest_token' => $resolvedIdentity['generated_guest_token'],
        ];
    }

    public function updateItem(
        Tenant $tenant,
        Request $request,
        int $cartItemId,
        int $quantity,
        ?int $variantId,
        bool $updateVariant,
    ): Cart {
        if ($updateVariant && ! $tenant->cart_editing_policy->allowsVariantChanges()) {
            throw ValidationException::withMessages([
                'variant_id' => __('api.cart.variant_change_disabled'),
            ]);
        }

        if (! $updateVariant && ! $tenant->cart_editing_policy->allowsQuantityChanges()) {
            throw ValidationException::withMessages([
                'cantidad' => __('api.cart.editing_disabled'),
            ]);
        }

        $identity = $this->requireIdentity($request);
        $cart = $this->findCartOrFail($tenant, $identity);
        $cart->updateItem($cartItemId, $quantity, $variantId, $updateVariant);

        return $this->loadCart($cart, $tenant);
    }

    public function removeItem(Tenant $tenant, Request $request, int $cartItemId): Cart
    {
        if (! $tenant->cart_editing_policy->allowsRemoval()) {
            throw ValidationException::withMessages([
                'cart_item' => __('api.cart.editing_disabled'),
            ]);
        }

        $identity = $this->requireIdentity($request);
        $cart = $this->findCartOrFail($tenant, $identity);
        $cart->removeItem($cartItemId);

        return $this->loadCart($cart, $tenant);
    }

    public function makeGuestTokenCookie(string $guestToken): Cookie
    {
        $secure = (bool) config('session.secure');
        $sameSite = config('session.same_site');
        $partitioned = (bool) config('session.partitioned')
            || ($secure && strtolower((string) $sameSite) === 'none');

        return Cookie::create(
            name: 'guest_token',
            value: $guestToken,
            expire: now()->addDays(180),
            path: '/',
            domain: config('session.domain'),
            secure: $secure,
            httpOnly: true,
            raw: false,
            sameSite: $sameSite,
            partitioned: $partitioned,
        );
    }

    protected function makeEmptyCart(Tenant $tenant): Cart
    {
        $cart = new Cart([
            'tenant_codigo' => $tenant->codigo,
            'status' => Cart::STATUS_ACTIVE,
        ]);

        $cart->setRelation('items', collect());

        return $cart;
    }

    protected function loadCart(Cart $cart, Tenant $tenant): Cart
    {
        $relations = [
            'items.catalogItem.attachments',
            'items.catalogItem.inventory',
            'items.catalogItem.itemAttributes.attribute',
            'items.variant.attachments',
            'items.variant.inventory',
            'items.variant.definitions.itemAttribute.attribute.options',
            'items.variant.eventDates',
            'items.variant.eventDate',
        ];

        if ($tenant->cart_editing_policy->allowsVariantChanges()) {
            $relations = [
                ...$relations,
                'items.catalogItem.variants' => fn ($query) => $query->orderBy('id'),
                'items.catalogItem.variants.inventory',
                'items.catalogItem.variants.definitions' => fn ($query) => $query->orderBy('id'),
                'items.catalogItem.variants.definitions.itemAttribute.attribute.options',
                'items.catalogItem.variants.eventDates',
                'items.catalogItem.variants.eventDate',
            ];
        }

        return $cart->fresh()->load($relations);
    }

    /**
     * @return array{identity: array{user_id: ?int, guest_token: ?string}, generated_guest_token: ?string}|null
     */
    protected function resolveIdentity(Request $request, bool $generateGuestToken = false): ?array
    {
        $user = $request->user() ?? Auth::guard('sanctum')->user();

        if ($user instanceof User) {
            return [
                'identity' => [
                    'user_id' => $user->getKey(),
                    'guest_token' => null,
                ],
                'generated_guest_token' => null,
            ];
        }

        $guestToken = $request->cookie('guest_token');

        if (is_string($guestToken) && $guestToken !== '') {
            return [
                'identity' => [
                    'user_id' => null,
                    'guest_token' => $guestToken,
                ],
                'generated_guest_token' => null,
            ];
        }

        if (! $generateGuestToken) {
            return null;
        }

        $generatedGuestToken = (string) Str::uuid();

        return [
            'identity' => [
                'user_id' => null,
                'guest_token' => $generatedGuestToken,
            ],
            'generated_guest_token' => $generatedGuestToken,
        ];
    }

    /**
     * @return array{user_id: ?int, guest_token: ?string}
     */
    protected function requireIdentity(Request $request): array
    {
        $resolvedIdentity = $this->resolveIdentity($request);

        if ($resolvedIdentity === null) {
            throw new NotFoundHttpException('Cart not found.');
        }

        return $resolvedIdentity['identity'];
    }

    /**
     * @param  array{user_id: ?int, guest_token: ?string}  $identity
     */
    protected function findCart(Tenant $tenant, array $identity): ?Cart
    {
        return Cart::query()
            ->where('tenant_codigo', $tenant->codigo)
            ->where('origin', Cart::ORIGIN_USER)
            ->whereIn('status', [Cart::STATUS_ACTIVE, Cart::STATUS_EXPIRED])
            ->when(
                $identity['user_id'] !== null,
                fn ($query) => $query->where('user_id', $identity['user_id']),
                fn ($query) => $query->where('guest_token', $identity['guest_token']),
            )
            ->orderByRaw('CASE WHEN status = ? THEN 0 ELSE 1 END', [Cart::STATUS_ACTIVE])
            ->first();
    }

    /**
     * @param  array{user_id: ?int, guest_token: ?string}  $identity
     */
    protected function findCartOrFail(Tenant $tenant, array $identity): Cart
    {
        $cart = $this->resolveCart($tenant, $identity, replaceExpired: false);

        if ($cart === null) {
            throw new NotFoundHttpException('Cart not found.');
        }

        return $cart;
    }

    /**
     * @param  array{user_id: ?int, guest_token: ?string}  $identity
     */
    protected function findOrCreateCart(Tenant $tenant, array $identity): Cart
    {
        return $this->resolveCart($tenant, $identity)
            ?? $this->createCart($tenant, $identity);
    }

    /**
     * @param  array{user_id: ?int, guest_token: ?string}  $identity
     */
    protected function resolveCart(
        Tenant $tenant,
        array $identity,
        bool $replaceExpired = true,
    ): ?Cart {
        $cart = $this->findCart($tenant, $identity);

        if ($cart?->status === Cart::STATUS_ACTIVE
            && $cart->current_stock_reservation_id !== null
            && app(ExpireStockReservationsService::class)
                ->expireIfOverdue($cart->current_stock_reservation_id)) {
            $cart = $this->findCart($tenant, $identity);
        }

        if ($cart?->status === Cart::STATUS_EXPIRED) {
            if (! $replaceExpired) {
                throw new StockReservationExpiredException;
            }

            return $this->replaceExpiredCart($cart, $tenant, $identity);
        }

        if ($cart !== null) {
            return $cart;
        }

        return null;
    }

    /**
     * @param  array{user_id: ?int, guest_token: ?string}  $identity
     */
    protected function replaceExpiredCart(Cart $expiredCart, Tenant $tenant, array $identity): Cart
    {
        return DB::transaction(function () use ($expiredCart, $tenant, $identity): Cart {
            /** @var Cart|null $lockedCart */
            $lockedCart = Cart::query()->lockForUpdate()->find($expiredCart->getKey());

            if ($lockedCart?->status === Cart::STATUS_EXPIRED) {
                $lockedCart->update([
                    'status' => Cart::STATUS_ABANDONED,
                    'current_purchase_id' => null,
                ]);
            }

            return $this->findCart($tenant, $identity)
                ?? $this->createCart($tenant, $identity);
        });
    }

    /**
     * @param  array{user_id: ?int, guest_token: ?string}  $identity
     */
    protected function createCart(Tenant $tenant, array $identity): Cart
    {
        $attributes = [
            'tenant_codigo' => $tenant->codigo,
            'status' => Cart::STATUS_ACTIVE,
            'origin' => Cart::ORIGIN_USER,
        ];

        if ($identity['user_id'] !== null) {
            $attributes['user_id'] = $identity['user_id'];
        } else {
            $attributes['guest_token'] = $identity['guest_token'];
        }

        return Cart::query()->firstOrCreate($attributes);
    }
}
