<?php

namespace App\Models;

use App\Casts\TrimmedDecimal;
use App\Concerns\RecordsCreator;
use App\Support\NumberFormat;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use OwenIt\Auditing\Auditable as AuditableTrait;
use OwenIt\Auditing\Contracts\Auditable;

class PurchaseProduct extends Model implements Auditable
{
    use AuditableTrait, RecordsCreator;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'purchase_unit_id',
        'base_unit_id',
        'factor_to_base',
        'name',
        'is_active',
    ];

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'is_active' => 'boolean',
            'purchase_unit_id' => 'integer',
            'base_unit_id' => 'integer',
            'factor_to_base' => TrimmedDecimal::class.':6',
            'stock_on_hand' => TrimmedDecimal::class.':3',
        ];
    }

    /**
     * @return BelongsTo<PurchaseUnit, $this>
     */
    public function unit(): BelongsTo
    {
        return $this->belongsTo(PurchaseUnit::class, 'purchase_unit_id');
    }

    /**
     * @return BelongsTo<PurchaseUnit, $this>
     */
    public function baseUnit(): BelongsTo
    {
        return $this->belongsTo(PurchaseUnit::class, 'base_unit_id');
    }

    public function usesBaseUnit(): bool
    {
        return $this->base_unit_id !== null && (float) $this->factor_to_base > 1;
    }

    public function quantityToBase(float $quantity): float
    {
        if (! $this->usesBaseUnit()) {
            return round($quantity, 6);
        }

        return round($quantity * (float) $this->factor_to_base, 6);
    }

    public function quantityFromBase(float $baseQuantity): float
    {
        if (! $this->usesBaseUnit()) {
            return round($baseQuantity, 6);
        }

        return round($baseQuantity / (float) $this->factor_to_base, 6);
    }

    /**
     * Recover the base-unit amount shown in forms after storing purchase-unit quantities.
     */
    public function displayBaseQuantityForForm(float $storedPurchaseUnits): float
    {
        if (! $this->usesBaseUnit()) {
            return round($storedPurchaseUnits, 3);
        }

        $factor = (float) $this->factor_to_base;
        $stored = round($storedPurchaseUnits, 6);
        $approximateBase = $stored * $factor;

        for ($decimals = 0; $decimals <= 3; $decimals++) {
            $step = $decimals === 0 ? 1.0 : 10 ** (-$decimals);
            $center = round($approximateBase, $decimals);
            $low = $center - ($step * 3);
            $high = $center + ($step * 3);

            for ($candidate = $low; $candidate <= $high + ($step / 2); $candidate += $step) {
                if ($candidate < 0) {
                    continue;
                }

                $roundedCandidate = round($candidate, $decimals);

                if (round($roundedCandidate / $factor, 3) === round($stored, 3)) {
                    return $roundedCandidate;
                }
            }
        }

        return round($approximateBase, 3);
    }

    public function displayStockOnHand(): string
    {
        if (! $this->usesBaseUnit()) {
            return (string) $this->stock_on_hand;
        }

        return NumberFormat::quantity(
            $this->quantityFromBase((float) $this->stock_on_hand),
        );
    }

    /**
     * @return array{base_unit_id: int|null, factor_to_base: float}
     */
    public static function resolveBaseUnitFieldsFromName(
        int|string|null $baseUnitName,
        int|string|float|null $factorToBase,
    ): array {
        $factor = max(1.0, (float) $factorToBase);
        $name = is_string($baseUnitName) ? trim($baseUnitName) : '';

        if ($factor <= 1 || $name === '') {
            return [
                'base_unit_id' => null,
                'factor_to_base' => 1,
            ];
        }

        $baseUnit = PurchaseUnit::query()->firstOrCreate(
            ['name' => $name],
            ['is_active' => true],
        );

        return [
            'base_unit_id' => $baseUnit->id,
            'factor_to_base' => $factor,
        ];
    }

    /**
     * @return array{base_unit_id: int|null, factor_to_base: float}
     */
    public static function resolveBaseUnitFields(
        int|string|null $baseUnitId,
        int|string|float|null $factorToBase,
    ): array {
        $factor = max(1.0, (float) $factorToBase);
        $baseUnit = $baseUnitId !== null && $baseUnitId !== '' ? (int) $baseUnitId : null;

        if ($factor <= 1 || $baseUnit === null) {
            return [
                'base_unit_id' => null,
                'factor_to_base' => 1,
            ];
        }

        return [
            'base_unit_id' => $baseUnit,
            'factor_to_base' => $factor,
        ];
    }

    /**
     * @param  list<int>  $currentProductIds
     * @return list<array{
     *     id: int,
     *     name: string,
     *     unit_name: string|null,
     *     base_unit_id: int|null,
     *     base_unit_name: string|null,
     *     factor_to_base: string,
     *     uses_base_unit: bool,
     *     stock_on_hand: string,
     *     stock_in_base: string,
     *     stock_in_units: string,
     *     is_active: bool
     * }>
     */
    public static function optionsForForm(array $currentProductIds = []): array
    {
        $currentProductIds = array_values(array_unique(array_filter($currentProductIds)));

        return self::query()
            ->with(['unit:id,name', 'baseUnit:id,name'])
            ->when(
                $currentProductIds !== [],
                fn ($query) => $query->where(function ($q) use ($currentProductIds): void {
                    $q->where('is_active', true)
                        ->orWhereIn('id', $currentProductIds);
                }),
                fn ($query) => $query->where('is_active', true),
            )
            ->orderBy('name')
            ->get([
                'id',
                'name',
                'purchase_unit_id',
                'base_unit_id',
                'factor_to_base',
                'stock_on_hand',
                'is_active',
            ])
            ->map(fn (self $product): array => [
                'id' => $product->id,
                'name' => $product->name,
                'unit_name' => $product->unit?->name,
                'base_unit_id' => $product->base_unit_id,
                'base_unit_name' => $product->baseUnit?->name,
                'factor_to_base' => (string) $product->factor_to_base,
                'uses_base_unit' => $product->usesBaseUnit(),
                'stock_on_hand' => (string) $product->stock_on_hand,
                'stock_in_base' => (string) $product->stock_on_hand,
                'stock_in_units' => $product->displayStockOnHand(),
                'is_active' => $product->is_active,
            ])
            ->values()
            ->all();
    }
}
