<?php

declare(strict_types=1);

namespace App\Filament\Resources\HeldPayments\Pages;

use App\Filament\Resources\Credits\CreditResource;
use App\Filament\Resources\HeldPayments\HeldPaymentResource;
use App\Filament\Resources\Payments\PaymentResource;
use App\Models\HeldPayment;
use App\Models\Payment;
use App\Services\Payments\HeldPaymentReviewException;
use App\Services\Payments\HeldPaymentReviewService;
use App\Support\Format;
use App\Support\SafeReport;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Infolists\Components\TextEntry;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ViewRecord;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Auth;

class ViewHeldPayment extends ViewRecord
{
    protected static string $resource = HeldPaymentResource::class;

    /**
     * approvalBlocker() hace varias consultas y la pantalla lo pide varias veces
     * por render (la entrada y la visibilidad del botón). Privadas: Livewire no
     * las guarda entre peticiones, así que valen para una sola.
     */
    private bool $blockerEvaluated = false;

    private ?string $blocker = null;

    private function held(): HeldPayment
    {
        $record = $this->getRecord();
        assert($record instanceof HeldPayment);

        return $record;
    }

    private function review(): HeldPaymentReviewService
    {
        return app(HeldPaymentReviewService::class);
    }

    private function blocker(): ?string
    {
        if (! $this->blockerEvaluated) {
            $this->blocker = $this->review()->approvalBlocker($this->held());
            $this->blockerEvaluated = true;
        }

        return $this->blocker;
    }

    /**
     * Tras una aprobación o un rechazo que no se completó: otro admin pudo haberlo
     * resuelto entretanto. Se relee para que la pantalla no ofrezca lo que ya no vale.
     * Corre dentro de los catch de las acciones: si la relectura también falla (la
     * base caída), se reporta y la pantalla queda como estaba, con el aviso ya enviado.
     */
    private function reloadAfterFailedReview(): void
    {
        try {
            $this->held()->refresh();
        } catch (\Throwable $e) {
            SafeReport::report($e);

            return;
        }

        $this->blockerEvaluated = false;
    }

    public function infolist(Schema $schema): Schema
    {
        return $schema
            ->columns(1)
            ->components([
                Section::make('Cobro')
                    ->columns(3)
                    ->schema([
                        TextEntry::make('company.name')->label('Empresa')
                            ->visible(fn (): bool => HeldPaymentResource::viewerIsSuperAdmin()),
                        TextEntry::make('capturedBy.name')->label('Cobrador')
                            ->state(fn (HeldPayment $record): ?string => HeldPaymentResource::capturerLabel($record))
                            ->color(fn (HeldPayment $record): ?string => HeldPaymentResource::capturerColor($record))
                            ->placeholder('Usuario eliminado'),
                        TextEntry::make('credit.client.name')->label('Cliente')->placeholder('Crédito no disponible'),
                        TextEntry::make('credit_id')->label('Crédito')
                            ->state(fn (HeldPayment $record): string => HeldPaymentResource::creditLabel($record))
                            ->url(fn (HeldPayment $record): ?string => $record->credit_id !== null
                                ? CreditResource::getUrl('view', ['record' => $record->credit_id])
                                : null),
                        // Filament no llama a formatStateUsing() con el estado vacío: sin monto va el placeholder.
                        TextEntry::make('amount')->label('Monto')
                            ->formatStateUsing(fn ($state): string => Format::money($state))
                            ->placeholder('Ilegible'),
                        TextEntry::make('payment_method')->label('Método')
                            ->formatStateUsing(fn (string $state): string => HeldPayment::paymentMethodLabel($state))
                            ->placeholder('—'),
                        TextEntry::make('payment_date')->label('Fecha de cobro')->date('d/m/Y')->placeholder('—'),
                        TextEntry::make('offline_created_at')->label('Capturado en el teléfono')->dateTime('d/m/Y H:i')->placeholder('—'),
                        TextEntry::make('created_at')->label('Llegó al servidor')->dateTime('d/m/Y H:i'),
                        TextEntry::make('syncedBy.name')->label('Subido por')->placeholder('Usuario eliminado'),
                    ]),

                Section::make('Por qué quedó retenido')
                    ->schema([
                        TextEntry::make('reason')->label('Motivo')->badge()
                            ->formatStateUsing(fn (string $state): string => HeldPayment::reasonLabel($state)),
                        TextEntry::make('reason_detail')->label('Detalle')->placeholder('—'),
                        TextEntry::make('se_puede_aprobar')->label('¿Se puede aprobar?')
                            ->state(fn (): string => $this->blocker() ?? 'Sí: el crédito admite este pago.')
                            ->color(fn (): string => $this->blocker() === null ? 'success' : 'danger')
                            ->visible(fn (HeldPayment $record): bool => $record->status === HeldPayment::STATUS_PENDING),
                    ]),

                Section::make('Posibles duplicados')
                    ->description(fn (HeldPayment $record): string => 'Pagos del mismo crédito, por el mismo monto, a '.HeldPayment::DUPLICATE_WINDOW_DAYS.' días o menos de la fecha de cobro.'
                        .($record->status === HeldPayment::STATUS_PENDING
                            ? ' Si alguno es este mismo cobro cargado a mano, rechaza el retenido.'
                            : ''))
                    ->schema([
                        TextEntry::make('duplicados')->hiddenLabel()
                            ->state(fn (HeldPayment $record): array => $record->possibleDuplicates()
                                ->map(fn (Payment $p): string => "Pago #{$p->id} · ".Format::money($p->amount).' · '.$p->payment_date->format('d/m/Y'))
                                ->whenEmpty(fn ($c) => $c->push('Ninguno'))
                                ->all())
                            ->listWithLineBreaks(),
                    ]),

                Section::make('Resolución')
                    ->columns(3)
                    ->visible(fn (HeldPayment $record): bool => $record->status !== HeldPayment::STATUS_PENDING)
                    ->schema([
                        TextEntry::make('status')->label('Estado')
                            ->formatStateUsing(fn (string $state): string => HeldPayment::statusLabel($state)),
                        TextEntry::make('resolvedBy.name')->label('Resuelto por')->placeholder('Usuario eliminado'),
                        TextEntry::make('resolved_at')->label('Fecha')->dateTime('d/m/Y H:i'),
                        TextEntry::make('payment_id')->label('Pago creado')
                            ->formatStateUsing(fn ($state): string => "Pago #{$state}")
                            ->url(fn (HeldPayment $record): ?string => $record->payment_id !== null
                                ? PaymentResource::getUrl('view', ['record' => $record->payment_id])
                                : null)
                            ->placeholder('—'),
                        TextEntry::make('resolution_notes')->label('Notas')->columnSpanFull()->placeholder('—'),
                    ]),
            ]);
    }

    /**
     * El último paso antes de aplicar dos veces el mismo cobro: si hay pagos
     * parecidos (o se retuvo por eso), la confirmación lo dice antes que nada.
     */
    private function duplicateWarning(HeldPayment $held): string
    {
        $duplicates = $held->possibleDuplicates();

        if ($duplicates->isNotEmpty()) {
            $ids = $duplicates->map(fn (Payment $p): string => "#{$p->id}")->implode(', ');

            return "Atención: hay {$duplicates->count()} pago(s) parecido(s) en este crédito ({$ids}); si alguno es este mismo cobro, rechaza en vez de aprobar. ";
        }

        if ($held->reason === HeldPayment::REASON_POSSIBLE_DUPLICATE) {
            return 'Atención: se retuvo como posible duplicado'
                .($held->reason_detail !== null ? " ({$held->reason_detail})" : '')
                .'; si es este mismo cobro, rechaza en vez de aprobar. ';
        }

        return '';
    }

    private function approveDescription(): string
    {
        $held = $this->held();
        $client = $held->credit?->client?->name;

        return $this->duplicateWarning($held)
            .'Se aplicarán '.Format::money($held->amount).' al crédito #'.$held->credit_id
            .($client !== null ? " de {$client}" : '')
            .', con la fecha de cobro original ('.$held->payment_date?->format('d/m/Y').') y a nombre de '
            .($held->capturedBy->name ?? 'el cobrador que lo capturó')
            .'. El dinero va a la cuota pendiente más antigua del crédito, y el ingreso queda con la fecha original: si es de un período ya cerrado, sus cifras cambian.';
    }

    protected function getHeaderActions(): array
    {
        return [
            Action::make('approve')
                ->label('Aprobar y aplicar')
                ->icon('heroicon-o-check-circle')
                ->color('success')
                ->requiresConfirmation()
                ->modalHeading('Aplicar este cobro al crédito')
                ->modalDescription(fn (): string => $this->approveDescription())
                ->authorize('approve')
                ->visible(fn (): bool => $this->blocker() === null)
                ->action(function (): void {
                    try {
                        $payment = $this->review()->approve($this->held(), Auth::user());
                    } catch (HeldPaymentReviewException $e) {
                        // Mensaje para el admin; si hacía falta reportar, el servicio ya lo hizo.
                        Notification::make()->title($e->getMessage())->danger()->send();
                        $this->reloadAfterFailedReview();

                        return;
                    } catch (\Throwable $e) {
                        // Carrera con otro pago sobre el mismo crédito, deadlock agotado, etc.:
                        // la transacción se revirtió y el retenido sigue pendiente.
                        // Sin datos del cobro: el mensaje de una QueryException trae el SQL con GPS y dispositivo.
                        SafeReport::report($e);
                        Notification::make()->title('No se pudo aplicar el cobro. Recarga la página e inténtalo de nuevo.')->danger()->send();
                        $this->reloadAfterFailedReview();

                        return;
                    }

                    Notification::make()->title("Cobro aplicado (pago #{$payment->id})")->success()->send();
                    $this->redirect(HeldPaymentResource::getUrl('view', ['record' => $this->held()]));
                }),

            Action::make('reject')
                ->label('Rechazar')
                ->icon('heroicon-o-x-circle')
                ->color('danger')
                ->authorize('reject')
                ->visible(fn (): bool => $this->held()->status === HeldPayment::STATUS_PENDING)
                ->schema([
                    Textarea::make('resolution_notes')
                        ->label('Motivo del rechazo')
                        ->helperText('Por ejemplo: "ya se había cargado a mano como pago #123".')
                        ->required()
                        ->rows(3),
                ])
                ->action(function (array $data): void {
                    try {
                        $this->review()->reject($this->held(), Auth::user(), (string) $data['resolution_notes']);
                    } catch (HeldPaymentReviewException $e) {
                        Notification::make()->title($e->getMessage())->danger()->send();
                        $this->reloadAfterFailedReview();

                        return;
                    } catch (\Throwable $e) {
                        // Sin datos del cobro: el mensaje de una QueryException trae el SQL con GPS y dispositivo.
                        SafeReport::report($e);
                        Notification::make()->title('No se pudo rechazar el cobro. Recarga la página e inténtalo de nuevo.')->danger()->send();
                        $this->reloadAfterFailedReview();

                        return;
                    }

                    Notification::make()->title('Cobro rechazado')->warning()->send();
                    $this->redirect(HeldPaymentResource::getUrl('view', ['record' => $this->held()]));
                }),
        ];
    }
}
