<?php

namespace App\Http\Controllers\Api\V1;

use App\Application\Services\IdempotencyService;
use App\Application\Services\InvoiceService;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreatePaymentRequest;
use App\Models\Invoice;
use App\Support\ApiResponse;
use App\Support\AtomicAmount;
use App\Support\PaymentQr;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use RuntimeException;

final class InvoiceController extends Controller
{
    public function store(CreatePaymentRequest $request, InvoiceService $invoices, IdempotencyService $idempotency)
    {
        $merchant = $request->attributes->get('merchant');
        $environment = $request->attributes->get('environment');
        $data = $request->validated();

        try {
            return $idempotency->execute(
                $request,
                $merchant,
                $environment,
                'invoices.create',
                fn (string $key) => ApiResponse::success($this->resource($invoices->create($merchant, $data, $environment, $key)), 201)
            );
        } catch (RuntimeException $e) {
            if ($e->getMessage() === 'UNSUPPORTED_CURRENCY') {
                return ApiResponse::error('UNSUPPORTED_CURRENCY', 'The requested asset or network is not enabled.', 422);
            }
            if ($e->getMessage() === 'CUSTOMER_NOT_FOUND') {
                return ApiResponse::error('CUSTOMER_NOT_FOUND', 'The requested customer does not exist or is not active.', 404);
            }
            throw $e;
        }
    }

    public function index(Request $request)
    {
        $query = Invoice::with(['asset', 'address', 'customer'])
            ->where('merchant_id', $request->attributes->get('merchant')->id)
            ->where('environment', $request->attributes->get('environment'))
            ->latest('id');

        foreach (['status', 'order_id'] as $field) {
            if ($request->filled($field)) {
                $query->where($field, $request->string($field));
            }
        }

        $page = $query->paginate(min(max($request->integer('per_page', 20), 1), 100));

        return ApiResponse::success(
            collect($page->items())->map(fn (Invoice $invoice) => $this->resource($invoice))->values(),
            200,
            ['pagination' => ['page' => $page->currentPage(), 'per_page' => $page->perPage(), 'total' => $page->total(), 'last_page' => $page->lastPage()]]
        );
    }

    public function show(Request $request, string $id)
    {
        $invoice = $this->findMerchantInvoice($request, $id);
        if (! $invoice) {
            return ApiResponse::error('INVOICE_NOT_FOUND', 'The requested invoice does not exist.', 404);
        }

        return ApiResponse::success($this->resource($invoice));
    }

    public function sendEmail(Request $request, string $id)
    {
        $invoice = $this->findMerchantInvoice($request, $id);
        if (! $invoice) {
            return ApiResponse::error('INVOICE_NOT_FOUND', 'The requested invoice does not exist.', 404);
        }

        $data = $request->validate(['email' => ['nullable', 'email', 'max:255']]);
        $recipient = $data['email'] ?? $invoice->payer_email;
        if (! $recipient) {
            return ApiResponse::error('VALIDATION_ERROR', 'Provide an email or create the invoice with payer.email.', 422);
        }

        Mail::raw($this->emailBody($invoice), function ($message) use ($recipient, $invoice) {
            $message->to($recipient)->subject('Invoice '.$invoice->public_id);
        });

        return ApiResponse::success(['invoice_id' => $invoice->public_id, 'email' => $recipient, 'status' => 'sent']);
    }

    public function pdf(Request $request, string $id)
    {
        $invoice = $this->findMerchantInvoice($request, $id);
        if (! $invoice) {
            return ApiResponse::error('INVOICE_NOT_FOUND', 'The requested invoice does not exist.', 404);
        }

        return response($this->pdfContent($invoice), 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'inline; filename="'.$invoice->public_id.'.pdf"',
        ]);
    }

    private function findMerchantInvoice(Request $request, string $id): ?Invoice
    {
        return Invoice::with(['asset', 'address', 'items', 'customer', 'invoiceTransactions.chainTransaction'])
            ->where('merchant_id', $request->attributes->get('merchant')->id)
            ->where('environment', $request->attributes->get('environment'))
            ->where('public_id', $id)
            ->first();
    }

    private function resource(Invoice $invoice): array
    {
        $invoice->loadMissing(['asset', 'address', 'items', 'customer']);
        $decimals = $invoice->asset->decimals;

        return [
            'invoice_id' => $invoice->public_id,
            'payment_id' => $invoice->public_id,
            'order_id' => $invoice->order_id,
            'price_amount' => number_format($invoice->price_amount_minor / 100, 2, '.', ''),
            'price_currency' => $invoice->price_currency,
            'pay_amount' => AtomicAmount::toDecimal((string) $invoice->quoted_pay_amount_atomic, $decimals),
            'actually_paid' => AtomicAmount::toDecimal((string) $invoice->paid_amount_atomic, $decimals),
            'pay_currency' => $invoice->asset->code,
            'network' => $invoice->asset->network,
            'pay_address' => $invoice->address?->address,
            'qr_code' => PaymentQr::forInvoice($invoice),
            'status' => $invoice->status,
            'customer_id' => $invoice->customer?->public_id,
            'payer' => ['id' => $invoice->payer_id, 'name' => $invoice->payer_name, 'email' => $invoice->payer_email],
            'items' => $invoice->items->map(fn ($item) => [
                'sku' => $item->sku,
                'name' => $item->name,
                'description' => $item->description,
                'quantity' => $item->quantity,
                'unit_amount' => number_format($item->unit_amount_minor / 100, 2, '.', ''),
                'currency' => $item->currency,
            ])->values(),
            'checkout_url' => rtrim(config('gateway.checkout_url'), '/').'/'.$invoice->public_id,
            'expires_at' => $invoice->expires_at?->utc()->toIso8601String(),
            'created_at' => $invoice->created_at?->utc()->toIso8601String(),
        ];
    }

    private function emailBody(Invoice $invoice): string
    {
        $data = $this->resource($invoice);

        return "Invoice {$data['invoice_id']}\nAmount: {$data['price_amount']} {$data['price_currency']}\nPay: {$data['pay_amount']} {$data['pay_currency']}\nAddress: {$data['pay_address']}\nCheckout: {$data['checkout_url']}";
    }

    private function pdfContent(Invoice $invoice): string
    {
        $data = $this->resource($invoice);
        $lines = [
            'Invoice '.$data['invoice_id'],
            'Order '.$data['order_id'],
            'Amount '.$data['price_amount'].' '.$data['price_currency'],
            'Pay '.$data['pay_amount'].' '.$data['pay_currency'],
            'Address '.$data['pay_address'],
            'Status '.$data['status'],
        ];
        $text = implode('\\n', array_map(fn ($line) => str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $line), $lines));
        $stream = "BT /F1 14 Tf 72 740 Td ($text) Tj ET";
        $objects = [
            "1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj",
            "2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj",
            "3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj",
            "4 0 obj << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> endobj",
            "5 0 obj << /Length ".strlen($stream)." >> stream\n$stream\nendstream endobj",
        ];
        $pdf = "%PDF-1.4\n";
        $offsets = [0];
        foreach ($objects as $object) {
            $offsets[] = strlen($pdf);
            $pdf .= $object."\n";
        }
        $xref = strlen($pdf);
        $pdf .= "xref\n0 ".(count($objects) + 1)."\n0000000000 65535 f \n";
        for ($i = 1; $i <= count($objects); $i++) {
            $pdf .= sprintf('%010d 00000 n ', $offsets[$i])."\n";
        }
        return $pdf."trailer << /Size ".(count($objects) + 1)." /Root 1 0 R >>\nstartxref\n$xref\n%%EOF";
    }
}
