<?php

namespace App\Services\Machine;

use App\Models\Machine\Machine;
use App\Models\Machine\MachineLog;
use App\Models\Machine\MachineSlot;
use App\Models\Machine\MachineStockMovement;
use App\Models\Machine\RemoteCommand;
use App\Models\Product\Product;
use App\Models\Transaction\Order;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;

class MachineService
{
    private const SPRING_SLOT_FLAGS = [
        [1, 10, 'is_spring_slot_1_10'],
        [11, 20, 'is_spring_slot_11_20'],
        [21, 30, 'is_spring_slot_21_30'],
        [31, 40, 'is_spring_slot_31_40'],
        [41, 50, 'is_spring_slot_41_50'],
        [51, 60, 'is_spring_slot_51_60'],
    ];

    /**
     * B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping)
     *
     * 代碼格式：Command 0x04 + Status Byte，例如 0x04+0x03 = '0403'
     * 出貨結果判定：僅 0402 (出貨成功) 與 0424 (商品取出成功) 視為成功，
     *               其餘出貨結果碼均應觸發退款。
     *
     * 參考文件：廠商硬體通訊協議 Command(0x04) 狀態定義
     */
    public const ERROR_CODE_MAP = [
        // ═══════════════════════════════════════════════════════════════
        // 出貨與物理動作類 (Command 0x04 - BUY_STATUS)
        // ═══════════════════════════════════════════════════════════════

        // --- 基本出貨流程 ---
        '0401' => ['label' => 'Dispensing in progress', 'level' => 'info'],           // 正在出貨
        '0402' => ['label' => 'Dispense successful', 'level' => 'info'],              // ✅ 出貨成功
        '0403' => ['label' => 'Slot jammed', 'level' => 'error'],                     // 貨道卡貨 (重大異常)
        '0404' => ['label' => 'Motor not stopped', 'level' => 'warning'],             // 電機未正常停止
        '0405' => ['label' => 'Motor not found', 'level' => 'error'],                 // 電機不存在
        '0406' => ['label' => 'Motor not found (0406)', 'level' => 'error'],          // 電機不存在 (同 0405)
        '0407' => ['label' => 'Elevator failure', 'level' => 'error'],                // 昇降機故障

        // --- 昇降機系統 ---
        '040A' => ['label' => 'Elevator belt self-check error', 'level' => 'error'],  // 昇降機皮帶自檢錯誤
        '0410' => ['label' => 'Elevator rising', 'level' => 'info'],                  // 昇降機正在上升
        '0411' => ['label' => 'Elevator descending', 'level' => 'info'],              // 昇降機正在下降
        '0412' => ['label' => 'Elevator rise error', 'level' => 'error'],             // 昇降機上升錯誤
        '0413' => ['label' => 'Elevator descent error', 'level' => 'error'],          // 昇降機下降錯誤
        '0425' => ['label' => 'Elevator support rod return error', 'level' => 'error'],  // 昇降台撐桿回位錯誤
        '0428' => ['label' => 'Elevator support rod push error', 'level' => 'error'],    // 昇降台撐桿推出錯誤
        '0429' => ['label' => 'Elevator enter microwave error', 'level' => 'error'],     // 昇降台進微波爐錯誤
        '0430' => ['label' => 'Elevator exit microwave error', 'level' => 'error'],      // 昇降台出微波爐錯誤

        // --- 微波爐取貨口門 ---
        '0414' => ['label' => 'Microwave pickup door closing', 'level' => 'info'],       // 正在關閉微波爐取貨口門
        '0415' => ['label' => 'Microwave pickup door close error', 'level' => 'error'],  // 微波爐取貨口門關閉錯誤
        '0426' => ['label' => 'Microwave pickup door opening', 'level' => 'info'],       // 正在打開微波爐取貨口門
        '0427' => ['label' => 'Microwave pickup door open error', 'level' => 'error'],   // 微波爐取貨口門打開錯誤

        // --- 微波爐進貨口門 ---
        '0416' => ['label' => 'Microwave delivery door opening', 'level' => 'info'],       // 正在打開微波爐進貨口門
        '0417' => ['label' => 'Microwave delivery door open error', 'level' => 'error'],    // 微波爐進貨口門打開錯誤
        '0418' => ['label' => 'Pushing product into microwave', 'level' => 'info'],          // 正在將商品推入微波爐
        '0419' => ['label' => 'Microwave delivery door closing', 'level' => 'info'],        // 正在關閉微波爐進貨口門
        '0420' => ['label' => 'Microwave delivery door close error', 'level' => 'error'],   // 微波爐進貨口門關閉錯誤

        // --- 微波爐內部 ---
        '0421' => ['label' => 'Product not detected in microwave', 'level' => 'error'],      // 微波爐內未檢測到商品
        '0422' => ['label' => 'Product heating', 'level' => 'info'],                         // 商品正在加熱
        '0423' => ['label' => 'Product heating remaining time', 'level' => 'info'],           // 商品加熱剩餘時間 (貨道號=秒數)
        '0424' => ['label' => 'Purchase successful', 'level' => 'info'],                     // ✅ 購買成功
        '0431' => ['label' => 'Microwave push rod push error', 'level' => 'error'],          // 微波爐內推桿推出錯誤
        '0432' => ['label' => 'Microwave push rod retract error', 'level' => 'error'],       // 微波爐內推桿收回錯誤

        // --- 鮮奶機專用 ---
        '040B' => ['label' => 'Milk dispense timeout', 'level' => 'error'],             // 按鍵出奶超時 (鮮奶機)
        '040C' => ['label' => 'Cup dropper failure', 'level' => 'error'],                // 漏杯器漏杯失敗 (鮮奶機)
        '040D' => ['label' => 'Cup drop not detected', 'level' => 'error'],              // 未檢測到杯子掉落 (鮮奶機)

        // --- 手環機專用 ---
        '040E' => ['label' => 'Bracelet scan failed', 'level' => 'error'],               // 沒掃碼到手環
        '040F' => ['label' => 'Bracelet drop not detected', 'level' => 'error'],         // 沒檢測到手環掉入取貨斗

        // --- 取貨口門 (購物車模式) ---
        '0433' => ['label' => 'Pickup door opened (cart mode)', 'level' => 'info'],      // 取貨口門打開成功 (購物車購買)
        '0434' => ['label' => 'Pickup door closed (cart mode)', 'level' => 'info'],      // 取貨口門關閉成功 (購物車購買)
        '0441' => ['label' => 'Pickup door unlocked', 'level' => 'info'],                // 取貨口門開鎖上報

        // --- 液體機專用 ---
        '0435' => ['label' => 'Liquid dispense paused', 'level' => 'warning'],           // 暫停出液

        // --- 終止 ---
        '04FF' => ['label' => 'Purchase terminated', 'level' => 'error'],                // 購買終止

        // ═══════════════════════════════════════════════════════════════
        // 貨道與馬達狀態類 (Prefix: 02 - SLOT_STATUS)
        // ═══════════════════════════════════════════════════════════════
        '0201' => ['label' => 'Slot normal', 'level' => 'info'],                              // 貨道正常
        '0202' => ['label' => 'Slot out of stock', 'level' => 'warning'],                     // 貨道缺貨
        '0203' => ['label' => 'Slot does not exist', 'level' => 'warning'],                   // 貨道不存在
        '0204' => ['label' => 'Slot suspended', 'level' => 'warning'],                        // 貨道暫停使用
        '0205' => ['label' => 'Elevator contains items', 'level' => 'warning'],               // 升降台裡面有物品
        '0206' => ['label' => 'Pickup door not closed', 'level' => 'warning'],                 // 取貨口門沒關上
        '0207' => ['label' => 'Elevator failure (0207)', 'level' => 'error'],                 // 升降機故障
        '0208' => ['label' => 'Elevator self-check error', 'level' => 'error'],               // 升降機自檢錯誤
        '0209' => ['label' => 'Microwave pickup door close error (0209)', 'level' => 'error'], // 微波爐取貨口門關閉錯誤
        '020B' => ['label' => 'Machine temperature exceeds limit', 'level' => 'warning'],      // 機器超過設定溫度，暫停購買
        '020C' => ['label' => 'Bento machine cooling water tank empty', 'level' => 'error'],  // 盒飯機冷卻水箱缺水
        '020D' => ['label' => 'Age verification required', 'level' => 'warning'],             // 貨道需要年齡驗證
        '020E' => ['label' => 'Water level too low', 'level' => 'warning'],                    // 水量過低
        '020F' => ['label' => 'Weight sensor communication error', 'level' => 'error'],       // 重量傳感器通訊錯誤
        '0210' => ['label' => 'Microwave delivery door open error (0210)', 'level' => 'error'],// 微波爐進貨口門打開錯誤
        '0211' => ['label' => 'Microwave delivery door close error (0211)', 'level' => 'error'],// 微波爐進貨口門關閉錯誤
        '0212' => ['label' => 'Bento not detected', 'level' => 'warning'],                     // 沒檢測到盒飯
        '0213' => ['label' => 'Bento heating', 'level' => 'info'],                             // 盒飯正在加熱
        '0214' => ['label' => 'Microwave pickup door open error (0214)', 'level' => 'error'],  // 微波爐取貨口門打開錯誤
        '0215' => ['label' => 'Please remove bento from microwave', 'level' => 'warning'],     // 請取出微波爐內盒飯
        '0216' => ['label' => 'Support rod return error', 'level' => 'error'],                 // 撐桿回位錯誤
        '0217' => ['label' => 'Main motor failure', 'level' => 'error'],                      // 主電機故障
        '0218' => ['label' => 'Translation motor failure', 'level' => 'error'],                 // 平移電機故障
        '0219' => ['label' => 'Support rod push error', 'level' => 'error'],                  // 撐桿推出錯誤
        '0220' => ['label' => 'Elevator enter microwave error (0220)', 'level' => 'error'],    // 升降台進去微波爐錯誤
        '0221' => ['label' => 'Elevator exit microwave error (0221)', 'level' => 'error'],     // 升降台出去微波爐錯誤
        '0222' => ['label' => 'Microwave push rod push error (0222)', 'level' => 'error'],     // 微波爐內推桿推出錯誤
        '0223' => ['label' => 'Microwave push rod retract error (0223)', 'level' => 'error'],  // 微波爐內推桿收回錯誤

        // ═══════════════════════════════════════════════════════════════
        // 機台整體狀態類 (Prefix: 54 - MACHINE_STATUS)
        // ═══════════════════════════════════════════════════════════════
        '5400' => ['label' => 'Machine normal', 'level' => 'info'],
        '5401' => ['label' => 'Elevator sensor error', 'level' => 'error'],
        '5402' => ['label' => 'Pickup door not closed', 'level' => 'warning'],
        '5403' => ['label' => 'Elevator failure', 'level' => 'error'],
    ];

    /**
     * 出貨成功結果碼 (Dispense Success Codes)
     *
     * 僅以下代碼視為出貨成功，其餘出貨結果碼均應觸發退款。
     * - 0402: 一般出貨成功
     * - 0424: 商品取出成功 (微波爐機型)
     */
    public const DISPENSE_SUCCESS_CODES = ['0402', '0424'];

    /**
     * Update machine heartbeat and status.
     * 
     * @param string $serialNo
     * @param array $data
     * @return Machine
     */
    public function updateHeartbeat(string $serialNo, array $data): Machine
    {
        return DB::transaction(function () use ($serialNo, $data) {
            $machine = Machine::where('serial_no', $serialNo)->firstOrFail();

            // 採用現代化語意命名 (Modern semantic naming)
            $temperature = $data['temperature'] ?? $machine->temperature;
            $currentPage = $data['current_page'] ?? $machine->current_page;
            $doorStatus = $data['door_status'] ?? $machine->door_status;
            $firmwareVersion = $data['firmware_version'] ?? $machine->firmware_version;
            $model = $data['model'] ?? $machine->model;

            $updateData = [
                'temperature' => $temperature,
                'current_page' => $currentPage,
                'door_status' => $doorStatus,
                'firmware_version' => $firmwareVersion,
                'model' => $model,
                'last_heartbeat_at' => now(),
            ];

            $machine->update($updateData);

            // Record log if provided
            if (!empty($data['log'])) {
                $machine->logs()->create([
                    'company_id' => $machine->company_id,
                    'type' => 'status',
                    'level' => $data['log_level'] ?? 'info',
                    'message' => $data['log'],
                    'context' => $data['log_payload'] ?? null,
                ]);
            }

            return $machine;
        });
    }

    /**
     * Sync machine API token to Redis for MQTT authentication.
     * 
     * @param Machine $machine
     */
    public function syncMqttAuth(Machine $machine): void
    {
        if (empty($machine->api_token)) {
            return;
        }

        // MQTT 連線認證：Username = serial_no, Password = hash(api_token)
        // 遵循 framework.md 4.5 規範
        $redisKey = "machine_auth:{$machine->serial_no}";
        
        // 這裡採用 SHA256 雜湊，與 EMQX 設定對應
        $hashedToken = hash('sha256', $machine->api_token);
        
        \Illuminate\Support\Facades\Redis::hSet($redisKey, 'password', $hashedToken);
        
        \Illuminate\Support\Facades\Log::info("MQTT Auth synced to Redis", [
            'serial_no' => $machine->serial_no,
            'key' => $redisKey
        ]);
    }

    /**
     * 統一寫入機台庫存異動紀錄
     *
     * @param MachineSlot $slot       貨道 Model（已含 machine_id, product_id, stock）
     * @param int         $delta      異動數量（正=增加，負=減少）
     * @param string      $type       異動類型（MachineStockMovement::TYPE_*）
     * @param mixed       $reference  關聯單據 Model（nullable）
     * @param string|null $note       備註說明
     */
    public function recordStockMovement(
        MachineSlot $slot,
        int $delta,
        string $type,
        mixed $reference = null,
        ?string $note = null,
        array $context = []
    ): MachineStockMovement {
        $before = $slot->stock - $delta; // 因為已更新 slot，反推異動前的值

        return MachineStockMovement::create([
            'company_id'     => $slot->machine->company_id ?? null,
            'machine_id'     => $slot->machine_id,
            'product_id'     => $slot->product_id,
            'slot_no'        => $slot->slot_no,
            'type'           => $type,
            'quantity'       => $delta,
            'before_qty'     => $before,
            'after_qty'      => $slot->stock,
            'reference_type' => $reference ? get_class($reference) : null,
            'reference_id'   => $reference?->id,
            'note'           => $note,
            'context'        => $context,
            'created_by'     => Auth::id(),
        ]);
    }

    /**
     * Sync machine slots based on replenishment report.
     * 
     * @param Machine $machine
     * @param array $slotsData
     */
    public function syncSlots(Machine $machine, array $slotsData): void
    {
        DB::transaction(function () use ($machine, $slotsData) {
            // 蒐集所有傳入的商品 ID (可能是 SKU 或 實際 ID)
            $productCodes = collect($slotsData)->pluck('product_id')->filter()->unique()->toArray();
            
            // 優先以 ID 查詢商品，若 ID 不存在則嘗試 Barcode (Prioritize ID lookup, fallback to Barcode)
            $products = \App\Models\Product\Product::whereIn('id', $productCodes)
                ->orWhereIn('barcode', $productCodes)
                ->get();

            foreach ($slotsData as $slotData) {
                $slotNo = $slotData['slot_no'] ?? null;
                if (!$slotNo) continue;

                $existingSlot = $machine->slots()->where('slot_no', $slotNo)->first();
                
                // 查找對應的實體 ID (支援 ID 與 Barcode 比對)
                $productCode = $slotData['product_id'] ?? null;
                $actualProductId = null;
                if ($productCode) {
                    $actualProductId = $products->first(function ($p) use ($productCode) {
                        return (string)$p->id === (string)$productCode || $p->barcode === (string)$productCode;
                    })?->id;
                }

                // 根據機台貨道機制設定決定上限與貨道類型 (1: 履帶, 2: 彈簧)
                $slotType = $this->resolveSlotType($machine, $slotNo);
                if ($actualProductId) {
                    $product = $products->find($actualProductId);
                    if ($product) {
                        $slotData['capacity'] = $this->calculateSlotMaxStock($product, $machine, $slotNo);
                    }
                }

                $newStock = (int) ($slotData['stock'] ?? 0);

                $updateData = [
                    'product_id' => $actualProductId,
                    'type' => $slotType,
                    'stock' => $newStock,
                    'max_stock' => $slotData['capacity'] ?? ($existingSlot->max_stock ?? 10),
                    'is_active' => true,
                ];

                if ($existingSlot) {
                    $oldStock = (int) $existingSlot->stock;
                    $oldProductId = $existingSlot->product_id;
                    
                    // 檢查是否有實質變動 (Check for actual changes)
                    $isStockChanged = ($oldStock !== $newStock);
                    $isProductChanged = ($oldProductId != $actualProductId); // 這裡用鬆散比對以處理 null/0 的情況
                    $isSlotConfigChanged = (string) $existingSlot->type !== (string) $updateData['type']
                        || (int) $existingSlot->max_stock !== (int) $updateData['max_stock'];

                    if ($isStockChanged || $isProductChanged || $isSlotConfigChanged) {
                        $existingSlot->update($updateData);
                        $existingSlot->refresh();

                        if ($isStockChanged || $isProductChanged) {
                            $delta = $newStock - $oldStock;
                            $note = $isProductChanged ? "movement.note.product_changed_and_adjusted" : "movement.note.replenishment_correction";

                            $this->recordStockMovement(
                                $existingSlot,
                                $delta,
                                MachineStockMovement::TYPE_ADJUSTMENT,
                                null,
                                $note,
                                ['slot_no' => $existingSlot->slot_no, 'old' => $oldStock, 'new' => $newStock, 'old_product_id' => $oldProductId, 'new_product_id' => $actualProductId]
                            );
                        }
                    }
                } else {
                    $newSlot = $machine->slots()->create(array_merge($updateData, ['slot_no' => $slotNo]));
                    
                    // 新建貨道若有初始庫存，也紀錄流水帳
                    if ($newStock > 0) {
                        $this->recordStockMovement(
                            $newSlot,
                            $newStock,
                            MachineStockMovement::TYPE_ADJUSTMENT,
                            null,
                            "movement.note.initial_sync_report",
                            ['slot_no' => $slotNo, 'old' => 0, 'new' => $newStock]
                        );
                    }
                }
            }

            // ─── B009 全量同步：移除不在清單中的貨道 ───
            // 取得本次上報的所有貨道編號
            $reportedSlotNos = collect($slotsData)
                ->pluck('slot_no')
                ->filter()
                ->map(fn($s) => (string) $s)
                ->values()
                ->toArray();

            // 找出資料庫中屬於此機台但不在清單內的舊貨道
            $orphanedSlots = $machine->slots()
                ->whereNotIn('slot_no', $reportedSlotNos)
                ->get();

            foreach ($orphanedSlots as $orphan) {
                $oldStock = (int) $orphan->stock;

                if ($oldStock > 0) {
                    // 先將 stock 設為 0 以便 recordStockMovement 正確計算 before/after
                    $orphan->stock = 0;
                    
                    $this->recordStockMovement(
                        $orphan,
                        -$oldStock,
                        MachineStockMovement::TYPE_DECOMMISSION,
                        null,
                        "movement.note.slot_decommissioned_by_b009",
                        ['old' => $oldStock, 'new' => 0, 'slot_no' => $orphan->slot_no]
                    );
                }

                // 更新貨道狀態為非使用中或直接刪除 (依據需求決定，目前採刪除以維持 B009 全量同步語意)
                $orphan->delete();
            }
        });
    }

    public function syncProductSlotMaxStock(Product $product): void
    {
        MachineSlot::query()
            ->where('product_id', $product->id)
            ->with(['machine' => fn($query) => $query->withoutGlobalScopes()])
            ->chunkById(500, function ($slots) use ($product) {
                foreach ($slots as $slot) {
                    if (!$slot->machine) {
                        continue;
                    }

                    $slot->update([
                        'type' => $this->resolveSlotType($slot->machine, $slot->slot_no),
                        'max_stock' => $this->calculateSlotMaxStock($product, $slot->machine, $slot->slot_no),
                    ]);
                }
            });
    }

    public function syncMachineSlotMaxStock(Machine $machine): void
    {
        $machine->slots()
            ->with(['product' => fn($query) => $query->withoutGlobalScopes()])
            ->chunkById(500, function ($slots) use ($machine) {
                foreach ($slots as $slot) {
                    if (!$slot->product) {
                        continue;
                    }

                    $slot->update([
                        'type' => $this->resolveSlotType($machine, $slot->slot_no),
                        'max_stock' => $this->calculateSlotMaxStock($slot->product, $machine, $slot->slot_no),
                    ]);
                }
            });
    }

    public function calculateSlotMaxStock(Product $product, Machine $machine, string|int|null $slotNo): int
    {
        return $this->isSpringSlot($machine, $slotNo)
            ? (int) $product->spring_limit
            : (int) $product->track_limit;
    }

    public function resolveSlotType(Machine $machine, string|int|null $slotNo): string
    {
        return $this->isSpringSlot($machine, $slotNo) ? '2' : '1';
    }

    public function isSpringSlot(Machine $machine, string|int|null $slotNo): bool
    {
        $slotNumber = (int) $slotNo;

        foreach (self::SPRING_SLOT_FLAGS as [$start, $end, $field]) {
            if ($slotNumber >= $start && $slotNumber <= $end) {
                return (bool) $machine->{$field};
            }
        }

        return false;
    }

    /**
     * Update machine slot stock, expiry, and batch.
     * 
     * @param Machine $machine
     * @param array $data
     * @param int|null $userId
     * @return void
     */
    public function updateSlot(Machine $machine, array $data, ?int $userId = null): void
    {
        DB::transaction(function () use ($machine, $data, $userId) {
            $slotNo = $data['slot_no'];
            $stock = $data['stock'] ?? null;
            $expiryDate = $data['expiry_date'] ?? null;
            $batchNo = $data['batch_no'] ?? null;
            
            $slot = $machine->slots()->where('slot_no', $slotNo)->lockForUpdate()->firstOrFail();

            $pendingCommand = RemoteCommand::where('machine_id', $machine->id)
                ->where('command_type', 'reload_stock')
                ->where('status', 'pending')
                ->whereJsonContains('payload->slot_no', (string)$slotNo)
                ->latest()
                ->first();

            if ($pendingCommand) {
                // 直接覆蓋：將前一個尚未執行的指令標記為「已取代」
                $pendingCommand->update([
                    'status' => 'superseded',
                    'note'   => __('Superseded by new command'),
                ]);
            }

            // 紀錄舊數據以供回滾使用
            $oldData = [
                'stock' => $slot->stock,
                'expiry_date' => $slot->expiry_date ? Carbon::parse($slot->expiry_date)->toDateString() : null,
                'batch_no' => $slot->batch_no,
            ];

            // 2. 執行樂觀更新 (Optimistic Update)
            $updateData = [
                'expiry_date' => $expiryDate,
                'batch_no' => $batchNo,
            ];
            if ($stock !== null) $updateData['stock'] = (int)$stock;
            $slot->update($updateData);

            // 3. 若庫存數值有異動，記錄 adjustment 流水帳
            if ($stock !== null && (int)$stock !== $oldData['stock']) {
                $delta = (int)$stock - $oldData['stock'];
                $slot->refresh();
                $this->recordStockMovement(
                    $slot,
                    $delta,
                    MachineStockMovement::TYPE_ADJUSTMENT,
                    null,
                    "movement.note.manual_adjustment",
                    ['slot_no' => $slot->slot_no, 'old' => $oldData['stock'], 'new' => $stock]
                );
            }

            // 3. 建立遠端指令紀錄
            $command = RemoteCommand::create([
                'machine_id' => $machine->id,
                'user_id' => $userId,
                'command_type' => 'reload_stock',
                'status' => 'pending',
                'payload' => [
                    'slot_no' => (string)$slotNo,
                    'old' => $oldData,
                    'new' => [
                        'stock' => $stock !== null ? (int)$stock : $oldData['stock'],
                        'expiry_date' => $expiryDate ?: null,
                        'batch_no' => $batchNo ?: null,
                    ]
                ]
            ]);

            // 4. 推播 MQTT
            $mqttPayload = [
                'slot_no' => $command->payload['slot_no'],
                'stock' => $command->payload['new']['stock'],
                'expiry_date' => $command->payload['new']['expiry_date'],
                'batch_no' => $command->payload['new']['batch_no'] ?? null,
            ];

            app(\App\Services\Machine\MqttService::class)->pushCommand(
                $machine->serial_no,
                'update_inventory',
                $mqttPayload,
                (string) $command->id
            );
        });
    }

    /**
     * 遠端出貨指令下發 (指令模式，不再預扣庫存)
     */
    public function dispatchDispense(Machine $machine, string $slotNo, ?int $userId = null, ?string $remark = null): RemoteCommand
    {
        return DB::transaction(function () use ($machine, $slotNo, $userId, $remark) {
            $slot = $machine->slots()->where('slot_no', $slotNo)->lockForUpdate()->firstOrFail();

            // 併行檢查：若有 pending 指令，超過 1 分鐘視為逾時可覆蓋
            $pendingCommand = RemoteCommand::where('machine_id', $machine->id)
                ->where('command_type', 'dispense')
                ->where('status', 'pending')
                ->whereJsonContains('payload->slot_no', (string)$slotNo)
                ->latest()
                ->first();

            if ($pendingCommand) {
                $isTimedOut = $pendingCommand->created_at->lt(now()->subMinute());

                if (!$isTimedOut) {
                    throw new \Exception(__('This slot has a pending command. Please wait.'));
                }

                // 超過 1 分鐘：視為逾時，自動標記舊指令
                $pendingCommand->update([
                    'status' => 'timeout',
                    'note'   => __('Superseded by new command (Timeout)'),
                ]);
            }

            if ($slot->stock <= 0) {
                throw new \Exception(__('Out of stock.'));
            }

            // 1. 建立指令紀錄 (不在此處扣庫存，等候交易結案上報)
            $command = RemoteCommand::create([
                'machine_id' => $machine->id,
                'user_id' => $userId,
                'command_type' => 'dispense',
                'status' => 'pending',
                'remark' => $remark,
                'payload' => [
                    'slot_no' => (string)$slotNo,
                    'current_stock' => $slot->stock,
                ]
            ]);

            // 2. 推播 MQTT
            app(\App\Services\Machine\MqttService::class)->pushCommand(
                $machine->serial_no,
                'dispense',
                $command->payload,
                (string) $command->id
            );

            return $command;
        });
    }


    /**
     * B013: Record machine hardware error/status log with auto-translation.
     * 
     * @param Machine $machine
     * @param array $data
     * @return MachineLog
     */
    public function recordErrorLog(Machine $machine, array $data): MachineLog
    {
        $errorCode = $data['error_code'] ?? '0000';
        $mapping = self::ERROR_CODE_MAP[$errorCode] ?? ['label' => 'Unknown Status', 'level' => 'error'];
        
        $slotNo = $data['tid'] ?? null;
        $label = $mapping['label'];
        
        // 儲存原始英文格式作為 DB 備用，前端顯示會優先使用 model accessor 的動態翻譯內容
        $message = $slotNo ? "Slot {$slotNo}: {$label} (Code: {$errorCode})" : "{$label} (Code: {$errorCode})";

        return $machine->logs()->create([
            'company_id' => $machine->company_id,
            'type' => 'submachine',
            'level' => $mapping['level'],
            'message' => $message,
            'context' => array_merge($data, [
                'translated_label' => $label,
                'raw_code' => $errorCode
            ]),
        ]);
    }

    /**
     * Update machine slot stock (single slot).
     * Legacy support for recordLog (Existing code).
     */
    public function recordLog(int $machineId, array $data): MachineLog
    {
        $machine = Machine::findOrFail($machineId);

        return $machine->logs()->create([
            'level' => $data['level'] ?? 'info',
            'message' => $data['message'],
            'context' => $data['context'] ?? null,
        ]);
    }

    /**
     * 取得艦隊整體統計（批次查詢，無 N+1）
     */
    public function getFleetStats(string $date): array
    {
        $start = Carbon::parse($date)->startOfDay();
        $end   = Carbon::parse($date)->endOfDay();

        $machines   = Machine::all(); // TenantScoped 已過濾
        $machineIds = $machines->pluck('id')->toArray();
        $totalMachines = $machines->count();
        $onlineCount   = Machine::online()->count();

        // 今日成功訂單：批次查詢，非逐台
        $ordersData = Order::whereIn('machine_id', $machineIds)
            ->where('payment_status', 1)
            ->whereBetween('created_at', [$start, $end])
            ->selectRaw('COUNT(*) as total_count, COALESCE(SUM(pay_amount), 0) as total_revenue')
            ->first();

        // 今日異常次數
        $alertCount = MachineLog::whereIn('machine_id', $machineIds)
            ->where('level', 'error')
            ->whereBetween('created_at', [$start, $end])
            ->count();

        return [
            'onlineCount'   => $onlineCount,
            'totalMachines' => $totalMachines,
            'totalOrders'   => (int)($ordersData->total_count ?? 0),
            'totalRevenue'  => (float)($ordersData->total_revenue ?? 0),
            'alertCount'    => $alertCount,
            // OEE / avgUptime 已移除，online/offline 直接從 status 讀取
        ];
    }

    /**
     * 取得單台機台詳細統計（接入真實 orders 資料）
     */
    public function getUtilizationStats(Machine $machine, string $date): array
    {
        $start = Carbon::parse($date)->startOfDay();
        $end   = Carbon::parse($date)->endOfDay();

        // 1. 今日成功訂單：筆數 + 金額
        $ordersAgg = $machine->orders()
            ->where('payment_status', 1)
            ->whereBetween('created_at', [$start, $end])
            ->selectRaw('COUNT(*) as cnt, COALESCE(SUM(pay_amount), 0) as rev')
            ->first();

        $salesCount = (int)($ordersAgg->cnt ?? 0);
        $revenue    = (float)($ordersAgg->rev ?? 0);

        // 2. 今日異常次數（dispense_records 失敗 或 machine_logs error）
        $errorCount = $machine->logs()
            ->where('level', 'error')
            ->whereBetween('created_at', [$start, $end])
            ->count();

        // 3. 逐小時銷售量（Bar Chart 用）
        $hourlySales = $machine->orders()
            ->where('payment_status', 1)
            ->whereBetween('created_at', [$start, $end])
            ->selectRaw('HOUR(created_at) as hour, COUNT(*) as count')
            ->groupBy('hour')
            ->pluck('count', 'hour');

        $labels = [];
        $values = [];
        for ($h = 0; $h < 24; $h++) {
            $labels[] = sprintf('%02d:00', $h);
            $values[] = (int)($hourlySales[$h] ?? 0);
        }

        // Flatten 格式，前端直接 spread 使用
        return [
            'output_count' => $salesCount,
            'revenue'      => round($revenue, 0),
            'error_count'  => $errorCount,
            'chart_data'   => [
                'labels' => $labels,
                'values' => $values,
            ],
        ];
    }
}
