<?php

namespace App\Application\Services;

use App\Domain\Blockchain\Contracts\BlockchainGateway;
use App\Domain\Blockchain\Data\AddressRequest;
use App\Domain\Blockchain\Data\DepositAddress;
use App\Models\Asset;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Merchant;
use App\Models\WalletAddress;
use App\Support\AtomicAmount;
use App\Support\Money;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use RuntimeException;
use Throwable;

final class InvoiceService
{
    public function __construct(private BlockchainGateway $gateway, private QuoteService $quotes, private OutboxService $outbox)
    {
    }

    public function create(Merchant $merchant, array $data, string $environment, string $idempotencyKey): Invoice
    {
        return DB::transaction(function () use ($merchant, $data, $environment, $idempotencyKey) {
            $asset = Asset::where('code', strtoupper($data['pay_currency']))->where('enabled', true)->first();
            if (! $asset) {
                throw new RuntimeException('UNSUPPORTED_CURRENCY');
            }

            $customer = null;
            if (! empty($data['customer_id'])) {
                $customer = Customer::query()
                    ->where('public_id', $data['customer_id'])
                    ->where('merchant_id', $merchant->id)
                    ->where('environment', $environment)
                    ->where('status', 'active')
                    ->first();

                if (! $customer) {
                    throw new RuntimeException('CUSTOMER_NOT_FOUND');
                }
            }

            $quote = $this->quotes->quote($data['price_amount'], $data['price_currency'], $asset);
            $minor = Money::toMinor((string) $data['price_amount'], 2);
            $payer = $data['payer'] ?? [];

            $invoice = Invoice::create([
                'public_id' => 'pay_'.Str::ulid(),
                'merchant_id' => $merchant->id,
                'environment' => $environment,
                'order_id' => $data['order_id'],
                'price_amount_minor' => $minor,
                'price_currency' => strtoupper($data['price_currency']),
                'pay_asset_id' => $asset->id,
                'quoted_pay_amount_atomic' => $quote['amount_atomic'],
                'paid_amount_atomic' => '0',
                'quote_rate' => $quote['rate'],
                'quote_expires_at' => $quote['expires_at'],
                'ipn_callback_url' => $data['ipn_callback_url'] ?? null,
                'success_url' => $data['success_url'] ?? null,
                'cancel_url' => $data['cancel_url'] ?? null,
                'status' => 'waiting',
                'expires_at' => now()->addMinutes(20),
                'metadata' => $data['metadata'] ?? [],
                'sub_merchant_id' => $data['sub_merchant_id'] ?? null,
                'customer_id' => $customer?->id,
                'payer_id' => $payer['id'] ?? $customer?->reference_id ?? $customer?->public_id,
                'payer_name' => $payer['name'] ?? $customer?->name,
                'payer_email' => $payer['email'] ?? $customer?->email,
                'accept_partial' => $data['accept_partial'] ?? true,
                'accept_overpayment' => $data['accept_overpayment'] ?? true,
                'underpayment_tolerance_atomic' => AtomicAmount::fromDecimal((string) ($data['underpayment_tolerance'] ?? '0'), $asset->decimals),
                'overpayment_tolerance_atomic' => AtomicAmount::fromDecimal((string) ($data['overpayment_tolerance'] ?? '0'), $asset->decimals),
                'late_payment_policy' => $data['late_payment_policy'] ?? 'manual_review',
                'locale' => $data['locale'] ?? 'en',
            ]);

            try {
                $deposit = $this->gateway->createDepositAddress(new AddressRequest($asset->network, $environment, $invoice->public_id));
            } catch (Throwable $e) {
                if ($environment !== 'sandbox') {
                    throw $e;
                }
                $deposit = new DepositAddress('TST'.strtoupper(substr(hash('sha256', $invoice->public_id), 0, 32)), 'sandbox_'.$invoice->public_id);
            }

            foreach ((array) ($data['items'] ?? []) as $item) {
                InvoiceItem::create([
                    'invoice_id' => $invoice->id,
                    'sku' => $item['sku'] ?? null,
                    'name' => $item['name'],
                    'description' => $item['description'] ?? null,
                    'quantity' => $item['quantity'],
                    'unit_amount_minor' => Money::toMinor((string) $item['unit_amount'], 2),
                    'currency' => $data['price_currency'],
                ]);
            }

            $this->outbox->record('invoice', $invoice->public_id, 'invoice.created', ['payment_id' => $invoice->public_id, 'merchant_id' => $merchant->id, 'idempotency_key' => $idempotencyKey], 'provider-webhooks');

            WalletAddress::create([
                'merchant_id' => $merchant->id,
                'invoice_id' => $invoice->id,
                'network' => $asset->network,
                'address' => $deposit->address,
                'provider_ref' => $deposit->providerReference,
                'environment' => $environment,
            ]);

            return $invoice->load(['asset', 'address', 'items', 'subMerchant', 'customer', 'invoiceTransactions.chainTransaction']);
        }, 5);
    }
}
