<?php

namespace App\Services;

use App\Models\MexcoCredit;
use App\Models\MexcoEventLicense;
use App\Models\MexcoInvoice;
use App\Models\MexcoInvoiceItem;
use App\Models\MexcoPayment;
use App\Models\MexcoPlan;
use App\Models\MexcoPromoCode;
use App\Models\MeetupEvent;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

class BillingService
{
    public function __construct(private SettingsService $settings) {}

    public function createDraftInvoiceForLicense(MeetupEvent $event, MexcoEventLicense $license): ?MexcoInvoice
    {
        if ($license->is_campus_sponsored || $license->price_cents <= 0) {
            return null;
        }

        $plan = $license->plan ?? MexcoPlan::query()->find($license->plan_id);
        if (! $plan || $plan->is_custom_pricing) {
            return null;
        }

        if (! $event->organisation_id) {
            return null;
        }

        $existing = MexcoInvoice::query()
            ->where('event_license_id', $license->id)
            ->whereNotIn('status', [MexcoInvoice::STATUS_CANCELLED])
            ->first();

        if ($existing) {
            if ($existing->status === MexcoInvoice::STATUS_DRAFT) {
                return $this->syncDraftInvoiceWithLicense($existing, $license);
            }

            return $existing;
        }

        return DB::transaction(function () use ($event, $license, $plan) {
            $invoice = MexcoInvoice::query()->create([
                'invoice_number' => $this->generateInvoiceNumber(),
                'organisation_id' => $event->organisation_id,
                'event_id' => $event->id,
                'event_license_id' => $license->id,
                'currency' => $license->currency ?? $plan->currency ?? 'MYR',
                'status' => MexcoInvoice::STATUS_DRAFT,
                'metadata' => [
                    'plan_code' => $plan->code,
                ],
            ]);

            MexcoInvoiceItem::query()->create([
                'invoice_id' => $invoice->id,
                'item_type' => MexcoInvoiceItem::TYPE_PLAN,
                'item_id' => $plan->id,
                'description' => $plan->name.' event license',
                'quantity' => 1,
                'unit_price_cents' => $license->price_cents,
                'total_cents' => $license->price_cents,
            ]);

            return $this->syncDraftInvoiceWithLicense($invoice->fresh(['items']), $license);
        });
    }

    public function syncDraftInvoiceWithLicense(MexcoInvoice $invoice, MexcoEventLicense $license): MexcoInvoice
    {
        if ($invoice->status !== MexcoInvoice::STATUS_DRAFT) {
            return $invoice;
        }

        return DB::transaction(function () use ($invoice, $license) {
            $invoice->items()
                ->where('item_type', MexcoInvoiceItem::TYPE_ADDON)
                ->delete();

            $license->loadMissing('eventAddons.addon');
            foreach ($license->eventAddons as $eventAddon) {
                if (! in_array($eventAddon->status, ['active', 'pending'], true) || $eventAddon->price_cents <= 0) {
                    continue;
                }

                MexcoInvoiceItem::query()->create([
                    'invoice_id' => $invoice->id,
                    'item_type' => MexcoInvoiceItem::TYPE_ADDON,
                    'item_id' => $eventAddon->addon_id,
                    'description' => ($eventAddon->addon?->name ?? 'Add-on').' add-on',
                    'quantity' => 1,
                    'unit_price_cents' => $eventAddon->price_cents,
                    'total_cents' => $eventAddon->price_cents,
                ]);
            }

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

    public function issueInvoice(MexcoInvoice $invoice): MexcoInvoice
    {
        $this->assertInvoiceStatus($invoice, [MexcoInvoice::STATUS_DRAFT]);

        $invoice->update([
            'status' => MexcoInvoice::STATUS_PENDING,
            'issued_at' => now(),
            'due_at' => now()->addDays((int) $this->settings->get('business', 'invoice_due_days', config('mexco.billing.default_due_days', 14))),
        ]);

        return $invoice->fresh(['items', 'organisation', 'event', 'license.plan']);
    }

    public function cancelInvoice(MexcoInvoice $invoice): MexcoInvoice
    {
        if ($invoice->status === MexcoInvoice::STATUS_PAID) {
            throw ValidationException::withMessages([
                'invoice' => 'Paid invoices cannot be cancelled.',
            ]);
        }

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

        return $invoice->fresh();
    }

    public function applyPromoCode(MexcoInvoice $invoice, string $code): MexcoInvoice
    {
        $this->assertInvoiceStatus($invoice, [
            MexcoInvoice::STATUS_DRAFT,
            MexcoInvoice::STATUS_PENDING,
        ]);

        $promo = MexcoPromoCode::query()
            ->where('code', strtoupper(trim($code)))
            ->first();

        if (! $promo || ! $promo->isValidNow()) {
            throw ValidationException::withMessages([
                'promo_code' => 'Promo code is invalid or expired.',
            ]);
        }

        return DB::transaction(function () use ($invoice, $promo) {
            $invoice->items()
                ->where('item_type', MexcoInvoiceItem::TYPE_DISCOUNT)
                ->delete();

            $discount = $this->calculatePromoDiscount($invoice, $promo);

            if ($discount > 0) {
                MexcoInvoiceItem::query()->create([
                    'invoice_id' => $invoice->id,
                    'item_type' => MexcoInvoiceItem::TYPE_DISCOUNT,
                    'item_id' => $promo->id,
                    'description' => 'Promo: '.$promo->code.' ('.$promo->discountLabel().')',
                    'quantity' => 1,
                    'unit_price_cents' => -$discount,
                    'total_cents' => -$discount,
                ]);
            }

            $invoice->update(['promo_code_id' => $promo->id]);

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

    public function applyOrganisationCredit(MexcoInvoice $invoice, ?int $amountCents = null): MexcoInvoice
    {
        $this->assertInvoiceStatus($invoice, [
            MexcoInvoice::STATUS_DRAFT,
            MexcoInvoice::STATUS_PENDING,
        ]);

        $available = $this->availableCreditCents($invoice->organisation_id);
        if ($available <= 0) {
            throw ValidationException::withMessages([
                'credit' => 'No organisation credit available.',
            ]);
        }

        $invoice->loadMissing('items');
        $subtotal = $invoice->items
            ->whereIn('item_type', [MexcoInvoiceItem::TYPE_PLAN, MexcoInvoiceItem::TYPE_ADDON])
            ->sum('total_cents');
        $existingDiscount = abs($invoice->items
            ->whereIn('item_type', [MexcoInvoiceItem::TYPE_DISCOUNT, MexcoInvoiceItem::TYPE_CREDIT])
            ->sum('total_cents'));
        $remaining = max(0, $subtotal - $existingDiscount);

        $apply = min($amountCents ?? $available, $available, $remaining);
        if ($apply <= 0) {
            throw ValidationException::withMessages([
                'credit' => 'Nothing left to apply credit against.',
            ]);
        }

        return DB::transaction(function () use ($invoice, $apply) {
            $invoice->items()
                ->where('item_type', MexcoInvoiceItem::TYPE_CREDIT)
                ->delete();

            MexcoInvoiceItem::query()->create([
                'invoice_id' => $invoice->id,
                'item_type' => MexcoInvoiceItem::TYPE_CREDIT,
                'item_id' => null,
                'description' => 'Organisation credit applied',
                'quantity' => 1,
                'unit_price_cents' => -$apply,
                'total_cents' => -$apply,
            ]);

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

    public function recordManualPayment(
        MexcoInvoice $invoice,
        int $amountCents,
        ?string $reference = null,
        ?string $notes = null,
    ): MexcoPayment {
        $this->assertInvoiceStatus($invoice, [
            MexcoInvoice::STATUS_PENDING,
            MexcoInvoice::STATUS_PAID,
        ]);

        if ($amountCents <= 0) {
            throw ValidationException::withMessages([
                'amount' => 'Payment amount must be greater than zero.',
            ]);
        }

        return DB::transaction(function () use ($invoice, $amountCents, $reference, $notes) {
            $payment = MexcoPayment::query()->create([
                'invoice_id' => $invoice->id,
                'amount_cents' => $amountCents,
                'currency' => $invoice->currency,
                'status' => MexcoPayment::STATUS_PAID,
                'method' => MexcoPayment::METHOD_MANUAL,
                'gateway_reference' => $reference,
                'paid_at' => now(),
                'metadata' => $notes ? ['notes' => $notes] : null,
            ]);

            $this->syncInvoicePaymentStatus($invoice->fresh(['payments', 'items', 'promoCode']));

            return $payment;
        });
    }

    public function syncInvoicePaymentStatus(MexcoInvoice $invoice): MexcoInvoice
    {
        $paid = $invoice->amountPaidCents();

        if ($paid >= $invoice->total_cents && $invoice->total_cents > 0) {
            if ($invoice->promo_code_id && $invoice->status !== MexcoInvoice::STATUS_PAID) {
                MexcoPromoCode::query()
                    ->where('id', $invoice->promo_code_id)
                    ->increment('used_count');
            }

            $this->consumeCreditFromInvoice($invoice);

            $invoice->update([
                'status' => MexcoInvoice::STATUS_PAID,
                'paid_at' => now(),
            ]);
        } elseif ($paid > 0 && $invoice->status === MexcoInvoice::STATUS_PENDING) {
            // Partial payment — keep pending
        }

        return $invoice->fresh();
    }

    public function recalculateInvoice(MexcoInvoice $invoice): MexcoInvoice
    {
        $invoice->loadMissing('items');

        $subtotal = $invoice->items
            ->whereIn('item_type', [MexcoInvoiceItem::TYPE_PLAN, MexcoInvoiceItem::TYPE_ADDON])
            ->sum('total_cents');

        $discount = abs($invoice->items
            ->whereIn('item_type', [MexcoInvoiceItem::TYPE_DISCOUNT, MexcoInvoiceItem::TYPE_CREDIT])
            ->sum('total_cents'));

        $taxRate = (float) $this->settings->get('business', 'tax_rate_percent', config('mexco.billing.tax_rate_percent', 0));
        $taxable = max(0, $subtotal - $discount);
        $tax = (int) round($taxable * ($taxRate / 100));
        $total = max(0, $taxable + $tax);

        $invoice->update([
            'subtotal_cents' => (int) $subtotal,
            'discount_cents' => (int) $discount,
            'tax_cents' => $tax,
            'total_cents' => $total,
        ]);

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

    public function availableCreditCents(int $organisationId): int
    {
        return (int) MexcoCredit::query()
            ->where('organisation_id', $organisationId)
            ->where('amount_cents', '>', 0)
            ->where(function ($query) {
                $query->whereNull('expires_at')->orWhere('expires_at', '>', now());
            })
            ->sum('amount_cents');
    }

    public function addCredit(int $organisationId, int $amountCents, string $source = 'manual', ?string $notes = null): MexcoCredit
    {
        return MexcoCredit::query()->create([
            'organisation_id' => $organisationId,
            'amount_cents' => $amountCents,
            'source' => $source,
            'notes' => $notes,
        ]);
    }

    public function dashboardStats(): array
    {
        $statusCounts = MexcoInvoice::query()
            ->selectRaw('status, COUNT(*) as total')
            ->groupBy('status')
            ->pluck('total', 'status');

        $outstanding = MexcoInvoice::query()
            ->whereIn('status', [MexcoInvoice::STATUS_PENDING])
            ->sum('total_cents');

        $paidThisMonth = MexcoInvoice::query()
            ->where('status', MexcoInvoice::STATUS_PAID)
            ->where('paid_at', '>=', now()->startOfMonth())
            ->sum('total_cents');

        $overdue = MexcoInvoice::query()
            ->where('status', MexcoInvoice::STATUS_PENDING)
            ->whereNotNull('due_at')
            ->where('due_at', '<', now())
            ->count();

        return [
            'total_invoices' => MexcoInvoice::query()->count(),
            'draft' => $statusCounts[MexcoInvoice::STATUS_DRAFT] ?? 0,
            'pending' => $statusCounts[MexcoInvoice::STATUS_PENDING] ?? 0,
            'paid' => $statusCounts[MexcoInvoice::STATUS_PAID] ?? 0,
            'outstanding_cents' => (int) $outstanding,
            'paid_this_month_cents' => (int) $paidThisMonth,
            'overdue' => $overdue,
        ];
    }

    protected function calculatePromoDiscount(MexcoInvoice $invoice, MexcoPromoCode $promo): int
    {
        $subtotal = $invoice->items
            ->whereIn('item_type', [MexcoInvoiceItem::TYPE_PLAN, MexcoInvoiceItem::TYPE_ADDON])
            ->sum('total_cents');

        if ($promo->discount_type === MexcoPromoCode::TYPE_PERCENT) {
            return (int) round($subtotal * min($promo->discount_value, 100) / 100);
        }

        return min($promo->discount_value, $subtotal);
    }

    protected function consumeCreditFromInvoice(MexcoInvoice $invoice): void
    {
        $creditApplied = abs($invoice->items()
            ->where('item_type', MexcoInvoiceItem::TYPE_CREDIT)
            ->sum('total_cents'));

        if ($creditApplied <= 0) {
            return;
        }

        $remaining = $creditApplied;
        $credits = MexcoCredit::query()
            ->where('organisation_id', $invoice->organisation_id)
            ->where('amount_cents', '>', 0)
            ->orderBy('expires_at')
            ->orderBy('id')
            ->lockForUpdate()
            ->get();

        foreach ($credits as $credit) {
            if ($remaining <= 0) {
                break;
            }

            if (! $credit->isAvailable()) {
                continue;
            }

            $deduct = min($remaining, $credit->amount_cents);
            $credit->update(['amount_cents' => $credit->amount_cents - $deduct]);
            $remaining -= $deduct;
        }
    }

    protected function generateInvoiceNumber(): string
    {
        $prefix = config('mexco.billing.invoice_prefix', 'INV');
        $year = now()->format('Y');

        do {
            $seq = str_pad((string) random_int(1, 99999), 5, '0', STR_PAD_LEFT);
            $number = "{$prefix}-{$year}-{$seq}";
        } while (MexcoInvoice::query()->where('invoice_number', $number)->exists());

        return $number;
    }

    protected function assertInvoiceStatus(MexcoInvoice $invoice, array $allowed): void
    {
        if (! in_array($invoice->status, $allowed, true)) {
            throw ValidationException::withMessages([
                'invoice' => 'Invoice cannot be updated in status: '.$invoice->statusLabel(),
            ]);
        }
    }
}
