<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\Pwa;

use App\Enums\ExpenseCategory;
use App\Http\Controllers\Api\Pwa\Traits\RoleAwareQueries;
use App\Http\Controllers\Controller;
use App\Models\CompanyFinancialSettings;
use App\Models\Expense;
use App\Services\FinanceAutoLogger;
use App\Support\SimplifiedAmount;
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 ExpenseController extends Controller
{
    use RoleAwareQueries;

    /**
     * Categorías disponibles para registro manual desde la PWA.
     * Se excluyen las categorías de capital (PROFIT_WITHDRAWAL, CAPITAL_RETURN,
     * DIVIDEND_PAYMENT) que solo se usan a través del módulo de socios.
     * Se excluyen también las categorías del sistema (crédito, ajuste financiero).
     */
    private const ALLOWED_CATEGORIES = [
        'salary_wages',
        'rent',
        'utilities',
        'transportation',
        'marketing',
        'office_supplies',
        'professional_services',
        'commission',
        'bank_fees',
        'other_operational',
        'bad_debt_provision',
        'refund',
        'adjustment',
    ];
    // bad_debt_writeoff queda FUERA: requiere credit_id (ExpenseCategory::requiresCreditId)
    // y el hook del modelo lanza si falta → un castigo no es un gasto operativo suelto;
    // se registra ligado a su crédito (p. ej. cierre/condonación), no desde este endpoint.

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

    /**
     * POST /api/pwa/expenses
     *
     * Registra un gasto operativo desde la PWA.
     * Accesible para collector, supervisor y admin.
     *
     * - Collector/Supervisor → requires_approval = true (entra al flujo de aprobación)
     * - Admin               → auto-aprobado
     */
    public function store(Request $request): JsonResponse
    {
        $user = $request->user();

        // Las mismas reglas que SyncController::syncExpenses(): desde PWA-002 la
        // misma fila de la cola llega por acá y, si la respuesta se pierde, por el
        // lote. Se convierte por el flag del ítem aunque la empresa haya apagado
        // el modo (un teléfono con los ajustes viejos en caché lo sigue mandando).
        // El flag se lee como lo lee el lote: "true" o "on" no son un 422.
        if ($request->has('is_simplified_amount')) {
            $request->merge(['is_simplified_amount' => SimplifiedAmount::flagged($request->all())]);
        }
        if (SimplifiedAmount::applies($request->all(), 'amount')) {
            $settings = CompanyFinancialSettings::forCompany($user->company_id);
            $request->merge(SimplifiedAmount::toReal($request->only('amount', 'is_simplified_amount'), 'amount', $settings));
        }

        $request->validate([
            'amount' => ['required', 'numeric', 'min:0.01', 'max:9999999.99'],
            'category' => ['required', 'string', 'in:'.implode(',', self::ALLOWED_CATEGORIES)],
            'notes' => ['nullable', 'string', 'max:1000'],
            // No futura, con un día de margen por la zona horaria del teléfono.
            'date' => ['nullable', 'date', 'before_or_equal:'.Carbon::tomorrow()->toDateString()],
            'is_simplified_amount' => ['nullable', 'boolean'],
            'simplified_amount_confirmed' => ['nullable', 'boolean'],
            'idempotency_key' => ['nullable', 'uuid'],
        ]);

        $companyId = (int) $user->company_id;
        $clave = $request->input('idempotency_key');

        // PWA-002: la respuesta de un envío anterior con esta clave se perdió y el
        // teléfono lo reintenta (o lo sube el lote). El gasto ya existe: se
        // devuelve en vez de crear otro.
        if (is_string($clave) && ($existente = $this->gastoConClave($companyId, $clave))) {
            return $this->gastoDuplicado($existente, $clave, $user->id, 'busqueda');
        }

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

        try {
            // Crear y aprobar van juntos: un gasto a medio escribir (insertado con
            // los defaults, sin la política de aprobación) quedaría como la
            // respuesta de todos los reintentos con esa clave. Igual que el lote.
            $expense = DB::transaction(function () use ($companyId, $user, $category, $opDate, $request, $clave) {
                $expense = $this->logger->logExpense([
                    'company_id' => $companyId,
                    'user_id' => $user->id,
                    'amount' => (float) $request->amount,
                    'category' => $category->value,
                    'operation_date' => $opDate,
                    'notes' => $request->notes,
                    'idempotency_key' => $clave,
                    // El lote deja `pwa_sync`: así se sabe por cuál de los dos entró.
                    'metadata' => ['source' => 'pwa_direct'],
                ]);

                // Flujo de aprobación según rol (fuente única en el modelo Expense):
                // admin → auto-aprobado; supervisor/collector → pendiente de aprobación.
                $expense->applyApprovalPolicyForCreator($user);
                $expense->save();

                return $expense;
            });
        } catch (UniqueConstraintViolationException $e) {
            // Carrera: otro envío con la misma clave se creó entre la búsqueda y
            // este insert, y el índice único frenó a este. La transacción ya se
            // deshizo, así que la búsqueda ve el gasto del otro envío.
            if (! is_string($clave) || ! ($existente = $this->gastoConClave($companyId, $clave))) {
                throw $e;
            }

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

        return response()->json([
            'message' => $expense->requires_approval
                ? 'Gasto registrado. Queda pendiente de aprobación.'
                : 'Gasto registrado y aprobado.',
            'expense' => $this->formatoGasto($expense),
        ], 201);
    }

    private function gastoConClave(int $companyId, string $clave): ?Expense
    {
        return Expense::withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->where('idempotency_key', $clave)
            ->first();
    }

    /**
     * @param  string  $via  Por dónde se atrapó el duplicado: 'busqueda' (antes de
     *                       crear) o 'carrera' (índice único).
     */
    private function gastoDuplicado(Expense $expense, string $clave, int $userId, string $via): JsonResponse
    {
        Log::info('PWA Expense: Duplicate prevented', [
            'idempotency_key' => $clave,
            'existing_expense_id' => $expense->id,
            'company_id' => $expense->company_id,
            'user_id' => $userId,
            'via' => $via,
        ]);

        return response()->json([
            'message' => 'Este gasto ya fue registrado.',
            'duplicate' => true,
            'expense' => $this->formatoGasto($expense),
        ]);
    }

    /** @return array<string, mixed> */
    private function formatoGasto(Expense $expense): array
    {
        $category = $expense->getRawOriginal('category');

        return [
            'id' => $expense->id,
            'category' => $category,
            'category_label' => ExpenseCategory::from($category)->getLabel(),
            'amount' => (float) $expense->amount,
            'date' => $expense->operation_date->toDateString(),
            'notes' => $expense->notes,
            'approval_status' => $expense->approval_status,
            'requires_approval' => $expense->requires_approval,
        ];
    }

    /**
     * GET /api/pwa/expenses/categories
     *
     * Devuelve las categorías de gasto disponibles para la PWA.
     */
    public function categories(): JsonResponse
    {
        $cats = collect(self::ALLOWED_CATEGORIES)->map(function (string $value) {
            $enum = ExpenseCategory::tryFrom($value);

            return [
                'value' => $value,
                'label' => $enum?->getLabel() ?? $value,
            ];
        });

        return response()->json(['data' => $cats->values()]);
    }

    /**
     * GET /api/pwa/expenses/mine
     *
     * Los gastos que registró QUIEN PREGUNTA, con su estado de aprobación.
     *
     * Existe porque el cobrador no tenía dónde verlos: el desglose de gastos
     * vive en `/pwa/financials`, que es solo de admin. Registraba un gasto y
     * perdía el rastro — sin saber siquiera si se lo habían aprobado.
     *
     * El filtro es `user_id` (quien lo registró) y no `collector_user_id` (a
     * quién se le imputa): lo que el cobrador quiere ver es lo que él metió.
     */
    public function mine(Request $request): JsonResponse
    {
        $user = $request->user();

        $datos = $request->validate([
            'period' => ['nullable', 'in:today,week,month'],
        ]);

        $desde = match ($datos['period'] ?? 'today') {
            'week' => Carbon::today()->startOfWeek(),
            'month' => Carbon::today()->startOfMonth(),
            default => Carbon::today(),
        };

        $gastos = Expense::query()
            // Los créditos que entregó no son gastos suyos, aunque el sistema
            // los registre como tal: ya tienen su propia cifra.
            ->withoutDisbursements()
            ->where('company_id', $user->company_id)
            ->where('user_id', $user->id)
            ->whereDate('operation_date', '>=', $desde)
            ->orderByDesc('operation_date')
            ->orderByDesc('id')
            ->limit(200)
            ->get();

        return response()->json([
            'data' => $gastos->map(fn (Expense $gasto): array => [
                'id' => $gasto->id,
                'date' => $gasto->operation_date?->toDateString(),
                'amount' => round((float) $gasto->amount, 2),
                // `category` viene casteada al enum por el modelo, así que no
                // hay que reconstruirla desde texto.
                'category' => $gasto->category->value,
                'category_label' => $gasto->category->getLabel(),
                'notes' => $gasto->notes,
                'approval_status' => $gasto->approval_status,
                'requires_approval' => (bool) $gasto->requires_approval,
                // Si la respuesta se perdió, el gasto está aquí y en la cola del
                // teléfono a la vez: la PWA los empareja por la clave para no
                // mostrarlo como "sin subir" cuando ya subió.
                'idempotency_key' => $gasto->idempotency_key,
            ])->values(),
            'totals' => [
                'amount' => round((float) $gastos->sum('amount'), 2),
                'count' => $gastos->count(),
                'pending' => $gastos->where('approval_status', 'pending')->count(),
            ],
            'period' => $datos['period'] ?? 'today',
        ]);
    }
}
