<?php

namespace App\Http\Controllers;

use App\Models\StockOpname;
use App\Models\StockOpnameDetail;
use App\Models\StockOpnameChangeLog;
use App\Models\Store;
use App\Models\RawMaterial;
use App\Models\StoreRawMaterial;
use App\Models\ItemRequest;
use App\Models\ItemRequestDetail;
use App\Models\ShipmentDelivery;
use App\Models\NumberingFormat;
use App\Services\CacheService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
use Carbon\Carbon;

class StockOpnamesController extends Controller
{

    public function index(Request $request)
    {
        if ($request->ajax()) {
            $query = StockOpname::select('t_stock_opnames.*')
                ->with(['store', 'editor']);

            // Searching
            if ($request->has('search') && is_array($request->search) && !empty($request->search['value'])) {
                $search = $request->search['value'];
                $query->where(function ($q) use ($search) {
                    $q->where('t_stock_opnames.so_number', 'ilike', "%{$search}%")
                        ->orWhereHas('store', function ($sq) use ($search) {
                            $sq->where('name', 'ilike', "%{$search}%");
                        })
                        ->orWhere('t_stock_opnames.status', 'ilike', "%{$search}%");
                });
            }

            $total = $query->count();

            // Sorting
            $columns = ['id', 'so_number', 'store_id', 'so_date', 'status', 'editing_by', 'created_at'];
            if ($request->has('order') && is_array($request->order) && isset($request->order[0])) {
                $orderColumn = $columns[$request->order[0]['column']] ?? 'created_at';
                $orderDir = $request->order[0]['dir'] ?? 'desc';
            } else {
                $orderColumn = 'created_at';
                $orderDir = 'desc';
            }

            $query->orderBy($orderColumn, $orderDir);

            // Pagination
            $start = $request->start;
            $length = $request->length;
            $data = $query->skip($start)->take($length)->get();

            // Format data
            $result = [];
            $no = $start + 1;
            foreach ($data as $row) {
                $statusBadge = $row->status == 'Draft' ? 'badge-secondary' : 
                              ($row->status == 'Submitted' ? 'badge-success' : 'badge-secondary');
                
                // Editing status
                $editingStatus = '-';
                if ($row->isBeingEdited()) {
                    $editingInfo = $row->getEditingInfo();
                    $editingStatus = '<span class="badge badge-danger" title="' . $editingInfo['timestamp'] . '">' .
                                    '<i class="fas fa-edit"></i> ' . $editingInfo['user'] .
                                    '</span><br><small class="text-muted">' . $editingInfo['since'] . '</small>';
                }
                
                $result[] = [
                    $no++,
                    '<a href="' . route('stock_opnames.show', $row->id) . '">' . $row->so_number . '</a>',
                    $row->store->name ?? '',
                    $row->so_date->format('d M Y'),
                    '<span class="badge ' . $statusBadge . '">' . $row->status . '</span>',
                    $editingStatus,
                    $row->created_at->format('M jS Y, H:i'),
                ];
            }

            return response()->json([
                "draw" => intval($request->draw),
                "recordsTotal" => $total,
                "recordsFiltered" => $total,
                "data" => $result,
            ]);
        }

        return view('t-stock-opnames.index', [
            'variables' => $this->variables()
        ]);
    }

    public function create()
    {
        // Check if store is selected
        if (!session('store_selected')) {
            return redirect()->route('stock_opnames.index')
                ->with('error', 'Silakan pilih store dari menu header sebelum membuat Stock Opname.');
        }

        // Business rule: LSM hanya bisa diinput hari berjalan
        $today = Carbon::today();
        
        // Check if user already has SO for today for the selected store
        $existingSO = StockOpname::where('so_date', $today)
            ->where('store_id', session('store_selected'))
            ->where('delete_flag', 0)
            ->first();

        if ($existingSO) {
            // Check if SO is already submitted
            if ($existingSO->status === 'Submitted') {
                return redirect()->route('stock_opnames.show', $existingSO->id)
                    ->with('error', 'Stock Opname untuk hari ini sudah di-submit dan tidak dapat diedit lagi. SO Number: ' . $existingSO->so_number);
            }
            
            // If still Draft, redirect to edit
            return redirect()->route('stock_opnames.edit', $existingSO->id)
                ->with('info', 'Stock Opname untuk hari ini sudah ada. Anda dapat melanjutkan edit.');
        }

        // Auto-create Stock Opname with all raw materials for the store
        try {
            DB::beginTransaction();
            
            // Create SO header
            $stockOpname = StockOpname::create([
                'store_id' => session('store_selected'),
                'so_date' => $today,
                'status' => 'Draft',
                'notes' => 'Auto-created Stock Opname for daily LSM',
                'created_by' => $this->logged_user_id,
                'delete_flag' => 0
            ]);

            // Get all raw materials for this store
            $rawMaterials = StoreRawMaterial::getRawMaterialsForStore(session('store_selected'));
            
            // Create SO details for all raw materials with zero values
            foreach ($rawMaterials as $rm) {
                StockOpnameDetail::create([
                    'so_id' => $stockOpname->id,
                    'raw_material_id' => $rm->id,
                    'qty_utuh' => 0,
                    'qty_pakai' => 0,
                    'adj_ekstra' => 0,
                    'adj_restan' => 0,
                    'adj_ringkas' => 0,
                    'adj_selisih' => 0,
                    'item_olahan' => 0,
                    'lsm_aktual' => 0,
                    'doi' => 0,
                    'notes' => null
                ]);
            }

            DB::commit();
            
            // Redirect to edit the newly created SO
            return redirect()->route('stock_opnames.edit', $stockOpname->id)
                ->with('success', 'Stock Opname berhasil dibuat. Semua item siap untuk diinput dengan autosave aktif.');
                
        } catch (\Exception $e) {
            DB::rollback();
            return redirect()->route('stock_opnames.index')
                ->with('error', 'Gagal membuat Stock Opname: ' . $e->getMessage());
        }
    }

    public function edit(StockOpname $stockOpname)
    {
        // Business rule: Edit maksimal H+1 jam 11:00
        $cutoffTime = Carbon::parse($stockOpname->so_date)->addDay()->setTime(11, 0);
        
        if (Carbon::now() > $cutoffTime && $stockOpname->status == 'Submitted') {
            return redirect()->route('stock_opnames.show', $stockOpname->id)
                ->with('error', 'Tidak dapat edit setelah H+1 jam 11:00 atau ketika status sudah Submitted.');
        }

        // Check if SO is being edited by another user
        if ($stockOpname->isBeingEditedByAnotherUser()) {
            $editingInfo = $stockOpname->getEditingInfo();
            return redirect()->route('stock_opnames.show', $stockOpname->id)
                ->with('error', "Dokumen SO ini sedang diedit oleh {$editingInfo['user']} sejak {$editingInfo['since']}. Silakan tunggu hingga selesai.");
        }

        // Lock SO for current user
        $stockOpname->lockForEditing();

        // Get raw materials for the selected store
        $rawMaterials = StoreRawMaterial::getRawMaterialsForStore($stockOpname->store_id);
        
        // Auto-create missing SO detail records for all raw materials
        $existingDetailIds = StockOpnameDetail::where('so_id', $stockOpname->id)
            ->pluck('raw_material_id')
            ->toArray();
        
        $missingCount = 0;
        foreach ($rawMaterials as $rawMaterial) {
            if (!in_array($rawMaterial->id, $existingDetailIds)) {
                StockOpnameDetail::create([
                    'so_id' => $stockOpname->id,
                    'raw_material_id' => $rawMaterial->id,
                    'qty_utuh' => 0,
                    'qty_pakai' => 0,
                    'adj_ekstra' => 0,
                    'adj_restan' => 0,
                    'adj_ringkas' => 0,
                    'adj_selisih' => 0,
                    'notes' => null,
                ]);
                $missingCount++;
            }
        }
        
        if ($missingCount > 0) {
            \Log::info("Auto-created {$missingCount} missing SO detail records for SO ID: {$stockOpname->id}");
        }

        $stockOpname->load('details.rawMaterial');
        $variables['judul'] = 'Edit Stock Opname';
        $variables['stores'] = Store::where('delete_flag', 0)->orderBy('name')->get();
        $variables['raw_materials'] = $rawMaterials;
        
        // Get mappings for inv_matrix_flag and other store-specific settings
        $mappings = StoreRawMaterial::active()
            ->forStore($stockOpname->store_id)
            ->get()
            ->keyBy('raw_material_id');
        $variables['mappings'] = $mappings;
        
        return view('t-stock-opnames.form', [
            'data' => $stockOpname,
            'variables' => $this->variables($variables)
        ]);
    }

    private function variables($variables = null)
    {
        $variables['header_routes'] = ' 
								<li class="breadcrumb-item">Transaction</li>  
								<li class="breadcrumb-item active">Stock Opname (LSM)</li>
							 ';
        return $variables;
    }

    public function store(Request $request, $prm_id)
    {
        $stockOpname = null;
        if ($prm_id)
            $stockOpname = StockOpname::findOrFail($prm_id);

        // Use session store_id if not provided in request
        if (!$request->has('store_id') || empty($request->store_id)) {
            $request->merge(['store_id' => session('store_selected')]);
        }

        // Validasi backdate
        $user = auth()->user();
        $soDate = $request->so_date;
        $today = date('Y-m-d');
        
        if ($soDate < $today && !$user->canBackdateSO()) {
            return response()->json([
                'success' => false,
                'message' => 'Anda tidak memiliki izin untuk backdate Stock Opname. Hubungi administrator untuk mengaktifkan akses backdate.'
            ], 403);
        }

        // Validasi header
        $rules = [
            'store_id' => 'required|exists:m_stores,id',
            'so_date' => 'required|date|before_or_equal:today',
            'notes' => 'nullable|string',
            'status' => 'required|in:Draft,Submitted',
        ];

        // Validasi detail items
        if ($request->has('items')) {
            $rules['items.*.raw_material_id'] = 'required|exists:m_raw_materials,id';
            $rules['items.*.qty_utuh'] = 'required|integer|min:0|max:9999'; // Integer only, no decimal
            $rules['items.*.qty_pakai'] = 'required|numeric|min:0|max:999999.99|regex:/^\d+(\.\d{1,2})?$/'; // Decimal max 2 digits
            $rules['items.*.adj_ekstra'] = 'nullable|numeric|regex:/^-?\d+(\.\d{1,2})?$/'; // Decimal max 2 digits, can be negative
            $rules['items.*.adj_restan'] = 'nullable|numeric|regex:/^-?\d+(\.\d{1,2})?$/'; // Decimal max 2 digits, can be negative
            $rules['items.*.adj_ringkas'] = 'nullable|numeric|regex:/^-?\d+(\.\d{1,2})?$/'; // Decimal max 2 digits, can be negative
            $rules['items.*.adj_selisih'] = 'nullable|numeric|regex:/^-?\d+(\.\d{1,2})?$/'; // Decimal max 2 digits, can be negative
        }

        $messages = [
            'store_id.required' => 'Please select a store from the header menu first.',
            'store_id.exists' => 'Selected store is invalid.',
            'so_date.before_or_equal' => 'Tanggal Stock Opname tidak boleh lebih dari hari ini.',
        ];
        $saveData = $request->validate($rules, $messages);

        try {
            DB::beginTransaction();
            
            if ($stockOpname) {
                $stockOpname->update(array_merge($saveData, [
                    'updated_by' => $this->logged_user_id,
                ]));
                
                // Delete existing details
                StockOpnameDetail::where('so_id', $stockOpname->id)->delete();
            } else {
                $stockOpname = StockOpname::create(array_merge($saveData, [
                    'created_by' => $this->logged_user_id,
                ]));
            }

            // Save details - only save items with qty > 0
            if ($request->has('items')) {
                foreach ($request->items as $item) {
                    if (!empty($item['raw_material_id']) && 
                        (($item['qty_utuh'] ?? 0) > 0 || ($item['qty_pakai'] ?? 0) > 0 || 
                         ($item['adj_ekstra'] ?? 0) != 0 || ($item['adj_restan'] ?? 0) != 0 || 
                         ($item['adj_ringkas'] ?? 0) != 0 || ($item['adj_selisih'] ?? 0) != 0)) {
                        
                        StockOpnameDetail::create([
                            'so_id' => $stockOpname->id,
                            'raw_material_id' => $item['raw_material_id'],
                            'qty_utuh' => $item['qty_utuh'] ?? 0,
                            'qty_pakai' => $item['qty_pakai'] ?? 0,
                            'adj_ekstra' => $item['adj_ekstra'] ?? 0,
                            'adj_restan' => $item['adj_restan'] ?? 0,
                            'adj_ringkas' => $item['adj_ringkas'] ?? 0,
                            'adj_selisih' => $item['adj_selisih'] ?? 0,
                            'notes' => $item['notes'] ?? null,
                        ]);
                    }
                }
            }

            // If status is Submitted, update current stock from SO data
            if ($saveData['status'] === 'Submitted') {
                try {
                    \App\Models\CurrentStock::initializeFromStockOpname($stockOpname->id);
                    \Log::info("Current stock updated from SO ID: {$stockOpname->id}");
                    
                    // Clear current stock cache after update
                    CacheService::clearCurrentStockCache($stockOpname->store_id);
                } catch (\Exception $stockError) {
                    \Log::error("Failed to update current stock from SO: " . $stockError->getMessage());
                    // Don't fail the whole transaction, just log the error
                }
            }

            DB::commit();
            
            // Unlock SO after successful save
            $stockOpname->unlockFromEditing();
            
            // Clear cache
            CacheService::clearStockOpnameCache($stockOpname->store_id);
            
            $notif = $stockOpname->wasRecentlyCreated ? 'Berhasil dibuat.' : 'Berhasil diupdate.';
            return response()->json(['success', $notif]);
            
        } catch (\Exception $e) {
            DB::rollback();

            $notif = $this->getErrorCatch($e->getMessage());
            return response()->json(['warning', $notif]);
        }
    }

    public function show(StockOpname $stockOpname)
    {
        $stockOpname->load(['creator', 'modificator', 'editor', 'store', 'details.rawMaterial']);
        $variables['judul'] = 'View Stock Opname #' . $stockOpname->so_number;
        return view('t-stock-opnames.show', [
            'data' => $stockOpname,
            'variables' => $this->variables($variables)
        ]);
    }

    public function destroy(StockOpname $stockOpname)
    {
        if ($stockOpname->status == 'Submitted') {
            return redirect()->route('stock_opnames.index')
                ->with('error', 'Tidak dapat menghapus Stock Opname yang sudah di-submit.');
        }

        $stockOpname->update([
            'delete_flag' => 1,
            'updated_by' => $this->logged_user_id
        ]);

        // Clear cache
        CacheService::clearStockOpnameCache($stockOpname->store_id);
        CacheService::clearCurrentStockCache($stockOpname->store_id);

        return redirect()->route('stock_opnames.index')
            ->with('success', 'Successfully deleted');
    }

    // Get raw materials for specific store (AJAX endpoint)
    public function getRawMaterialsForStore(Request $request)
    {
        $storeId = $request->store_id;
        
        if (!$storeId) {
            return response()->json(['success' => false, 'message' => 'Store ID diperlukan']);
        }

        $rawMaterials = StoreRawMaterial::active()
            ->forStore($storeId)
            ->with(['rawMaterial.activeConversion'])
            ->get()
            ->map(function ($mapping) {
                $rm = $mapping->rawMaterial;
                $activeConversion = $rm->activeConversion;
                
                // Check if conversion mapping exists
                $hasConversion = $activeConversion !== null;
                $conversionFactor = $hasConversion ? $activeConversion->conversion_factor : 1;
                $uomUtuh = $hasConversion ? $activeConversion->uom_utuh : $rm->uom;
                $uomPakai = $hasConversion ? $activeConversion->uom_pakai : $rm->uom;
                
                return [
                    'id' => $rm->id,
                    'code' => $rm->code,
                    'name' => $rm->name,
                    'uom' => $rm->uom,
                    'uom_utuh' => $uomUtuh,
                    'uom_pakai' => $uomPakai,
                    'tipe_barang' => $rm->tipe_barang,
                    'conversion_factor' => $conversionFactor,
                    'has_conversion' => $hasConversion,
                    'conversion_warning' => !$hasConversion ? 'No conversion mapping! Using 1:1 ratio.' : null,
                    'inv_matrix_flag' => $mapping->inv_matrix_flag, // Read from mapping table
                    'min_stock' => $mapping->min_stock,
                    'max_stock' => $mapping->max_stock,
                    'reorder_point' => $mapping->reorder_point,
                ];
            });

        return response()->json([
            'success' => true,
            'raw_materials' => $rawMaterials
        ]);
    }

    // Auto-load all raw materials for selected store
    public function autoLoadRawMaterials(Request $request)
    {
        $storeId = $request->store_id;
        
        if (!$storeId) {
            return response()->json(['success' => false, 'message' => 'Store ID diperlukan']);
        }

        $rawMaterials = StoreRawMaterial::active()
            ->forStore($storeId)
            ->with('rawMaterial')
            ->get()
            ->map(function ($mapping) {
                return [
                    'raw_material_id' => $mapping->rawMaterial->id,
                    'raw_material' => array_merge($mapping->rawMaterial->toArray(), [
                        'inv_matrix_flag' => $mapping->inv_matrix_flag, // Read from mapping table
                        'uom_pakai' => $mapping->rawMaterial->uom_pakai,
                    ]),
                    'qty_utuh' => 0,
                    'qty_pakai' => 0,
                    'min_stock' => $mapping->min_stock,
                    'max_stock' => $mapping->max_stock,
                    'reorder_point' => $mapping->reorder_point,
                ];
            });

        return response()->json([
            'success' => true,
            'raw_materials' => $rawMaterials
        ]);
    }

    // Auto-save field changes with audit trail
    public function autoSaveField(Request $request)
    {
        
        $request->validate([
            'so_detail_id' => 'required|exists:t_stock_opname_details,id',
            'field_name' => 'required|in:qty_utuh,qty_pakai,adj_ekstra,adj_restan,adj_ringkas,adj_selisih',
            'value' => 'required|numeric',
            'qty_utuh' => 'nullable|numeric',
            'qty_pakai' => 'nullable|numeric',
        ]);

        $soDetail = StockOpnameDetail::findOrFail($request->so_detail_id);
        \Log::warning('step 1a');
        // Check if SO is still editable
        if ($soDetail->stockOpname->status !== 'Draft') {
            return response()->json([
                'success' => false,
                'message' => 'Stock Opname tidak dapat diedit dalam status saat ini'
            ]);
        }
        
        $fieldName = $request->field_name;
        $value = $request->value;

        // Validate adjustment fields - only allowed if inv_matrix_flag = 1
        $adjustmentFields = ['adj_ekstra', 'adj_restan', 'adj_ringkas', 'adj_selisih'];
        if (in_array($fieldName, $adjustmentFields)) {
            // Get mapping to check inv_matrix_flag
            $mapping = StoreRawMaterial::getMapping($soDetail->stockOpname->store_id, $soDetail->raw_material_id);
            
            if (!$mapping || !$mapping->inv_matrix_flag) {
                return response()->json([
                    'success' => false,
                    'message' => 'Field adjustment tidak diizinkan untuk raw material ini (inv_matrix_flag = 0)'
                ]);
            }
        }
        
        // Validate field constraints
        if ($fieldName === 'qty_utuh' && strlen(str_replace('.', '', $value)) > 4) {
            return response()->json([
                'success' => false,
                'message' => 'Sisa Utuh maximum 4 digits'
            ]);
        }
        
        if ($fieldName === 'qty_pakai' && strlen(str_replace(['.', ','], '', $value)) > 6) {
            return response()->json([
                'success' => false,
                'message' => 'Sisa Pakai maximum 6 digits'
            ]);
        }

        // Convert comma to dot for qty_pakai
        if ($fieldName === 'qty_pakai') {
            $value = str_replace(',', '.', $value);
        }

        try {
            
            DB::beginTransaction();

            // Load relationships before update to ensure we can calculate
            $soDetail->load('rawMaterial.activeConversion');
            
            $oldValue = $soDetail->$fieldName;
            
            \Log::info('AutoSaveField - Before update', [
                'so_detail_id' => $soDetail->id,
                'field_name' => $fieldName,
                'old_value' => $oldValue,
                'new_value' => $value,
                'raw_material_id' => $soDetail->raw_material_id,
                'has_raw_material' => $soDetail->rawMaterial !== null,
                'has_active_conversion' => $soDetail->rawMaterial && $soDetail->rawMaterial->activeConversion !== null,
                'conversion_factor' => $soDetail->rawMaterial && $soDetail->rawMaterial->activeConversion 
                    ? $soDetail->rawMaterial->activeConversion->conversion_factor 
                    : 'N/A',
                'current_qty_utuh' => $soDetail->qty_utuh,
                'current_qty_pakai' => $soDetail->qty_pakai,
            ]);
            
            // Update the field - this will update the model attribute
            $soDetail->$fieldName = $value;
            
            // IMPORTANT: Also update qty_utuh and qty_pakai from frontend
            // This ensures both fields are saved together
            if ($fieldName === 'qty_utuh' || $fieldName === 'qty_pakai') {
                // For qty fields, always update both from frontend values
                if ($request->has('qty_utuh')) {
                    $soDetail->qty_utuh = $request->qty_utuh;
                }
                if ($request->has('qty_pakai')) {
                    $soDetail->qty_pakai = $request->qty_pakai;
                }
            }
            
            // Ensure qty_utuh and qty_pakai are not null
            $soDetail->qty_utuh = $soDetail->qty_utuh ?? 0;
            $soDetail->qty_pakai = $soDetail->qty_pakai ?? 0;
            
            \Log::info('AutoSaveField - Values to save', [
                'field_name' => $fieldName,
                'field_value' => $value,
                'qty_utuh_from_request' => $request->qty_utuh,
                'qty_pakai_from_request' => $request->qty_pakai,
                'qty_utuh_to_save' => $soDetail->qty_utuh,
                'qty_pakai_to_save' => $soDetail->qty_pakai,
            ]);
            
            // Manual calculation (since boot() might not work with update())
            $rawMaterial = $soDetail->rawMaterial;
            if ($rawMaterial) {
                $activeConversion = $rawMaterial->activeConversion;
                $conversionFactor = $activeConversion ? $activeConversion->conversion_factor : 1;
                
                \Log::info('AutoSaveField - Calculation inputs', [
                    'qty_utuh' => $soDetail->qty_utuh,
                    'qty_pakai' => $soDetail->qty_pakai,
                    'conversion_factor' => $conversionFactor,
                    'has_active_conversion' => $activeConversion !== null,
                ]);
                
                // Calculate qty_converted and lsm_aktual (Ending Stock)
                $soDetail->qty_converted = $soDetail->qty_utuh * $conversionFactor;
                $soDetail->lsm_aktual = $soDetail->qty_converted + $soDetail->qty_pakai + ($soDetail->item_olahan ?? 0);
                
                \Log::info('AutoSaveField - Manual calculation', [
                    'conversion_factor' => $conversionFactor,
                    'qty_utuh' => $soDetail->qty_utuh,
                    'qty_pakai' => $soDetail->qty_pakai,
                    'qty_converted' => $soDetail->qty_converted,
                    'lsm_aktual' => $soDetail->lsm_aktual,
                ]);
            } else {
                \Log::error('AutoSaveField - No raw material found!', [
                    'raw_material_id' => $soDetail->raw_material_id,
                ]);
            }
            
            // Calculate DOI BEFORE commit so we can include it in response
            try {
                $stockOpname = $soDetail->stockOpname;
                if ($stockOpname) {
                    $startDate = $stockOpname->so_date->copy()->addDay();
                    $endDate = $startDate->copy()->addDays(6);
                    
                    // Fix: Query from t_forecast_details, not t_forecasts
                    $avgForecast = \DB::table('t_forecast_details')
                        ->join('t_forecasts', 't_forecast_details.forecast_header_id', '=', 't_forecasts.id')
                        ->where('t_forecasts.store_id', $stockOpname->store_id)
                        ->where('t_forecast_details.raw_material_id', $soDetail->raw_material_id)
                        ->whereBetween('t_forecast_details.forecast_date', [$startDate, $endDate])
                        ->where('t_forecast_details.delete_flag', false)
                        ->where('t_forecasts.delete_flag', false)
                        ->avg('t_forecast_details.forecast_qty');

                    $doi = $avgForecast > 0 ? ($soDetail->lsm_aktual / $avgForecast) : 0;
                    
                    // Update DOI in the same save
                    $soDetail->doi = $doi;
                    
                    \Log::info('AutoSaveField - DOI calculated', [
                        'avg_forecast' => $avgForecast,
                        'lsm_aktual' => $soDetail->lsm_aktual,
                        'doi' => $doi,
                    ]);
                }
            } catch (\Exception $calcError) {
                // If calculation fails, continue without it
                \Log::warning('Calculation error in autosave: ' . $calcError->getMessage());
            }
            
            // Save the model with DOI included
            $saved = $soDetail->save();
            
            \Log::info('AutoSaveField - After save', [
                'save_result' => $saved,
                'qty_utuh' => $soDetail->qty_utuh,
                'qty_pakai' => $soDetail->qty_pakai,
                'qty_converted' => $soDetail->qty_converted,
                'lsm_aktual' => $soDetail->lsm_aktual,
                'doi' => $soDetail->doi,
                'model_exists' => $soDetail->exists,
                'model_id' => $soDetail->id,
            ]);
            
            // Verify data was saved by querying database
            $verifyFromDb = \DB::table('t_stock_opname_details')
                ->where('id', $soDetail->id)
                ->first();
            
            \Log::info('AutoSaveField - Verify from DB', [
                'db_qty_utuh' => $verifyFromDb->qty_utuh ?? 'NULL',
                'db_qty_pakai' => $verifyFromDb->qty_pakai ?? 'NULL',
                'db_lsm_aktual' => $verifyFromDb->lsm_aktual ?? 'NULL',
                'db_doi' => $verifyFromDb->doi ?? 'NULL',
            ]);
            
            // Log the change
            \Log::info('AutoSaveField - Checking if should log change', [
                'old_value' => $oldValue,
                'new_value' => $value,
                'are_different' => $oldValue != $value,
                'field_name' => $fieldName,
            ]);
            
            if ($oldValue != $value) {
                try {
                    StockOpnameChangeLog::logChange(
                        $soDetail->id,
                        $fieldName,
                        $oldValue,
                        $value
                    );
                    \Log::info('AutoSaveField - Change logged successfully');
                } catch (\Exception $e) {
                    \Log::error('AutoSaveField - Failed to log change: ' . $e->getMessage());
                }
            } else {
                \Log::info('AutoSaveField - Change not logged (values are same)');
            }

            DB::commit();

            // Don't refresh! Use current model values
            \Log::info('AutoSaveField - Final values', [
                'lsm_aktual' => $soDetail->lsm_aktual,
                'doi' => $soDetail->doi,
            ]);

            return response()->json([
                'success' => true,
                'message' => 'Field updated successfully',
                'data' => [
                    'lsm_aktual' => $soDetail->lsm_aktual ?? 0,
                    'doi' => $soDetail->doi ?? 0,
                    'qty_converted' => $soDetail->qty_converted ?? 0,
                ]
            ]);

        } catch (\Exception $e) {
            DB::rollback();
            \Log::error('Autosave error: ' . $e->getMessage() . ' | Trace: ' . $e->getTraceAsString());
            return response()->json([
                'success' => false,
                'message' => 'Error mengupdate field: ' . $e->getMessage()
            ]);
        }
    }

    // Clear all inputs for SO detail
    public function clearInputs(Request $request)
    {
        $request->validate([
            'so_detail_id' => 'required|exists:t_stock_opname_details,id',
        ]);

        $soDetail = StockOpnameDetail::findOrFail($request->so_detail_id);
        
        // Check if SO is still editable
        if ($soDetail->stockOpname->status !== 'Draft') {
            return response()->json([
                'success' => false,
                'message' => 'Stock Opname tidak dapat diedit dalam status saat ini'
            ]);
        }

        try {
            DB::beginTransaction();

            // Clear all input fields but keep change log
            $soDetail->update([
                'qty_utuh' => 0,
                'qty_pakai' => 0,
                'adj_ekstra' => 0,
                'adj_restan' => 0,
                'adj_ringkas' => 0,
                'adj_selisih' => 0,
                'item_olahan' => 0,
            ]);

            // Recalculate dependent fields
            $soDetail->calculateLsmAktual();
            $soDetail->calculateDoi();

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => 'Inputs cleared successfully'
            ]);

        } catch (\Exception $e) {
            DB::rollback();
            return response()->json([
                'success' => false,
                'message' => 'Error menghapus input: ' . $e->getMessage()
            ]);
        }
    }

    // Save SO without creating SPB
    public function saveOnly(Request $request)
    {
        $request->validate([
            'so_id' => 'required|exists:t_stock_opnames,id',
        ]);

        $stockOpname = StockOpname::findOrFail($request->so_id);
        
        if ($stockOpname->status !== 'Draft') {
            return response()->json([
                'success' => false,
                'message' => 'Stock Opname tidak dapat disimpan dalam status saat ini'
            ]);
        }

        try {
            // Just update the updated_at timestamp
            $stockOpname->touch();

            return response()->json([
                'success' => true,
                'message' => 'Stock Opname saved successfully',
                'warning' => 'Stock Opname must be processed into SPB by 10:00 AM the next day.'
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Error menyimpan Stock Opname: ' . $e->getMessage()
            ]);
        }
    }

    // Create SPB from SO
    public function createSPB(Request $request)
    {
        $request->validate([
            'so_id' => 'required|exists:t_stock_opnames,id',
            'spb_type' => 'required|in:Regular,Adjustment',
        ]);

        $stockOpname = StockOpname::findOrFail($request->so_id);
        
        if ($stockOpname->status !== 'Draft') {
            return response()->json([
                'success' => false,
                'message' => 'Stock Opname tidak dapat diproses dalam status saat ini'
            ]);
        }

        try {
            DB::beginTransaction();

            // Update SO status to Submitted (data becomes read-only)
            $stockOpname->update(['status' => 'Submitted']);

            // Create SPB (this will be handled by ItemRequestsController)
            $spbUrl = route('item_requests.create', ['so_id' => $stockOpname->id]);

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => 'Stock Opname submitted successfully. Submitted data cannot be edited.',
                'redirect_url' => $spbUrl
            ]);

        } catch (\Exception $e) {
            DB::rollback();
            return response()->json([
                'success' => false,
                'message' => 'Error membuat SPB: ' . $e->getMessage()
            ]);
        }
    }

    // Get change history for SO detail
    public function getChangeHistory(Request $request)
    {
        $request->validate([
            'so_detail_id' => 'required|exists:t_stock_opname_details,id',
        ]);

        $changeLogs = StockOpnameChangeLog::getChangeHistory($request->so_detail_id);

        return response()->json([
            'success' => true,
            'change_logs' => $changeLogs->map(function ($log) {
                return [
                    'field_name' => $log->field_name,
                    'old_value' => $log->old_value,
                    'new_value' => $log->new_value,
                    'change_reason' => $log->change_reason,
                    'changed_by' => $log->changedBy->fullname ?? 'Unknown',
                    'changed_at' => $log->changed_at->format('d M Y H:i:s'),
                    'ip_address' => $log->ip_address,
                ];
            })
        ]);
    }

    // Fix adjustment fields for raw materials with inv_matrix_flag = 0
    public function fixAdjustmentFields(Request $request)
    {
        try {
            DB::beginTransaction();

            // Reset adjustment fields to 0 for raw materials where inv_matrix_flag = 0
            $affectedRows = DB::table('t_stock_opname_details')
                ->join('m_raw_materials', 't_stock_opname_details.raw_material_id', '=', 'm_raw_materials.id')
                ->where(function($query) {
                    $query->where('m_raw_materials.inv_matrix_flag', '=', 0)
                          ->orWhereNull('m_raw_materials.inv_matrix_flag');
                })
                ->update([
                    't_stock_opname_details.adj_ekstra' => 0,
                    't_stock_opname_details.adj_restan' => 0,
                    't_stock_opname_details.adj_ringkas' => 0,
                    't_stock_opname_details.adj_selisih' => 0,
                    't_stock_opname_details.updated_at' => now(),
                ]);

            DB::commit();

            return response()->json([
                'success' => true,
                'message' => "Fixed adjustment fields for {$affectedRows} records where inv_matrix_flag = 0"
            ]);

        } catch (\Exception $e) {
            DB::rollback();
            return response()->json([
                'success' => false,
                'message' => 'Error memperbaiki field adjustment: ' . $e->getMessage()
            ]);
        }
    }

    // Get raw materials with inv_matrix_flag info for debugging
    public function getRawMaterialsInfo(Request $request)
    {
        $storeId = $request->store_id;
        
        if ($storeId) {
            // Get from mapping table if store_id provided
            $mappings = StoreRawMaterial::active()
                ->forStore($storeId)
                ->with(['rawMaterial', 'rawMaterial.activeConversion'])
                ->get();
            
            return response()->json([
                'success' => true,
                'raw_materials' => $mappings->map(function ($mapping) {
                    $rm = $mapping->rawMaterial;
                    $activeConversion = $rm->activeConversion;
                    $hasConversion = $activeConversion !== null;
                    
                    return [
                        'id' => $rm->id,
                        'code' => $rm->code,
                        'name' => $rm->name,
                        'tipe_barang' => $rm->tipe_barang,
                        'inv_matrix_flag' => $mapping->inv_matrix_flag, // From mapping
                        'uom_pakai' => $hasConversion ? $activeConversion->uom_pakai : $rm->uom,
                        'conversion_factor' => $hasConversion ? $activeConversion->conversion_factor : 1,
                        'has_conversion' => $hasConversion,
                        'has_adjustments' => $mapping->inv_matrix_flag == 1 ? 'Yes' : 'No',
                    ];
                })
            ]);
        }
        
        // Fallback: get all raw materials (without store-specific flags)
        $rawMaterials = RawMaterial::select('id', 'code', 'name', 'tipe_barang', 'uom')
            ->with('activeConversion')
            ->where('delete_flag', 0)
            ->orderBy('code')
            ->get();

        return response()->json([
            'success' => true,
            'raw_materials' => $rawMaterials->map(function ($rm) {
                $activeConversion = $rm->activeConversion;
                $hasConversion = $activeConversion !== null;
                
                return [
                    'id' => $rm->id,
                    'code' => $rm->code,
                    'name' => $rm->name,
                    'tipe_barang' => $rm->tipe_barang,
                    'inv_matrix_flag' => null, // Not available without store context
                    'uom_pakai' => $hasConversion ? $activeConversion->uom_pakai : $rm->uom,
                    'conversion_factor' => $hasConversion ? $activeConversion->conversion_factor : 1,
                    'has_conversion' => $hasConversion,
                    'has_adjustments' => 'N/A',
                ];
            })
        ]);
    }

    // Manual trigger to update current stock from submitted SO
    public function updateCurrentStockFromSO(Request $request)
    {
        $request->validate([
            'so_id' => 'required|exists:t_stock_opnames,id'
        ]);

        $stockOpname = StockOpname::findOrFail($request->so_id);
        
        if ($stockOpname->status !== 'Submitted') {
            return response()->json([
                'success' => false,
                'message' => 'Hanya Stock Opname yang sudah di-submit yang dapat mengupdate current stock'
            ]);
        }

        try {
            $result = \App\Models\CurrentStock::initializeFromStockOpname($stockOpname->id);
            
            if ($result) {
                return response()->json([
                    'success' => true,
                    'message' => 'Current stock updated successfully from SO #' . $stockOpname->so_number
                ]);
            } else {
                return response()->json([
                    'success' => false,
                    'message' => 'Gagal mengupdate current stock'
                ]);
            }
        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => 'Error mengupdate current stock: ' . $e->getMessage()
            ]);
        }
    }

    // Get current stock for display in Stock Opname form
    public function getCurrentStockForStore(Request $request)
    {
        $request->validate([
            'store_id' => 'required|exists:m_stores,id',
            'raw_material_ids' => 'required|array',
            'raw_material_ids.*' => 'exists:m_raw_materials,id'
        ]);

        $stocks = [];
        foreach ($request->raw_material_ids as $rawMaterialId) {
            $currentStock = \App\Models\CurrentStock::getCurrentStock($request->store_id, $rawMaterialId);
            
            if ($currentStock) {
                $stocks[$rawMaterialId] = [
                    'qty_utuh' => $currentStock->qty_utuh,
                    'qty_pakai' => $currentStock->qty_pakai,
                    'qty_total_converted' => $currentStock->qty_total_converted,
                    'last_updated' => $currentStock->last_updated_date->format('d M Y'),
                    'status' => $currentStock->getStockStatus(),
                    'status_color' => $currentStock->getStatusColor(),
                    'uom_utuh' => $currentStock->uom_utuh,
                    'uom_pakai' => $currentStock->uom_pakai
                ];
            } else {
                // No stock record found - initialize with zero
                $rawMaterial = RawMaterial::find($rawMaterialId);
                $stocks[$rawMaterialId] = [
                    'qty_utuh' => 0,
                    'qty_pakai' => 0,
                    'qty_total_converted' => 0,
                    'last_updated' => 'Never',
                    'status' => 'Out of Stock',
                    'status_color' => 'danger',
                    'uom_utuh' => $rawMaterial->uom ?? 'PCS',
                    'uom_pakai' => $rawMaterial->uom_pakai ?? 'GR'
                ];
            }
        }

        return response()->json([
            'success' => true,
            'stocks' => $stocks
        ]);
    }

    /**
     * Generate SPB Regular otomatis dari SO yang sudah Submitted
     */
    public function generateSPB(Request $request, $soId)
    {
        Log::info("=== Generate SPB START ===");
        Log::info("SO ID: {$soId}");
        Log::info("User ID: " . auth()->id());
        Log::info("Request data: " . json_encode($request->all()));
        
        try {
            $stockOpname = StockOpname::with(['details.rawMaterial', 'store'])->findOrFail($soId);
            
            Log::info("SO found: {$stockOpname->so_number}, Status: {$stockOpname->status}");
            Log::info("Store: {$stockOpname->store->name} (ID: {$stockOpname->store_id})");
            Log::info("Details count: " . $stockOpname->details->count());
            
            // Validasi: SO harus sudah Submitted
            if ($stockOpname->status !== 'Submitted') {
                Log::warning("SO status is not Submitted: {$stockOpname->status}");
                return response()->json([
                    'success' => false,
                    'message' => 'SPB hanya dapat dibuat dari SO yang sudah berstatus Submitted.'
                ], 400);
            }
            
            // Validasi: Cek apakah sudah ada SPB Regular untuk SO ini
            $hasExistingSPB = ItemRequest::hasRegularSPBForSO($soId);
            Log::info("Has existing SPB: " . ($hasExistingSPB ? 'Yes' : 'No'));
            
            if ($hasExistingSPB) {
                Log::warning("SPB Regular already exists for SO ID: {$soId}");
                return response()->json([
                    'success' => false,
                    'message' => 'SPB Regular sudah pernah dibuat untuk SO ini.'
                ], 400);
            }
            
            // Get available shipment schedules (minimal H-2)
            $minDeliveryDate = Carbon::now()->addDays(2)->format('Y-m-d');
            $shipmentSchedules = ShipmentDelivery::where('store_id', $stockOpname->store_id)
                ->where('status', 'Active')
                ->where('delete_flag', false)
                ->get();
            
            Log::info("Shipment schedules query - Store ID: {$stockOpname->store_id}");
            Log::info("Found {$shipmentSchedules->count()} shipment schedules");
            
            if ($shipmentSchedules->isEmpty()) {
                Log::warning("No shipment schedules found for store ID: {$stockOpname->store_id}");
                return response()->json([
                    'success' => false,
                    'message' => 'Tidak ada jadwal pengiriman yang tersedia untuk store ini. Silakan setup jadwal pengiriman terlebih dahulu di Master → Shipment Deliveries.'
                ], 400);
            }
            
            // Group SO details by tipe_dokumen and tipe_barang
            Log::info("Starting grouping process...");
            $groupedItems = $this->groupSODetailsByType($stockOpname);
            
            Log::info("Grouped items into " . count($groupedItems) . " groups");
            Log::info("Groups: " . json_encode(array_keys($groupedItems)));
            
            if (empty($groupedItems)) {
                Log::warning("No items to create SPB");
                return response()->json([
                    'success' => false,
                    'message' => 'Tidak ada item yang dapat dibuatkan SPB. Pastikan semua item memiliki mapping tipe_dokumen dan tipe_barang.'
                ], 400);
            }
            
            DB::beginTransaction();
            
            $createdSPBs = [];
            $errors = [];
            
            Log::info("Starting SPB creation for " . count($groupedItems) . " groups");
            
            // Create SPB for each group
            foreach ($groupedItems as $groupKey => $items) {
                try {
                    Log::info("Processing group: {$groupKey} with " . count($items) . " items");
                    
                    list($tipeDokumen, $tipeBarang) = explode('|', $groupKey);
                    
                    Log::info("Tipe Dokumen: {$tipeDokumen}, Tipe Barang: {$tipeBarang}");
                    
                    // Generate SPB number
                    Log::info("Generating SPB number...");
                    $spbNumber = NumberingFormat::generateSPBNumber(
                        $stockOpname->store_id,
                        'Regular',
                        $tipeDokumen,
                        $tipeBarang
                    );
                    Log::info("Generated SPB number: {$spbNumber}");
                    
                    // Get appropriate shipment schedule
                    $shipmentSchedule = $this->getAppropriateShipmentSchedule(
                        $shipmentSchedules,
                        $tipeDokumen,
                        $tipeBarang
                    );
                    Log::info("Shipment schedule ID: " . ($shipmentSchedule ? $shipmentSchedule->id : 'null'));
                    
                    // Calculate delivery date based on shipment schedule
                    $deliveryDate = $this->calculateDeliveryDate($shipmentSchedule);
                    Log::info("Calculated delivery date: {$deliveryDate}");
                    
                    // Create SPB header
                    Log::info("Creating SPB header...");
                    $spb = ItemRequest::create([
                        'spb_number' => $spbNumber,
                        'store_id' => $stockOpname->store_id,
                        'stock_opname_id' => $stockOpname->id,
                        'request_type' => 'Regular',
                        'request_date' => Carbon::now()->format('Y-m-d'),
                        'delivery_date' => $deliveryDate,
                        'shipment_schedule_id' => $shipmentSchedule ? $shipmentSchedule->id : null,
                        'status' => 'Draft',
                        'notes' => "Auto-generated from SO #{$stockOpname->so_number}",
                        'delete_flag' => false,
                        'created_by' => auth()->id()
                    ]);
                    Log::info("SPB created with ID: {$spb->id}");
                    
                    // Create SPB details
                    Log::info("Creating SPB details...");
                    foreach ($items as $item) {
                        ItemRequestDetail::create([
                            'item_request_id' => $spb->id,
                            'raw_material_id' => $item['raw_material_id'],
                            'forecast_qty' => 0, // Will be filled later if needed
                            'so_qty' => 0, // Will be filled later if needed
                            'tsm_qty' => 0, // Will be filled later if needed
                            'suggested_qty' => $item['qty_request'], // Qty from SO
                            'adjusted_qty' => 0, // Not adjusted yet
                            'notes' => $item['notes']
                        ]);
                    }
                    Log::info("Created " . count($items) . " SPB details");
                    
                    $createdSPBs[] = [
                        'spb_number' => $spbNumber,
                        'tipe_dokumen' => $tipeDokumen,
                        'tipe_barang' => $tipeBarang,
                        'item_count' => count($items),
                        'delivery_date' => $deliveryDate
                    ];
                    
                    Log::info("Successfully created SPB: {$spbNumber}");
                    
                } catch (\Exception $e) {
                    $errorMsg = $e->getMessage();
                    $errors[] = [
                        'group' => $groupKey,
                        'error' => $errorMsg
                    ];
                    Log::error("Failed to create SPB for group {$groupKey}: " . $errorMsg);
                    Log::error("Stack trace: " . $e->getTraceAsString());
                }
            }
            
            DB::commit();
            
            Log::info("Transaction committed");
            Log::info("Created SPBs: " . count($createdSPBs));
            Log::info("Errors: " . count($errors));
            
            // Prepare response message
            $successCount = count($createdSPBs);
            $errorCount = count($errors);
            
            if ($successCount > 0 && $errorCount == 0) {
                // All success
                $message = "Berhasil membuat {$successCount} SPB Regular dari SO #{$stockOpname->so_number}:\n\n";
                foreach ($createdSPBs as $spb) {
                    $message .= "• {$spb['spb_number']} ({$spb['tipe_dokumen']}-{$spb['tipe_barang']}) - {$spb['item_count']} items\n";
                }
                
                Log::info("=== Generate SPB SUCCESS ===");
                Log::info($message);
                
                return response()->json([
                    'success' => true,
                    'message' => $message,
                    'data' => [
                        'created_spbs' => $createdSPBs,
                        'total_created' => $successCount
                    ]
                ]);
                
            } elseif ($successCount > 0 && $errorCount > 0) {
                // Partial success
                $message = "Berhasil membuat {$successCount} SPB, namun {$errorCount} SPB gagal dibuat:\n\n";
                $message .= "✓ Berhasil:\n";
                foreach ($createdSPBs as $spb) {
                    $message .= "• {$spb['spb_number']} ({$spb['tipe_dokumen']}-{$spb['tipe_barang']})\n";
                }
                $message .= "\n✗ Gagal:\n";
                foreach ($errors as $error) {
                    $message .= "• {$error['group']}: {$error['error']}\n";
                }
                
                Log::warning("=== Generate SPB PARTIAL SUCCESS ===");
                Log::warning($message);
                
                return response()->json([
                    'success' => true,
                    'message' => $message,
                    'data' => [
                        'created_spbs' => $createdSPBs,
                        'errors' => $errors,
                        'total_created' => $successCount,
                        'total_errors' => $errorCount
                    ]
                ]);
                
            } else {
                // All failed
                DB::rollback();
                $message = "Gagal membuat SPB. Error:\n\n";
                foreach ($errors as $error) {
                    $message .= "• {$error['group']}: {$error['error']}\n";
                }
                
                Log::error("=== Generate SPB ALL FAILED ===");
                Log::error($message);
                
                return response()->json([
                    'success' => false,
                    'message' => $message,
                    'data' => [
                        'errors' => $errors
                    ]
                ], 500);
            }
            
        } catch (\Exception $e) {
            DB::rollback();
            
            $errorMsg = $e->getMessage();
            $errorFile = $e->getFile();
            $errorLine = $e->getLine();
            $errorTrace = $e->getTraceAsString();
            
            Log::error("=== Generate SPB EXCEPTION ===");
            Log::error("Message: {$errorMsg}");
            Log::error("File: {$errorFile}");
            Log::error("Line: {$errorLine}");
            Log::error("Stack trace: {$errorTrace}");
            
            return response()->json([
                'success' => false,
                'message' => 'Terjadi kesalahan saat membuat SPB: ' . $errorMsg,
                'error_details' => [
                    'file' => $errorFile,
                    'line' => $errorLine,
                    'trace' => config('app.debug') ? $errorTrace : null
                ]
            ], 500);
        }
    }
    
    /**
     * Group SO details by tipe_dokumen and tipe_barang
     */
    private function groupSODetailsByType($stockOpname)
    {
        $grouped = [];
        
        Log::info("Grouping SO details - Total details: " . $stockOpname->details->count());
        
        foreach ($stockOpname->details as $detail) {
            Log::info("Processing detail ID: {$detail->id}, Raw Material ID: {$detail->raw_material_id}, LSM Aktual: {$detail->lsm_aktual}");
            
            if ($detail->lsm_aktual <= 0) {
                Log::info("Skipping detail {$detail->id} - lsm_aktual is zero or negative");
                continue; // Skip items with zero quantity
            }
            
            $rawMaterial = $detail->rawMaterial;
            if (!$rawMaterial) {
                Log::warning("Detail {$detail->id} - Raw material not found");
                continue;
            }
            
            Log::info("Raw Material: {$rawMaterial->code} - {$rawMaterial->name}");
            
            // Get tipe_dokumen from mapping
            $mapping = StoreRawMaterial::where('store_id', $stockOpname->store_id)
                ->where('raw_material_id', $rawMaterial->id)
                ->where('is_active', true)
                ->first();
            
            if (!$mapping) {
                Log::warning("Item {$rawMaterial->code} tidak memiliki mapping aktif di store {$stockOpname->store_id}");
                continue;
            }
            
            if (!$mapping->tipe_dokumen) {
                Log::warning("Item {$rawMaterial->code} tidak memiliki tipe_dokumen di mapping");
                continue;
            }
            
            Log::info("Mapping found - Tipe Dokumen: {$mapping->tipe_dokumen}");
            
            // Get tipe_barang from raw material
            $tipeBarang = $rawMaterial->tipe_barang;
            if (!$tipeBarang) {
                Log::warning("Item {$rawMaterial->code} tidak memiliki tipe_barang");
                continue;
            }
            
            Log::info("Tipe Barang: {$tipeBarang}");
            
            // Group key: tipe_dokumen|tipe_barang
            $groupKey = $mapping->tipe_dokumen . '|' . $tipeBarang;
            
            if (!isset($grouped[$groupKey])) {
                $grouped[$groupKey] = [];
                Log::info("Created new group: {$groupKey}");
            }
            
            $grouped[$groupKey][] = [
                'raw_material_id' => $rawMaterial->id,
                'qty_request' => $detail->lsm_aktual,
                'uom' => $rawMaterial->uom,
                'notes' => "From SO detail: {$rawMaterial->code} - {$rawMaterial->name}"
            ];
            
            Log::info("Added item to group {$groupKey}");
        }
        
        Log::info("Grouping complete - Total groups: " . count($grouped));
        foreach ($grouped as $key => $items) {
            Log::info("Group {$key}: " . count($items) . " items");
        }
        
        return $grouped;
    }
    
    /**
     * Get appropriate shipment schedule
     */
    private function getAppropriateShipmentSchedule($shipmentSchedules, $tipeDokumen, $tipeBarang)
    {
        // Logic untuk memilih shipment schedule yang sesuai
        // Bisa disesuaikan dengan business rule
        
        // Untuk saat ini, ambil schedule pertama yang tersedia
        // TODO: Implement logic berdasarkan tipe_dokumen dan tipe_barang
        
        return $shipmentSchedules->first();
    }
    
    /**
     * Calculate delivery date based on shipment schedule
     */
    private function calculateDeliveryDate($shipmentSchedule)
    {
        if (!$shipmentSchedule) {
            // Default: H+2
            return Carbon::now()->addDays(2)->format('Y-m-d');
        }
        
        // Calculate based on h_minus
        $hMinus = $shipmentSchedule->h_minus ?? 2;
        
        // Find next delivery day
        $today = Carbon::now();
        $deliveryDay = $shipmentSchedule->day_name; // e.g., "Monday", "Tuesday"
        
        // Get next occurrence of delivery day
        $nextDelivery = $today->copy();
        while ($nextDelivery->format('l') !== $deliveryDay) {
            $nextDelivery->addDay();
        }
        
        // Ensure minimal H-2
        $minDeliveryDate = $today->copy()->addDays(2);
        if ($nextDelivery->lt($minDeliveryDate)) {
            $nextDelivery->addWeek();
        }
        
        return $nextDelivery->format('Y-m-d');
    }

    /**
     * Unlock SO from editing (called when user navigates away)
     */
    public function unlockSO(Request $request)
    {
        $request->validate([
            'so_id' => 'required|exists:t_stock_opnames,id'
        ]);

        try {
            $stockOpname = StockOpname::findOrFail($request->so_id);
            
            // Only unlock if current user is the one editing
            if ($stockOpname->isBeingEditedByCurrentUser()) {
                $stockOpname->unlockFromEditing();
                
                Log::info("SO unlocked by user navigating away", [
                    'so_id' => $stockOpname->id,
                    'so_number' => $stockOpname->so_number,
                    'user_id' => auth()->id()
                ]);
            }

            return response()->json([
                'success' => true,
                'message' => 'SO unlocked successfully'
            ]);

        } catch (\Exception $e) {
            Log::error("Failed to unlock SO: " . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Gagal unlock SO: ' . $e->getMessage()
            ], 500);
        }
    }

    /**
     * Heartbeat - Keep lock alive (called every 2 minutes from frontend)
     */
    public function heartbeat(Request $request)
    {
        $request->validate([
            'so_id' => 'required|exists:t_stock_opnames,id'
        ]);

        try {
            $stockOpname = StockOpname::findOrFail($request->so_id);
            
            // Update heartbeat timestamp
            if ($stockOpname->updateHeartbeat()) {
                return response()->json([
                    'success' => true,
                    'message' => 'Heartbeat updated',
                    'timestamp' => now()->toISOString()
                ]);
            } else {
                return response()->json([
                    'success' => false,
                    'message' => 'Not authorized to update heartbeat'
                ], 403);
            }

        } catch (\Exception $e) {
            Log::error("Heartbeat failed: " . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Heartbeat gagal: ' . $e->getMessage()
            ], 500);
        }
    }

    /**
     * Force unlock SO (Admin only)
     */
    public function forceUnlock(Request $request)
    {
        $request->validate([
            'so_id' => 'required|exists:t_stock_opnames,id'
        ]);

        try {
            $stockOpname = StockOpname::findOrFail($request->so_id);
            
            // Get info before unlock for logging
            $wasEditingBy = $stockOpname->editing_by;
            $wasEditingByUser = $stockOpname->editor ? $stockOpname->editor->fullname : 'Unknown';
            
            // Force unlock regardless of who is editing
            $stockOpname->unlockFromEditing();
            
            Log::warning("SO force unlocked by admin", [
                'so_id' => $stockOpname->id,
                'so_number' => $stockOpname->so_number,
                'was_editing_by' => $wasEditingBy,
                'was_editing_by_user' => $wasEditingByUser,
                'admin_user_id' => auth()->id(),
                'admin_user_name' => auth()->user()->fullname
            ]);

            return response()->json([
                'success' => true,
                'message' => "SO berhasil di-unlock. Sebelumnya sedang diedit oleh: {$wasEditingByUser}"
            ]);

        } catch (\Exception $e) {
            Log::error("Force unlock failed: " . $e->getMessage());
            
            return response()->json([
                'success' => false,
                'message' => 'Force unlock gagal: ' . $e->getMessage()
            ], 500);
        }
    }

    /**
     * Check if SO exists for today (for frontend validation)
     */
    public function checkTodaySO(Request $request)
    {
        $storeId = $request->store_id ?? session('store_selected');
        
        if (!$storeId) {
            return response()->json([
                'success' => false,
                'message' => 'Store not selected'
            ], 400);
        }

        $today = Carbon::today();
        
        $existingSO = StockOpname::where('so_date', $today)
            ->where('store_id', $storeId)
            ->where('delete_flag', 0)
            ->first();

        if ($existingSO) {
            return response()->json([
                'success' => true,
                'exists' => true,
                'so_number' => $existingSO->so_number,
                'status' => $existingSO->status,
                'can_edit' => $existingSO->status === 'Draft',
                'message' => $existingSO->status === 'Submitted' 
                    ? 'SO hari ini sudah di-submit dan tidak dapat diedit.'
                    : 'SO hari ini sudah ada dan masih bisa diedit.'
            ]);
        }

        return response()->json([
            'success' => true,
            'exists' => false,
            'message' => 'Belum ada SO untuk hari ini.'
        ]);
    }

    /**
     * Preview SPB grouping before generation
     */
    public function previewSPBGrouping($soId)
    {
        try {
            $stockOpname = StockOpname::with(['details.rawMaterial', 'store'])->findOrFail($soId);
            
            // Validasi: SO harus sudah Submitted
            if ($stockOpname->status !== 'Submitted') {
                return response()->json([
                    'success' => false,
                    'message' => 'SPB hanya dapat dibuat dari SO yang sudah berstatus Submitted.'
                ]);
            }
            
            // Group SO details by tipe_dokumen and tipe_barang
            $groupedItems = $this->groupSODetailsByType($stockOpname);
            
            if (empty($groupedItems)) {
                return response()->json([
                    'success' => false,
                    'message' => 'Tidak ada item yang dapat dibuatkan SPB.'
                ]);
            }
            
            // Format groups for display
            $groups = [];
            foreach ($groupedItems as $groupKey => $items) {
                list($tipeDokumen, $tipeBarang) = explode('|', $groupKey);
                
                // Get sample items (max 3)
                $sampleItems = array_slice($items, 0, 3); 
                  
                $groups[] = [
                    'tipe_dokumen' => $tipeDokumen,
                    'tipe_barang' => $tipeBarang,
                    'item_count' => count($items)
                ];
            }
            
            return response()->json([
                'success' => true,
                'total_groups' => count($groups),
                'groups' => $groups
            ]);
            
        } catch (\Exception $e) {
            Log::error('Error previewing SPB grouping: ' . $e->getMessage());
            return response()->json([
                'success' => false,
                'message' => 'Terjadi kesalahan: ' . $e->getMessage()
            ], 500);
        }
    }

    private function getErrorCatch($e)
    {
        if (strpos($e, '#error#') !== false) {
            $e = explode('#error#', $e);
            $e = $e[1];
        }

        return $e;
    }
}
