<?php

namespace App\Services;

use App\Enums\InvoiceStatus;
use App\Enums\PaymentStatus;
use App\Exceptions\DocumentException;
use App\Models\Booking;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Quotation;
use App\Models\QuotationItem;
use App\Support\PropertyDate;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

class InvoiceService
{
    public function __construct(
        private readonly DocumentNumberService $documentNumbers,
        private readonly PricingService $pricing,
        private readonly SettingsService $settings,
        private readonly AuditService $audit,
        private readonly QuotationConversionService $conversionService,
        private readonly QuotationService $quotationService,
        private readonly PaymentStatusService $paymentStatus,
        private readonly BookingService $bookings,
    ) {}

    public function createFromBooking(Booking $booking): Invoice
    {
        $booking->loadMissing(['customer', 'items']);

        return DB::transaction(function () use ($booking): Invoice {
            $invoice = $this->createDirect([
                'customer_id' => $booking->customer_id,
                'booking_id' => $booking->id,
                'guest_count' => $booking->guest_count,
                'currency' => (string) $this->settings->get('currency', 'KES'),
                'payment_terms' => $booking->customer?->preferred_payment_terms,
                'items' => $booking->items->map(fn ($item) => [
                    'description' => $item->description,
                    'quantity' => $item->quantity,
                    'unit_price' => $item->unit_price,
                    'charge_basis' => $item->charge_basis->value,
                    'guest_count' => $item->guest_count,
                    'billable_days' => $item->billable_days,
                    'billable_hours' => $item->billable_hours,
                    'discount_amount' => $item->discount_amount,
                    'tax_rate' => $item->tax_rate,
                    'product_id' => $item->product_id,
                    'package_id' => $item->package_id,
                ])->all(),
            ], false);

            $this->audit->log('invoice.created_from_booking', $invoice, null, [
                'booking_id' => $booking->id,
            ]);

            return $invoice->fresh(['items']);
        });
    }

    public function createFromQuotation(Quotation $quotation): Invoice
    {
        return $this->conversionService->convert($quotation);
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function createDirect(array $data, bool $audit = true): Invoice
    {
        $depositPercent = (float) $this->settings->get('default_deposit_percentage', 30);

        $invoice = Invoice::query()->create([
            'invoice_number' => $this->documentNumbers->next('invoice'),
            'invoice_date' => $data['invoice_date'] ?? now()->toDateString(),
            'due_date' => $data['due_date'] ?? now()->addDays(30)->toDateString(),
            'quotation_id' => $data['quotation_id'] ?? null,
            'booking_id' => $data['booking_id'] ?? null,
            'customer_id' => $data['customer_id'],
            'guest_count' => $data['guest_count'] ?? 0,
            'currency' => $data['currency'] ?? (string) $this->settings->get('currency', 'KES'),
            'payment_terms' => $data['payment_terms'] ?? null,
            'notes' => $data['notes'] ?? null,
            'terms_and_conditions' => $data['terms_and_conditions'] ?? (string) $this->settings->get('invoice_terms', ''),
            'status' => InvoiceStatus::Draft,
            'amount_paid' => 0,
            'outstanding_balance' => 0,
            'created_by' => Auth::id(),
        ]);

        if (! empty($data['items']) && is_array($data['items'])) {
            $this->syncItems($invoice, $data['items']);
        }

        if (! empty($invoice->booking_id) || ! empty($data['items'])) {
            $this->recalculateTotals($invoice, $depositPercent);
            $invoice->outstanding_balance = $invoice->grand_total;
            $invoice->save();
        }

        if ($audit) {
            $this->audit->log('invoice.created', $invoice, null, $invoice->toArray());
        }

        return $invoice->fresh(['items']);
    }

    /**
     * @param  array<string, mixed>  $data
     */
    public function updateDraft(Invoice $invoice, array $data): Invoice
    {
        if (! $invoice->isEditable()) {
            throw new DocumentException('Only draft invoices can be updated.');
        }

        return DB::transaction(function () use ($invoice, $data): Invoice {
            $old = $invoice->toArray();

            $invoice->fill(collect($data)->only([
                'invoice_date',
                'due_date',
                'customer_id',
                'booking_id',
                'quotation_id',
                'guest_count',
                'currency',
                'payment_terms',
                'notes',
                'terms_and_conditions',
            ])->all());

            $invoice->save();

            if (array_key_exists('items', $data) && is_array($data['items'])) {
                $invoice->items()->delete();
                $this->syncItems($invoice, $data['items']);
            }

            $this->recalculateTotals($invoice);
            $invoice->outstanding_balance = $invoice->grand_total;
            $invoice->save();

            $this->audit->log('invoice.updated', $invoice->fresh(), $old, $invoice->fresh()->toArray());

            return $invoice->fresh(['items']);
        });
    }

    public function issue(Invoice $invoice): Invoice
    {
        if (! $invoice->isDraft()) {
            throw new DocumentException('Only draft invoices can be issued.');
        }

        return DB::transaction(function () use ($invoice): Invoice {
            $invoice = Invoice::query()->lockForUpdate()->findOrFail($invoice->id);

            $snapshots = $this->buildSnapshots($invoice);
            $invoice->fill($snapshots);
            $invoice->status = InvoiceStatus::Issued;
            $invoice->issued_at = now();
            $invoice->outstanding_balance = $invoice->grand_total;
            $invoice->save();

            $this->freezeItemSnapshots($invoice);

            $this->audit->log('invoice.issued', $invoice, null, [
                'invoice_number' => $invoice->invoice_number,
            ]);

            return $this->refreshOverdueStatus($invoice->fresh(['items', 'booking', 'payments']));
        });
    }

    public function void(Invoice $invoice, string $reason): Invoice
    {
        if ($invoice->isDraft()) {
            throw new DocumentException('Draft invoices should be deleted, not voided.');
        }

        if ($invoice->isVoid()) {
            throw new DocumentException('Invoice is already void.');
        }

        return DB::transaction(function () use ($invoice, $reason): Invoice {
            $invoice = Invoice::query()->lockForUpdate()->findOrFail($invoice->id);

            $invoice->update([
                'status' => InvoiceStatus::Void,
                'voided_at' => now(),
                'void_reason' => $reason,
                'voided_by' => Auth::id(),
            ]);

            $this->audit->log('invoice.voided', $invoice, null, null, $reason);

            return $invoice->fresh();
        });
    }

    /**
     * Manual status update from the invoice show page.
     */
    public function updateStatus(Invoice $invoice, InvoiceStatus $status, ?string $reason = null): Invoice
    {
        $invoice = $this->enforcePaymentDrivenStatus($invoice);

        if ($invoice->status === $status) {
            return $invoice;
        }

        $allowed = $this->allowedManualStatuses($invoice);
        if (! in_array($status, $allowed, true)) {
            throw new DocumentException('That status is not allowed for this invoice given its payment history.');
        }

        if ($status === InvoiceStatus::Void) {
            return $this->void($invoice, (string) $reason);
        }

        if ($status === InvoiceStatus::Issued && $invoice->isDraft()) {
            return $this->issue($invoice);
        }

        if ($invoice->isVoid()) {
            throw new DocumentException('Void invoices cannot change status.');
        }

        if ($invoice->isDraft() && $status !== InvoiceStatus::Issued) {
            throw new DocumentException('Draft invoices must be issued before other statuses can be applied.');
        }

        if ($status === InvoiceStatus::Draft && $invoice->isIssued()) {
            throw new DocumentException('Issued invoices cannot return to draft.');
        }

        return DB::transaction(function () use ($invoice, $status): Invoice {
            $invoice = Invoice::query()->lockForUpdate()->findOrFail($invoice->id);
            $old = $invoice->status;

            $invoice->update(['status' => $status]);

            $this->audit->log('invoice.status_updated', $invoice, ['status' => $old?->value], [
                'status' => $status->value,
            ]);

            $fresh = $invoice->fresh(['booking']);

            if (in_array($status, [InvoiceStatus::PartiallyPaid, InvoiceStatus::Paid], true)) {
                $this->paymentStatus->confirmLinkedBooking($fresh);
            }

            return $fresh;
        });
    }

    /**
     * Force Partially Paid / Paid from posted payment balances (unless Refunded/Void/Draft).
     */
    public function enforcePaymentDrivenStatus(Invoice $invoice): Invoice
    {
        if (in_array($invoice->status, [InvoiceStatus::Draft, InvoiceStatus::Void, InvoiceStatus::Refunded], true)) {
            return $invoice;
        }

        $postedCount = $invoice->payments()
            ->where('status', PaymentStatus::Posted)
            ->count();

        if ($postedCount === 0) {
            return $invoice;
        }

        $outstanding = (float) $invoice->outstanding_balance;
        $forced = $outstanding > 0.009
            ? InvoiceStatus::PartiallyPaid
            : InvoiceStatus::Paid;

        if ($invoice->status !== $forced) {
            $invoice->update(['status' => $forced]);
            $invoice->refresh();
        }

        if (in_array($forced, [InvoiceStatus::PartiallyPaid, InvoiceStatus::Paid], true)) {
            $this->paymentStatus->confirmLinkedBooking($invoice->loadMissing('booking'));
        }

        return $invoice;
    }

    /**
     * Statuses available in the invoice show dropdown.
     *
     * @return list<InvoiceStatus>
     */
    public function allowedManualStatuses(Invoice $invoice): array
    {
        if ($invoice->isDraft() || $invoice->isVoid()) {
            return [];
        }

        if ($invoice->status === InvoiceStatus::Refunded) {
            return [InvoiceStatus::Refunded];
        }

        $postedCount = $invoice->relationLoaded('payments')
            ? $invoice->payments->filter(fn ($payment) => $payment->status === PaymentStatus::Posted)->count()
            : $invoice->payments()->where('status', PaymentStatus::Posted)->count();

        if ($postedCount > 0) {
            $outstanding = (float) $invoice->outstanding_balance;

            if ($outstanding > 0.009) {
                return [InvoiceStatus::PartiallyPaid, InvoiceStatus::Refunded];
            }

            return [InvoiceStatus::Paid, InvoiceStatus::Refunded];
        }

        return [
            InvoiceStatus::Issued,
            InvoiceStatus::Due,
            InvoiceStatus::Overdue,
            InvoiceStatus::Void,
        ];
    }

    /**
     * @return array<string, string>
     */
    public function statusOptionsForShow(Invoice $invoice): array
    {
        $options = [];
        foreach ($this->allowedManualStatuses($invoice) as $status) {
            $options[$status->value] = $status->label();
        }

        return $options;
    }

    public function refreshOverdueStatus(Invoice $invoice): Invoice
    {
        if (in_array($invoice->status, [
            InvoiceStatus::Draft,
            InvoiceStatus::Void,
            InvoiceStatus::Refunded,
            InvoiceStatus::Paid,
            InvoiceStatus::PartiallyPaid,
        ], true)) {
            return $invoice;
        }

        $postedCount = $invoice->relationLoaded('payments')
            ? $invoice->payments->filter(fn ($payment) => $payment->status === PaymentStatus::Posted)->count()
            : $invoice->payments()->where('status', PaymentStatus::Posted)->count();

        if ($postedCount > 0) {
            return $this->paymentStatus->syncInvoice($invoice);
        }

        if ((float) $invoice->outstanding_balance <= 0 && (float) $invoice->amount_paid > 0) {
            return $invoice;
        }

        $target = $this->paymentStatus->unpaidAwaitingStatus($invoice);

        if ($invoice->status !== $target && in_array($invoice->status, [
            InvoiceStatus::Issued,
            InvoiceStatus::Due,
            InvoiceStatus::Overdue,
        ], true)) {
            $oldStatus = $invoice->status->value;
            $invoice->update(['status' => $target]);
            $invoice->refresh();

            $this->audit->log(
                'invoice.status_auto_updated',
                $invoice,
                ['status' => $oldStatus],
                ['status' => $target->value],
                match ($target) {
                    InvoiceStatus::Due => 'Booking confirmed; awaiting payment (not overdue).',
                    InvoiceStatus::Overdue => 'Invoice past due date with outstanding balance.',
                    InvoiceStatus::Issued => 'Awaiting payment; booking not confirmed or status reset.',
                    default => 'Automatic unpaid status refresh.',
                },
            );
        }

        return $invoice;
    }

    /**
     * When a booking is confirmed without payment, mark linked unpaid invoices as Due (if not overdue).
     */
    public function refreshStatusesForBooking(Booking $booking): void
    {
        $booking->loadMissing('invoices.payments');

        foreach ($booking->invoices as $invoice) {
            $this->refreshOverdueStatus($invoice);
        }
    }

    public function deleteDraft(Invoice $invoice): void
    {
        if (! $invoice->isDraft()) {
            throw new DocumentException('Only draft invoices can be deleted.');
        }

        if ($invoice->payments()->exists()) {
            throw new DocumentException('Invoices with payments cannot be deleted.');
        }

        DB::transaction(function () use ($invoice): void {
            $this->audit->log('invoice.deleted', $invoice, $invoice->toArray());
            $invoice->items()->delete();
            $invoice->forceDelete();
        });
    }

    public function recalculatePaidFromPayments(Invoice $invoice): Invoice
    {
        return $this->paymentStatus->syncInvoice($invoice);
    }

    public function createQuotationFromInvoice(Invoice $invoice): Quotation
    {
        $invoice->loadMissing('items');

        return DB::transaction(function () use ($invoice): Quotation {
            $quotation = $this->quotationService->createDraft([
                'quotation_number' => $this->documentNumbers->quotationFromInvoice($invoice->invoice_number),
                'customer_id' => $invoice->customer_id,
                'booking_id' => $invoice->booking_id,
                'guest_count' => $invoice->guest_count,
                'currency' => $invoice->currency,
                'payment_terms' => $invoice->payment_terms,
                'notes' => $invoice->notes,
                'terms_and_conditions' => $invoice->terms_and_conditions,
            ], false);

            foreach ($invoice->items as $item) {
                QuotationItem::query()->create([
                    'quotation_id' => $quotation->id,
                    'description' => $item->description,
                    'quantity' => $item->quantity,
                    'unit_price' => $item->unit_price,
                    'charge_basis' => $item->charge_basis,
                    'guest_count' => $item->guest_count,
                    'billable_days' => $item->billable_days,
                    'billable_hours' => $item->billable_hours,
                    'discount_amount' => $item->discount_amount,
                    'tax_rate' => $item->tax_rate,
                    'tax_amount' => $item->tax_amount,
                    'line_total' => $item->line_total,
                    'product_id' => $item->product_id,
                    'package_id' => $item->package_id,
                    'product_snapshot' => $item->product_snapshot,
                    'package_snapshot' => $item->package_snapshot,
                    'sort_order' => $item->sort_order,
                ]);
            }

            $this->quotationService->recalculate($quotation);

            $this->audit->log('quotation.created_from_invoice', $quotation, null, [
                'invoice_id' => $invoice->id,
            ]);

            return $quotation->fresh(['items']);
        });
    }

    /**
     * @return array<string, string|null>
     */
    private function buildSnapshots(Invoice $invoice): array
    {
        $invoice->loadMissing(['customer', 'booking.eventType', 'booking.spaces.space']);

        $quotation = new Quotation([
            'customer_id' => $invoice->customer_id,
            'booking_id' => $invoice->booking_id,
            'guest_count' => $invoice->guest_count,
        ]);
        $quotation->setRelation('customer', $invoice->customer);
        $quotation->setRelation('booking', $invoice->booking);

        return $this->quotationService->buildSnapshots($quotation);
    }

    /**
     * @param  list<array<string, mixed>>  $items
     */
    private function syncItems(Invoice $invoice, array $items): void
    {
        foreach ($items as $index => $itemData) {
            $line = $this->pricing->calculateLineAmount($itemData);
            InvoiceItem::query()->create([
                'invoice_id' => $invoice->id,
                'description' => $itemData['description'],
                'quantity' => $itemData['quantity'] ?? 1,
                'unit_price' => $itemData['unit_price'] ?? 0,
                'charge_basis' => $itemData['charge_basis'],
                'guest_count' => $itemData['guest_count'] ?? null,
                'billable_days' => $itemData['billable_days'] ?? null,
                'billable_hours' => $itemData['billable_hours'] ?? null,
                'discount_amount' => $line['discount'],
                'tax_rate' => $itemData['tax_rate'] ?? 0,
                'tax_amount' => $line['tax'],
                'line_total' => $line['total'],
                'product_id' => $itemData['product_id'] ?? null,
                'package_id' => $itemData['package_id'] ?? null,
                'sort_order' => $itemData['sort_order'] ?? $index,
            ]);
        }
    }

    private function recalculateTotals(Invoice $invoice, ?float $depositPercent = null): void
    {
        $invoice->loadMissing(['items', 'booking.spaces']);

        $depositPercent ??= (float) $this->settings->get('default_deposit_percentage', 30);
        $serviceChargeRate = (float) $this->settings->get('default_service_charge', 0);
        $defaultTax = (float) $this->settings->get('default_tax', 0);

        $lines = $invoice->items->map(fn (InvoiceItem $item) => [
            'unit_price' => $item->unit_price,
            'quantity' => $item->quantity,
            'guest_count' => $item->guest_count ?? 1,
            'billable_days' => $item->billable_days ?? 1,
            'billable_hours' => $item->billable_hours ?? 1,
            'discount_amount' => $item->discount_amount,
            'tax_rate' => $item->tax_rate,
            'charge_basis' => $item->charge_basis,
        ])->all();

        foreach ($invoice->booking?->spaces ?? [] as $space) {
            $lines[] = [
                'unit_price' => $space->unit_price,
                'quantity' => 1,
                'charge_basis' => $space->charge_basis,
                'guest_count' => $space->guest_count ?: $invoice->guest_count,
                'billable_days' => $space->billable_days ?? 1,
                'billable_hours' => $space->billable_hours ?? 1,
                'discount_amount' => 0,
                'tax_rate' => $defaultTax,
            ];
        }

        $totals = $this->pricing->calculateDocumentTotals($lines, [
            'service_charge_rate' => $serviceChargeRate,
            'deposit_percent' => $depositPercent,
            'amount_paid' => $invoice->amount_paid,
        ]);

        $invoice->update([
            'subtotal' => $totals['subtotal'],
            'discount_amount' => $totals['discount'],
            'service_charge_amount' => $totals['service_charge'],
            'tax_amount' => $totals['tax'],
            'grand_total' => $totals['grand_total'],
            'deposit_required' => $totals['deposit_required'],
            'outstanding_balance' => $totals['outstanding'],
        ]);

        $this->bookings->syncFinancialsFromInvoice($invoice->fresh());
    }

    private function freezeItemSnapshots(Invoice $invoice): void
    {
        foreach ($invoice->items as $item) {
            if ($item->product_id && $item->product_snapshot === null) {
                $product = $item->product;
                if ($product) {
                    $item->update([
                        'product_snapshot' => json_encode($product->only([
                            'id', 'name', 'code', 'default_price', 'default_tax_rate', 'default_charge_basis',
                        ]), JSON_THROW_ON_ERROR),
                    ]);
                }
            }
        }
    }
}
