<?php

namespace App\Console\Commands;

use App\Console\Commands\Concerns\ResolvesBranchAdmins;
use App\Domain\Inventory\Models\Product;
use App\Domain\Notification\Services\NotificationService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

/**
 * Tell each branch what it is running out of — and only what it is running out of.
 *
 * Console runs with branch enforcement off (docs/agent-rules/19), so the sweep
 * legitimately sees every branch's stock. Mailing that sweep to every admin in
 * the academy was the leak: a manager at one branch received product names and
 * on-hand quantities for all of them.
 *
 * Two things decide the branch, and neither is the product:
 *
 *   - `products.branch_id` is catalogue. NULL there means "every branch sells
 *     this", so it can never be the branch that is short of it.
 *   - `inventory_levels.branch_id` is the stock itself, and that is what runs
 *     low. One shared product low at one branch is one branch's problem.
 *
 * The old grouping was by academy and the quantity shown came from
 * `inventoryLevels->first()`, which is whichever row Postgres returned first —
 * frequently a perfectly healthy one at another branch.
 */
class NotifyLowStock extends Command
{
    use ResolvesBranchAdmins;

    protected $signature = 'inventory:notify-low-stock';
    protected $description = 'Notify admins about products below minimum stock level';

    /** Roles at or above this level receive the alert. */
    private const ADMIN_LEVEL = 70;

    /** Bucket key for stock that carries no branch. */
    private const UNATTRIBUTED = 0;

    public function handle(NotificationService $notificationService): int
    {
        $lowStockProducts = Product::where('track_inventory', true)
            ->where('is_active', true)
            ->whereNotNull('min_stock_level')
            ->whereHas('inventoryLevels', function ($q) {
                $q->whereRaw('quantity_on_hand < (SELECT min_stock_level FROM products WHERE products.id = inventory_levels.product_id)');
            })
            ->with(['inventoryLevels'])
            ->get();

        if ($lowStockProducts->isEmpty()) {
            $this->info('No low stock products found.');

            return self::SUCCESS;
        }

        $shortages = $this->shortagesByAcademyAndBranch($lowStockProducts);

        foreach ($shortages as $academyId => $byBranch) {
            $this->notify($academyId, $byBranch, $notificationService);
        }

        return self::SUCCESS;
    }

    /**
     * academy id => branch id (0 = no branch) => product id => shortage line data.
     *
     * `whereHas` proves a product has at least one low level; it says nothing
     * about the others, so each level is re-checked here. A product short at two
     * warehouses in the same branch is reported once, at its worst count.
     *
     * @param  iterable<int, Product>  $products
     * @return array<int, array<int, array<int, array<string, mixed>>>>
     */
    private function shortagesByAcademyAndBranch(iterable $products): array
    {
        $shortages = [];

        foreach ($products as $product) {
            $minimum = (int) $product->min_stock_level;

            foreach ($product->inventoryLevels as $level) {
                $onHand = (int) $level->quantity_on_hand;

                if ($onHand >= $minimum) {
                    continue;
                }

                $academyId = (int) $product->academy_id;
                // The stock's branch, never the catalogue's: a shared product
                // (branch_id NULL) is short at whichever branch holds the level.
                $branchKey = (int) ($level->branch_id ?? $product->branch_id);
                $existing = $shortages[$academyId][$branchKey][$product->id]['on_hand'] ?? null;

                if ($existing !== null && $existing <= $onHand) {
                    continue;
                }

                $shortages[$academyId][$branchKey][$product->id] = [
                    'name' => $product->name_ar ?? $product->name,
                    'on_hand' => $onHand,
                    'minimum' => $minimum,
                ];
            }
        }

        return $shortages;
    }

    /**
     * @param  array<int, array<int, array<string, mixed>>>  $byBranch
     */
    private function notify(int $academyId, array $byBranch, NotificationService $notificationService): void
    {
        if (! empty($byBranch[self::UNATTRIBUTED])) {
            $count = count($byBranch[self::UNATTRIBUTED]);
            $this->warn("  Academy {$academyId}: {$count} low-stock level(s) carry no branch — only academy-wide admins are told about them.");
        }

        $names = $this->branchNames($academyId);
        $admins = $this->branchAdmins($academyId, self::ADMIN_LEVEL);
        $notified = 0;

        foreach ($admins as $admin) {
            $keys = $admin->academyWide ? array_keys($byBranch) : $admin->branchIds;
            $entries = $this->merge($byBranch, $keys);

            // An empty alert is noise, not reassurance: a branch with nothing
            // low is simply not written to.
            if (empty($entries)) {
                continue;
            }

            try {
                $notificationService->send(
                    eventType: 'low_stock_alert',
                    recipientType: 'user',
                    recipientId: $admin->id,
                    academyId: $academyId,
                    variables: [
                        'branch_name' => $admin->academyWide
                            ? __('كل الفروع')
                            : $this->branchLabel($admin->branchIds, $names),
                        'product_count' => (string) count($entries),
                        'product_list' => $this->productList($entries),
                    ],
                    recipientEmail: $admin->email,
                );

                $notified++;
            } catch (\Throwable $e) {
                Log::warning("Low stock notification failed: " . $e->getMessage());
            }
        }

        $total = count($this->merge($byBranch, array_keys($byBranch)));

        $this->info("Notified {$notified} admin(s) for academy {$academyId}: {$total} low-stock product(s) academy-wide.");
    }

    /**
     * Collapse the named branches into one product list, keeping the worst
     * count when the same product is short at more than one of them.
     *
     * @param  array<int, array<int, array<string, mixed>>>  $byBranch
     * @param  array<int, int>  $keys
     * @return array<int, array<string, mixed>>
     */
    private function merge(array $byBranch, array $keys): array
    {
        $merged = [];

        foreach ($keys as $key) {
            foreach ($byBranch[(int) $key] ?? [] as $productId => $entry) {
                if (isset($merged[$productId]) && $merged[$productId]['on_hand'] <= $entry['on_hand']) {
                    continue;
                }

                $merged[$productId] = $entry;
            }
        }

        return $merged;
    }

    /**
     * @param  array<int, array<string, mixed>>  $entries
     */
    private function productList(array $entries): string
    {
        return implode(', ', array_map(
            fn (array $entry) => "{$entry['name']} ({$entry['on_hand']}/{$entry['minimum']})",
            $entries
        ));
    }
}
