<?php

namespace App\Domain\Payments\Models;

use App\Domain\Applicants\Models\Applicant;
use App\Domain\Billing\Models\Installment;
use App\Domain\Billing\Models\Invoice;
use App\Domain\Identity\Models\User;
use App\Domain\Organization\Models\Branch;
use App\Domain\Organization\Models\Company;
use App\Domain\Payments\Enums\PaymentMethod;
use App\Domain\Payments\Enums\PaymentStatus;
use App\Support\Concerns\HasUuid;
use App\Support\Money\Money;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use RuntimeException;

/**
 * A payment (§29). Confirmed payments are immutable: correcting one means
 * reversing it and recording a new one (§30, §70).
 */
class Payment extends Model
{
    use HasFactory, HasUuid;

    protected $guarded = ['id', 'uuid'];

    /** Columns that may still change after a payment is confirmed. */
    private const MUTABLE_AFTER_CONFIRMATION = [
        'status', 'reversed_at', 'reversed_by', 'reversal_reason',
        'verified_by', 'verified_at', 'receipt_no', 'updated_at',
    ];

    protected function casts(): array
    {
        return [
            'status' => PaymentStatus::class,
            'method' => PaymentMethod::class,
            'received_at' => 'datetime',
            'verified_at' => 'datetime',
            'reversed_at' => 'datetime',
            'deposit_date' => 'date',
        ];
    }

    protected static function booted(): void
    {
        // Financial history is not edited in place (§70 rule 5).
        static::updating(function (self $payment) {
            // getOriginal() runs the enum cast, so compare against the enum
            // itself rather than its backing value.
            if ($payment->getOriginal('status') !== PaymentStatus::Confirmed) {
                return;
            }

            $touched = array_diff(array_keys($payment->getDirty()), self::MUTABLE_AFTER_CONFIRMATION);

            if ($touched !== []) {
                throw new RuntimeException(
                    'A confirmed payment cannot be edited; reverse it and record a corrected payment.'
                );
            }
        });

        static::deleting(fn () => throw new RuntimeException('Payments cannot be deleted.'));
    }

    public function company(): BelongsTo
    {
        return $this->belongsTo(Company::class);
    }

    public function branch(): BelongsTo
    {
        return $this->belongsTo(Branch::class);
    }

    public function applicant(): BelongsTo
    {
        return $this->belongsTo(Applicant::class);
    }

    public function invoice(): BelongsTo
    {
        return $this->belongsTo(Invoice::class);
    }

    public function installment(): BelongsTo
    {
        return $this->belongsTo(Installment::class);
    }

    public function receivedBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'received_by');
    }

    public function verifiedBy(): BelongsTo
    {
        return $this->belongsTo(User::class, 'verified_by');
    }

    public function reverses(): BelongsTo
    {
        return $this->belongsTo(self::class, 'reverses_payment_id');
    }

    public function money(): Money
    {
        return Money::of((int) $this->amount, $this->currency);
    }

    public function scopeConfirmed(Builder $query): Builder
    {
        return $query->where('status', PaymentStatus::Confirmed->value);
    }

    public function scopeAwaitingVerification(Builder $query): Builder
    {
        return $query->where('status', PaymentStatus::Pending->value);
    }

    public function isReversal(): bool
    {
        return $this->reverses_payment_id !== null;
    }
}
