<?php

namespace App\Services;

use App\Models\Lookups\Status;
use App\Models\Returns\CreditNote;
use App\Models\Returns\ReturnDoc;
use App\Models\Sales\Invoice;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class CreditNoteService
{
    public function __construct(
        private readonly DocumentNumberService $numbers,
        private readonly CustomerCreditService $customerCredit,
    ) {}

    public function issueForOverpayment(int $customerId, float $amount, int $transactionId): CreditNote
    {
        return DB::transaction(function () use ($customerId, $amount, $transactionId) {
            $cn = CreditNote::create([
                'cn_number' => $this->numbers->next('credit_note'),
                'customer_id' => $customerId,
                'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::ISSUED),
                'amount' => $amount,
                'note' => "Auto-generated from overpayment on transaction #{$transactionId}",
                'issued_at' => now(),
            ]);

            if ($cn->customer) {
                $this->customerCredit->credit(
                    customer: $cn->customer,
                    amount: $amount,
                    reference: $cn,
                    note: "Credit note {$cn->cn_number} issued from overpayment.",
                );
            }

            AuditLog::record('credit_note.issued_overpayment', $cn, [
                'amount' => (string) $amount,
                'transaction_id' => (string) $transactionId,
            ]);

            return $cn;
        });
    }

    public function issueForCheckoutOverpayment(int $customerId, float $amount, Invoice $invoice): CreditNote
    {
        return DB::transaction(function () use ($customerId, $amount, $invoice) {
            $cn = CreditNote::create([
                'cn_number' => $this->numbers->next('credit_note'),
                'customer_id' => $customerId,
                'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::ISSUED),
                'amount' => $amount,
                'note' => "Overpayment credit note from invoice {$invoice->invoice_number}.",
                'issued_at' => now(),
            ]);

            if ($cn->customer) {
                $this->customerCredit->credit(
                    customer: $cn->customer,
                    amount: $amount,
                    reference: $cn,
                    note: "Credit note {$cn->cn_number} issued from overpayment on {$invoice->invoice_number}.",
                );
            }

            AuditLog::record('credit_note.issued_overpayment', $cn, [
                'amount' => (string) $amount,
                'invoice_id' => (string) $invoice->id,
            ]);

            return $cn;
        });
    }

    public function issueForReturn(ReturnDoc $return, float $amount): CreditNote
    {
        return DB::transaction(function () use ($return, $amount) {
            $cn = CreditNote::create([
                'cn_number' => $this->numbers->next('credit_note'),
                'customer_id' => $return->customer_id,
                'return_id' => $return->id,
                'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::ISSUED),
                'amount' => $amount,
                'issued_at' => now(),
            ]);

            if ($return->customer && $amount > 0) {
                $this->customerCredit->credit(
                    customer: $return->customer,
                    amount: $amount,
                    reference: $cn,
                    note: "Credit note {$cn->cn_number} issued from return {$return->return_number}.",
                );
            }

            AuditLog::record('credit_note.issued', $cn, [
                'amount' => (string) $amount,
                'return' => $return->return_number,
            ]);

            return $cn;
        });
    }

    /**
     * Issue a fresh credit note for credit that was carried forward (e.g. the
     * unused portion of notes redeemed at checkout). The customer credit pool
     * already holds this value, so we deliberately do NOT credit it again.
     */
    public function issueRemainder(int $customerId, float $amount, ?int $sourceInvoiceId = null): CreditNote
    {
        $cn = CreditNote::create([
            'cn_number' => $this->numbers->next('credit_note'),
            'customer_id' => $customerId,
            'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::ISSUED),
            'amount' => $amount,
            'note' => $sourceInvoiceId
                ? "Unused credit carried forward from invoice #{$sourceInvoiceId}."
                : 'Unused credit carried forward.',
            'issued_at' => now(),
        ]);

        AuditLog::record('credit_note.remainder_issued', $cn, ['amount' => (string) $amount]);

        return $cn;
    }

    public function createManualDraft(int $customerId, float $amount, ?string $note = null): CreditNote
    {
        $cn = CreditNote::create([
            'cn_number' => $this->numbers->next('credit_note'),
            'customer_id' => $customerId,
            'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::DRAFT),
            'amount' => $amount,
            'note' => $note,
        ]);

        AuditLog::record('credit_note.draft_created', $cn, ['amount' => (string) $amount]);

        return $cn;
    }

    public function issue(CreditNote $cn): CreditNote
    {
        if ($cn->status->code !== Status::DRAFT) {
            throw new RuntimeException('Only draft credit notes can be issued.');
        }
        $cn->update([
            'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::ISSUED),
            'issued_at' => now(),
        ]);

        if ($cn->customer && (float) $cn->amount > 0) {
            $this->customerCredit->credit(
                customer: $cn->customer,
                amount: (float) $cn->amount,
                reference: $cn,
                note: "Manual credit note {$cn->cn_number} issued.",
            );
        }

        AuditLog::record('credit_note.issued', $cn);

        return $cn;
    }

    /**
     * Spend some (or all) of a credit note against an invoice.
     *
     * A note is a store of value, not a coupon: it can be drawn down across
     * several invoices until it is used up. $amount defaults to whatever is
     * left on the note; anything more than that is refused rather than
     * silently capped, because a caller asking to spend Rs. 5,000 from a note
     * holding Rs. 200 has miscounted somewhere and should hear about it.
     *
     * The note stays ISSUED while it still has value and only becomes CLAIMED
     * once fully spent — so {@see CreditNote::scopeSpendable()} keeps offering
     * a part-used note for its real remaining worth.
     *
     * This records the DRAW-DOWN only. The money side — the invoice payment
     * itself — belongs to PaymentService, which calls this as it books a
     * `credit` tender. Applying a note must never touch the invoice's discount
     * or net_total: the sale was for what it was for, and the note is how part
     * of it got paid.
     */
    public function claim(CreditNote $cn, Invoice $invoice, ?float $amount = null): CreditNote
    {
        return DB::transaction(function () use ($cn, $invoice, $amount) {
            // Locked for the read-modify-write on applied_amount: two
            // concurrent checkouts spending the same note would otherwise both
            // see the same remaining balance and overdraw it.
            $cn = CreditNote::query()->lockForUpdate()->findOrFail($cn->getKey());

            if ($cn->status->code !== Status::ISSUED) {
                throw new RuntimeException("Credit note {$cn->cn_number} is not available to spend.");
            }
            if ($cn->customer_id !== $invoice->customer_id) {
                throw new RuntimeException('Credit note belongs to a different customer.');
            }

            $remaining = $cn->remainingAmount();
            $claimAmount = round($amount ?? $remaining, 2);

            if ($claimAmount <= 0.001) {
                throw new RuntimeException('The amount to apply must be greater than zero.');
            }

            if ($claimAmount > $remaining + 0.001) {
                throw new RuntimeException(
                    'Credit note '.$cn->cn_number.' only has Rs. '.number_format($remaining, 2)
                    .' left; Rs. '.number_format($claimAmount, 2).' was requested.'
                );
            }

            if ($invoice->customer) {
                $this->customerCredit->debit(
                    customer: $invoice->customer,
                    amount: $claimAmount,
                    reference: $invoice,
                    note: "Credit note {$cn->cn_number} applied to invoice {$invoice->invoice_number}.",
                );
            }

            $newApplied = round((float) $cn->applied_amount + $claimAmount, 2);
            $fullySpent = $newApplied >= (float) $cn->amount - 0.001;

            $cn->update([
                'applied_amount' => $newApplied,
                // A part-spent note stays ISSUED so it keeps showing up as
                // available for its remaining value.
                'status_id' => $fullySpent
                    ? Status::idOf(Status::CTX_CREDIT_NOTE, Status::CLAIMED)
                    : $cn->status_id,
                // The last invoice it was spent against. Partial history lives
                // in the audit log and the customer credit ledger.
                'claimed_invoice_id' => $invoice->id,
                'claimed_at' => $fullySpent ? now() : $cn->claimed_at,
            ]);

            AuditLog::record('credit_note.claimed', $cn, [
                'invoice' => $invoice->invoice_number,
                'applied' => number_format($claimAmount, 2),
                'remaining' => number_format($cn->refresh()->remainingAmount(), 2),
            ]);

            return $cn;
        });
    }

    public function void(CreditNote $cn, string $reason): CreditNote
    {
        if ($cn->status->code === Status::CLAIMED) {
            throw new RuntimeException('Cannot void a claimed credit note. Reverse the invoice first.');
        }
        $cn->update([
            'status_id' => Status::idOf(Status::CTX_CREDIT_NOTE, Status::VOID),
            'voided_at' => now(),
            'void_reason' => $reason,
        ]);
        AuditLog::record('credit_note.voided', $cn, ['reason' => $reason]);

        return $cn;
    }
}
