<?php

namespace App\Models\Catalog;

use App\Models\BranchStock;
use App\Models\Concerns\Auditable;
use App\Models\Concerns\BelongsToTenant;
use App\Models\Concerns\HasStoredImage;
use App\Models\Inventory\ProductSerial;
use App\Models\Inventory\StockBatch;
use App\Models\Lookups\UnitOfMeasure;
use App\Models\Scopes\BranchScope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;

class Product extends Model
{
    use Auditable, BelongsToTenant, HasStoredImage, SoftDeletes;

    protected $fillable = [
        'uuid', 'name', 'description', 'sku', 'barcode', 'image_url',
        'vat_rate',
        'main_category_id', 'sub_category_id', 'is_active', 'is_batch_tracked', 'is_serial_tracked',
        'is_expiry_tracked', 'base_unit_id',
    ];

    protected function casts(): array
    {
        return [
            'vat_rate' => 'decimal:2',
            'is_active' => 'boolean',
            'is_batch_tracked' => 'boolean',
            'is_serial_tracked' => 'boolean',
            'is_expiry_tracked' => 'boolean',
        ];
    }

    // image_url stays the raw storage path; $product->image_display_url is the
    // browser-loadable URL. See HasStoredImage — the previous accessor here
    // turned every path into an app-origin URL ("/tenants/2/products/x.png"),
    // which only ever resolved for local storage and 404s on S3/R2.

    /**
     * Add a `stock` aggregate from branch_stock. Pass a branch id to scope the
     * total to a single branch; pass null for the "All branches" total.
     *
     * A given $branchId is always an explicit, caller-validated business
     * parameter (e.g. a form's own branch picker) that may legitimately
     * differ from the acting user's ambient session branch, so it bypasses
     * BranchStock's own BranchScope rather than risk AND-ing two conflicting
     * branch filters together. A null $branchId (the "All branches" total)
     * does NOT bypass — it deliberately inherits BranchScope so a
     * branch-restricted user's aggregate total only reflects branches they
     * can access, not every branch in the tenant.
     *
     * @param  Builder<Product>  $query
     */
    public function scopeWithStockFor(Builder $query, ?int $branchId): void
    {
        $query->withSum(
            ['branchStock as stock' => function ($q) use ($branchId) {
                if ($branchId !== null) {
                    $q->withoutGlobalScope(BranchScope::class)->where('branch_id', $branchId);
                }
            }],
            'quantity'
        );
    }

    protected static function booted(): void
    {
        static::creating(function (self $p): void {
            if (! $p->uuid) {
                $p->uuid = (string) Str::ulid();
            }
        });
    }

    public function mainCategory(): BelongsTo
    {
        return $this->belongsTo(MainCategory::class);
    }

    public function subCategory(): BelongsTo
    {
        return $this->belongsTo(SubCategory::class);
    }

    public function branchStock(): HasMany
    {
        return $this->hasMany(BranchStock::class);
    }

    public function baseUnit(): BelongsTo
    {
        return $this->belongsTo(UnitOfMeasure::class, 'base_unit_id');
    }

    /**
     * Add an `available_serials_count` aggregate: how many serials are on
     * file, available for sale, at (optionally) one branch.
     *
     * Follows the same BranchScope convention as {@see scopeWithStockFor()}:
     * a caller-supplied $branchId is an explicit, validated business
     * parameter and bypasses BranchScope with an explicit filter; null
     * inherits BranchScope so a branch-restricted user sees only serials
     * they can access.
     *
     * @param  Builder<Product>  $query
     */
    public function scopeWithAvailableSerialCount(Builder $query, ?int $branchId): void
    {
        $query->withCount(['serials as available_serials_count' => function ($q) use ($branchId) {
            $q->available();

            if ($branchId !== null) {
                $q->withoutGlobalScope(BranchScope::class)->where('branch_id', $branchId);
            }
        }]);
    }

    public function units(): HasMany
    {
        return $this->hasMany(ProductUnit::class);
    }

    public function lots(): HasMany
    {
        return $this->hasMany(StockBatch::class, 'product_id');
    }

    public function serials(): HasMany
    {
        return $this->hasMany(ProductSerial::class);
    }

    /**
     * Whether this product currently has any available serials on file —
     * the data-driven replacement for "is this product serial-tracked".
     * Null branch inherits BranchScope; a given branch is explicit.
     */
    public function hasAvailableSerials(?int $branchId = null): bool
    {
        return ProductSerial::query()
            ->where('product_id', $this->id)
            ->available()
            ->when($branchId !== null, fn (Builder $q) => $q
                ->withoutGlobalScope(BranchScope::class)
                ->where('branch_id', $branchId))
            ->exists();
    }

    public function attributes(): HasMany
    {
        return $this->hasMany(ProductAttribute::class)->orderBy('sort_order');
    }
}
