<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\Pwa;

use App\Http\Controllers\Api\Pwa\Traits\RoleAwareQueries;
use App\Http\Controllers\Controller;
use App\Http\Requests\Pwa\StorePaymentRequest;
use App\Models\CompanyFinancialSettings;
use App\Models\Credit;
use App\Models\HeldPayment;
use App\Models\Payment;
use App\Services\CollectorVisibilityResolver;
use App\Services\CreditStatusSyncService;
use App\Services\PaymentManager;
use App\Services\Payments\PaymentPolicyException;
use App\Services\Payments\PaymentPolicyValidator;
use App\Services\PaymentSyncService;
use Carbon\Carbon;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class PaymentController extends Controller
{
    use RoleAwareQueries;

    public function __construct(
        private readonly PaymentManager $paymentManager
    ) {}

    /**
     * POST /api/pwa/payments
     *
     * Registra un nuevo pago.
     * Soporta idempotency_key para prevenir duplicados en sync offline.
     */
    public function store(StorePaymentRequest $request): JsonResponse
    {
        $user = $request->user();
        $idempotencyKey = $request->idempotency_key;

        // Verificar si ya existe un pago con esta idempotency_key (prevenir duplicados).
        // idempotency_key siempre está presente: el request la auto-genera si el cliente no la envía.
        $existingPayment = Payment::query()
            ->where('company_id', $user->company_id)
            ->where('idempotency_key', $idempotencyKey)
            ->first();

        if ($existingPayment) {
            Log::info('PWA Payment: Duplicate prevented', [
                'idempotency_key' => $idempotencyKey,
                'existing_payment_id' => $existingPayment->id,
                'user_id' => $user->id,
            ]);

            return response()->json([
                'message' => 'Este pago ya fue registrado.',
                'duplicate' => true,
                'payment' => $this->formatPaymentResponse($existingPayment),
            ], 200); // 200, no 409, porque el pago SÍ existe
        }

        // El lote ya retuvo esta clave: este POST llegó tarde (el teléfono se rindió
        // a los 10 s y el motor subió la fila) y aplicarlo ahora se saltaría la
        // revisión, o la decisión ya tomada. Va sin `payment`: si el teléfono todavía
        // escucha, la fila se queda y el lote se la suelta como `held`.
        $heldPayment = HeldPayment::query()
            ->withoutGlobalScopes()
            ->where('company_id', $user->company_id)
            ->where('idempotency_key', $idempotencyKey)
            ->first();

        if ($heldPayment) {
            Log::info('PWA Payment: key already held by the sync batch', [
                'idempotency_key' => $idempotencyKey,
                'held_payment_id' => $heldPayment->id,
                'status' => $heldPayment->status,
                'user_id' => $user->id,
            ]);

            return response()->json([
                'message' => match ($heldPayment->status) {
                    HeldPayment::STATUS_PENDING => 'Este cobro quedó en revisión: un administrador decide si se aplica.',
                    HeldPayment::STATUS_REJECTED => 'Este cobro ya se revisó y no se aplicó.',
                    default => 'Este cobro ya se revisó.',
                },
                'held' => true,
                'held_payment' => [
                    'id' => $heldPayment->id,
                    'reason' => $heldPayment->reason,
                    'reason_label' => HeldPayment::reasonLabel($heldPayment->reason),
                    'status' => $heldPayment->status,
                ],
            ], 200);
        }

        // Verificar si el usuario puede registrar pagos
        if (! $this->canRegisterPayments($request)) {
            return response()->json([
                'message' => 'No tienes permiso para registrar pagos.',
            ], 403);
        }

        // Obtener crédito y validar acceso según rol.
        // activeLeafCredits() = estados activos + solo créditos hoja (sin hijos):
        // antes solo se filtraba por estado, permitiendo en teoría pagar un crédito
        // padre. Solo los créditos hoja son operacionales (ver CLAUDE.md).
        $query = Credit::query()
            ->where('company_id', $user->company_id)
            ->activeLeafCredits();

        // Collectors solo pueden registrar pagos en sus créditos asignados
        // Admins pueden registrar pagos en cualquier crédito de la empresa
        $role = $this->getUserPwaRole($request);
        if ($role === 'collector') {
            $query->where('collector_user_id', $user->id);
        }

        $credit = $query->find($request->credit_id);

        if (! $credit) {
            return response()->json([
                'message' => 'Crédito no encontrado o no tienes acceso.',
                'errors' => [
                    'credit_id' => ['El crédito no existe o no está asignado a ti.'],
                ],
            ], 422);
        }

        // ── Políticas de negocio previas al registro ─────────────────────────
        // Saldo, cuota pendiente y políticas parcial/sobrepago se validan con la
        // fuente única PaymentPolicyValidator, compartida con el panel Filament.
        $amount = (float) $request->amount;
        $settings = CompanyFinancialSettings::forCompany($user->company_id);

        try {
            app(PaymentPolicyValidator::class)->validate($credit, $amount, $settings);
        } catch (PaymentPolicyException $e) {
            return response()->json([
                'message' => $e->getMessage(),
                'errors' => $e->errors(),
            ], 422);
        }

        // Un cobro igual (mismo monto, mismo día, otra clave) ya está en el crédito.
        // El lote lo retiene como posible duplicado; acá el cobrador está frente al
        // cliente, así que se le pregunta (409) y solo se registra si confirma que
        // es otro cobro. El caso que importa: tras "No se pudo confirmar con el
        // servidor" el primero sí había llegado, y repetirlo sale con otra clave.
        $sameDay = app(PaymentSyncService::class)->sameDayDuplicate(
            $credit,
            $amount,
            Carbon::parse($request->payment_date)->toDateString(),
            $idempotencyKey,
        );

        if ($sameDay !== null) {
            if (! $request->boolean('confirm_duplicate')) {
                return $this->possibleDuplicateResponse($sameDay);
            }

            Log::info('PWA Payment: same-day duplicate confirmed by the user', [
                'idempotency_key' => $idempotencyKey,
                'existing_payment_id' => $sameDay->id,
                'credit_id' => $credit->id,
                'user_id' => $user->id,
            ]);
        }

        // Guardar balance antes del pago
        $balanceBefore = (float) $credit->remaining_balance;

        try {
            $payment = DB::transaction(function () use ($credit, $request, $user, $idempotencyKey) {
                $payment = $this->paymentManager->registerPayment(
                    amount: (float) $request->amount,
                    paymentDate: Carbon::parse($request->payment_date),
                    registeredByUserId: $user->id,
                    credit: $credit,
                    paymentMethod: $request->payment_method,
                );

                // Guardar campos PWA en una sola actualización
                $payment->idempotency_key = $idempotencyKey;
                $payment->device_id = $request->device_id;
                $payment->offline_created_at = $request->offline_created_at;
                $payment->latitude = $request->latitude;
                $payment->longitude = $request->longitude;
                $payment->save();

                return $payment;
            });

            Log::info('PWA Payment: Success', [
                'payment_id' => $payment->id,
                'credit_id' => $credit->id,
                'amount' => $request->amount,
                'user_id' => $user->id,
                'idempotency_key' => $idempotencyKey,
            ]);

            // Refrescar crédito para obtener nuevo balance
            $credit->refresh();
            $credit->load('installments');

            // Si el saldo llegó a cero y la empresa tiene activado el cierre automático,
            // forzar la transición de estado SÍNCRONAMENTE para garantizar que
            // status='paid' quede persistido antes de devolver la respuesta.
            if ((float) $credit->remaining_balance <= 0 && $settings->auto_close_on_full_payment) {
                app(CreditStatusSyncService::class)->forceSync($credit);
                $credit->refresh();
            }

            // Calcular cuotas restantes
            $installmentsRemaining = $credit->installments
                ->where('status', '!=', 'paid')
                ->count();

            // Encontrar próxima cuota
            $nextInstallment = $credit->installments
                ->whereIn('status', ['pending', 'partial_paid', 'overdue'])
                ->sortBy('due_date')
                ->first();

            return response()->json([
                'message' => 'Pago registrado correctamente.',
                'payment' => $this->formatPaymentResponse($payment),
                'credit_new_balance' => round((float) $credit->remaining_balance, 2),
                'credit_status' => $credit->status,
                'credit_is_paid' => $credit->status === Credit::STATUS_PAID,
                'installments_remaining' => $installmentsRemaining,
                'balance_before' => round($balanceBefore, 2),
                'balance_after' => round((float) $credit->remaining_balance, 2),
                'next_payment' => $nextInstallment ? [
                    'date' => $nextInstallment->due_date->toDateString(),
                    'amount' => round((float) $nextInstallment->remaining_amount, 2),
                    'installment_number' => $nextInstallment->installment_number,
                ] : null,
            ], 201);

        } catch (QueryException $e) {
            // Race de idempotencia: dos requests con la misma key pasaron el SELECT
            // inicial y el segundo violó el índice único de payments.idempotency_key.
            // El pago SÍ quedó registrado (por el otro request) → devolverlo como
            // duplicado/200, no como error 500 (que haría reintentar al cliente offline).
            if ($this->isDuplicateKeyViolation($e)) {
                $existing = Payment::query()
                    ->where('company_id', $user->company_id)
                    ->where('idempotency_key', $idempotencyKey)
                    ->first();

                if ($existing) {
                    return response()->json([
                        'message' => 'Este pago ya fue registrado.',
                        'duplicate' => true,
                        'payment' => $this->formatPaymentResponse($existing),
                    ], 200);
                }
            }

            Log::error('PWA Payment: Error', [
                'credit_id' => $request->credit_id,
                'amount' => $request->amount,
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);

            return response()->json([
                'message' => 'Error al registrar el pago. Intenta de nuevo.',
            ], 500);
        } catch (\Exception $e) {
            Log::error('PWA Payment: Error', [
                'credit_id' => $request->credit_id,
                'amount' => $request->amount,
                'user_id' => $user->id,
                'error' => $e->getMessage(),
            ]);

            return response()->json([
                'message' => 'Error al registrar el pago. Intenta de nuevo.',
            ], 500);
        }
    }

    /**
     * POST /api/pwa/payments/{id}/void
     *
     * Anula (reversa) un pago. Solo admin. "Anular" = REVERSAR con auditoria
     * (mismo mecanismo que el panel /admin), nunca un borrado fisico.
     */
    public function void(Request $request, int $id): JsonResponse
    {
        if (! $this->canVoidPayments($request)) {
            return response()->json([
                'message' => 'No tienes permiso para anular pagos.',
            ], 403);
        }

        $user = $request->user();

        $payment = Payment::where('company_id', $user->company_id)->find($id);

        if (! $payment) {
            return response()->json([
                'message' => 'Pago no encontrado o no tienes acceso.',
            ], 404);
        }

        $data = $request->validate([
            'reason' => ['required', 'string', 'max:500'],
        ]);

        if (! $this->paymentManager->canDeletePayment($payment)) {
            return response()->json([
                'message' => 'Este pago no se puede anular (ya anulado, credito cerrado o con auditoria critica).',
            ], 422);
        }

        $this->paymentManager->deletePayment($payment, $data['reason']);

        return response()->json([
            'message' => 'Pago anulado correctamente.',
        ]);
    }

    /**
     * 409 "posible duplicado": con los datos del pago que ya está, para que la PWA
     * pregunte "¿Es un cobro distinto?". El mensaje lo muestra tal cual un teléfono
     * con el bundle anterior, que no sabe preguntar: por eso dice cómo seguir.
     */
    private function possibleDuplicateResponse(Payment $existing): JsonResponse
    {
        // La hora en que se cobró: la del teléfono si vino de la cola, si no la de llegada.
        $capturedAt = ($existing->offline_created_at ?? $existing->created_at)
            ->copy()
            ->setTimezone((string) config('app.timezone'));
        $amount = (float) $existing->amount;
        $day = $existing->payment_date->isToday() ? 'hoy' : 'el '.$existing->payment_date->format('d/m/Y');
        $pesos = '$'.number_format($amount, floor($amount) === $amount ? 0 : 2, ',', '.');

        return response()->json([
            'message' => "Ya hay un pago de {$pesos} en este crédito {$day} a las {$capturedAt->format('H:i')} (pago #{$existing->id}). "
                .'Si es un cobro distinto, cierra y vuelve a abrir la app para confirmarlo.',
            'possible_duplicate' => true,
            'existing_payment' => [
                'id' => $existing->id,
                'amount' => round($amount, 2),
                'payment_date' => $existing->payment_date->toDateString(),
                'captured_at' => $capturedAt->toIso8601String(),
            ],
        ], 409);
    }

    /**
     * ¿La excepción es una violación de clave única (MySQL 1062 / SQLSTATE 23000)?
     */
    private function isDuplicateKeyViolation(QueryException $e): bool
    {
        return (int) ($e->errorInfo[1] ?? 0) === 1062;
    }

    /**
     * GET /api/pwa/payments/today
     *
     * Lista pagos registrados hoy.
     * - Collector: solo los propios
     * - Supervisor: solo los de los cobradores de su equipo asignado
     *   (acepta ?collector_id para filtrar a uno de su equipo)
     * - Admin: todos los de la empresa (acepta ?collector_id para filtrar)
     */
    public function today(Request $request): JsonResponse
    {
        $user = $request->user();
        $today = Carbon::today()->toDateString();
        $role = $this->getUserPwaRole($request);

        $query = Payment::query()
            ->where('company_id', $user->company_id)
            ->whereDate('payment_date', $today)
            ->collections()
            ->with(['credit.client:id,name'])
            ->orderBy('created_at', 'desc');

        if ($role === 'collector') {
            $query->where('registered_by_user_id', $user->id);
        } elseif ($role === 'supervisor') {
            $visibility = app(CollectorVisibilityResolver::class)->resolve($user);
            if ($visibility['mode'] === CollectorVisibilityResolver::MODE_ALL) {
                // supervisor con sees_all_collectors — ve toda la empresa; acepta
                // filtro opcional validado contra la empresa
                $this->applyCollectorIdFilter($query, $request, 'registered_by_user_id');
            } elseif ($visibility['mode'] === CollectorVisibilityResolver::MODE_ASSIGNED) {
                $visibleIds = $visibility['collector_ids'];
                if ($request->filled('collector_id') && in_array((int) $request->get('collector_id'), $visibleIds)) {
                    $query->where('registered_by_user_id', (int) $request->get('collector_id'));
                } else {
                    $query->whereIn('registered_by_user_id', $visibleIds);
                }
            } else {
                // MODE_UNASSIGNED: sin cobradores asignados → resultado vacío
                $query->whereRaw('1 = 0');
            }
        } elseif ($request->filled('collector_id')) {
            // admin con ?collector_id explícito, validado contra la empresa
            $this->applyCollectorIdFilter($query, $request, 'registered_by_user_id');
        }

        $payments = $query->get();

        $data = $payments->map(fn ($payment) => [
            'id' => $payment->id,
            'credit_id' => $payment->credit_id,
            'client_name' => $payment->credit?->client?->name ?? 'N/A',
            'amount' => round((float) $payment->amount, 2),
            'payment_method' => $payment->payment_method,
            'payment_method_label' => $this->getPaymentMethodLabel($payment->payment_method),
            // payment_date se incluye explícitamente para que el frontend pueda
            // persistir cada registro en db.payments con el índice correcto.
            // Sin este campo, el offline query por payment_date no encontraría
            // los pagos que se descargaron por este endpoint (vs. syncData).
            'payment_date' => $payment->payment_date->toDateString(),
            'registered_at' => $payment->created_at->toIso8601String(),
            'idempotency_key' => $payment->idempotency_key,
        ]);

        return response()->json([
            'data' => $data->values(),
            'total_amount' => round($data->sum('amount'), 2),
            'total_count' => $data->count(),
            'date' => $today,
        ]);
    }

    /**
     * GET /api/pwa/payments/{id}
     *
     * Detalle de un pago específico.
     * - Collector: solo pagos que él registró
     * - Supervisor/Admin: cualquier pago de la empresa
     */
    public function show(Request $request, int $id): JsonResponse
    {
        $user = $request->user();
        $role = $this->getUserPwaRole($request);

        $query = Payment::query()
            ->where('company_id', $user->company_id)
            ->with([
                'credit.client:id,name,phone',
                'installments',
            ]);

        if ($role === 'collector') {
            $query->where('registered_by_user_id', $user->id);
        }

        $payment = $query->find($id);

        if (! $payment) {
            return response()->json([
                'message' => 'Pago no encontrado.',
            ], 404);
        }

        return response()->json([
            'id' => $payment->id,
            'credit_id' => $payment->credit_id,
            'client' => [
                'id' => $payment->credit?->client?->id,
                'name' => $payment->credit?->client?->name,
                'phone' => $payment->credit?->client?->phone,
            ],
            'amount' => round((float) $payment->amount, 2),
            'payment_method' => $payment->payment_method,
            'payment_method_label' => $this->getPaymentMethodLabel($payment->payment_method),
            'payment_date' => $payment->payment_date->toDateString(),
            'registered_at' => $payment->created_at->toIso8601String(),
            'voided' => $payment->voided,
            'installments_affected' => $payment->installments->map(fn ($inst) => [
                'id' => $inst->id,
                'number' => $inst->installment_number,
                'amount_applied' => round((float) $inst->pivot->applied_amount, 2),
            ])->values(),
        ]);
    }

    /**
     * GET /api/pwa/payments/{id}/receipt
     *
     * Datos completos para generar recibo de pago.
     * - Collector: solo recibos de pagos que él registró
     * - Supervisor/Admin: cualquier recibo de la empresa
     */
    public function receipt(Request $request, int $id): JsonResponse
    {
        $user = $request->user();
        $role = $this->getUserPwaRole($request);

        $query = Payment::query()
            ->where('company_id', $user->company_id)
            ->with([
                'credit.client:id,name,phone,identification',
                'credit.company:id,name,phone,email,address',
                'installments',
                'registeredBy:id,name',
            ]);

        // El cobrador puede emitir el recibo de cualquier pago de UN CRÉDITO
        // SUYO, no solo de los que registró él.
        //
        // Antes el filtro era `registered_by_user_id`, y eso rompía justo el caso
        // de uso: en el historial del crédito el cobrador ya ve todos los pagos,
        // así que negarle el comprobante de uno que registró el supervisor sería
        // incoherente. El recibo no revela nada que no esté viendo ya. La
        // frontera sigue siendo la de siempre: los créditos asignados a él.
        if ($role === 'collector') {
            $query->whereHas('credit', fn ($q) => $q->where('collector_user_id', $user->id));
        }

        $payment = $query->find($id);

        if (! $payment) {
            return response()->json([
                'message' => 'Pago no encontrado.',
            ], 404);
        }

        $credit = $payment->credit;

        // Calcular balance antes y después del pago
        // El balance actual es el balance después del pago
        $balanceAfter = (float) $credit->remaining_balance;
        $balanceBefore = $balanceAfter + (float) $payment->amount;

        // Calcular cuotas restantes al momento del pago
        $installmentsRemaining = $credit->installments
            ->where('status', '!=', 'paid')
            ->count();

        return response()->json([
            'receipt' => [
                'id' => $payment->id,
                'receipt_number' => str_pad((string) $payment->id, 8, '0', STR_PAD_LEFT),
                'date' => $payment->payment_date->toDateString(),
                'time' => $payment->created_at->format('H:i'),
                'amount' => round((float) $payment->amount, 2),
                'payment_method' => $payment->payment_method,
                'payment_method_label' => $this->getPaymentMethodLabel($payment->payment_method),
            ],
            'company' => [
                'name' => $credit->company?->name ?? 'Empresa',
                'phone' => $credit->company?->phone,
                'email' => $credit->company?->email,
                'address' => $credit->company?->address,
            ],
            'client' => [
                'name' => $credit->client?->name ?? 'N/A',
                'identification' => $credit->client?->identification,
                'phone' => $credit->client?->phone,
            ],
            'credit' => [
                'id' => $credit->id,
                'code' => $credit->code,
                'company_credit_number' => $credit->company_credit_number,
                'amount' => round((float) $credit->amount, 2),
                'total_receivable' => round((float) $credit->total_receivable, 2),
                'periodicity' => $credit->periodicity,
            ],
            'balance' => [
                'before' => round($balanceBefore, 2),
                'after' => round($balanceAfter, 2),
                'paid_amount' => round((float) $payment->amount, 2),
            ],
            'installments' => [
                'affected' => $payment->installments->map(fn ($inst) => [
                    'number' => $inst->installment_number,
                    'amount_applied' => round((float) $inst->pivot->applied_amount, 2),
                    'new_status' => $inst->status,
                    'new_status_label' => $inst->getStatusLabel(),
                ])->values(),
                'remaining' => $installmentsRemaining,
                'total' => $credit->installments->count(),
            ],
            // Historial completo del crédito, estilo factura de supermercado: el
            // cliente ve de un vistazo todo lo que ha abonado y no solo el pago
            // de hoy. Es el soporte que pidió el cobrador para entregar en mano.
            'history' => $this->receiptHistory($credit, $payment),
            'registered_by' => $payment->registeredBy?->name ?? 'Sistema',
            'registered_at' => $payment->created_at->toIso8601String(),
        ]);
    }

    /**
     * Pagos del crédito hasta la fecha del recibo, del más antiguo al más nuevo.
     *
     * Se corta en el pago del recibo: un comprobante no puede mostrar abonos
     * posteriores a sí mismo — si se reimprime meses después, tiene que seguir
     * diciendo lo mismo que el día que se entregó.
     *
     * `collections()` excluye anulados y sus asientos de reversión: en el papel
     * del cliente no caben movimientos que no fueron dinero recibido.
     *
     * @return array<int, array<string, mixed>>
     */
    private function receiptHistory(Credit $credit, Payment $payment): array
    {
        $acumulado = 0.0;

        return $credit->payments()
            ->collections()
            ->where('id', '<=', $payment->id)
            ->orderBy('payment_date')
            ->orderBy('id')
            ->get()
            ->map(function (Payment $p) use (&$acumulado, $payment): array {
                $acumulado += (float) $p->amount;

                return [
                    'id' => $p->id,
                    'date' => $p->payment_date->toDateString(),
                    'amount' => round((float) $p->amount, 2),
                    'accumulated' => round($acumulado, 2),
                    'method_label' => $this->getPaymentMethodLabel($p->payment_method),
                    // Para resaltar en el recibo cuál es el pago de hoy.
                    'is_current' => $p->id === $payment->id,
                ];
            })
            ->values()
            ->all();
    }

    /**
     * Formatea la respuesta de un pago.
     */
    private function formatPaymentResponse(Payment $payment): array
    {
        $payment->load(['installments', 'credit.client:id,name']);

        return [
            'id' => $payment->id,
            'credit_id' => $payment->credit_id,
            'client_name' => $payment->credit?->client?->name ?? 'N/A',
            'amount' => round((float) $payment->amount, 2),
            'payment_method' => $payment->payment_method,
            'payment_date' => $payment->payment_date->toDateString(),
            'registered_at' => $payment->created_at->toIso8601String(),
            'idempotency_key' => $payment->idempotency_key,
            'installments_affected' => $payment->installments->map(fn ($inst) => [
                'id' => $inst->id,
                'number' => $inst->installment_number,
                'new_status' => $inst->status,
                'amount_applied' => round((float) $inst->pivot->applied_amount, 2),
            ])->values(),
        ];
    }

    /**
     * Obtiene label legible del método de pago.
     */
    private function getPaymentMethodLabel(?string $method): string
    {
        return match ($method) {
            'cash' => 'Efectivo',
            'transfer' => 'Transferencia',
            'mobile' => 'Pago móvil',
            default => 'No especificado',
        };
    }
}
