<?php

namespace App\Services;

use App\Enums\PaymentStatus;
use App\Enums\TransactionStatus;
use App\Enums\TransactionType;
use App\Exceptions\DomainException;
use App\Models\Appointment;
use App\Models\PaymentTransaction;
use App\Models\User;
use Illuminate\Support\Facades\DB;

class PaymentService
{
    public function __construct(
        private readonly AccessService $access,
        private readonly AuditService $audit,
        private readonly ClinicSettings $settings,
    ) {}

    public function collect(User $actor, Appointment $appointment, array $data): PaymentTransaction
    {
        $appointment->loadMissing('patient');

        if (! $this->access->canCollectPayment($actor, $appointment)) {
            throw new DomainException('FORBIDDEN', 'You cannot collect payments for this appointment.', 403);
        }

        return DB::transaction(function () use ($actor, $appointment, $data) {
            $appointment = Appointment::query()->lockForUpdate()->findOrFail($appointment->id);

            $existing = $this->findExistingIdempotentTransaction($actor, $data);
            if ($existing) {
                if ((int) $existing->appointment_id !== (int) $appointment->id) {
                    throw new DomainException(
                        'IDEMPOTENCY_KEY_REUSED',
                        'This idempotency/provider reference was already used for another appointment.',
                        409
                    );
                }

                return $existing;
            }

            if (! in_array($data['method'], $this->settings->paymentMethods($actor->organization_id), true)) {
                throw new DomainException('INVALID_PAYMENT_METHOD', 'The selected payment method is not enabled.', 422);
            }

            $amount = round((float) $data['amount'], 2);
            $summary = $this->summary($appointment);
            $balance = max(0, (float) $appointment->final_fee - $summary['applied']);

            if ($amount > $balance + 0.0001) {
                throw new DomainException(
                    'PAYMENT_EXCEEDS_BALANCE',
                    'Payment amount exceeds the appointment balance.',
                    422,
                    ['amount' => ['Amount exceeds outstanding balance.']],
                    ['outstanding_balance' => $balance]
                );
            }

            $transaction = PaymentTransaction::create([
                'organization_id' => $actor->organization_id,
                'appointment_id' => $appointment->id,
                'patient_id' => $appointment->patient_id,
                'location_id' => $appointment->location_id,
                'created_by_user_id' => $actor->id,
                'type' => TransactionType::PAYMENT,
                'amount' => $amount,
                'method' => $data['method'],
                'status' => TransactionStatus::SUCCEEDED,
                'reference' => $data['reference'] ?? null,
                'provider_reference' => $data['provider_reference'] ?? null,
                'idempotency_key' => $data['idempotency_key'] ?? null,
                'reason' => $data['reason'] ?? null,
                'metadata' => $data['metadata'] ?? null,
            ]);

            $transaction->update([
                'receipt_number' => 'RCT-'.date('Ymd').'-'.str_pad((string) $transaction->id, 6, '0', STR_PAD_LEFT),
            ]);

            $this->syncPaymentStatus($appointment);

            $this->audit->log('payment.created', $actor, $transaction, null, [
                'appointment_id' => $appointment->id,
                'amount' => $amount,
                'method' => $transaction->method,
                'receipt_number' => $transaction->receipt_number,
            ]);

            return $transaction->fresh('appointment');
        }, 3);
    }

    public function waive(User $actor, Appointment $appointment, float $amount, string $reason): PaymentTransaction
    {
        if (! $this->access->isAdmin($actor)) {
            throw new DomainException('FORBIDDEN', 'Only an administrator can waive a balance.', 403);
        }

        return DB::transaction(function () use ($actor, $appointment, $amount, $reason) {
            $appointment = Appointment::query()->lockForUpdate()->findOrFail($appointment->id);
            $summary = $this->summary($appointment);
            $balance = max(0, (float) $appointment->final_fee - $summary['applied']);

            if ($amount <= 0 || $amount > $balance + 0.0001) {
                throw new DomainException('INVALID_WAIVER_AMOUNT', 'Waiver amount is invalid.', 422);
            }

            $transaction = PaymentTransaction::create([
                'organization_id' => $actor->organization_id,
                'appointment_id' => $appointment->id,
                'patient_id' => $appointment->patient_id,
                'location_id' => $appointment->location_id,
                'created_by_user_id' => $actor->id,
                'type' => TransactionType::WAIVER,
                'amount' => round($amount, 2),
                'method' => null,
                'status' => TransactionStatus::SUCCEEDED,
                'reason' => $reason,
            ]);

            $transaction->update([
                'receipt_number' => 'WVR-'.date('Ymd').'-'.str_pad((string) $transaction->id, 6, '0', STR_PAD_LEFT),
            ]);

            $this->syncPaymentStatus($appointment);
            $this->audit->log('payment.waived', $actor, $transaction, null, [
                'amount' => $amount,
                'reason' => $reason,
            ]);

            return $transaction;
        }, 3);
    }

    public function refund(User $actor, PaymentTransaction $original, array $data): PaymentTransaction
    {
        if ($original->type !== TransactionType::PAYMENT || $original->status !== TransactionStatus::SUCCEEDED) {
            throw new DomainException('INVALID_REFUND', 'Only successful payment transactions can be refunded.', 422);
        }

        $appointment = $original->appointment()->firstOrFail();

        if (! $this->access->canCollectPayment($actor, $appointment)) {
            throw new DomainException('FORBIDDEN', 'You cannot refund this transaction.', 403);
        }

        return DB::transaction(function () use ($actor, $original, $appointment, $data) {
            $original = PaymentTransaction::query()->lockForUpdate()->findOrFail($original->id);
            $appointment = Appointment::query()->lockForUpdate()->findOrFail($appointment->id);

            $existing = $this->findExistingIdempotentTransaction($actor, $data);
            if ($existing) {
                if ((int) $existing->reference_transaction_id !== (int) $original->id) {
                    throw new DomainException(
                        'IDEMPOTENCY_KEY_REUSED',
                        'This idempotency key was already used for another transaction.',
                        409
                    );
                }

                return $existing;
            }

            $alreadyRefunded = abs((float) PaymentTransaction::query()
                ->where('reference_transaction_id', $original->id)
                ->where('type', TransactionType::REFUND->value)
                ->where('status', TransactionStatus::SUCCEEDED->value)
                ->sum('amount'));

            $amount = round((float) $data['amount'], 2);
            $remainingRefundable = max(0, (float) $original->amount - $alreadyRefunded);

            if ($amount <= 0 || $amount > $remainingRefundable + 0.0001) {
                throw new DomainException('INVALID_REFUND_AMOUNT', 'Refund amount exceeds the refundable balance.', 422);
            }

            $refund = PaymentTransaction::create([
                'organization_id' => $actor->organization_id,
                'appointment_id' => $appointment->id,
                'patient_id' => $appointment->patient_id,
                'location_id' => $appointment->location_id,
                'created_by_user_id' => $actor->id,
                'type' => TransactionType::REFUND,
                'amount' => -$amount,
                'method' => $original->method,
                'status' => TransactionStatus::SUCCEEDED,
                'reference' => $data['reference'] ?? null,
                'idempotency_key' => $data['idempotency_key'] ?? null,
                'reason' => $data['reason'],
                'reference_transaction_id' => $original->id,
            ]);

            $refund->update([
                'receipt_number' => 'RFD-'.date('Ymd').'-'.str_pad((string) $refund->id, 6, '0', STR_PAD_LEFT),
            ]);

            $this->syncPaymentStatus($appointment);
            $this->audit->log('payment.refunded', $actor, $refund, null, [
                'original_transaction_id' => $original->id,
                'amount' => $amount,
                'reason' => $refund->reason,
            ]);

            return $refund->fresh('appointment');
        }, 3);
    }

    private function findExistingIdempotentTransaction(User $actor, array $data): ?PaymentTransaction
    {
        if (! empty($data['idempotency_key'])) {
            $existing = PaymentTransaction::query()
                ->where('organization_id', $actor->organization_id)
                ->where('idempotency_key', $data['idempotency_key'])
                ->first();

            if ($existing) {
                return $existing;
            }
        }

        if (! empty($data['provider_reference'])) {
            return PaymentTransaction::query()
                ->where('organization_id', $actor->organization_id)
                ->where('provider_reference', $data['provider_reference'])
                ->first();
        }

        return null;
    }

    public function summary(Appointment $appointment): array
    {
        $rows = PaymentTransaction::query()
            ->where('appointment_id', $appointment->id)
            ->where('status', TransactionStatus::SUCCEEDED->value)
            ->get(['type', 'amount']);

        $payments = (float) $rows->where('type', TransactionType::PAYMENT)->sum('amount');
        $refunds = abs((float) $rows->where('type', TransactionType::REFUND)->sum('amount'));
        $waivers = (float) $rows->where('type', TransactionType::WAIVER)->sum('amount');
        $netCash = max(0, $payments - $refunds);
        $applied = $netCash + $waivers;

        return compact('payments', 'refunds', 'waivers', 'netCash', 'applied');
    }

    public function syncPaymentStatus(Appointment $appointment): PaymentStatus
    {
        $summary = $this->summary($appointment);
        $fee = (float) $appointment->final_fee;

        $status = PaymentStatus::PENDING;

        if ($summary['payments'] > 0 && $summary['netCash'] <= 0 && $summary['waivers'] <= 0) {
            $status = PaymentStatus::REFUNDED;
        } elseif ($summary['waivers'] >= $fee && $summary['netCash'] <= 0) {
            $status = PaymentStatus::WAIVED;
        } elseif ($summary['applied'] >= $fee - 0.0001) {
            $status = PaymentStatus::PAID;
        } elseif ($summary['applied'] > 0) {
            $status = PaymentStatus::PARTIALLY_PAID;
        }

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

        return $status;
    }
}
