<?php

namespace App\Services;

use Illuminate\Support\Facades\Http;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\Utils;
use Illuminate\Support\Str;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Support\Facades\Bus;
class BusinessCentralService
{
    public function getAccessToken()
    {
        $client = new Client();
        $response = $client->post("https://login.microsoftonline.com/".env('AZURE_TENANT_ID')."/oauth2/v2.0/token", [
            'form_params' => [
                'grant_type' => 'client_credentials',
                'client_id' => env('AZURE_CLIENT_ID'),
                'client_secret' => env('AZURE_CLIENT_SECRET'),
                'scope' => 'https://api.businesscentral.dynamics.com/.default'
            ]
        ]);
        $body = json_decode((string)$response->getBody(), true);

        return $body['access_token'];
    }

    public function getCompanyId($token)
    {
        $client = new Client();
        $response = $client->get("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies", [
            'headers' => [
                'Authorization' => "Bearer {$token}",
                'Accept'        => 'application/json'
            ]
        ]);
        $companies = json_decode((string)$response->getBody(), true);
        $role = session('user')['role'] ?? null; 
        $companyId1 = $companies['value'][2]['id'];
        $companyId2 = $companies['value'][3]['id'];

        return [
            'companyId1' => $companyId1,
            'companyId2' => $companyId2
        ];
    }

    public function __construct()
    {
     $this->token = $this->getAccessToken();
     $companyIds = $this->getCompanyId($this->token);
     $this->companyRegent = $companyIds['companyId1'];
     $this->companyHIN    = $companyIds['companyId2'];
     
     $activeCompany = session('current_company_name') ?? session('user')['role'];
    if ($activeCompany === 'Regent' or $activeCompany === 'SUPER') {
        $this->companyId = $this->companyRegent;
    } elseif ($activeCompany === 'HIN' ) {
        $this->companyId = $this->companyHIN;
    }
     $this->client = new Client();
    }

    
    function getMetadata() {

        $customersResponse = $this->client->get("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/\$metadata", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=1000'
            ]
        ]);
        $xml = simplexml_load_string((string)$customersResponse->getBody());
        $xml->registerXPathNamespace('edmx', 'http://docs.oasis-open.org/odata/ns/edmx'); 
        $xml->registerXPathNamespace('edm', 'http://docs.oasis-open.org/odata/ns/edm');
        $schemas = $xml->xpath('//edm:Schema');

        $entities = [];

        foreach ($schemas as $schema) {
            foreach ($schema->EntityType as $entity) {
                $entityName = (string) $entity['Name'];
                $properties = [];

                foreach ($entity->Property as $prop) {
                    $properties[] = [
                        'name' => (string) $prop['Name'],
                        'type' => (string) $prop['Type'],
                        'nullable' => isset($prop['Nullable']) ? (string) $prop['Nullable'] : 'true',
                    ];
                }

                $entities[$entityName] = $properties;
            }
        }
        
        if (isset($entities['purchaseOrderLine'])) {
        $logData = [];

        foreach ($entities['purchaseOrderLine'] as $field) {
            $logData[] = [
                'name'     => $field['name'],
                'type'     => $field['type'],
                'nullable' => $field['nullable'],
            ];
        }

        Log::info('📦 Entity: purchaseOrderLine', $logData);
    } else {
        Log::warning('Entity purchaseOrderLine tidak ditemukan di metadata');
    }

    return $schemas;

        echo "<pre>";
        echo "ðŸ“¦ Entity: purchaseOrderLine\n\n";

        foreach ($entities['purchaseOrderLine'] as $field) {
            echo " â–¸ {$field['name']} ({$field['type']})";
            echo $field['nullable'] === 'false' ? " [NOT NULL]" : "";
            echo "\n";
        }
        echo "</pre>";
        exit;
    }

      public function getData(string $endpoint, array $queryParams = [])
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/v2.0/companies({$this->companyId})/{$endpoint}";

    if (!empty($queryParams)) {
        $url .= '?' . http_build_query($queryParams);
    }

    try {
        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ]
        ]);

        $data = json_decode((string)$response->getBody(), true);
        $items = $data['value'] ?? [];

        // === Group dan sort otomatis hanya jika endpoint = 'items' ===
        if ($endpoint === 'items') {
            $groupedItems = [];
            $itemsWithoutPrefix = [];

            foreach ($items as $item) {
                $number = $item['number'] ?? '';
                if (preg_match('/^[A-Z]+/i', $number, $matches)) {
                    $prefix = strtoupper($matches[0]);
                    if (!isset($groupedItems[$prefix])) {
                        $groupedItems[$prefix] = [];
                    }
                    $groupedItems[$prefix][] = $item;
                } else {
                    $itemsWithoutPrefix[] = $item;
                }
            }

            if (!empty($itemsWithoutPrefix)) {
                $groupedItems['OTHER'] = $itemsWithoutPrefix;
            }

            // Sort setiap grup berdasarkan angka di number
            foreach ($groupedItems as $prefix => &$group) {
                usort($group, function($a, $b) {
                    $numA = (int) preg_replace('/\D/', '', $a['number'] ?? 0);
                    $numB = (int) preg_replace('/\D/', '', $b['number'] ?? 0);
                    return $numA <=> $numB;
                });
            }
            unset($group);

            // Ganti data['value'] menjadi grouped & sorted
            $data['value'] = $groupedItems;
        }

        return [
            'status' => 'success',
            'data' => $data
        ];
    } catch (\Exception $e) {
        return [
            'status' => 'error',
            'message' => $e->getMessage()
        ];
    }
}



    public function getAllPurchaseQtys()
    {
        $start = microtime(true);
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Purchase_Lines_Excel?\$filter=Outstanding_Quantity gt 0 and (Status eq 'Released' or Status eq 'Pending Approval' or Status eq 'Pending Prepayment')  and (Location_Code eq 'CI.1010') &\$select=No,Location_Code,Outstanding_Quantity";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json'
            ]
        ];

        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);
            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }
            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);

        $result = [];
        foreach ($allData as $line) {
            $key = $line['No'] . '_' . $line['Location_Code'];
            $result[$key] = ($result[$key] ?? 0) + ($line['Outstanding_Quantity'] ?? 0);
        }
        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Purchaseqty: {$elapsed} seconds");
        

        return $result;
    }

    public function getAllTransferQtys()
    {
        $start = microtime(true);
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Transfer_Order_Line_Excel?\$filter=Status eq 'Released' and Quantity_Shipped lt Quantity and (Transfer_From_Code eq 'CI.1010'
            ) &\$select=Document_No,Item_No,Transfer_From_Code,OutstandingShipped";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=1000'
            ]
        ];

        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);
            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }
            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);

        $result = [];
        foreach ($allData as $line) {
            $key = $line['Item_No'] . '_' . $line['Transfer_from_Code'];
            $result[$key] = ($result[$key] ?? 0) + ($line['OutstandingShipped'] ?? 0);
        }

        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Transfer Qty: {$elapsed} seconds");
        return $result;
    }

    public function getTransferLinesFromBC()
    {
        $start = microtime(true);
        $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Transfer_Order_Line_Excel?\$filter=Status eq 'Released' and Transfer_from_Code eq 'CI.1010'";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=5000',
            ]
        ];

        $allData = [];
        $url = $baseUrl;

        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);
        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Transfer Qty: {$elapsed} seconds");
        return $allData;
    }


    function getPurchaseLineFromBC() {
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
         . env('AZURE_TENANT_ID') . "/"
         . env('BC_ENVIRONMENT')
         . "/api/v2.0/companies(" . $this->companyId . ")/purchaseOrders(83ea2b56-d008-ee11-8f70-00224857ecae)/purchaseOrderLines";

    try {
        $resp = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=1000',
            ],
            // Optional: let 4xx/5xx come back without throwing
            // 'http_errors' => false,
        ]);

        return [
            'ok'      => true,
            'status'  => $resp->getStatusCode(),
            'reason'  => $resp->getReasonPhrase(),
            'headers' => $resp->getHeaders(),
            'json'    => json_decode((string)$resp->getBody(), true),
            'raw'     => (string)$resp->getBody(),
        ];

    } catch (RequestException $e) {
        $res     = $e->getResponse();
        $status  = $res ? $res->getStatusCode() : null;
        $reason  = $res ? $res->getReasonPhrase() : null;
        $headers = $res ? $res->getHeaders() : [];
        $raw     = $res ? (string)$res->getBody() : $e->getMessage();
        $json    = json_decode($raw, true);

        // Log everything (no truncation in logs)
        Log::error('BC GET purchaseOrderLines failed', [
            'url'      => $url,
            'status'   => $status,
            'reason'   => $reason,
            'headers'  => $headers,
            'raw'      => $raw,
            'exception'=> $e->getMessage(),
        ]);

        // Return a rich error object to caller
        return [
            'ok'       => false,
            'status'   => $status,
            'reason'   => $reason,
            'headers'  => $headers,
            'errorRaw' => $raw,
            'error'    => $json ?? $raw,  // parsed JSON if possible
            'message'  => $e->getMessage(),
        ];
    } catch (\Throwable $e) {
        Log::error('Unexpected error calling BC', [
            'url'      => $url,
            'exception'=> $e->getMessage(),
        ]);

        return [
            'ok'      => false,
            'status'  => 500,
            'reason'  => 'Unexpected Error',
            'error'   => $e->getMessage(),
        ];
    }
}

public function billOfMaterial()
{
    try {
        set_time_limit(300);

        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('" . $this->companyId . "')";

        $endpoint = "$baseOdata/BillOfMaterial";

        $headers = [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ];

        $options = [
            'headers' => $headers,
            'timeout' => 300,
            'connect_timeout' => 60
        ];

        $response = $this->client->get($endpoint, $options);
        $body = json_decode($response->getBody()->getContents(), true);
        $rawData = $body['value'] ?? [];

        // 🔹 Filter hanya yang Business_Unit == 'RBC'
        $filteredData = array_filter($rawData, function ($row) {
            return isset($row['Business_Unit']) && strtoupper(trim($row['Business_Unit'])) === 'RBC';
        });

        // 🔧 Kelompokkan berdasarkan BOM_Parent
        $grouped = [];

        foreach ($filteredData as $row) {
            $parent = $row['BOM_Parent'] ?? null;
            if (!$parent) continue;

            if (!isset($grouped[$parent])) {
                $grouped[$parent] = [
                    'BOM_Parent'      => $parent,
                    'Description_BOM' => $row['Description_BOM'] ?? '',
                    'Uom_BOM_Parent'  => $row['Uom_BOM_Parent'] ?? '',
                    'Standart_Cost'   => $row['Standart_Cost'] ?? 0,
                    'Business_Unit'   => $row['Business_Unit'] ?? '',
                    'Item_Operation'  => []
                ];
            }

            // Tambahkan child item ke dalam Item_Operation
            $grouped[$parent]['Item_Operation'][] = [
                '@odata.etag'      => $row['@odata.etag'] ?? null,
                'Row_No'           => $row['Row_No'] ?? null,
                'Operation'        => $row['Operation'] ?? null,
                'BOM'              => $row['BOM'] ?? null,
                'BOM_Description'  => $row['BOM_Description'] ?? '',
                'Quantity'         => $row['Quantity'] ?? 0,
                'Uom_BOM'          => $row['Uom_BOM'] ?? '',
                'Price'            => $row['Price'] ?? 0
            ];
        }

        $finalData = array_values($grouped); // reset index numerik

        return [
            'status' => 'success',
            'count'  => count($finalData),
            'items'  => $finalData
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}
    

    public function getStockV2()
{
    try {
        // ⏱️ Perpanjang batas waktu eksekusi (5 menit)
        set_time_limit(300);

        // 🔹 Susun base URL dengan env + companyId
        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('" . $this->companyId . "')";

        // 🔹 Filter hanya item dengan Qty_RBC_2040 > 0
        $filter = urlencode("Qty gt 0 and LocationCode eq 'RBC.2040'");

        // 🔹 Select field penting agar lebih ringan
        // $select = urlencode("ItemNo,ItemDescription,Qty,LocationCode,Replenishment_System");

        // 🔹 Endpoint StockV2 dengan filter + select
        $endpoint = "$baseOdata/StockV2?\$filter={$filter}";

        // 🔹 Header standar BC API
        $headers = [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ];

        $options = [
            'headers' => $headers,
            'timeout' => 300,           // maksimal 5 menit
            'connect_timeout' => 60     // waktu tunggu koneksi
        ];

        // 🔹 Ambil data dari API
        $response = $this->client->get($endpoint, $options);
        $body = json_decode($response->getBody()->getContents(), true);

        $data = $body['value'] ?? [];

        return [
            'status' => 'success',
            'count'  => count($data),
            'items'  => $data
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}


public function getCombinedBOMWithStock()
{
    $cacheKey = 'bc_bom_with_stock_v3';
    $cacheTtl = now()->addMinutes(30);

    // 🔹 Jika cache sudah ada, kirim dulu hasil cache sambil refresh background
    if (Cache::has($cacheKey)) {
        $cached = Cache::get($cacheKey);

        dispatch(function () use ($cacheKey, $cacheTtl) {
            try {
                $fresh = $this->generateCombinedBOMWithStock(); // pakai versi baru
                Cache::put($cacheKey, $fresh, $cacheTtl);
                Log::info('[BC] BOM+Stock cache refreshed in background');
            } catch (\Throwable $e) {
                Log::error('[BC] Failed to refresh BOM+Stock cache: ' . $e->getMessage());
            }
        })->afterResponse();

        return [
            'status'  => 'success',
            'source'  => 'cache',
            'message' => 'Loaded from cache (background refresh running)',
            'count'   => count($cached),
            'items'   => $cached,
        ];
    }

    // 🚀 Jika cache belum ada, buat baru
    $fresh = $this->generateCombinedBOMWithStock();
    Cache::put($cacheKey, $fresh, $cacheTtl);

    return [
        'status'  => 'success',
        'source'  => 'fresh',
        'message' => 'Generated new cache',
        'count'   => count($fresh),
        'items'   => $fresh,
    ];
}


protected function generateCombinedBOMWithStock()
{
    // Ambil data BOM + Stock + TransferLine
    $bomResponse   = $this->billOfMaterial();
    $stockResponse = $this->getCombinedStock();
    $transferLines = $this->getAllTransferLinesData(); // tetap dipakai untuk statusTO

    $bomData       = $bomResponse['items'] ?? [];
    $stockData     = $stockResponse['items'] ?? [];
    $transferData  = $transferLines ?? [];

    // 🔹 Filter hanya untuk Business Unit = RBC
    $bomData = array_filter($bomData, function ($item) {
        return isset($item['Business_Unit']) && strtoupper(trim($item['Business_Unit'])) === 'RBC';
    });

    // 🔹 Buat peta stok (Inventory + Minimum_Stock + Maximum_Inventory)
    $stockMap = [];
    foreach ($stockData as $s) {
        if (!empty($s['Item_No'])) {
            $stockMap[$s['Item_No']] = [
                'Inventory'          => $s['Inventory'] ?? 0,
                'Minimum_Stock'      => $s['Minimum_Stock'] ?? 0,
                'Maximum_Inventory'  => $s['Maximum_Inventory'] ?? 0,
            ];
        }
    }

    // 🔹 Buat peta transfer line (true hanya jika ada status = 'Open')
    $transferMap = [];
    foreach ($transferData as $t) {
        if (!empty($t['itemNo']) && isset($t['status'])) {
            $normalizedItem = trim(strtoupper($t['itemNo']));
            $status = strtolower(trim($t['status']));

            // Hanya tandai true kalau ada satupun baris yang statusnya 'open'
            if (!isset($transferMap[$normalizedItem])) {
                $transferMap[$normalizedItem] = false; // default
            }

            if ($status === 'open') {
                $transferMap[$normalizedItem] = true;
            }
        }
    }

    // 🔹 Gabungkan semua
    $final = [];
    foreach ($bomData as $parent) {
        $parentNo = $parent['BOM_Parent'] ?? null;
        if (!$parentNo) continue;

        $normalizedParent = trim(strtoupper($parentNo));

        // Tambahkan stok parent (Inventory, Minimum_Stock, Maximum_Inventory)
        $parent['Inventory']         = $stockMap[$parentNo]['Inventory'] ?? 0;
        $parent['Minimum_Stock']     = $stockMap[$parentNo]['Minimum_Stock'] ?? 0;
        $parent['Maximum_Inventory'] = $stockMap[$parentNo]['Maximum_Inventory'] ?? 0;

        // Tambahkan stok di setiap item operation (hanya Inventory)
        if (!empty($parent['Item_Operation'])) {
            foreach ($parent['Item_Operation'] as &$child) {
                $childNo = $child['BOM'] ?? null;
                $child['Inventory'] = $stockMap[$childNo]['Inventory'] ?? 0;
            }
            unset($child);
        }

        // Tambahkan statusTO (berdasarkan transfer line)
        $parent['statusTO'] = $transferMap[$normalizedParent] ?? false;

        $final[] = $parent;
    }

    Log::info('[BC] BOM+Stock (RBC only, +Min/Max Inventory) combined generated: ' . count($final) . ' items');
    return $final;
}


protected function getAllTransferLinesData()
{
    // ✅ Versi ringan dari getAllTransferLines(), tanpa response()->json() dan overhead HTTP
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);
        return $result['value'] ?? [];

    } catch (\Throwable $e) {
        Log::error('BC getAllTransferLinesData error: '.$e->getMessage());
        return [];
    }
}


public function getCombinedStock()
{
    $fresh = $this->generateCombinedStockOptimized();

    return [
        'status' => 'success',
        'source' => 'fresh',
        'message' => 'Fetched live data from BC (optimized with filtered UoM)',
        'count'  => count($fresh),
        'items'  => $fresh,
    ];
}

/**
 * ⚡ Versi optimized: gabungkan Stockkeeping + StockV2 + filter UoM berdasarkan Item_No yang ada
 */
protected function generateCombinedStockOptimized()
{
    $stockkeeping = $this->getStockkeepingFromBC();
    $stockv2      = $this->getStockV2();

    $keepItems = $stockkeeping['value'] ?? [];
    $v2Items   = $stockv2['items'] ?? [];

    $merged = [];
    $usedItemNos = [];

    // 🧩 Step 1: ambil dari Stockkeeping
    foreach ($keepItems as $item) {
        $itemNo = $item['Item_No'] ?? null;
        if (!$itemNo) continue;

        $merged[] = [
            'Item_No' => $itemNo,
            'Description' => $item['Description'] ?? '',
            'Inventory' => $item['Inventory'] ?? 0,
            'Minimum_Stock' => $item['Safety_Stock_Quantity'] ?? 0,
            'Maximum_Inventory' => $item['Maximum_Inventory'] ?? 0,
            'Replenishment_System' => $item['Replenishment_System'] ?? null,
            'UoM' => [],
        ];

        $usedItemNos[] = $itemNo;
    }

    // 🧩 Step 2: tambahkan dari StockV2 (jika belum ada)
    foreach ($v2Items as $item) {
        $itemNo = $item['ItemNo'] ?? null;
        if (!$itemNo || in_array($itemNo, $usedItemNos)) continue;

        $merged[] = [
            'Item_No' => $itemNo,
            'Description' => $item['ItemDescription'] ?? '',
            'Inventory' => $item['Qty'] ?? 0,
            'Minimum_Stock' => 0,
            'Maximum_Inventory' => 0,
            'Replenishment_System' => $item['Replenishment_System'] ?? null,
            'UoM' => [],
        ];

        $usedItemNos[] = $itemNo;
    }

    // ✅ Step 3: ambil UOM hanya untuk Item_No yang digunakan
    $uomIndex = $this->fetchUoMForItems($usedItemNos);

    // 🧩 Step 4: tempelkan ke hasil merge
    foreach ($merged as &$item) {
        $itemNo = $item['Item_No'];
        $item['UoM'] = $uomIndex[$itemNo] ?? [];
    }

    Log::info('[BC] Combined stock (optimized UoM): ' . count($merged) . ' items');
    return $merged;
}

/**
 * 🚀 Ambil Item_UOM hanya untuk item tertentu (dalam batch kecil)
 */
protected function fetchUoMForItems(array $itemNos)
{
    $uomIndex = [];
    $chunkSize = 50; // aman untuk panjang URL BC
    $headers = [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=1000',
        ]
    ];

    $baseUrl = "https://api.businesscentral.dynamics.com/v2.0/" 
        . env('AZURE_TENANT_ID') . "/" 
        . env('BC_ENVIRONMENT') 
        . "/ODataV4/Company('{$this->companyId}')/Item_UOM";

    $client = new \GuzzleHttp\Client(['timeout' => 60]);

    // 🔁 bagi menjadi beberapa batch
    foreach (array_chunk($itemNos, $chunkSize) as $chunk) {
        // Buat filter OData
        $filterList = array_map(fn($no) => "Item_No eq '$no'", $chunk);
        $filterQuery = '$filter=' . implode(' or ', $filterList);
        $url = "{$baseUrl}?{$filterQuery}";

        try {
            $response = $client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);
            $values = $decoded['value'] ?? [];

            foreach ($values as $uom) {
                $itemNo = $uom['Item_No'] ?? null;
                $code   = $uom['Code'] ?? null;
                if (!$itemNo || !$code) continue;

                if (!isset($uomIndex[$itemNo])) {
                    $uomIndex[$itemNo] = [];
                }

                if (!in_array($code, $uomIndex[$itemNo])) {
                    $uomIndex[$itemNo][] = $code;
                }
            }
        } catch (\Exception $e) {
            Log::warning('[BC] Failed fetching UOM batch: ' . $e->getMessage());
        }
    }

    return $uomIndex;
}

    function getStockkeepingFromBC()
    {
        $filter = urlencode("(Location_Code eq 'RBC.2040')");
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/StockkeepingUnit?\$filter={$filter}";
        
        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=50000'
            ]
        ];
        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);
        return ['value' => $allData];
    }

    function itemUoM()
    {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Item_UOM";
        
        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer' => 'odata.maxpagesize=1000'
            ]
        ];
        $allData = [];
        do {
            $response = $this->client->get($url, $headers);
            $decoded = json_decode((string)$response->getBody(), true);

            if (isset($decoded['value'])) {
                $allData = array_merge($allData, $decoded['value']);
            }

            $url = $decoded['@odata.nextLink'] ?? null;
        } while ($url);

        return ['value' => $allData];
    }

    public function getItemCardExcelRaw()
{
    try {
        // ⏱️ Perpanjang waktu eksekusi PHP
        set_time_limit(300);

        $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/"
            . env('AZURE_TENANT_ID') . "/"
            . env('BC_ENVIRONMENT')
            . "/ODataV4/Company('PT.CI%20LIVE')";

        // 🔹 Ambil hanya field No, Description, dan AssemblyBOM
        // 🔹 Filter langsung di API: AssemblyBOM eq true
        // 🔹 Batasi hanya 100 hasil
        $endpoint = "$baseOdata/Item_Card_Excel?"
            . "\$filter=Business_Unit eq 'RBC'";

        $headers = [
            'Authorization' => "Bearer {$this->token}",
            'Accept'        => 'application/json',
            'Prefer'        => 'odata.maxpagesize=50000'
        ];

        $options = [
            'headers' => $headers,
            'timeout' => 300, // waktu maksimum request 5 menit
            'connect_timeout' => 60
        ];

        // 🔹 Kirim request
        $response = $this->client->get($endpoint, $options);
        $body = json_decode($response->getBody()->getContents(), true);

        $data = $body['value'] ?? [];

        return [
            'status' => 'success',
            'count'  => count($data),
            'items'  => $data
        ];

    } catch (\Exception $e) {
        return [
            'status'  => 'error',
            'message' => $e->getMessage()
        ];
    }
}


    public function getAllVendorsMap()
    {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/vendors?\$select=number,displayName";
        $vendors = [];
        do {
            $response = $this->client->get($url, [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept' => 'application/json',
                    'Prefer' => 'odata.maxpagesize=1000'
                ]
            ]);
            $data = json_decode($response->getBody(), true);

            foreach ($data['value'] as $vendor) {
                $vendors[$vendor['number']] = $vendor['displayName'];
            }

            $url = $data['@odata.nextLink'] ?? null; 

        } while ($url);

        return $vendors;
    }


    public function getVendor() {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/vendors";

        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json'
            ]
        ]);
        return json_decode($response->getBody(), true);
    }

    
public function createItem(array $itemData)
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/v2.0/companies({$this->companyId})/items";

    // Ambil unitPrice
    $unitPrice = 0;
    if (isset($itemData['unitPrice'])) {
        $unitPrice = $itemData['unitPrice'];
    } elseif (!empty($itemData['vendorQuotes']) && is_array($itemData['vendorQuotes']) && isset($itemData['vendorQuotes'][0]['price'])) {
        $unitPrice = $itemData['vendorQuotes'][0]['price'];
    }
    $unitPrice = is_numeric($unitPrice) ? (float) $unitPrice : 0.0;

    // Siapkan payload
    $payload = [
        'number' => $itemData['number'] ?? null,
        'displayName' => $itemData['displayName'] ?? null,
        'type' => $itemData['type'] ?? 'Inventory',
        'baseUnitOfMeasureCode' => $itemData['baseUnitOfMeasureCode'] ?? 'PTN',
        'generalProductPostingGroupCode' => $itemData['generalProductPostingGroupCode'] ?? 'GP.FOD075',
        'inventoryPostingGroupCode' => $itemData['inventoryPostingGroupCode'] ?? 'INV_FOOD',
        'unitPrice' => $unitPrice,
    ];

    // 🧩 Client Guzzle dengan timeout panjang
    $client = new \GuzzleHttp\Client([
        'timeout' => 180,          // Maksimal 3 menit per request
        'connect_timeout' => 20,   // Maksimal waktu membuka koneksi
    ]);

    try {
        $response = $client->post($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
            'json' => $payload,
        ]);

        return [
            'status' => 'success',
            'data' => json_decode((string) $response->getBody(), true)
        ];
    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $responseBody = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : null;

        \Log::error('BC createItem RequestException: ' . $e->getMessage(), [
            'payload' => $payload,
            'response' => $responseBody
        ]);

        return [
            'status' => 'error',
            'message' => $e->getMessage(),
            'response' => $responseBody
        ];
    } catch (\Exception $e) {
        \Log::error('BC createItem general error: ' . $e->getMessage(), [
            'payload' => $payload
        ]);

        return [
            'status' => 'error',
            'message' => $e->getMessage()
        ];
    }
}


public function getAllAssemblyOrder()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyHeaderCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);

        return response()->json([
            'status' => 'success',
            'data' => $result,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('BC getAllTransferHeaders error (full): ' . $body);

        return response()->json([
            'status' => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('BC getAllTransferHeaders general error: ' . $e->getMessage());

        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}


public function getAllTransferHeaders()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferHeaderCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);

        return response()->json([
            'status' => 'success',
            'data' => $result,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('BC getAllTransferHeaders error (full): ' . $body);

        return response()->json([
            'status' => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('BC getAllTransferHeaders general error: ' . $e->getMessage());

        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}

// Controller
public function getAllTransferLines()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->get($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
        ]);

        $result = json_decode((string) $response->getBody(), true);

        return response()->json([
            'status' => 'success',
            'data' => $result,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('BC getAllTransferLines error (full): ' . $body);

        return response()->json([
            'status' => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('BC getAllTransferLines general error: ' . $e->getMessage());

        return response()->json([
            'status' => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}

public function getAllTransferWithLines()
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        // =========================================================
        // STEP 1: Ambil semua Transfer Header
        // =========================================================
        $headerUrl = $base . "/transferHeaderCustom";
        $responseHeader = $client->get($headerUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $headers = json_decode((string) $responseHeader->getBody(), true);
        $headersData = $headers['value'] ?? $headers ?? [];

        // =========================================================
        // STEP 2: Ambil semua Transfer Line
        // =========================================================
        $lineUrl = $base . "/transferLineCustom";
        $responseLine = $client->get($lineUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
            ],
        ]);

        $lines = json_decode((string) $responseLine->getBody(), true);
        $linesData = $lines['value'] ?? $lines ?? [];

        // =========================================================
        // STEP 3: Filter header hanya yang transferFromCode = RBC.2040
        // =========================================================
        $filteredHeaderNos = collect($headersData)
            ->filter(fn($h) => ($h['transferFromCode'] ?? '') === 'RBC.2040')
            ->pluck('no')
            ->values()
            ->all();

        // =========================================================
        // STEP 4: Ambil hanya line yang documentNo ada di header RBC.2040
        // =========================================================
        $filteredLines = collect($linesData)
            ->filter(fn($line) => in_array($line['documentNo'] ?? '', $filteredHeaderNos))
            ->values()
            ->all();

        // =========================================================
        // STEP 5: Return hasil line saja
        // =========================================================
        return response()->json([
            'status' => 'success',
            'count'  => count($filteredLines),
            'data'   => $filteredLines,
        ], 200);

    } catch (\GuzzleHttp\Exception\RequestException $e) {
        $body = $e->hasResponse()
            ? (string) $e->getResponse()->getBody()
            : $e->getMessage();

        \Log::error('❌ BC getAllTransferWithLines RequestException: ' . $body);

        return response()->json([
            'status'  => 'error',
            'message' => $body,
        ], 500);

    } catch (\Exception $e) {
        \Log::error('❌ BC getAllTransferWithLines General error: ' . $e->getMessage());

        return response()->json([
            'status'  => 'error',
            'message' => $e->getMessage(),
        ], 500);
    }
}


public function createAssemblyHeader(array $data)
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/assemblyHeaderCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    // =========================================================
    // STEP 1: POST HEADER
    // =========================================================
    try {
        $postPayload = [
            'docType' => 'Order', // ⚙️ sesuai key BC
            'docNo'   => '',       // BC akan isi otomatis
        ];

        $response = $client->post($base, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ],
            'json' => $postPayload,
        ]);

        $created = json_decode((string) $response->getBody(), true);
        Log::info('✅ STEP 1: Assembly Header Created', $created);

        $docType = $created['docType'] ?? 'Order';
        $docNo   = $created['docNo'] ?? null;
        $etag    = $created['@odata.etag'] ?? null;

        if (!$docNo || !$etag) {
            throw new \Exception('docNo atau ETag tidak ditemukan di response POST.');
        }

    } catch (\Exception $e) {
        Log::error('❌ STEP 1 Failed: POST Assembly Header', ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'POST Header',
            'message' => $e->getMessage(),
        ];
    }

    // 🔑 PATCH URL berdasarkan key composite (Document Type + No)
    $patchUrl = $base . "(docType='" . rawurlencode($docType) . "',docNo='" . rawurlencode($docNo) . "')";

    // =========================================================
    // STEP 2: PATCH ITEM NO
    // =========================================================
    try {
        $itemPayload = [
            'itemNo' => $data['itemNo'] ?? null,
        ];

        $patch1 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $itemPayload,
        ]);

        $patch1Data = json_decode((string) $patch1->getBody(), true);
        Log::info("✅ STEP 2: itemNo patched", $itemPayload);

        $etag = $patch1Data['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 2 Failed: PATCH itemNo", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH itemNo',
            'message' => $e->getMessage(),
        ];
    }

    // =========================================================
    // STEP 3: PATCH POSTING DATE & DUE DATE
    // =========================================================
    try {
        $postingDate = $data['postingDate'] ?? now()->format('Y-m-d');

        $postingPayload = [
            'postingDate' => $postingDate,
        ];

        $patch2 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $postingPayload,
        ]);

        $patch2Data = json_decode((string) $patch2->getBody(), true);
        Log::info("✅ STEP 3: postingDate & dueDate patched", $postingPayload);

        $etag = $patch2Data['@odata.etag'] ?? $etag;

    } catch (\Exception $e) {
        Log::error("❌ STEP 3 Failed: PATCH postingDate/dueDate", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH postingDate/dueDate',
            'message' => $e->getMessage(),
        ];
    }

    // =========================================================
    // STEP 4: PATCH QUANTITY
    // =========================================================
    try {
        $qtyPayload = [
            'quantity' => $data['quantity'] ?? 0,
        ];

        $patch3 = $client->patch($patchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'If-Match'      => $etag,
                'Content-Type'  => 'application/json',
            ],
            'json' => $qtyPayload,
        ]);

        $finalData = json_decode((string) $patch3->getBody(), true);
        Log::info("✅ STEP 4: quantity patched", $qtyPayload);

    } catch (\Exception $e) {
        Log::error("❌ STEP 4 Failed: PATCH quantity", ['error' => $e->getMessage()]);
        return [
            'status'  => 'error',
            'step'    => 'PATCH quantity',
            'message' => $e->getMessage(),
        ];
    }

    // =========================================================
    // ✅ DONE
    // =========================================================
    return [
        'status'  => 'success',
        'data'    => $finalData,
        'message' => "Assembly Header {$docNo} (Type: {$docType}) berhasil dibuat & diperbarui.",
    ];
}


public function createTransferHeader(array $data)
{
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferHeaderCustom";

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    // =========================================================
    // STEP 1: POST → Create Header
    // =========================================================
    \Log::error('STEP 1: Creating BC Transfer Header', $data);

    $createPayload = [
        'noSeries' => $data['noSeries'] ?? null,
        'no' => ''
    ];

    $response = $client->post($base, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'Content-Type' => 'application/json',
        ],
        'json' => $createPayload,
    ]);

    $created = json_decode((string)$response->getBody(), true);
    \Log::info('STEP 1 RESULT:', $created);

    $no   = $created['no'] ?? null;
    $etag = $created['@odata.etag'] ?? null;

    if (!$no) {
        \Log::error('STEP 1 ERROR: Tidak ada nomor header pada response.', ['response' => $created]);
        throw new \Exception('Nomor transfer header tidak ditemukan di response.');
    }

    if (!$etag) {
        \Log::error('STEP 1 ERROR: Tidak ada ETag pada response.', ['response' => $created]);
        throw new \Exception('ETag tidak ditemukan di response.');
    }

    $patchUrl = $base . "(no='" . rawurlencode($no) . "')";

    // =========================================================
    // STEP 2: PATCH transSpec
    // =========================================================
    \Log::info("STEP 2: PATCH transSpec untuk {$no}");
    $transSpecPayload = [
        'transSpec' => $data['transSpec'] ?? null,
    ];

    $patch1 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $transSpecPayload,
    ]);

    $patch1Data = json_decode((string)$patch1->getBody(), true);
    $etag = $patch1Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 2 RESULT:', $patch1Data);

    // =========================================================
    // STEP 3: PATCH businessUnit
    // =========================================================
    \Log::info("STEP 3: PATCH businessUnit untuk {$no}");
    $businessUnitPayload = [
        'businessUnit' => $data['businessUnit'] ?? null,
    ];

    $patch2 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $businessUnitPayload,
    ]);

    $patch2Data = json_decode((string)$patch2->getBody(), true);
    $etag = $patch2Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 3 RESULT:', $patch2Data);

    // =========================================================
    // STEP 4: PATCH department
    // =========================================================
    \Log::info("STEP 4: PATCH department untuk {$no}");
    $departmentPayload = [
        'department' => $data['department'] ?? null,
    ];

    $patch3 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $departmentPayload,
    ]);

    $patch3Data = json_decode((string)$patch3->getBody(), true);
    $etag = $patch3Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 4 RESULT:', $patch3Data);

    // =========================================================
    // STEP 5: PATCH transferFromCode + transfertoCode
    // =========================================================
    \Log::info("STEP 5: PATCH transferFromCode & transfertoCode untuk {$no}");
    $transferPayload = [
        'transferFromCode' => $data['transferFromCode'] ?? null,
        'transfertoCode'   => $data['transfertoCode'] ?? null,
    ];

    $patch4 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $transferPayload,
    ]);

    $patch4Data = json_decode((string)$patch4->getBody(), true);
    $etag = $patch4Data['@odata.etag'] ?? $etag;
    \Log::info('STEP 5 RESULT:', $patch4Data);

    // =========================================================
    // STEP 6: PATCH postingDate (tanggal hari ini)
    // =========================================================
    \Log::info("STEP 6: PATCH postingDate untuk {$no}");
    $postingDate = now()->format('Y-m-d');
    $postingPayload = [
        'postingDate' => $postingDate,
    ];

    $patch5 = $client->patch($patchUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag,
            'Content-Type' => 'application/json',
        ],
        'json' => $postingPayload,
    ]);

    $finalData = json_decode((string)$patch5->getBody(), true);
    \Log::info('STEP 6 RESULT:', $finalData);

    // =========================================================
    // STEP 7: CREATE TRANSFER LINES (langsung setelah header)
    // =========================================================
    $lineBase = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/api/citbi/sku/v1.0/companies({$this->companyId})/transferLineCustom";

    $lines = $data['lines'] ?? [];
    $lineCounter = 10000;
    $createdLines = [];

    foreach ($lines as $idx => $line) {
        \Log::info("CREATE LINE {$idx} untuk DocumentNo {$no}");

        // === STEP 7.1 POST Create Line ===
        $linePayload = [
            'documentNo' => $no,
            'lineNo' => $lineCounter,
        ];

        $res = $client->post($lineBase, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
            'json' => $linePayload,
        ]);

        $createdLine = json_decode((string)$res->getBody(), true);
        $lineEtag = $createdLine['@odata.etag'] ?? null;

        if (!isset($createdLine['documentNo'])) {
            \Log::error("Gagal membuat line {$idx}", ['response' => $createdLine]);
            continue;
        }

        $linePatchUrl = $lineBase . "(documentNo='" . rawurlencode($no) . "',lineNo=" . $lineCounter . ")";

        // === STEP 7.2 PATCH itemNo ===
        $patchItem = $client->patch($linePatchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $lineEtag,
                'Content-Type' => 'application/json',
            ],
            'json' => ['itemNo' => $line['itemNo'] ?? null],
        ]);
        $patched1 = json_decode((string)$patchItem->getBody(), true);
        $lineEtag = $patched1['@odata.etag'] ?? $lineEtag;

        // === STEP 7.3 PATCH quantity ===
        $patchQty = $client->patch($linePatchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $lineEtag,
                'Content-Type' => 'application/json',
            ],
            'json' => ['quantity' => $line['quantity'] ?? 0],
        ]);
        $patched2 = json_decode((string)$patchQty->getBody(), true);
        $lineEtag = $patched2['@odata.etag'] ?? $lineEtag;

        // === STEP 7.4 PATCH uomCode ===
        $patchUom = $client->patch($linePatchUrl, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $lineEtag,
                'Content-Type' => 'application/json',
            ],
            'json' => ['uomCode' => $line['uomCode'] ?? null],
        ]);
        $patched3 = json_decode((string)$patchUom->getBody(), true);

        $createdLines[] = $patched3;
        $lineCounter += 10000;
    }

    // =========================================================
    // DONE
    // =========================================================
    return [
        'status' => 'success',
        'header' => $finalData,
        'lines' => $createdLines,
        'message' => "Transfer Header {$no} dan " . count($createdLines) . " line berhasil dibuat.",
    ];
}




public function createVendor(array $vendorData): array
{
    // =====================
    // 1️⃣ Buat Vendor
    // =====================
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})/vendors";

    $payload = [];
    if (!empty($vendorData['number'])) $payload['number'] = $vendorData['number'];
    if (!empty($vendorData['displayName'])) $payload['displayName'] = $vendorData['displayName'];
    if (!empty($vendorData['addressLine1'])) $payload['addressLine1'] = $vendorData['addressLine1'];
    if (!empty($vendorData['addressLine2'])) $payload['addressLine2'] = $vendorData['addressLine2'];
    if (!empty($vendorData['city'])) $payload['city'] = $vendorData['city'];
    if (!empty($vendorData['state'])) $payload['state'] = $vendorData['state'];
    if (!empty($vendorData['country'])) $payload['country'] = $vendorData['country'];
    if (!empty($vendorData['postalCode'])) $payload['postalCode'] = $vendorData['postalCode'];
    if (!empty($vendorData['phoneNumber'])) $payload['phoneNumber'] = $vendorData['phoneNumber'];
    if (!empty($vendorData['email'])) $payload['email'] = $vendorData['email'];
    if (!empty($vendorData['website'])) $payload['website'] = $vendorData['website'];
    if (!empty($vendorData['taxRegistrationNumber'])) $payload['taxRegistrationNumber'] = $vendorData['taxRegistrationNumber'];
    if (!empty($vendorData['currencyId']) && $vendorData['currencyId'] !== '00000000-0000-0000-0000-000000000000') $payload['currencyId'] = $vendorData['currencyId'];
    if (!empty($vendorData['paymentTermsId']) && $vendorData['paymentTermsId'] !== '00000000-0000-0000-0000-000000000000') $payload['paymentTermsId'] = $vendorData['paymentTermsId'];
    if (!empty($vendorData['paymentMethodId']) && $vendorData['paymentMethodId'] !== '00000000-0000-0000-0000-000000000000') $payload['paymentMethodId'] = $vendorData['paymentMethodId'];
    if (isset($vendorData['taxLiable'])) $payload['taxLiable'] = (bool)$vendorData['taxLiable'];
    if (isset($vendorData['blocked'])) $payload['blocked'] = $vendorData['blocked'];

    $client = new \GuzzleHttp\Client([
        'timeout' => 180,
        'connect_timeout' => 20,
    ]);

    try {
        $response = $client->post($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ],
            'json' => $payload,
        ]);

        $vendorCreated = json_decode((string)$response->getBody(), true);
    } catch (\Exception $e) {
        \Log::error('BC createVendor error: ' . $e->getMessage(), ['payload' => $payload]);
        return [
            'status' => 'error',
            'message' => $e->getMessage(),
        ];
    }

    // =====================
    // 2️⃣ Buat PriceListHeaderCustom
    // =====================
    $base = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT');
$root = $base . "/api/citbi/sku/v1.0";
$priceUrl = "{$root}/companies({$this->companyId})/priceListHeaderCustom";

$client = new \GuzzleHttp\Client([
    'timeout' => 180,
    'connect_timeout' => 20,
]);
$etag = $priceCreated['@odata.etag'] ?? null;
$priceCode = $vendorCreated['number'] ?? $vendorData['number'];
// ===================
// Tahap 1: Create PriceListHeaderCustom (priceCode + Description)
// ===================
$createPayload = [
    'priceCode' => $vendorCreated['number'] ?? $vendorData['number'],
    'Description' => $vendorCreated['displayName'] ?? $vendorData['displayName'],
];

try {
    $response = $client->post($priceUrl, [
        'headers' => [
            'Authorization' => "Bearer {$this->token}",
            'Accept' => 'application/json',
            'If-Match' => $etag ?? '*',
        ],
        'json' => $createPayload,
    ]);

    $priceCreated = json_decode((string)$response->getBody(), true);
    $priceCode = $priceCreated['priceCode'] ?? $createPayload['priceCode'];
} catch (\Exception $e) {
    \Log::error('BC createVendorPrice (create) error: ' . $e->getMessage(), [
        'payload' => $createPayload
    ]);
    $priceCreated = null;
}

// ===================
// Tahap 2: Patch sourceType
// ===================
if ($priceCode) {
    try {
        $client->patch("{$priceUrl}('{$priceCode}')", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $etag ?? '*',
            ],
            'json' => [
                'sourceType' => 'Vendor'
            ]
        ]);
    } catch (\Exception $e) {
        \Log::error('BC createVendorPrice (patch sourceType) error: ' . $e->getMessage());
    }
}

// ===================
// Tahap 3: Patch sourceNo
// ===================
if ($priceCode) {
    try {
        $client->patch("{$priceUrl}('{$priceCode}')", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $etag ?? '*',
            ],
            'json' => [
                'sourceNo' => $vendorCreated['number'] ?? $vendorData['number']
            ]
        ]);
    } catch (\Exception $e) {
        \Log::error('BC createVendorPrice (patch sourceNo) error: ' . $e->getMessage());
    }
}

// ===================
// Tahap 4: Patch status
// ===================
if ($priceCode) {
    try {
        $client->patch("{$priceUrl}('{$priceCode}')", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'If-Match' => $etag ?? '*',
            ],
            'json' => [
                'status' => 'Active'
            ]
        ]);
    } catch (\Exception $e) {
        \Log::error('BC createVendorPrice (patch status) error: ' . $e->getMessage());
    }
}

    return [
        'status' => 'success',
        'data' => [
            'vendor' => $vendorCreated,
            'priceListHeaderCustom' => $priceCreated
        ]
    ];
}


    public function getNoSeries()
{
    $url = "https://api.businesscentral.dynamics.com/v2.0/"
        . env('AZURE_TENANT_ID') . "/"
        . env('BC_ENVIRONMENT')
        . "/ODataV4/Company('" . $this->companyId . "')/seriesNo";

    try {
        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
            ]
        ]);
        \Log::error($this->companyId);

        $data = json_decode((string)$response->getBody(), true);
        return [
            'status' => 'success',
            'data' => $data
        ];
    } catch (\Exception $e) {
        \Log::error('BC getNoSeries error: ' . $e->getMessage(), [
            'url' => $url,
            'exception' => $e
        ]);

        return [
            'status' => 'error',
            'message' => $e->getMessage()
        ];
    }
}
        
    function CreatePurchaseOrder() {
        $poPayload = [
            "VendorNumber" => "VFB0011", 
            "orderDate" => date('Y-m-d')
            ];

        $response = $this->$client->post("https://api.businesscentral.dynamics.com/v2.0/".env('AZURE_TENANT_ID')."/".env('BC_ENVIRONMENT')."/api/v2.0/companies({$this->companyId})/purchaseOrders", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json',
                'Content-Type' => 'application/json',
            ],
            'body' => json_encode($poPayload)
        ]);

        $data = json_decode((string)$response->getBody(), true);
        return $data;
    }

    function getPOFromBC() {

        $response = $this->client->get(
            "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/purchaseOrders?\$filter=number eq '30858'",
            [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept'        => 'application/json'
                ]
            ]
        );

        return json_decode((string)$response->getBody(), true);
    }

    public function getPoSuggestions($userName)
        {
            $start = microtime(true);
            $stockkeeping = $purchaseRaw = $transferRaw = $priceRaw = $vendorRaw = [];
            $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/ODataV4/Company('" . $this->companyId . "')";
            $baseApi = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})";
            $activeCompany = session('current_company_name') ?? session('user')['role'];
            if ($activeCompany === 'Regent' or $activeCompany === 'SUPER') {
                $transferLocationCode = "(Transfer_from_Code eq 'CI.1010' or Transfer_from_Code eq 'CI.1020')";
                $locationCode = "(Location_Code eq 'CI.1010' or Location_Code eq 'CI.1020')";
            } else if ($activeCompany === 'HIN') {
                $transferLocationCode = "(Transfer_from_Code eq 'HIN.1000' or Transfer_from_Code eq 'HIN.1200' or Transfer_from_Code eq 'HIN.1300' or Transfer_from_Code eq 'HIN.3000')";
                $locationCode = "(Location_Code eq 'HIN.1000' or Location_Code eq 'HIN.1200' or Location_Code eq 'HIN.1300' or Location_Code eq 'HIN.3000')";
            }
            
            
            $headers = [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=50000'
            ];
            
            $prefixes = ['FO', 'BV', 'HO', 'MT', 'BG'];
            $itemInvenLocPromises = [];
            
            foreach ($prefixes as $prefix) {
                $itemInvenLocPromises[$prefix] = $this->client->getAsync(
                    "$baseOdata/ItemInvenLoc?\$filter=" . urlencode("startswith(ItemNo,'$prefix')"),
                    ['headers' => $headers]
                );
            }

            $promises = [
                'stockkeeping' => $this->client->getAsync(
                    "$baseOdata/APIStockkeeping?\$filter=" . urlencode($locationCode),
                    ['headers' => $headers]
                ),

                'purchaseQtys' => $this->client->getAsync("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
                        "/ODataV4/Company('" . $this->companyId . "')/Purchase_Lines_Excel?\$filter=Outstanding_Quantity gt 0 and (Status eq 'Released' or Status eq 'Pending Approval' or Status eq 'Pending Prepayment') 
                        and $locationCode &\$select=No,Document_No,Approved_Date,Status,Location_Code,Outstanding_Quantity,Unit_of_Measure_Code,Buy_from_Vendor_Name",
                        ['headers' => $headers]
                    ),
                'openPurchase' => $this->client->getAsync("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
                        "/ODataV4/Company('" . $this->companyId . "')/Purchase_Lines_Excel?\$filter=Outstanding_Quantity gt 0 and (Status eq 'Open') 
                        and $locationCode &\$select=No,Document_No,Approved_Date,Status,Location_Code,Outstanding_Quantity,Unit_of_Measure_Code",
                        ['headers' => $headers]
                    ),
                'transferQtys' => $this->client->getAsync(
                    "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
                    "/ODataV4/Company('" . $this->companyId . "')/Transfer_Order_Line_Excel?" .
                    "\$filter=" . urlencode("Status eq 'Released' and Quantity_Shipped lt Quantity and (Transfer_from_Code eq 'CI.1010' or Transfer_from_Code eq 'CI.1020')") .
                    "&\$select=Document_No,Item_No,Transfer_From_Code,OutstandingShipped,Shipment_Date,Description,Unit_of_Measure_Code",
                    ['headers' => $headers]
                ),
                'priceLists' => $this->client->getAsync(
                    "$baseOdata/Price_List_Lines?" .
                    '$filter=' . urlencode("SourceNo ne null and StartingDate le " . now()->toDateString() . " and EndingDate ge " . now()->toDateString()) .
                    '&$select=Asset_No,Product_No,Unit_of_Measure_Code,DirectUnitCost,SourceNo',
                    ['headers' => $headers]
                ),
                'vendors' => $this->client->getAsync(
                    "$baseApi/vendors?\$select=number,displayName",
                    ['headers' => $headers]
                ),
                'itemUoM' => $this->client->getAsync(
                    "$baseOdata/Item_UOM?\$select=Item_No,Code,Qty_per_Unit_of_Measure",
                    ['headers' => $headers]
                )
            ];
            
            $promises = array_merge($promises, $itemInvenLocPromises);
            $responses = Utils::settle($promises)->wait();
            $elapsed = microtime(true) - $start;
            Log::debug('Fetch Runtime:', ['seconds' => $elapsed]);
            if ($responses['stockkeeping']['state'] === 'fulfilled') {
                $stockkeeping = json_decode($responses['stockkeeping']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            if ($responses['purchaseQtys']['state'] === 'fulfilled') {
                $purchaseRaw = json_decode($responses['purchaseQtys']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            if ($responses['openPurchase']['state'] === 'fulfilled') {
                $openRaw = json_decode($responses['openPurchase']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            if ($responses['transferQtys']['state'] === 'fulfilled') {
                $transferRaw = json_decode($responses['transferQtys']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            if ($responses['priceLists']['state'] === 'fulfilled') {
                $priceRaw = json_decode($responses['priceLists']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            $itemUoMMap = [];
            if ($responses['itemUoM']['state'] === 'fulfilled') {
                $itemUoMRaw = json_decode($responses['itemUoM']['value']->getBody()->getContents(), true)['value'] ?? [];
                foreach ($itemUoMRaw as $uom) {
                    $itemNo = $uom['Item_No'];
                    $uomCode = $uom['Code'];
                    $qtyPerUOM = $uom['Qty_per_Unit_of_Measure'] ?? 1;
                    $itemUoMMap[$itemNo][$uomCode] = $qtyPerUOM;
                }
            }
            $ItemInvenLoc = [];
            foreach (['FO', 'BV', 'HO', 'MT', 'BG'] as $prefix) {
                if (isset($responses[$prefix]['state']) && $responses[$prefix]['state'] === 'fulfilled') {
                    $body = json_decode($responses[$prefix]['value']->getBody()->getContents(), true);
                    $ItemInvenLoc = array_merge($ItemInvenLoc, $body['value']);
                }
            }
            if ($responses['vendors']['state'] === 'fulfilled') {
                $vendorRaw = json_decode($responses['vendors']['value']->getBody()->getContents(), true)['value'] ?? [];
            }
            $purchaseQtys = [];
            $purchaseDetailsMap = [];   
            foreach ($purchaseRaw as $line) {
                if (strpos($line['Document_No'], 'PRO') !== false) {
                    continue;
                }
                $uomCode = $line['Unit_of_Measure_Code'];
                $multiplier = $itemUoMMap[$line['No']][$uomCode] ?? 1;
                $qtyBaseUoM = $line['Outstanding_Quantity'] * $multiplier;
                $key = $line['No'] . '_' . $line['Location_Code'];
                $purchaseQtys[$key] = ($purchaseQtys[$key] ?? 0) + $qtyBaseUoM;

                $purchaseDetailsMap[$key][] = [
                    'document_no' => $line['Document_No'],
                    'approved_date' => isset($line['Approved_Date']) ? date('d-m-Y', strtotime($line['Approved_Date'])) : null,
                    'status' => $line['Status'],
                    'outstanding_qty' => $qtyBaseUoM, 
                    'uom'             => $uomCode,
                    'multiplier'      => $multiplier,
                    'vendor' => $line['Buy_from_Vendor_Name']
                ];
            }
            $openPurchaseMap = [];
            foreach ($openRaw as $line) {
                if (strpos($line['Document_No'], 'PRO') !== false || strpos($line['Document_No'], 'PI') !== false || strpos($line['Document_No'], 'PPPRE') !== false || strpos($line['Document_No'], 'PPPCM') !== false) {
                    continue;
                }
                $uomCode = $line['Unit_of_Measure_Code'];
                $multiplier = $itemUoMMap[$line['No']][$uomCode] ?? 1;

                $qtyBaseUoM = $line['Outstanding_Quantity'] * $multiplier;


                $key = $line['No'] . '_' . $line['Location_Code'];

                $openPurchaseMap[$key][] = [
                    'document_no' => $line['Document_No'],
                    'approved_date' => isset($line['Approved_Date']) ? date('d-m-Y', strtotime($line['Approved_Date'])) : null,
                    'status' => $line['Status'],
                    'outstanding_qty' => $qtyBaseUoM, 
                    'uom'             => $uomCode,
                    'multiplier'      => $multiplier
                ];
            }
            $transferQtys = [];
            $transferDates = [];
            $transferDetailsMap = [];
            foreach ($transferRaw as $line) {
                $itemNo = $line['Item_No'];
                $location = $line['Transfer_from_Code'];
                if (!in_array($location, ['CI.1010', 'CI.1020', 'HIN.1000', 'HIN.1200', 'HIN.1300', 'HIN.3000'])) {
                    continue;
                }
                $key = $line['Item_No'] . '_' . $line['Transfer_from_Code'];
                $outstanding = $line['OutstandingShipped'] ?? 0;
                if ($activeCompany === 'Regent' or $activeCompany === 'SUPER') {
                    $outstandingBaseUoM = ($line['OutstandingShipped'] ?? 0) * $multiplier;
                } if ($activeCompany === 'HIN') {
                    $outstandingBaseUoM = ($line['OutstandingShipped'] ?? 0) / $multiplier;
                }

                if ($outstanding > 0) {
                    $transferQtys[$key] = ($transferQtys[$key] ?? 0) + $outstandingBaseUoM;
                    $transferDetailsMap[$itemNo . '_' . $location][] = [
                        'document_no' => $line['Document_No'],
                        'shipment_date' => $line['Shipment_Date'],
                        'quantity'      => $outstandingBaseUoM,
                        'uom'           => $uomCode,
                        'multiplier'    => $multiplier
                    ];
                    if (!empty($line['Shipment_Date'])) {
                        $date = $line['Shipment_Date'];
                        if (!isset($transferDates[$key]) || $date < $transferDates[$key]) {
                            $transferDates[$key] = $date;
                        }
                    }
                }
            }
            $priceMap = [];
            $unitCostsByItem = []; 
            foreach ($priceRaw as $line) {
                $itemNo   = $line['Asset_No'] ?? null;
                $vendorNo = $line['SourceNo'] ?? null;
                $uomCode  = $line['Unit_of_Measure_Code'] ?? null;
                $unitCost = $line['DirectUnitCost'] ?? null;

                if (!empty($itemNo) && !empty($vendorNo)) {
                    $priceMap[$itemNo][$vendorNo] = true;
                    if ($unitCost !== null) {
                        $multiplier = $uomMap[$itemNo][$uomCode] ?? 1; 
                        $costPerBaseUoM = $unitCost / $multiplier;
                        $unitCostsByItem[$itemNo][$vendorNo] = $costPerBaseUoM;
                    }
                }
            }
            foreach ($priceMap as $itemNo => $vendorList) {
                $priceMap[$itemNo] = array_keys($vendorList);
            }

            $vendorMap = [];
            foreach ($vendorRaw as $v) {
                $vendorMap[$v['number']] = $v['displayName'];
            }
            $skuKeys = [];
            foreach ($stockkeeping as $sku) {
                $skuKeys[$sku['Item_No'] . '_' . $sku['Location_Code']] = true;
            }
            $itemInvenKey = [];
            foreach ($ItemInvenLoc as $i => $item) {
                $itemInvenKey[$item['ItemNo'] . '_' . $item['Location']] = $i;
            }
            $results = [];
            foreach ($stockkeeping as $sku) {
                $itemNo = $sku['Item_No'];
                $location = $sku['Location_Code'];
                $stock = $sku['Inventory'];
                $minQty = $sku['MinInven'];
                $maxQty = $sku['MaxInven'];
                $description = $sku['Description'];
                $active = $sku['Active'];
                $comment = $sku['Comment'];
                $dbStatus = $sku['Status'];
                $key = $itemNo . '_' . $location;
                $normalizedDescription = strtoupper(preg_replace('/[^A-Z0-9]/', '', $description));

                if ($location === 'CI.1010' && str_contains($normalizedDescription, 'FRUITVEGETABLE')) {
                    continue;
                }
                $onPO = $purchaseQtys[$key] ?? 0;
                $inTransfer = $transferQtys[$key] ?? 0;
                $qtyToOrder = max(0, ceil(($maxQty + $inTransfer) - ($stock + $onPO)));
                $vendorNos = $priceMap[$itemNo] ?? [];
                $transferDate = $transferDates[$key] ?? null;
                
                if ($qtyToOrder <= 0) {
                    $status = "No Need Order";
                }
                if ($qtyToOrder > 0) {
                    if ( ($stock + $onPO) - ($inTransfer) < $minQty ) {
                        $status = "Need Order";
                        if ($inTransfer > 0) {
                            $status = "Need Order For TO";
                        }
                    } else
                    {
                        $status = "Order For Stock";
                    }
                }
                if ($onPO > 0 && $stock < ($minQty+$inTransfer) && ($stock+$onPO) >= ($minQty+$inTransfer)){
                    $status = "Follow Up PO";
                }

                if (empty($vendorNos)) {
                $results[] = [  
                    'item_no'       => $itemNo,
                    'description'   => $description,
                    'stock'         => $stock,
                    'min_qty'       => $minQty,
                    'max_qty'       => $maxQty,
                    'on_po'         => $onPO,
                    'in_transfer'   => $inTransfer,
                    'qty_to_order'  => $qtyToOrder,
                    'vendor_name'   => 'No Vendor',
                    'vendor_no'     => '0',
                    'location_code' => $location,
                    'unit_cost' => '0',
                    'status'        => $status,
                    'transfer_lines'=> $transferDetailsMap[$key] ?? [],
                    'po_lines' => $purchaseDetailsMap[$key] ?? [],
                    'openPo' => $openPurchaseMap[$key] ?? [],
                    'Active' => $active,
                    'Comment' => $comment,
                    'Status' => $dbStatus,
                    'need_shipment' => ($stock > 0 && $inTransfer > 0)
                ];
            } else {
                foreach ($vendorNos as $vendorNo) {
                    $results[] = [
                        'item_no'       => $itemNo,
                        'description'   => $description,
                        'stock'         => $stock,
                        'min_qty'       => $minQty,
                        'max_qty'       => $maxQty,
                        'on_po'         => $onPO,
                        'in_transfer'   => $inTransfer,
                        'qty_to_order'  => $qtyToOrder,
                        'vendor_name'   => $vendorMap[$vendorNo] ?? null,
                        'vendor_no'     => $vendorNo,
                        'location_code' => $location,
                        'unit_cost' => $unitCostsByItem[$itemNo][$vendorNo],
                        'status'        => $status,
                        'transfer_lines'=> $transferDetailsMap[$key] ?? [],
                        'po_lines' => $purchaseDetailsMap[$key] ?? [],
                        'openPo' => $openPurchaseMap[$key] ?? [],
                        'Active' => $active,
                        'Comment' => $comment,
                        'Status' => $dbStatus,
                        'need_shipment' => ($stock > 0 && $inTransfer > 0)
                    ];
                }
                }

            }

            foreach ($transferRaw as $line) {
                $itemNo = $line['Item_No'];
                $location = $line['Transfer_from_Code'];
                $key = $itemNo . '_' . $location;
                if (!in_array($location, ['CI.1010', 'CI.1020', 'HIN.1000', 'HIN.1200', 'HIN.1300', 'HIN.3000'])) {
                    continue;
                }
                if (isset($skuKeys[$key])) {
                    continue;
                }
                $onPO = $purchaseQtys[$key] ?? 0;
                $inTransfer = $transferQtys[$key] ?? 0;
                $transferDate = $line['Shipment_Date'] ?? null;

                $description = $line['Description'] ?? '';
                $normalizedDescription = strtoupper(preg_replace('/[^A-Z0-9]/', '', $description));
                if ($location === 'CI.1010' && str_contains($normalizedDescription, 'FRUITVEGETABLE')) {
                    continue;
                }
                
                $stock = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Inventory'] : 0;
                $active = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Active'] : true;
                $Comment = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Comment'] : '';
                $dbStatus = isset($itemInvenKey[$key]) ? $ItemInvenLoc[$itemInvenKey[$key]]['Status'] : '__';
                $minQty = 0;
                $maxQty = 0;
                $qtyToOrder = max(0, ceil($maxQty + $inTransfer - ($stock + $onPO)));

                if ($qtyToOrder <= 0) {
                    $status = "No Need Order";
                } else {
                    if (($stock + $onPO) - $inTransfer < $minQty) {
                        $status = "Need Order";
                        if ($inTransfer > 0) {
                                $status = "Need Order For TO";
                        }
                    } else {
                        $status = "Order For Stock";
                    }
                }
                if ($onPO > 0 && $stock < ($minQty+$inTransfer) && ($stock+$onPO) > ($minQty+$inTransfer)){
                    $status = "Follow Up PO";
                }

                $vendorNos = $priceMap[$itemNo] ?? [];

                if (empty($vendorNos)) {
                    $results[] = [
                        'item_no'       => $itemNo,
                        'description'   => $description,
                        'stock'         => $stock,
                        'min_qty'       => $minQty,
                        'max_qty'       => $maxQty,
                        'on_po'         => $onPO,
                        'in_transfer'   => $inTransfer,
                        'qty_to_order'  => $qtyToOrder,
                        'vendor_name'   => 'No Vendor',
                        'vendor_no'     => '0',
                        'location_code' => $location,
                        'unit_cost'     => '0',
                        'status'        => $status,
                        'transfer_lines' => $transferDetailsMap[$key] ?? [],
                        'po_lines' => $purchaseDetailsMap[$key] ?? [],
                        'openPo' => $openPurchaseMap[$key] ?? [],
                        'Active' => $active,
                        'Comment' => $Comment,
                        'Status' => $dbStatus,
                        'need_shipment' => ($stock > 0 && $inTransfer > 0)
                        
                    ];
                } else {
                    foreach ($vendorNos as $vendorNo) {
                        $results[] = [
                            'item_no'       => $itemNo,
                            'description'   => $description,
                            'stock'         => $stock,
                            'min_qty'       => $minQty,
                            'max_qty'       => $maxQty,
                            'on_po'         => $onPO,
                            'in_transfer'   => $inTransfer,
                            'qty_to_order'  => $qtyToOrder,
                            'vendor_name'   => $vendorMap[$vendorNo] ?? null,
                            'vendor_no'     => $vendorNo,
                            'location_code' => $location,
                            'unit_cost'     => $unitCostsByItem[$itemNo][$vendorNo] ?? 0,
                            'status'        => $status,
                            'transfer_lines' => $transferDetailsMap[$key] ?? [],
                            'po_lines' => $purchaseDetailsMap[$key] ?? [],
                            'openPo' => $openPurchaseMap[$key] ?? [],
                            'Active' => $active,
                            'Comment' => $Comment,
                            'Status' => $dbStatus,
                            'need_shipment' => ($stock > 0 && $inTransfer > 0)
                        ];
                    }
                }
            }

            $results = array_filter($results, function ($item) {
                $desc = strtoupper(preg_replace('/[^A-Z0-9]/', '', $item['description'] ?? ''));
                return !(($item['location_code'] === 'CI.1010') && str_contains($desc, 'FRUITVEGETABLE'));
            });
            return [
                'items' => $results,
                'vendors' => $vendorMap
            ];
        }
public function getallitemdata($userName)
{
    $start = microtime(true);
    $priceRaw = $vendorRaw = [];
    $baseOdata = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/ODataV4/Company('" . $this->companyId . "')";
    $baseApi = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})";

    $headers = [
        'Authorization' => "Bearer {$this->token}",
        'Accept'        => 'application/json',
        'Prefer'        => 'odata.maxpagesize=50000'
    ];

    // -------------------------------
    // Ambil Price List dan Vendor
    // -------------------------------
    $promises = [
        'priceLists' => $this->client->getAsync(
            "$baseOdata/Price_List_Lines?\$select=Asset_No,Product_No,Unit_of_Measure_Code,DirectUnitCost,SourceNo,StartingDate,EndingDate,Status",
            ['headers' => $headers]
        ),
        'vendors' => $this->client->getAsync("$baseApi/vendors?\$select=number,displayName", ['headers' => $headers])
    ];

    $responses = Utils::settle($promises)->wait();

    $priceRaw  = $responses['priceLists']['state'] === 'fulfilled' ? json_decode($responses['priceLists']['value']->getBody()->getContents(), true)['value'] ?? [] : [];
    $vendorRaw = $responses['vendors']['state'] === 'fulfilled' ? json_decode($responses['vendors']['value']->getBody()->getContents(), true)['value'] ?? [] : [];

    // -------------------------------
    // Price processing
    // -------------------------------
    $priceMap = [];
    $unitCostsByItem = [];
    $priceDateMap = [];
    $statusPriceMap = [];

    foreach ($priceRaw as $line) {
        $itemNo   = $line['Asset_No'] ?? null;
        $vendorNo = $line['SourceNo'] ?? null;
        $uomCode  = $line['Unit_of_Measure_Code'] ?? null;
        $unitCost = $line['DirectUnitCost'] ?? null;
        $startDate = isset($line['StartingDate']) ? strtotime($line['StartingDate']) : 0;
        $endDate   = isset($line['EndingDate']) ? strtotime($line['EndingDate']) : 0;
        if (!$itemNo || !$vendorNo) continue;

        $priceMap[$itemNo][$vendorNo][] = [
            'unit_cost' => $unitCost,
            'uom'       => $uomCode,
            'start'     => $startDate,
            'end'       => $endDate,
            'starting_date' => $line['StartingDate'] ?? null,
            'ending_date'   => $line['EndingDate'] ?? null
        ];

        $statusPriceMap[$itemNo][$vendorNo] = $line['Status'] ?? null;
    }

    foreach ($priceMap as $itemNo => $vendors) {
        foreach ($vendors as $vendorNo => $prices) {
            usort($prices, function($a,$b){
                if ($a['end'] !== $b['end']) return $b['end'] - $a['end'];
                return $b['start'] - $a['start'];
            });
            $selected = $prices[0];
            $unitCostsByItem[$itemNo][$vendorNo] = $selected['unit_cost'];
            $priceDateMap[$itemNo][$vendorNo] = [
                'starting_date' => $selected['starting_date'],
                'ending_date'   => $selected['ending_date']
            ];
            $priceMap[$itemNo][$vendorNo] = true;
        }
    }
    foreach ($priceMap as $itemNo => $vendorList) {
        $priceMap[$itemNo] = array_keys($vendorList);
    }

    // -------------------------------
    // Vendor map
    // -------------------------------
    $vendorMap = [];
    foreach ($vendorRaw as $v) {
        $vendorMap[$v['number']] = $v['displayName'];
    }

    // -------------------------------
    // Build results hanya dari Price List + Vendor
    // -------------------------------
    $results = [];
    $seenKeys = [];

    foreach ($priceMap as $itemNo => $vendorNos) {
        foreach ($vendorNos as $vendorNo) {
            $uniqueKey = $itemNo.'_'.$vendorNo;
            if (isset($seenKeys[$uniqueKey])) continue;
            $seenKeys[$uniqueKey] = true;

            $dates = $priceDateMap[$itemNo][$vendorNo] ?? ['starting_date'=>null,'ending_date'=>null];

            $results[] = [
                'item_no' => $itemNo,
                'vendor_name' => $vendorMap[$vendorNo] ?? null,
                'vendor_no' => $vendorNo,
                'unit_cost' => $unitCostsByItem[$itemNo][$vendorNo] ?? 0,
                'starting_date' => $dates['starting_date'] ? date('d/m/Y', strtotime($dates['starting_date'])) : null,
                'ending_date'   => $dates['ending_date'] ? date('d/m/Y', strtotime($dates['ending_date'])) : null,
                'status_price' => $statusPriceMap[$itemNo][$vendorNo] ?? null
            ];
        }
    }

    return [
        'items' => $results,
        'vendors' => $vendorMap
    ];
}




    public function getItemIdByNo(string $itemNo)
    {
        $filter = urlencode("number eq '$itemNo'");
        $response = $this->client->get("https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
       "/api/v2.0/companies({$this->companyId})/items?\$filter=$filter", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ]
        ]);

        $items = json_decode($response->getBody(), true);
        return $items['value'][0]['id'] ?? null;

        //$url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/citbi/sku/v1.0/companies(" . rawurlencode($this->companyId) . ")/GeneralJournalLines";
    }


    public function createPurchaseOrderForVendor($vendorNo, $userName, $location)
    {
        if ($location == 'CI.1010') {
            $purchaser = 'TN01';
        } 
        else if ($location == 'CI.1020'){
            $purchaser = 'UT01';
        } else if ($location == 'HIN.1200' || $location == 'HIN.1300') {
            $purchaser = 'PE01';
        } else if ($location == 'HIN.1000') {
            $purchaser = 'PW01';
        } else if ($location == 'HIN.3000') {
            $purchaser = 'FB01';
        }

        $payload = [
            'vendorNumber' => $vendorNo,
            'orderDate' => now()->toDateString(),
            'purchaser' => $purchaser,
            'shortcutDimension2Code' => "100000",
            'requestedReceiptDate' => today()->toDateString()
        ];

        $response = $this->client->post("https://api.businesscentral.dynamics.com/v2.0/".env('AZURE_TENANT_ID')."/".env('BC_ENVIRONMENT')."/api/v2.0/companies({$this->companyId})/purchaseOrders", [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
            ],
            'body' => json_encode($payload)
        ]);
        
        $body = json_decode($response->getBody(), true);
        return json_decode($response->getBody(), true);
    }

    public function getLocationIdByCode($locationCode)
    {
        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/api/v2.0/companies({$this->companyId})/locations?\$filter=code eq '{$locationCode}'";

        $response = $this->client->get($url, [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept' => 'application/json'
            ]
        ]);

        $data = json_decode((string)$response->getBody(), true);
        
        return $data['value'][0]['id'] ?? null;
    }


    // public function addPurchaseLineToPO($poId, $itemNo, $quantity, $cost, $location, $userName)
    // {
    //     $start = microtime(true);
    
    //     $itemId = $this->getItemIdByNo($itemNo);
    //     $locationCode = $location;
    //     if (str_contains($itemNo, 'BV') && $cost > 1000000) $locationCode = 'CI.1051';
    //     $locationId = $this->getLocationIdByCode($locationCode);
    
    //     $payload = json_encode([
    //         '@odata.type'       => '#Microsoft.NAV.purchaseOrderLine',
    //         'lineType'  => 'Item',
    //         'itemId'     => $itemId,
    //         'quantity'   => (float)$quantity
    //     ]);
    
    //     $url = "https://api.businesscentral.dynamics.com/v2.0/"
    //          . env('AZURE_TENANT_ID') . "/"
    //          . env('BC_ENVIRONMENT')
    //          . "/api/v2.0/companies({$this->companyId})/purchaseOrders({$poId})/purchaseOrderLines";

    //     try {
    //         $response = $this->client->post($url, [
    //             'headers' => [
    //                 'Authorization' => "Bearer {$this->token}",
    //                 'Accept'        => 'application/json',
    //                 'Content-Type'  => 'application/json',
    //                 'OData-Version'     => '4.0',
    //                 'OData-MaxVersion'  => '4.0',
    //             ],
    //             'body' => $payload,
    //         ]);
    
    //         Log::info("Create POLine runtime: " . (microtime(true) - $start) . " seconds");
    //         return json_decode((string)$response->getBody(), true);
    
    //     } catch (RequestException $e) {
    //         $res     = $e->getResponse();
    //         $status  = $res?->getStatusCode();
    //         $reason  = $res?->getReasonPhrase();
    //         $headers = $res?->getHeaders() ?? [];
    //         $raw     = $res ? (string)$res->getBody() : $e->getMessage();
    
    //         // Optional telemetry IDs (if present)
    //         $corrId = $headers['x-correlation-id'][0] ?? ($headers['MS-CorrelationId'][0] ?? null);
    //         $reqId  = $headers['request-id'][0] ?? null;
    
    //         Log::error('BC addPurchaseLineToPO failed', [
    //             'url'           => $url,
    //             'status'        => $status,
    //             'reason'        => $reason,
    //             'headers'       => $headers,
    //             'payload_json'  => json_decode($payload, true),
    //             'error_raw'     => $raw,      // ← full upstream body (no truncation)
    //             'requestId'     => $reqId,
    //             'correlationId' => $corrId,
    //             'exception'     => $e->getMessage(),
    //         ]);
    //         throw $e; // or return a normalized error if you prefer
    //     }
    // }
    
    
    public function getAsyncAddPurchaseLinePromise($poId, $itemNo, $quantity, $userName)
    {   
        $start = microtime(true);
        $itemId     = $this->getItemIdByNo($itemNo);
        $locationId = $this->getLocationIdByCode(in_array($userName, [
                'titania@citbi.onmicrosoft.com',
                'adminbc@citbi.onmicrosoft.com'
            ]) ? 'CI.1010' : 'CI.1020');

        $payload = [
            'itemId'     => $itemId,
            'quantity'   => (float) $quantity,
            'locationId' => $locationId,
        ];
        $elapsed = microtime(true) - $start;
        Log::info("Create POLine runtime: {$elapsed} seconds");
        return $this->client->postAsync(
            "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') . "/api/v2.0/companies({$this->companyId})/purchaseOrders({$poId})/purchaseOrderLines",
            [
                'headers' => [
                    'Authorization' => "Bearer {$this->token}",
                    'Accept'        => 'application/json',
                    'Content-Type'  => 'application/json',
                ],
                'body' => json_encode($payload),
            ]
        );
    }

    public function addPurchaseLineToPO(string $poNo, string $itemNo, float $quantity, $cost, string $locationCode = '', $userName, string $poId)
    {
        $base = "https://api.businesscentral.dynamics.com/v2.0/"
          . env('AZURE_TENANT_ID') . "/"
          . env('BC_ENVIRONMENT');

        $root = $base . "/api/citbi/sku/v1.0";
        $linesUrl = "{$root}/companies({$this->companyId})/purchaseOrderLinesCustom"; // <-- exact casing!

        $headersJson = [
            'Authorization'    => "Bearer {$this->token}",
            'Accept'           => 'application/json',
            'Content-Type'     => 'application/json',
        ];

        try {
            $flt = urlencode("documentType eq 'Order' and documentNo eq '{$poNo}'");
            $lastUrl = $linesUrl . "?\$filter={$flt}&\$orderby=lineNo desc&\$top=1&\$select=lineNo";
            $lastResp = $this->client->get($lastUrl, ['headers' => $headersJson]);
            $lastJson = json_decode((string)$lastResp->getBody(), true);
            $lastLineNo = (int)($lastJson['value'][0]['lineNo'] ?? 0);
            $nextLineNo = max(10000, $lastLineNo + 10000);

            $payload = [
                'documentType' => 'Order',
                'documentNo'   => (string)$poNo,
                'lineNo'       => $nextLineNo,
                'itemNo'       => $itemNo,
                'type'         => 'Item',
                'quantity'     => (float)$quantity
            ];
            if ($locationCode !== '') {
                $payload['locationCode'] = $locationCode;
            }
            $resp = $this->client->post($linesUrl, [ 'headers' => $headersJson, 'body' => json_encode($payload, JSON_UNESCAPED_SLASHES), ]); 
            $data = json_decode((string)$resp->getBody(), true); 
            $etag  = $data['@odata.etag'] ?? '*';
            $idUrl = $data['@odata.id']   ?? null;
            \Log::info('API line created (doc keys)', [ 'url' => $linesUrl, 'payload' => $payload, 'response' => $data ]);

            return $data;

        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $res = $e->getResponse();
            $raw = $res ? (string)$res->getBody() : $e->getMessage();
            \Log::error('Custom API purchase line POST failed', [
                'url'       => $linesUrl,
                'status'    => $res?->getStatusCode(),
                'reason'    => $res?->getReasonPhrase(),
                'headers'   => $res?->getHeaders(),
                'payload'   => $payload ?? null,
                'error_raw' => $raw,
                'requestId' => $res?->getHeader('request-id')[0] ?? null,
            ]);
            throw $e;
        }
    }

    public function getAllPurchasePrices()
    {
        $start = microtime(true);
        $today = now()->toDateString(); // e.g. "2025-06-17"
        $filter = "SourceNo ne null and StartingDate le $today and EndingDate ge $today";
        $encodedFilter = urlencode($filter);

        $url = "https://api.businesscentral.dynamics.com/v2.0/" . env('AZURE_TENANT_ID') . "/" . env('BC_ENVIRONMENT') .
            "/ODataV4/Company('" . $this->companyId . "')/Price_List_Lines?\$filter={$encodedFilter}";

        $headers = [
            'headers' => [
                'Authorization' => "Bearer {$this->token}",
                'Accept'        => 'application/json',
                'Prefer'        => 'odata.maxpagesize=1000'
            ]
        ];

        $allData = [];

        try {
            do {
                $response = $this->client->get($url, $headers);
                $decoded = json_decode((string)$response->getBody(), true);

                if (isset($decoded['value'])) {
                    $allData = array_merge($allData, $decoded['value']);
                }

                $url = $decoded['@odata.nextLink'] ?? null;
            } while ($url);

            return collect($allData)->groupBy('Asset_No');
        } catch (\Exception $e) {
            Log::error("Failed to fetch Price List Lines: " . $e->getMessage());
            return collect(); 
        }
        $elapsed = microtime(true) - $start;
        Log::info("Fetch runtime Priccss: {$elapsed} seconds");
        
    }
}
