<?php

declare(strict_types=1);

namespace Plugins\Larablog\Payment\Domain;

use App\Domain\Plugin\Hook;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Plugins\Larablog\Payment\Models\Entitlement;
use Plugins\Larablog\Payment\Models\Order;
use Plugins\Larablog\Payment\Models\OrderItem;
use Plugins\Larablog\Payment\Models\PaymentTransaction;
use RuntimeException;

class OrderService
{
    public function createOrder(
        User $user,
        string $productType,
        int $productId,
        string $title,
        string $amount,
        string $currency = 'CNY',
        string $gateway = 'stub',
    ): Order {
        return DB::transaction(function () use ($user, $productType, $productId, $title, $amount, $currency, $gateway): Order {
            if ($this->hasEntitlement((int) $user->id, $productType, $productId)) {
                throw new RuntimeException(
                    __('payment.errors.already_entitled', [
                        'product_type' => $productType,
                        'product_id' => $productId,
                    ])
                );
            }

            $pending = $this->pendingOrderFor((int) $user->id, $productType, $productId);
            if ($pending !== null) {
                $pending->forceFill([
                    'amount' => $amount,
                    'currency' => $currency,
                    'gateway' => $gateway,
                ])->save();

                $pending->items()->where('product_type', $productType)
                    ->where('product_id', $productId)
                    ->update(['title' => $title, 'amount' => $amount]);

                return $pending->load('items');
            }

            $order = Order::query()->create([
                'user_id' => $user->id,
                'status' => Order::STATUS_PENDING,
                'amount' => $amount,
                'currency' => $currency,
                'gateway' => $gateway,
            ]);

            OrderItem::query()->create([
                'order_id' => $order->id,
                'product_type' => $productType,
                'product_id' => $productId,
                'title' => $title,
                'amount' => $amount,
            ]);

            return $order->load('items');
        });
    }

    public function pendingOrderFor(int $userId, string $productType, int $productId): ?Order
    {
        return Order::query()
            ->where('user_id', $userId)
            ->where('status', Order::STATUS_PENDING)
            ->whereHas('items', function ($query) use ($productType, $productId): void {
                $query->where('product_type', $productType)
                    ->where('product_id', $productId);
            })
            ->latest('id')
            ->first();
    }

    /**
     * @param  array<string, mixed>  $transactionPayload
     */
    public function markPaid(Order $order, array $transactionPayload = []): Order
    {
        if ($order->isPaid()) {
            return $order->loadMissing(['items', 'transactions']);
        }

        if (! $order->isPending()) {
            throw new RuntimeException(
                __('payment.errors.order_not_payable', ['status' => $order->status])
            );
        }

        return DB::transaction(function () use ($order, $transactionPayload): Order {
            $order->refresh();

            if ($order->isPaid()) {
                return $order->loadMissing(['items', 'transactions']);
            }

            if (! $order->isPending()) {
                throw new RuntimeException(
                    __('payment.errors.order_not_payable', ['status' => $order->status])
                );
            }

            $order->loadMissing('items');

            foreach ($order->items as $item) {
                $ownedElsewhere = Entitlement::query()
                    ->active()
                    ->where('user_id', $order->user_id)
                    ->where('product_type', $item->product_type)
                    ->where('product_id', $item->product_id)
                    ->where(function ($query) use ($order): void {
                        $query->whereNull('source_order_id')
                            ->orWhere('source_order_id', '!=', $order->id);
                    })
                    ->exists();

                if ($ownedElsewhere) {
                    throw new RuntimeException(
                        __('payment.errors.already_entitled', [
                            'product_type' => $item->product_type,
                            'product_id' => $item->product_id,
                        ])
                    );
                }
            }

            $order->forceFill([
                'status' => Order::STATUS_PAID,
                'paid_at' => now(),
            ])->save();

            PaymentTransaction::query()->create([
                'order_id' => $order->id,
                'gateway' => (string) ($order->gateway ?: 'stub'),
                'external_id' => isset($transactionPayload['external_id'])
                    ? (string) $transactionPayload['external_id']
                    : null,
                'payload' => $transactionPayload !== [] ? $transactionPayload : ['source' => 'stub'],
                'status' => 'succeeded',
            ]);

            foreach ($order->items as $item) {
                Entitlement::query()->updateOrCreate(
                    [
                        'user_id' => $order->user_id,
                        'product_type' => $item->product_type,
                        'product_id' => $item->product_id,
                    ],
                    [
                        'source_order_id' => $order->id,
                        'granted_at' => now(),
                        'revoked_at' => null,
                        // expires_at is set by product plugins via order.paid when needed.
                        'expires_at' => null,
                    ],
                );
            }

            $order->load(['items', 'transactions']);

            Hook::dispatch('order.paid', $order);

            return $order;
        });
    }

    public function hasEntitlement(int $userId, string $productType, int $productId): bool
    {
        return Entitlement::query()
            ->active()
            ->where('user_id', $userId)
            ->where('product_type', $productType)
            ->where('product_id', $productId)
            ->exists();
    }

    public function hasActiveAny(int $userId, string $productType): bool
    {
        return Entitlement::query()
            ->active()
            ->where('user_id', $userId)
            ->where('product_type', $productType)
            ->exists();
    }
}
