<?php

namespace App\Services;

use App\Models\GarmentType;
use App\Models\LaundryPiecePrice;
use App\Models\LaundrySaleProduct;
use App\Models\PricingRule;
use App\Models\Etapa;
use App\Support\LaundryPieceQuantityBilling;
use App\Support\LaundryWeightRounding;
use App\Support\OrdenLavanderiaDetalleLineValidator;
use Carbon\Carbon;
use Illuminate\Validation\ValidationException;

/**
 * Resolución de precios TIBU (C2): matriz por pieza + precio por kg vigente.
 * Los totales se persisten en la línea de orden (snapshot); cambios posteriores en tablas no alteran órdenes guardadas.
 */
class LaundryPricingService
{
    /**
     * Precio por pieza para modo by_garment_type: primero fila con load_type_id concreto, luego fila con load_type_id null.
     */
    public function resolvePieceUnitPrice(int $etapaId, int $garmentTypeId, ?int $loadTypeId): ?float
    {
        $base = LaundryPiecePrice::query()
            ->where('is_active', true)
            ->where('etapa_id', $etapaId)
            ->where('garment_type_id', $garmentTypeId);

        if ($loadTypeId !== null) {
            $specific = (clone $base)->where('load_type_id', $loadTypeId)->first();
            if ($specific !== null) {
                return (float) $specific->unit_price;
            }
        }

        $fallback = (clone $base)->whereNull('load_type_id')->first();

        return $fallback !== null ? (float) $fallback->unit_price : null;
    }

    /**
     * Precio por kg vigente a la fecha (cobro por peso). Sin regla activa → null.
     *
     * @param  \Carbon\Carbon|string|null  $fecha
     */
    public function resolvePricePerKg($fecha = null): ?float
    {
        $d = $fecha === null ? Carbon::today() : ($fecha instanceof Carbon ? $fecha : Carbon::parse($fecha));

        $rule = PricingRule::query()
            ->vigenteEn($d)
            ->orderByDesc('effective_from')
            ->orderByDesc('id')
            ->first();

        return $rule !== null ? (float) $rule->price_per_kg : null;
    }

    /**
     * Vista previa de totales TIBU (C6 + D2):
     * - aplica default de garment_types solo para tipo de carga cuando falta,
     * - el modo de cobro se infiere del ítem (POS); precios TIBU por pieza vía matriz, por kg vía pricing_rules.
     *
     * @return array{
     *   precio_unitario: float,
     *   subtotal: float,
     *   cantidad: float,
     *   weight_kg: float|null,
     *   resolved_billing_mode: string,
     *   resolved_load_type_id: int|null,
     *   used_default_billing: bool,
     *   used_default_load: bool
     * }
     */
    /**
     * Precio para línea con {@see LaundrySaleProduct} (ADR-001). Usa fila en `laundry_sale_product_prices`
     * para el par (producto, service_type) si existe; si no, `unit_price` del producto.
     * - billing_unit / modo pieza: subtotal = cantidad × unit_price.
     * - modo peso: subtotal = weight_kg × unit_price (el precio del catálogo se interpreta como $/kg).
     *
     * @return array{
     *   precio_unitario: float,
     *   subtotal: float,
     *   cantidad: float,
     *   weight_kg: float|null,
     *   resolved_billing_mode: string,
     *   resolved_load_type_id: int|null,
     *   resolved_etapa_id: int,
     *   used_default_billing: bool,
     *   used_default_load: bool,
     *   product_name: string
     * }
     */
    public function previewSaleCatalogLineMoney(array $item, int $index = 0): array
    {
        $product = LaundrySaleProduct::query()
            ->where('is_active', true)
            ->with('servicePrices')
            ->find((int) ($item['laundry_sale_product_id'] ?? 0));

        if ($product === null) {
            throw ValidationException::withMessages([
                "items.{$index}.laundry_sale_product_id" => ['El producto del catálogo POS no existe o está inactivo.'],
            ]);
        }

        $etapaId = isset($item['etapa_id']) && $item['etapa_id'] !== '' && $item['etapa_id'] !== null
            ? (int) $item['etapa_id']
            : (int) $product->default_etapa_id;

        if ($etapaId <= 0) {
            throw ValidationException::withMessages([
                "items.{$index}.etapa_id" => ['El tipo de servicio es obligatorio si el producto no tiene servicio por defecto.'],
            ]);
        }

        $billingMode = isset($item['billing_mode']) && $item['billing_mode'] !== ''
            ? (string) $item['billing_mode']
            : ($product->billing_unit === LaundrySaleProduct::BILLING_UNIT_KG
                ? OrdenLavanderiaDetalleLineValidator::BILLING_BY_WEIGHT
                : OrdenLavanderiaDetalleLineValidator::BILLING_BY_GARMENT);

        $loadId = isset($item['load_type_id']) && $item['load_type_id'] !== '' && $item['load_type_id'] !== null
            ? (int) $item['load_type_id']
            : $product->default_load_type_id;

        $usedDefaultBilling = ! isset($item['billing_mode']) || $item['billing_mode'] === '' || $item['billing_mode'] === null;
        $usedDefaultLoad = ! isset($item['load_type_id']) || $item['load_type_id'] === '' || $item['load_type_id'] === null;

        $unitPrice = (float) $product->unit_price;
        $byService = $product->servicePrices->firstWhere('etapa_id', $etapaId);
        if ($byService !== null) {
            $unitPrice = (float) $byService->amount;
        }
        if (isset($item['precio_unitario_override']) && $item['precio_unitario_override'] !== '' && $item['precio_unitario_override'] !== null) {
            $unitPrice = max(0, (float) $item['precio_unitario_override']);
            $extraCost = 0.0;
        } elseif (isset($item['extra_cost']) && $item['extra_cost'] !== '' && $item['extra_cost'] !== null) {
            $extraCost = max(0, (float) $item['extra_cost']);
        } else {
            $extraCost = max(0, (float) ($product->extra_cost ?? 0));
        }

        if ($billingMode === OrdenLavanderiaDetalleLineValidator::BILLING_BY_WEIGHT) {
            $realW = (float) ($item['weight_kg'] ?? 0);
            if ($realW <= 0) {
                throw ValidationException::withMessages([
                    "items.{$index}.weight_kg" => ['El peso (kg) es obligatorio y debe ser mayor a cero cuando el cobro es por peso.'],
                ]);
            }
            $billW = LaundryWeightRounding::round($realW);
            $ppk = $unitPrice;
            $sub = round($billW * $ppk + $extraCost, 2);

            return [
                'precio_unitario' => $ppk,
                'subtotal' => $sub,
                'extra_cost' => $extraCost > 0 ? $extraCost : null,
                'cantidad' => $realW,
                'cantidad_cobro' => $billW,
                'weight_kg' => $realW,
                'resolved_billing_mode' => $billingMode,
                'resolved_load_type_id' => $loadId,
                'resolved_etapa_id' => $etapaId,
                'used_default_billing' => $usedDefaultBilling,
                'used_default_load' => $usedDefaultLoad,
                'product_name' => (string) $product->name,
            ];
        }

        $realCant = (float) ($item['cantidad'] ?? 0);
        if ($realCant <= 0) {
            throw ValidationException::withMessages([
                "items.{$index}.cantidad" => ['La cantidad debe ser mayor a cero.'],
            ]);
        }
        $billCant = LaundryPieceQuantityBilling::billingQuantity($realCant);
        $sub = round($billCant * $unitPrice + $extraCost, 2);

        return [
            'precio_unitario' => $unitPrice,
            'subtotal' => $sub,
            'extra_cost' => $extraCost > 0 ? $extraCost : null,
            'cantidad' => $realCant,
            'cantidad_cobro' => $billCant,
            'weight_kg' => isset($item['weight_kg']) ? (float) $item['weight_kg'] : null,
            'resolved_billing_mode' => $billingMode,
            'resolved_load_type_id' => $loadId,
            'resolved_etapa_id' => $etapaId,
            'used_default_billing' => $usedDefaultBilling,
            'used_default_load' => $usedDefaultLoad,
            'product_name' => (string) $product->name,
        ];
    }

    public function previewTibuLineMoney(array $item, int $index = 0): array
    {
        $defaults = $this->resolveItemDefaults($item);
        $mode = $defaults['resolved_billing_mode'];

        if ($mode === OrdenLavanderiaDetalleLineValidator::BILLING_BY_WEIGHT) {
            $ppk = $this->resolvePricePerKg();
            if ($ppk === null) {
                throw ValidationException::withMessages([
                    "items.{$index}.weight_kg" => ['No hay precio por kg vigente configurado (pricing_rules).'],
                ]);
            }
            $realW = (float) ($item['weight_kg'] ?? 0);
            $billW = LaundryWeightRounding::round($realW);
            $sub = round($billW * $ppk, 2);

            return [
                'precio_unitario' => $ppk,
                'subtotal' => $sub,
                'cantidad' => $realW,
                'cantidad_cobro' => $billW,
                'weight_kg' => $realW,
                'resolved_billing_mode' => $mode,
                'resolved_load_type_id' => $defaults['resolved_load_type_id'],
                'used_default_billing' => $defaults['used_default_billing'],
                'used_default_load' => $defaults['used_default_load'],
            ];
        }

        $loadId = $defaults['resolved_load_type_id'];
        $unit = $this->resolvePieceUnitPrice(
            (int) $item['etapa_id'],
            (int) $item['garment_type_id'],
            $loadId
        );
        if ($unit === null) {
            throw ValidationException::withMessages([
                "items.{$index}.garment_type_id" => ['No hay precio de pieza configurado para esta combinación de servicio, prenda y tipo de carga (laundry_piece_prices).'],
            ]);
        }
        if (isset($item['precio_unitario_override']) && $item['precio_unitario_override'] !== '' && $item['precio_unitario_override'] !== null) {
            $unit = max(0, (float) $item['precio_unitario_override']);
        }
        $realCant = (float) $item['cantidad'];
        $billCant = LaundryPieceQuantityBilling::billingQuantity($realCant);

        return [
            'precio_unitario' => $unit,
            'subtotal' => round($billCant * $unit, 2),
            'cantidad' => $realCant,
            'cantidad_cobro' => $billCant,
            'weight_kg' => isset($item['weight_kg']) ? (float) $item['weight_kg'] : null,
            'resolved_billing_mode' => $mode,
            'resolved_load_type_id' => $loadId,
            'used_default_billing' => $defaults['used_default_billing'],
            'used_default_load' => $defaults['used_default_load'],
        ];
    }

    /**
     * @param  array<string, mixed>  $item
     * @return array{resolved_billing_mode: string, resolved_load_type_id: int|null, used_default_billing: bool, used_default_load: bool}
     */
    private function resolveItemDefaults(array $item): array
    {
        $billing = isset($item['billing_mode']) && $item['billing_mode'] !== '' ? (string) $item['billing_mode'] : null;
        $loadId = isset($item['load_type_id']) && $item['load_type_id'] !== '' ? (int) $item['load_type_id'] : null;
        $usedDefaultBilling = ($billing === null || $billing === '');
        $usedDefaultLoad = false;

        if (isset($item['garment_type_id']) && $item['garment_type_id'] !== null && $item['garment_type_id'] !== '') {
            $garment = GarmentType::query()
                ->select(['id', 'default_load_type_id'])
                ->find((int) $item['garment_type_id']);

            if ($garment !== null) {
                if ($loadId === null && $garment->default_load_type_id !== null) {
                    $loadId = (int) $garment->default_load_type_id;
                    $usedDefaultLoad = true;
                }
            }
        }

        if ($billing === null || $billing === '') {
            $billing = OrdenLavanderiaDetalleLineValidator::billingModeForItem($item);
        }

        return [
            'resolved_billing_mode' => $billing,
            'resolved_load_type_id' => $loadId,
            'used_default_billing' => $usedDefaultBilling,
            'used_default_load' => $usedDefaultLoad,
        ];
    }
}
