<?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\Transaction\Order;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;

class MachineService
{
    /**
     * B013: 硬體狀態代碼對照表 (Hardware Status Code Mapping)
     */
    public const ERROR_CODE_MAP = [
        // 出貨狀態類 (Prefix: 04 - 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'],
        '0406' => ['label' => 'Slot not found', 'level' => 'error'],
        '0407' => ['label' => 'Dispense error (0407)', 'level' => 'error'],
        '0408' => ['label' => 'Dispense error (0408)', 'level' => 'error'],
        '0409' => ['label' => 'Dispense error (0409)', 'level' => 'error'],
        '040A' => ['label' => 'Dispense error (040A)', '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'],
        '0414' => ['label' => 'Pickup door closed', 'level' => 'info'],
        '0415' => ['label' => 'Pickup door error', 'level' => 'error'],
        '0416' => ['label' => 'Delivery door opened', 'level' => 'info'],
        '0417' => ['label' => 'Delivery door open error', 'level' => 'error'],
        '0418' => ['label' => 'Delivering product', 'level' => 'info'],
        '0419' => ['label' => 'Delivery door closed', 'level' => 'info'],
        '0420' => ['label' => 'Delivery door close error', 'level' => 'error'],
        '0421' => ['label' => 'Hopper empty', 'level' => 'warning'],
        '0422' => ['label' => 'Hopper overheated', 'level' => 'warning'],
        '0423' => ['label' => 'Hopper heating timeout', 'level' => 'error'],
        '0424' => ['label' => 'Hopper error (0424)', 'level' => 'error'],
        '0426' => ['label' => 'Microwave door opened', 'level' => 'info'],
        '0427' => ['label' => 'Microwave door error', 'level' => 'error'],
        '04FF' => ['label' => 'Dispense stopped', 'level' => 'info'],

        // 貨道狀態類 (Prefix: 02 - SLOT_STATUS)
        '0201' => ['label' => 'Slot normal', 'level' => 'info'],
        '0202' => ['label' => 'Product empty', 'level' => 'warning'],
        '0203' => ['label' => 'Slot empty', 'level' => 'warning'],
        '0206' => ['label' => 'Slot not closed', 'level' => 'warning'],
        '0207' => ['label' => 'Slot motor error (0207)', 'level' => 'error'],
        '0208' => ['label' => 'Slot motor error (0208)', 'level' => 'error'],
        '0209' => ['label' => 'Slot motor error (0209)', 'level' => 'error'],
        '0212' => ['label' => 'Hopper empty (0212)', 'level' => 'warning'],

        // 機台整體狀態類 (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'],
    ];

    /**
     * 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;
                }

                // 根據貨道類型自動決定上限 (Auto-calculate max_stock based on slot type)
                // 若未提供 type，預設為 '1' (履帶/Track)
                $slotType = $slotData['type'] ?? $existingSlot->type ?? '1';
                if ($actualProductId) {
                    $product = $products->find($actualProductId);
                    if ($product) {
                        // 1: 履帶, 2: 彈簧
                        $calculatedMaxStock = ($slotType == '1') ? $product->track_limit : $product->spring_limit;
                        $slotData['capacity'] = $calculatedMaxStock ?? $slotData['capacity'] ?? null;
                    }
                }

                $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 的情況

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

                        $delta = $newStock - $oldStock;
                        $note = $isProductChanged ? "movement.note.product_changed_and_adjusted" : "movement.note.replenishment_correction";
                        
                        $this->recordStockMovement(
                            $existingSlot,
                            $delta,
                            MachineStockMovement::TYPE_ADJUSTMENT,
                            null,
                            $note,
                            ['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",
                            ['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) {
                    $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();
            }
        });
    }

    /**
     * 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();

            // 1. 併行檢查：確保同一個貨道在同一時間只有一個指令在執行中 (Pending)
            // 包含 reload_stock 與 dispense 兩種會異動庫存的類型
            $isPending = RemoteCommand::where('machine_id', $machine->id)
                ->whereIn('command_type', ['reload_stock', 'dispense'])
                ->where('status', 'pending')
                ->whereJsonContains('payload->slot_no', (string)$slotNo)
                ->exists();

            if ($isPending) {
                throw new \Exception(__('This slot has a pending update. Please wait for the previous command to complete.'));
            }

            // 紀錄舊數據以供回滾使用
            $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",
                    ['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): RemoteCommand
    {
        return DB::transaction(function () use ($machine, $slotNo, $userId) {
            $slot = $machine->slots()->where('slot_no', $slotNo)->lockForUpdate()->firstOrFail();

            // 併行檢查
            $isPending = RemoteCommand::where('machine_id', $machine->id)
                ->whereIn('command_type', ['reload_stock', 'dispense'])
                ->where('status', 'pending')
                ->whereJsonContains('payload->slot_no', (string)$slotNo)
                ->exists();

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

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

            $oldStock = $slot->stock;
            $newStock = $oldStock - 1;

            // 1. 執行樂觀扣除
            $slot->update(['stock' => $newStock]);

            // 2. 建立指令紀錄
            $command = RemoteCommand::create([
                'machine_id' => $machine->id,
                'user_id' => $userId,
                'command_type' => 'dispense',
                'status' => 'pending',
                'payload' => [
                    'slot_no' => (string)$slotNo,
                    'old_stock' => $oldStock,
                    'new_stock' => $newStock,
                ]
            ]);

            // 3. 寫入庫存異動流水帳（remote_dispense，樂觀扣除）
            $slot->refresh();
            $this->recordStockMovement(
                $slot,
                -1,
                MachineStockMovement::TYPE_REMOTE_DISPENSE,
                $command,
                "遠端出貨指令 (B055)，指令 ID: {$command->id}"
            );

            // 4. 推播 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,
            ],
        ];
    }
}
