<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\Pwa;

use App\Enums\ContributionType;
use App\Enums\PartnerStatus;
use App\Enums\WithdrawalType;
use App\Http\Controllers\Api\Pwa\Traits\RoleAwareQueries;
use App\Http\Controllers\Controller;
use App\Models\CapitalContribution;
use App\Models\CompanyFinancialSettings;
use App\Models\Partner;
use App\Models\PartnerWithdrawal;
use App\Services\FinanceAutoLogger;
use Carbon\Carbon;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class PartnerController extends Controller
{
    use RoleAwareQueries;

    public function __construct(
        private readonly FinanceAutoLogger $logger
    ) {}

    /**
     * GET /api/pwa/partners
     *
     * Lista los socios activos del tenant con sus balances de capital.
     * Exclusivo para el rol admin.
     */
    public function index(Request $request): JsonResponse
    {
        if ($this->getUserPwaRole($request) !== 'admin') {
            return response()->json(['message' => 'No autorizado. Solo el admin puede ver los socios.'], 403);
        }

        $companyId = $request->user()->company_id;

        $partners = Partner::where('company_id', $companyId)
            ->orderBy('name')
            ->get();

        $totalCapital = $partners->sum(fn (Partner $p) => (float) $p->getTotalContributed()->value())
            - $partners->sum(fn (Partner $p) => (float) $p->getTotalWithdrawn()->value());

        return response()->json([
            'data' => $partners->map(fn (Partner $p) => $this->formatPartner($p)),
            'meta' => [
                'total_partners' => $partners->count(),
                'active_partners' => $partners->filter->isActive()->count(),
                'total_capital' => round($totalCapital, 2),
            ],
        ]);
    }

    /**
     * POST /api/pwa/partners/{id}/transaction
     *
     * Registra un aporte o retiro para un socio.
     * Exclusivo para el rol admin.
     *
     * Body:
     *   transaction_type: 'contribution' | 'withdrawal'
     *   amount: float
     *   withdrawal_type: 'profit_withdrawal' | 'capital_return' | 'dividend_payment'  (solo para retiro)
     *   contribution_type: 'initial' | 'additional' | 'reinvestment'  (solo para aporte, default: additional)
     *   date: Y-m-d (opcional, default: today)
     *   notes: string (opcional)
     *   idempotency_key: uuid (opcional; un reintento con la misma clave devuelve el movimiento original)
     */
    public function storeTransaction(Request $request, int $id): JsonResponse
    {
        if ($this->getUserPwaRole($request) !== 'admin') {
            return response()->json(['message' => 'No autorizado.'], 403);
        }

        if ($request->boolean('is_simplified_amount') && $request->filled('amount')) {
            $settings = CompanyFinancialSettings::forCompany($request->user()->company_id);
            if ($settings->isSimplifiedInputEnabled()) {
                $request->merge(['amount' => $settings->convertSimplifiedToReal((float) $request->amount)]);
            }
        }

        $request->validate([
            'transaction_type' => ['required', 'in:contribution,withdrawal'],
            'amount' => ['required', 'numeric', 'min:0.01', 'max:9999999.99'],
            'withdrawal_type' => ['required_if:transaction_type,withdrawal', 'nullable',
                'in:profit_withdrawal,capital_return,dividend_payment'],
            'contribution_type' => ['nullable', 'in:initial,additional,reinvestment'],
            'method' => ['nullable', 'in:cash,transfer,card,mobile_money,other'],
            'date' => ['nullable', 'date'],
            'notes' => ['nullable', 'string', 'max:1000'],
            'is_simplified_amount' => ['nullable', 'boolean'],
            'simplified_amount_confirmed' => ['nullable', 'boolean'],
            'idempotency_key' => ['nullable', 'uuid'],
        ]);

        $user = $request->user();
        $companyId = (int) $user->company_id;

        $partner = Partner::where('company_id', $companyId)->findOrFail($id);
        $clave = $request->input('idempotency_key');

        // PWA-002: reintento de un movimiento que ya quedó (se perdió la
        // respuesta). Se devuelve el original ANTES del chequeo de saldo: el
        // primer intento ya lo descontó y el reintento se rechazaría por eso.
        if (is_string($clave) && ($existente = $this->movimientoConClave($companyId, $clave))) {
            return $this->movimientoDuplicado($existente, $partner, $clave, $user->id, 'busqueda');
        }

        // Validar saldo disponible antes de iniciar la transacción DB.
        // Un retiro que supere el balance actual dejaría al socio en negativo.
        if ($request->transaction_type === 'withdrawal') {
            $currentBalance = (float) $partner->getCurrentBalance()->value();
            $requestedAmount = (float) $request->amount;

            if ($requestedAmount > $currentBalance + 0.01) {
                // Carrera: otro envío con la misma clave pudo registrar su retiro
                // (y bajar el saldo) entre la búsqueda y este chequeo. Ese 422 no
                // es un rechazo real: el teléfono soltaría la clave aunque el
                // retiro exista. Se busca de nuevo antes de rechazar.
                if (is_string($clave) && ($existente = $this->movimientoConClave($companyId, $clave))) {
                    return $this->movimientoDuplicado($existente, $partner, $clave, $user->id, 'saldo');
                }

                return response()->json([
                    'message' => 'El monto del retiro supera el saldo disponible del socio.',
                    'errors' => [
                        'amount' => [
                            "El saldo disponible es \${$currentBalance}. No se puede retirar \${$requestedAmount}.",
                        ],
                    ],
                    'current_balance' => round($currentBalance, 2),
                ], 422);
            }
        }

        $opDate = $request->filled('date')
            ? Carbon::parse($request->date)->toDateString()
            : Carbon::today()->toDateString();

        try {
            $result = DB::transaction(function () use ($request, $partner, $user, $companyId, $opDate, $clave) {
                if ($request->transaction_type === 'contribution') {
                    return $this->createContribution($request, $partner, $user, $companyId, $opDate, $clave);
                }

                return $this->createWithdrawal($request, $partner, $user, $companyId, $opDate, $clave);
            });
        } catch (UniqueConstraintViolationException $e) {
            // Carrera: otro envío con la misma clave se registró entre la búsqueda
            // y este insert; el índice único lo frenó y la transacción (con el
            // gasto del retiro) volvió atrás. El movimiento es el del otro envío.
            $existente = is_string($clave) ? $this->movimientoConClave($companyId, $clave) : null;
            if (! $existente) {
                throw $e;
            }

            return $this->movimientoDuplicado($existente, $partner, $clave, $user->id, 'carrera');
        }

        // Refrescar partner para devolver balances actualizados
        $partner->refresh();

        return response()->json([
            'message' => $result['message'],
            'transaction' => $result['transaction'],
            'partner' => $this->formatPartner($partner),
        ], 201);
    }

    // ─────────────────────────────────────────────────────────────────────
    // Private helpers
    // ─────────────────────────────────────────────────────────────────────

    /** La clave nombra la operación, sea aporte o retiro. */
    private function movimientoConClave(int $companyId, string $clave): CapitalContribution|PartnerWithdrawal|null
    {
        return CapitalContribution::query()
            ->where('company_id', $companyId)
            ->where('idempotency_key', $clave)
            ->first()
            ?? PartnerWithdrawal::query()
                ->where('company_id', $companyId)
                ->where('idempotency_key', $clave)
                ->first();
    }

    /**
     * @param  string  $via  Por dónde se atrapó el duplicado: 'busqueda' (antes del
     *                       chequeo de saldo), 'saldo' (el saldo lo rechazó porque el
     *                       otro envío ya lo había reducido) o 'carrera' (índice único).
     */
    private function movimientoDuplicado(
        CapitalContribution|PartnerWithdrawal $movimiento, Partner $partner, string $clave, int $userId, string $via
    ): JsonResponse {
        Log::info('PWA Partner: Duplicate prevented', [
            'idempotency_key' => $clave,
            'existing_transaction_id' => $movimiento->id,
            'transaction_type' => $movimiento instanceof CapitalContribution ? 'contribution' : 'withdrawal',
            'user_id' => $userId,
            'company_id' => $movimiento->company_id,
            'via' => $via,
        ]);

        // El socio del movimiento, no el de la URL: si una clave se reusara con
        // otro socio, la PWA actualizaría la tarjeta equivocada.
        $socio = Partner::where('company_id', $movimiento->company_id)->find($movimiento->partner_id) ?? $partner;
        $socio->refresh();

        return response()->json([
            'message' => 'Este movimiento ya estaba registrado.',
            'duplicate' => true,
            'transaction' => $movimiento instanceof CapitalContribution
                ? $this->formatoAporte($movimiento)
                : $this->formatoRetiro($movimiento),
            'partner' => $this->formatPartner($socio),
        ]);
    }

    /** @return array<string, mixed> */
    private function formatoAporte(CapitalContribution $contribution): array
    {
        return [
            'id' => $contribution->id,
            'type' => 'contribution',
            'label' => $contribution->type->getLabel(),
            'amount' => (float) $contribution->amount,
            'date' => $contribution->operation_date->toDateString(),
            'notes' => $contribution->notes,
        ];
    }

    /** @return array<string, mixed> */
    private function formatoRetiro(PartnerWithdrawal $withdrawal): array
    {
        return [
            'id' => $withdrawal->id,
            'type' => 'withdrawal',
            'label' => $withdrawal->type->getLabel(),
            'amount' => (float) $withdrawal->amount,
            'date' => $withdrawal->operation_date->toDateString(),
            'notes' => $withdrawal->notes,
            'expense_id' => $withdrawal->expense_id,
        ];
    }

    private function createContribution(
        Request $request, Partner $partner, $user, int $companyId, string $opDate, ?string $clave
    ): array {
        $contribType = ContributionType::from($request->contribution_type ?? 'additional');

        $contribution = CapitalContribution::create([
            'company_id' => $companyId,
            'partner_id' => $partner->id,
            'amount' => (float) $request->amount,
            'type' => $contribType->value,
            'method' => $request->method,
            'operation_date' => $opDate,
            'notes' => $request->notes,
            'user_id' => $user->id,
            'idempotency_key' => $clave,
        ]);

        return [
            'message' => 'Aporte registrado correctamente.',
            'transaction' => $this->formatoAporte($contribution),
        ];
    }

    private function createWithdrawal(
        Request $request, Partner $partner, $user, int $companyId, string $opDate, ?string $clave
    ): array {
        $withdrawalType = WithdrawalType::from($request->withdrawal_type);
        $expenseCategory = $withdrawalType->toExpenseCategory();

        // Crear el gasto contable vía FinanceAutoLogger
        $expense = $this->logger->logExpense([
            'company_id' => $companyId,
            'user_id' => $user->id,
            'amount' => (float) $request->amount,
            'category' => $expenseCategory->value,
            'operation_date' => $opDate,
            'notes' => $request->notes,
        ]);

        // Auto-aprobar: el admin que crea el retiro también es el aprobador.
        // (Endpoint admin-only → applyApprovalPolicyForCreator deja el egreso aprobado.)
        $expense->applyApprovalPolicyForCreator($user);
        $expense->save();

        // Crear el retiro de socio vinculado al gasto
        $withdrawal = PartnerWithdrawal::create([
            'company_id' => $companyId,
            'partner_id' => $partner->id,
            'amount' => (float) $request->amount,
            'type' => $withdrawalType->value,
            'method' => $request->method,
            'expense_id' => $expense->id,
            'operation_date' => $opDate,
            'notes' => $request->notes,
            'user_id' => $user->id,
            'approval_status' => 'approved',
            'approved_by_user_id' => $user->id,
            'approved_at' => now(),
            'idempotency_key' => $clave,
        ]);

        return [
            'message' => 'Retiro registrado correctamente.',
            'transaction' => $this->formatoRetiro($withdrawal),
        ];
    }

    private function formatPartner(Partner $p): array
    {
        $contributed = (float) $p->getTotalContributed()->value();
        $withdrawn = (float) $p->getTotalWithdrawn()->value();
        $balance = (float) $p->getCurrentBalance()->value();

        return [
            'id' => $p->id,
            'name' => $p->name,
            'email' => $p->email,
            'phone' => $p->phone,
            'status' => $p->status instanceof PartnerStatus
                ? $p->status->value
                : (string) $p->status,
            'is_active' => $p->isActive(),
            'joined_at' => $p->joined_at?->toDateString(),
            'balance' => [
                'contributed' => round($contributed, 2),
                'withdrawn' => round($withdrawn, 2),
                'current' => round($balance, 2),
            ],
        ];
    }
}
