<?php

namespace App\Http\Controllers;

use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Material;
use App\Models\Product;
use App\Models\Customer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;

class OrderController extends Controller
{
    private const MINIMUM_ORDER_AMOUNT = 10000;
    private const PRICE_ROUNDING_AMOUNT = 1000;

    public function __construct()
    {
        $this->middleware(\App\Http\Middleware\PermissionMiddleware::class . ':create orders')
            ->only(['create', 'store', 'edit', 'update', 'destroy']);
    }

    public function notifyWa(\App\Models\Order $order)
    {
        if (!$order->customer_phone) {
            return back();
        }

        if (!$order->wa_notified_at) {
            $order->forceFill([
                'wa_notified_at' => now(),
            ])->save();
        }

        $phone = preg_replace('/[^0-9]/', '', $order->customer_phone);

        if (str_starts_with($phone, '08')) {
            $phone = '62' . substr($phone, 1);
        } elseif (str_starts_with($phone, '8')) {
            $phone = '62' . $phone;
        } elseif (str_starts_with($phone, '+62')) {
            $phone = substr($phone, 1);
        }

        $waMessage = urlencode(\App\Models\Setting::buildWaMessage($order));
        $waUrl = "https://wa.me/{$phone}?text={$waMessage}";

        return redirect()->away($waUrl);
    }

    public function index(Request $request)
    {
        $query = Order::with(['designer', 'cashier', 'operator', 'items']);

        if ($request->filled('cancelled') && $request->cancelled == '1') {
            $query->cancelled();
        } else {
            $query->notCancelled();
        }

        if ($request->filled('status')) {
            $query->where('payment_status', $request->status);
        }

        if ($request->filled('production')) {
            $query->where('production_status', $request->production);
        }

        if ($request->filled('pickup')) {
            $query->where('pickup_status', $request->pickup);
        }

        if ($request->filled('date')) {
            $query->whereDate('created_at', $request->date);
        }

        if ($request->filled('search')) {
            $search = $request->search;

            $query->where(function ($q) use ($search) {
                $q->where('invoice_number', 'like', "%{$search}%")
                    ->orWhere('customer_name', 'like', "%{$search}%")
                    ->orWhereHas('items', function ($itemQuery) use ($search) {
                        $itemQuery->where('file_name', 'like', "%{$search}%")
                            ->orWhere('product_name', 'like', "%{$search}%");
                    });
            });
        }

        $orders = $query->latest()->paginate(100)->withQueryString();

        return view('pages.orders.index', [
            'title' => 'Daftar Order',
            'orders' => $orders,
        ]);
    }

    public function create()
    {
        $materials = $this->loadAvailableOrderMaterials();
        $customers = Customer::orderBy('name')->get();

        $products = Product::active()
            ->with('materials:id')
            ->orderBy('name')
            ->get();

        $materialsJson = $this->buildMaterialsJson($materials);

        return view('pages.orders.create', [
            'title' => 'Buat Order Baru',
            'materials' => $materials,
            'materialsJson' => $materialsJson,
            'customers' => $customers,
            'products' => $products,
        ]);
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'customer_id' => ['required', 'exists:customers,id'],
            'notes' => ['nullable', 'string', 'max:1000'],
            'is_credit' => ['nullable'],
            'items' => ['required', 'array', 'min:1'],
            'items.*.product_id' => ['nullable', 'exists:products,id'],
            'items.*.product_name' => ['required', 'string', 'max:255'],
            'items.*.material_id' => ['nullable', 'exists:materials,id'],
            'items.*.material_name' => ['nullable', 'string', 'max:255'],
            'items.*.file_name' => ['required', 'string', 'max:255'],
            'items.*.file_upload' => ['nullable', 'file', 'max:51200'],
            'items.*.preview_image' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'],
            'items.*.size_description' => ['nullable', 'string', 'max:100'],
            'items.*.size_value' => ['nullable', 'numeric', 'min:0.0001'],
            'items.*.size_unit' => ['nullable', 'string', 'max:20'],
            'items.*.qty' => ['required', 'integer', 'min:1'],
            'items.*.sisi' => ['nullable', 'integer', 'in:1,2'],
            'items.*.base_price' => ['required', 'numeric', 'min:0'],
            'items.*.manual_price' => ['nullable', 'numeric', 'min:0'],
            'items.*.finishing_note' => ['nullable', 'string', 'max:500'],
        ]);

        $customer = Customer::findOrFail($validated['customer_id']);

        DB::beginTransaction();

        try {
            $order = Order::create([
                'customer_id' => $customer->id,
                'customer_name' => $customer->name,
                'customer_phone' => $customer->phone,
                'notes' => $validated['notes'] ?? null,
                'is_credit' => $request->boolean('is_credit'),
                'total_price' => 0,
                'designer_id' => $request->user()->id,
            ]);

            foreach ($validated['items'] as $index => $itemData) {
                $filePath = null;

                if ($request->hasFile("items.{$index}.file_upload")) {
                    $filePath = $request->file("items.{$index}.file_upload")->store('orders', 'public');
                }

                $previewImagePath = null;

                if ($request->hasFile("items.{$index}.preview_image")) {
                    $previewImagePath = $this->storeCompressedOrderPreviewImage(
                        $request->file("items.{$index}.preview_image")
                    );
                }

                $material = !empty($itemData['material_id'])
                    ? Material::with(['sourceMaterial', 'components.componentMaterial'])->find($itemData['material_id'])
                    : null;

                $measurement = $this->resolveOrderItemMeasurement($itemData, $material);
                $pricing = $this->resolveOrderItemFinalPricing($itemData, $measurement);

                if ($material && $measurement['stock_usage'] > 0) {
                    $this->ensureMaterialCanBeReduced(
                        $material,
                        $measurement['stock_usage'],
                        $itemData['file_name'] ?? $itemData['product_name'] ?? 'item'
                    );
                }

                $orderItemData = [
                    'product_id' => $itemData['product_id'] ?? null,
                    'product_name' => $itemData['product_name'],
                    'material_id' => $itemData['material_id'] ?? null,
                    'material_name' => $itemData['material_name'] ?? null,
                    'file_name' => $itemData['file_name'],
                    'file_path' => $filePath,
                    'size_description' => $measurement['size_description'],
                    'size_value' => $measurement['size_value'],
                    'size_unit' => $measurement['size_unit'],
                    'qty' => $itemData['qty'],
                    'sisi' => $itemData['sisi'] ?? 1,
                    'base_price' => $itemData['base_price'],
                    'manual_price' => $pricing['manual_price'],
                    'system_subtotal' => $pricing['system_subtotal'],
                    'minimum_order_applied' => $pricing['minimum_order_applied'],
                    'subtotal' => $pricing['subtotal'],
                    'finishing_note' => $itemData['finishing_note'] ?? null,
                ];

                if ($this->orderItemSupportsPreviewImage()) {
                    $orderItemData['preview_image_path'] = $previewImagePath;
                }

                $orderItem = new OrderItem($orderItemData);

                $order->items()->save($orderItem);

                if ($material && $measurement['stock_usage'] > 0) {
                    $this->reduceMaterialStock(
                        $material,
                        $measurement['stock_usage'],
                        'Order #' . $order->invoice_number . ' - ' . $itemData['file_name'],
                        $order
                    );
                }
            }

            $order->recalculateTotal();

            DB::commit();

            return redirect()->route('orders.show', $order)
                ->with('success', 'Order berhasil dibuat dengan nomor ' . $order->invoice_number);
        } catch (\Throwable $e) {
            DB::rollBack();

            return back()->withInput()->with('error', 'Gagal membuat order: ' . $e->getMessage());
        }
    }

    public function show(Order $order)
    {
        $order->load([
            'designer',
            'cashier',
            'operator',
            'items.product',
            'items.material',
            'customer',
            'canceller',
            'cancellationRequests.requester',
        ]);

        return view('pages.orders.show', [
            'title' => 'Detail Order #' . $order->invoice_number,
            'order' => $order,
        ]);
    }

    public function edit(Order $order)
    {
        if ($order->isPaid()) {
            return redirect()->route('orders.show', $order)
                ->with('error', 'Order yang sudah dibayar tidak dapat diedit.');
        }

        $order->load(['items', 'customer']);

        $forceMaterialIds = $order->items
            ->pluck('material_id')
            ->filter()
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();

        $materials = $this->loadAvailableOrderMaterials($forceMaterialIds);
        $customers = Customer::orderBy('name')->get();

        $products = Product::active()
            ->with('materials:id')
            ->orderBy('name')
            ->get();

        $materialsJson = $this->buildMaterialsJson($materials);

        $itemsJson = $order->items->map(function ($item) use ($order) {
            return [
                'id' => $item->id,
                'product_id' => $item->product_id,
                'product_name' => $item->product_name,
                'material_id' => $item->material_id,
                'material_name' => $item->material_name,
                'file_name' => $item->file_name,
                'size_description' => $item->size_description,
                'size_value' => (float) $item->size_value,
                'size_unit' => $item->size_unit,
                'qty' => (float) $item->qty,
                'sisi' => $item->sisi ?? 1,
                'base_price' => (float) $item->base_price,
                'manual_price' => (float) ($item->manual_price ?? 0),
                'system_subtotal' => (float) ($item->system_subtotal ?? 0),
                'minimum_order_applied' => (bool) ($item->minimum_order_applied ?? false),
                'finishing_note' => $item->finishing_note,
                'subtotal' => (float) $item->subtotal,
                'file_url' => $item->file_path ? Storage::url($item->file_path) : null,
                'preview_image_url' => ($this->orderItemSupportsPreviewImage() && $item->preview_image_path) ? route('orders.items.preview', [$order, $item]) : null,
            ];
        })->values();

        return view('pages.orders.edit', [
            'title' => 'Edit Order #' . $order->invoice_number,
            'order' => $order,
            'materials' => $materials,
            'customers' => $customers,
            'products' => $products,
            'materialsJson' => $materialsJson,
            'itemsJson' => $itemsJson,
        ]);
    }

    public function update(Request $request, Order $order)
    {
        if ($order->isPaid()) {
            return redirect()->route('orders.show', $order)
                ->with('error', 'Order yang sudah dibayar tidak dapat diedit.');
        }

        $validated = $request->validate([
            'customer_id' => ['required', 'exists:customers,id'],
            'notes' => ['nullable', 'string', 'max:1000'],
            'is_credit' => ['nullable'],
            'items' => ['required', 'array', 'min:1'],
            'items.*.id' => ['nullable', 'exists:order_items,id'],
            'items.*.product_id' => ['nullable', 'exists:products,id'],
            'items.*.product_name' => ['required', 'string', 'max:255'],
            'items.*.material_id' => ['nullable', 'exists:materials,id'],
            'items.*.material_name' => ['nullable', 'string', 'max:255'],
            'items.*.file_name' => ['required', 'string', 'max:255'],
            'items.*.file_upload' => ['nullable', 'file', 'max:51200'],
            'items.*.size_description' => ['nullable', 'string', 'max:100'],
            'items.*.size_value' => ['nullable', 'numeric', 'min:0.0001'],
            'items.*.size_unit' => ['nullable', 'string', 'max:20'],
            'items.*.qty' => ['required', 'integer', 'min:1'],
            'items.*.sisi' => ['nullable', 'integer', 'in:1,2'],
            'items.*.base_price' => ['required', 'numeric', 'min:0'],
            'items.*.manual_price' => ['nullable', 'numeric', 'min:0'],
            'items.*.finishing_note' => ['nullable', 'string', 'max:500'],
        ]);

        $customer = Customer::findOrFail($validated['customer_id']);

        DB::beginTransaction();

        try {
            $order->update([
                'customer_id' => $customer->id,
                'customer_name' => $customer->name,
                'customer_phone' => $customer->phone,
                'notes' => $validated['notes'] ?? null,
                'is_credit' => $request->boolean('is_credit'),
            ]);

            foreach ($order->items as $existingItem) {
                if ($existingItem->material_id && !$existingItem->is_cancelled) {
                    $material = Material::with(['sourceMaterial', 'components.componentMaterial'])
                        ->find($existingItem->material_id);

                    if ($material) {
                        $oldUsageQty = $this->getSavedOrderItemStockUsage($existingItem);

                        $this->restoreMaterialStock(
                            $material,
                            $oldUsageQty,
                            'Stok dikembalikan - Edit Order #' . $order->invoice_number,
                            $order
                        );
                    }
                }
            }

            $existingItemIds = $order->items->pluck('id')->toArray();
            $updatedItemIds = [];

            foreach ($validated['items'] as $index => $itemData) {
                $filePath = null;
                $previewImagePath = null;

                if (!empty($itemData['id'])) {
                    $orderItem = OrderItem::find($itemData['id']);

                    if ($orderItem && $orderItem->order_id === $order->id) {
                        $filePath = $orderItem->file_path;
                        $previewImagePath = $this->orderItemSupportsPreviewImage() ? ($orderItem->preview_image_path ?? null) : null;
                        $updatedItemIds[] = $orderItem->id;
                    }
                }

                if ($request->hasFile("items.{$index}.file_upload")) {
                    if ($filePath) {
                        Storage::disk('public')->delete($filePath);
                    }

                    $filePath = $request->file("items.{$index}.file_upload")->store('orders', 'public');
                }

                if ($request->hasFile("items.{$index}.preview_image")) {
                    if ($previewImagePath) {
                        Storage::disk('public')->delete($previewImagePath);
                    }

                    $previewImagePath = $this->storeCompressedOrderPreviewImage(
                        $request->file("items.{$index}.preview_image")
                    );
                }

                $material = !empty($itemData['material_id'])
                    ? Material::with(['sourceMaterial', 'components.componentMaterial'])->find($itemData['material_id'])
                    : null;

                $measurement = $this->resolveOrderItemMeasurement($itemData, $material);
                $pricing = $this->resolveOrderItemFinalPricing($itemData, $measurement);

                if ($material && $measurement['stock_usage'] > 0) {
                    $this->ensureMaterialCanBeReduced(
                        $material,
                        $measurement['stock_usage'],
                        $itemData['file_name'] ?? $itemData['product_name'] ?? 'item'
                    );
                }

                $orderItemData = [
                    'product_id' => $itemData['product_id'] ?? null,
                    'product_name' => $itemData['product_name'],
                    'material_id' => $itemData['material_id'] ?? null,
                    'material_name' => $itemData['material_name'] ?? null,
                    'file_name' => $itemData['file_name'],
                    'file_path' => $filePath,
                    'size_description' => $measurement['size_description'],
                    'size_value' => $measurement['size_value'],
                    'size_unit' => $measurement['size_unit'],
                    'qty' => $itemData['qty'],
                    'sisi' => $itemData['sisi'] ?? 1,
                    'base_price' => $itemData['base_price'],
                    'manual_price' => $pricing['manual_price'],
                    'system_subtotal' => $pricing['system_subtotal'],
                    'minimum_order_applied' => $pricing['minimum_order_applied'],
                    'finishing_note' => $itemData['finishing_note'] ?? null,
                    'subtotal' => $pricing['subtotal'],
                ];

                if ($this->orderItemSupportsPreviewImage()) {
                    $orderItemData['preview_image_path'] = $previewImagePath;
                }

                if (!empty($itemData['id']) && in_array($itemData['id'], $existingItemIds, true)) {
                    OrderItem::where('id', $itemData['id'])->update($orderItemData);
                    $updatedItemIds[] = $itemData['id'];
                } else {
                    $newItem = new OrderItem($orderItemData);
                    $order->items()->save($newItem);
                    $updatedItemIds[] = $newItem->id;
                }

                if ($material && $measurement['stock_usage'] > 0) {
                    $this->reduceMaterialStock(
                        $material,
                        $measurement['stock_usage'],
                        'Order #' . $order->invoice_number . ' - ' . $itemData['file_name'],
                        $order
                    );
                }
            }

            $itemsToDelete = array_diff($existingItemIds, $updatedItemIds);

            if (!empty($itemsToDelete)) {
                $filesToDelete = OrderItem::whereIn('id', $itemsToDelete)
                    ->pluck('file_path')
                    ->filter();

                foreach ($filesToDelete as $file) {
                    Storage::disk('public')->delete($file);
                }

                if ($this->orderItemSupportsPreviewImage()) {
                    $previewFilesToDelete = OrderItem::whereIn('id', $itemsToDelete)
                        ->pluck('preview_image_path')
                        ->filter();

                    foreach ($previewFilesToDelete as $file) {
                        Storage::disk('public')->delete($file);
                    }
                }

                OrderItem::whereIn('id', $itemsToDelete)->delete();
            }

            $order->refresh();
            $order->recalculateTotal();

            DB::commit();

            return redirect()->route('orders.show', $order)
                ->with('success', 'Order berhasil diperbarui.');
        } catch (\Throwable $e) {
            DB::rollBack();

            return back()->withInput()->with('error', 'Gagal memperbarui order: ' . $e->getMessage());
        }
    }

    public function destroy(Order $order)
    {
        if ($order->isPaid()) {
            return redirect()->route('orders.index')
                ->with('error', 'Order yang sudah dibayar tidak dapat dihapus.');
        }

        DB::beginTransaction();

        try {
            foreach ($order->items as $item) {
                if ($item->material_id && !$item->is_cancelled) {
                    $material = Material::with(['sourceMaterial', 'components.componentMaterial'])
                        ->find($item->material_id);

                    if ($material) {
                        $usageQty = $this->getSavedOrderItemStockUsage($item);

                        $this->restoreMaterialStock(
                            $material,
                            $usageQty,
                            'Stok dikembalikan - Hapus Order #' . $order->invoice_number,
                            $order
                        );
                    }
                }

                if ($item->file_path) {
                    Storage::disk('public')->delete($item->file_path);
                }

                if ($this->orderItemSupportsPreviewImage() && $item->preview_image_path) {
                    Storage::disk('public')->delete($item->preview_image_path);
                }
            }

            $invoiceNumber = $order->invoice_number;
            $order->delete();

            DB::commit();

            return redirect()->route('orders.index')
                ->with('success', "Order #{$invoiceNumber} berhasil dihapus.");
        } catch (\Throwable $e) {
            DB::rollBack();

            return redirect()->route('orders.index')
                ->with('error', 'Gagal menghapus order: ' . $e->getMessage());
        }
    }

    public function markPickedUp(Order $order)
    {
        if (!$order->canMarkAsPickedUp()) {
            return redirect()->route('orders.show', $order)
                ->with('error', 'Order belum bisa ditandai sudah diambil. Pastikan produksi sudah selesai.');
        }

        $order->markAsPickedUp();

        return redirect()->route('orders.show', $order)
            ->with('success', 'Order berhasil ditandai sudah diambil.');
    }



    public function previewItemImage(Order $order, OrderItem $orderItem)
    {
        if ((int) $orderItem->order_id !== (int) $order->id) {
            abort(404);
        }

        if (!$this->orderItemSupportsPreviewImage()) {
            abort(404);
        }

        $path = trim((string) ($orderItem->preview_image_path ?? ''));

        if ($path === '') {
            abort(404);
        }

        if (str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {
            return redirect()->away($path);
        }

        $path = ltrim($path, '/');

        foreach ([
            'storage/app/public/',
            'app/public/',
            'public/',
            'storage/',
        ] as $prefix) {
            if (str_starts_with($path, $prefix)) {
                $path = substr($path, strlen($prefix));
                break;
            }
        }

        if (Storage::disk('public')->exists($path)) {
            $fullPath = Storage::disk('public')->path($path);
            $mimeType = Storage::disk('public')->mimeType($path) ?: 'image/jpeg';

            return response()->file($fullPath, [
                'Content-Type' => $mimeType,
                'Cache-Control' => 'public, max-age=86400',
            ]);
        }

        if (is_file($path)) {
            return response()->file($path, [
                'Cache-Control' => 'public, max-age=86400',
            ]);
        }

        abort(404);
    }


    private function orderItemSupportsPreviewImage(): bool
    {
        return Schema::hasColumn((new OrderItem())->getTable(), 'preview_image_path');
    }

    private function storeCompressedOrderPreviewImage($file): ?string
    {
        if (!$file) {
            return null;
        }

        $folder = 'order-previews';
        $filename = 'preview-' . now()->format('YmdHis') . '-' . uniqid() . '.jpg';
        $path = $folder . '/' . $filename;

        if (!extension_loaded('gd')) {
            return $file->store($folder, 'public');
        }

        $sourcePath = $file->getRealPath();

        if (!$sourcePath || !is_file($sourcePath)) {
            return null;
        }

        $info = @getimagesize($sourcePath);

        if (!$info || empty($info['mime'])) {
            return $file->store($folder, 'public');
        }

        $mime = $info['mime'];

        if ($mime === 'image/jpeg') {
            $source = @imagecreatefromjpeg($sourcePath);
        } elseif ($mime === 'image/png') {
            $source = @imagecreatefrompng($sourcePath);
        } elseif ($mime === 'image/webp' && function_exists('imagecreatefromwebp')) {
            $source = @imagecreatefromwebp($sourcePath);
        } else {
            return $file->store($folder, 'public');
        }

        if (!$source) {
            return $file->store($folder, 'public');
        }

        $width = imagesx($source);
        $height = imagesy($source);

        if ($width <= 0 || $height <= 0) {
            imagedestroy($source);
            return $file->store($folder, 'public');
        }

        $maxDimension = 700;
        $ratio = min($maxDimension / $width, $maxDimension / $height, 1);
        $newWidth = max(1, (int) round($width * $ratio));
        $newHeight = max(1, (int) round($height * $ratio));

        $canvas = imagecreatetruecolor($newWidth, $newHeight);

        $white = imagecolorallocate($canvas, 255, 255, 255);
        imagefilledrectangle($canvas, 0, 0, $newWidth, $newHeight, $white);

        imagecopyresampled(
            $canvas,
            $source,
            0,
            0,
            0,
            0,
            $newWidth,
            $newHeight,
            $width,
            $height
        );

        $binary = null;

        foreach ([72, 65, 58, 52, 46, 40, 35] as $quality) {
            ob_start();
            imagejpeg($canvas, null, $quality);
            $candidate = ob_get_clean();

            if ($candidate !== false) {
                $binary = $candidate;

                if (strlen($candidate) <= 51200) {
                    break;
                }
            }
        }

        imagedestroy($source);
        imagedestroy($canvas);

        if (!$binary) {
            return $file->store($folder, 'public');
        }

        Storage::disk('public')->put($path, $binary);

        return $path;
    }

    private function loadAvailableOrderMaterials(array $forceIncludeIds = [])
    {
        $forceIncludeIds = collect($forceIncludeIds)
            ->filter()
            ->map(fn ($id) => (int) $id)
            ->unique()
            ->values()
            ->all();

        $query = Material::with([
            'prices',
            'sourceMaterial',
            'components.componentMaterial',
        ])->orderBy('name');

        if (!empty($forceIncludeIds)) {
            $query->where(function ($q) use ($forceIncludeIds) {
                $q->where('is_active', true)
                    ->orWhereIn('id', $forceIncludeIds);
            });
        } else {
            $query->active();
        }

        return $query->get()->values();
    }

    private function isMaterialAvailableForOrder(Material $material): bool
    {
        if (!$material->is_active) {
            return false;
        }

        if ((bool) $material->is_custom) {
            if (method_exists($material, 'canReduceStock')) {
                return $material->canReduceStock(1);
            }

            return true;
        }

        return (float) $material->stock >= 1;
    }

    private function getMaterialAvailabilityMessage(Material $material): ?string
    {
        if (!$material->is_active) {
            return 'Tidak aktif';
        }

        if ((bool) $material->is_custom) {
            if (method_exists($material, 'canReduceStock') && !$material->canReduceStock(1)) {
                return 'Stok komponen habis';
            }

            return null;
        }

        if ((float) $material->stock < 1) {
            return 'Stok 0';
        }

        return null;
    }

    private function buildMaterialsJson($materials)
    {
        return $materials->map(function ($m) {
            $availableForOrder = $this->isMaterialAvailableForOrder($m);
            $availabilityMessage = $this->getMaterialAvailabilityMessage($m);

            return [
                'id' => $m->id,
                'name' => $m->name,
                'base_price' => (float) $m->base_price,
                'unit' => $m->unit,
                'stock' => (float) ($m->stock ?? 0),
                'min_stock' => (float) ($m->min_stock ?? 0),
                'is_custom' => (bool) ($m->is_custom ?? false),
                'available_for_order' => $availableForOrder,
                'availability_message' => $availabilityMessage,
                'measurement_mode' => $m->measurement_mode ?? 'manual',
                'roll_width_cm' => (float) ($m->roll_width_cm ?? 0),
                'allow_rotation' => (bool) ($m->allow_rotation ?? true),
                'components' => $m->components->map(function ($component) {
                    return [
                        'material_id' => (int) $component->component_material_id,
                        'material_name' => $component->componentMaterial->name ?? '-',
                        'qty' => (float) $component->qty,
                        'unit' => $component->componentMaterial->unit ?? '-',
                        'stock' => (float) ($component->componentMaterial->stock ?? 0),
                    ];
                })->values()->toArray(),
                'prices' => $m->prices->map(function ($p) {
                    return [
                        'member_type' => $p->member_type,
                        'sisi' => $p->sisi,
                        'min_qty' => $p->min_qty,
                        'max_qty' => $p->max_qty,
                        'price' => (float) $p->price,
                    ];
                })->values()->toArray(),
            ];
        })->values();
    }

    private function ensureMaterialCanBeReduced(Material $material, float $qty, string $label = 'item'): void
    {
        $qty = round(max($qty, 0), 4);

        if ($qty <= 0) {
            return;
        }

        if (method_exists($material, 'canReduceStock') && !$material->canReduceStock($qty)) {
            $message = (bool) $material->is_custom
                ? 'Stok komponen bahan custom habis, order tidak bisa dilanjutkan.'
                : (((float) $material->stock < 1)
                    ? 'Stok 0, order tidak bisa dilanjutkan.'
                    : 'Stok bahan tidak cukup, order tidak bisa dilanjutkan.');

            throw new \RuntimeException($message . ' Item: ' . $label . ' (' . $material->name . ').');
        }
    }


    private function resolveOrderItemFinalPricing(array $itemData, array $measurement): array
    {
        $systemSubtotal = round(max((float) ($measurement['subtotal'] ?? 0), 0), 2);

        $unit = strtolower(trim((string) ($itemData['size_unit'] ?? $itemData['unit'] ?? 'lembar')));
        $unit = match ($unit) {
            'm2', 'm²', 'meter2', 'meter persegi', 'per meter persegi' => 'm2',
            'meter', 'm', 'per meter' => 'meter',
            default => $unit !== '' ? $unit : 'lembar',
        };

        $isMeterBased = in_array($unit, ['m2', 'meter'], true);

        $manualRaw = $itemData['manual_price'] ?? null;
        $manualPrice = is_numeric($manualRaw) ? round(max((float) $manualRaw, 0), 2) : 0;
        $hasManualPrice = $manualPrice > 0;

        // Harga manual adalah nilai final yang sengaja diinput user.
        // Jangan dipaksa minimal order agar harga manual tidak berubah diam-diam.
        if ($hasManualPrice) {
            return [
                'manual_price' => $manualPrice,
                'system_subtotal' => $systemSubtotal,
                'minimum_order_applied' => false,
                'subtotal' => $manualPrice,
            ];
        }

        // Minimal Rp10.000 + pembulatan Rp1.000 hanya berlaku untuk unit meter / m².
        // Unit lain seperti pcs, lembar, unit, dan sejenisnya tetap memakai harga asli.
        if (!$isMeterBased) {
            return [
                'manual_price' => null,
                'system_subtotal' => $systemSubtotal,
                'minimum_order_applied' => false,
                'subtotal' => $systemSubtotal,
            ];
        }

        $minimumApplied = $systemSubtotal > 0 && $systemSubtotal < self::MINIMUM_ORDER_AMOUNT;
        $subtotalBeforeRounding = $minimumApplied ? self::MINIMUM_ORDER_AMOUNT : $systemSubtotal;
        $finalSubtotal = $this->roundPriceUp($subtotalBeforeRounding);

        return [
            'manual_price' => null,
            'system_subtotal' => $systemSubtotal,
            'minimum_order_applied' => $minimumApplied,
            'subtotal' => $finalSubtotal,
        ];
    }

    private function roundPriceUp(float $amount): int
    {
        $roundTo = (int) self::PRICE_ROUNDING_AMOUNT;

        if ($amount <= 0) {
            return 0;
        }

        if ($roundTo <= 1) {
            return (int) ceil($amount);
        }

        return (int) (ceil($amount / $roundTo) * $roundTo);
    }


    private function resolveOrderItemMeasurement(array $itemData, ?Material $material = null): array
    {
        $sizeUnit = strtolower(trim((string) ($itemData['size_unit'] ?? $material?->unit ?? 'lembar')));

        if ($sizeUnit === 'm²') {
            $sizeUnit = 'm2';
        }

        if ($sizeUnit === 'pcs') {
            $sizeUnit = 'lembar';
        }

        $sizeDescription = trim((string) ($itemData['size_description'] ?? ''));
        $qty = max((float) ($itemData['qty'] ?? 1), 1);
        $basePrice = max((float) ($itemData['base_price'] ?? 0), 0);
        $manualSizeValue = max((float) ($itemData['size_value'] ?? 1), 0.0001);

        if ($sizeUnit === 'm2') {
            $sizeValue = $this->extractAreaInSquareMeter($sizeDescription, $manualSizeValue);
            $subtotal = $basePrice * $sizeValue * $qty;
            $stockUsage = $sizeValue * $qty;
        } elseif ($sizeUnit === 'meter') {
            $sizeValue = $this->extractLinearMeter($sizeDescription, $manualSizeValue, $material);
            $subtotal = $basePrice * $sizeValue * $qty;
            $stockUsage = $sizeValue * $qty;
        } else {
            $sizeValue = 1;
            $subtotal = $basePrice * $qty;
            $stockUsage = $qty;
        }

        return [
            'size_description' => $sizeDescription !== '' ? $sizeDescription : '1x1',
            'size_unit' => $sizeUnit,
            'size_value' => round($sizeValue, 4),
            'subtotal' => round($subtotal, 2),
            'stock_usage' => round($stockUsage, 4),
        ];
    }

    private function extractAreaInSquareMeter(string $sizeDescription, float $fallback = 1): float
    {
        $dims = $this->parseTwoDimensions($sizeDescription);

        if (!$dims) {
            return round(max($fallback, 0.0001), 4);
        }

        [$a, $b, $unit] = $dims;

        if ($unit === 'cm') {
            return round(($a * $b) / 10000, 4);
        }

        return round($a * $b, 4);
    }

    private function extractLinearMeter(string $sizeDescription, float $fallback = 1, ?Material $material = null): float
    {
        $dims = $this->parseTwoDimensions($sizeDescription);

        if (!$dims) {
            return round(max($fallback, 0.0001), 4);
        }

        [$a, $b, $unit] = $dims;

        $aCm = $unit === 'cm' ? $a : ($a * 100);
        $bCm = $unit === 'cm' ? $b : ($b * 100);

        $rollWidthCm = (float) ($material->roll_width_cm ?? 0);
        $allowRotation = (bool) ($material->allow_rotation ?? true);

        if ($rollWidthCm > 0) {
            $candidates = [];

            if ($aCm <= $rollWidthCm) {
                $candidates[] = $bCm;
            }

            if ($allowRotation && $bCm <= $rollWidthCm) {
                $candidates[] = $aCm;
            }

            if (!empty($candidates)) {
                return round(min($candidates) / 100, 4);
            }

            return round(max($aCm, $bCm) / 100, 4);
        }

        return round(min($aCm, $bCm) / 100, 4);
    }

    private function parseTwoDimensions(string $text): ?array
    {
        $clean = strtolower(trim($text));

        if ($clean === '') {
            return null;
        }

        $clean = preg_replace('/\s+/', '', $clean);
        $clean = str_replace(['×', '*'], 'x', $clean);

        if (!preg_match('/^(\d+(?:[.,]\d+)?)x(\d+(?:[.,]\d+)?)(cm|m)?$/', $clean, $matches)) {
            return null;
        }

        $a = (float) str_replace(',', '.', $matches[1]);
        $b = (float) str_replace(',', '.', $matches[2]);
        $suffix = $matches[3] ?? '';

        if ($suffix === 'cm') {
            return [$a, $b, 'cm'];
        }

        if ($suffix === 'm') {
            return [$a, $b, 'm'];
        }

        $unit = ($a >= 10 && $b >= 10) ? 'cm' : 'm';

        return [$a, $b, $unit];
    }

    private function getSavedOrderItemStockUsage(OrderItem $item): float
    {
        $unit = strtolower((string) ($item->size_unit ?? ''));

        if ($unit === 'm²') {
            $unit = 'm2';
        }

        if ($unit === 'pcs') {
            $unit = 'lembar';
        }

        if (in_array($unit, ['m2', 'meter'], true)) {
            return round((float) $item->size_value * (float) $item->qty, 4);
        }

        return round((float) $item->qty, 4);
    }

    private function reduceMaterialStock(Material $material, float $qty, string $note, Order $order): void
    {
        $qty = round(max($qty, 0), 4);

        if ($qty <= 0) {
            return;
        }

        if (method_exists($material, 'reduceStock')) {
            $material->reduceStock($qty, $note, $order);
            return;
        }

        foreach (['stock', 'stock_qty', 'quantity', 'qty', 'current_stock', 'available_stock'] as $column) {
            if (Schema::hasColumn($material->getTable(), $column)) {
                $material->decrement($column, $qty);
                return;
            }
        }

        throw new \RuntimeException('Method reduceStock() atau kolom stok tidak ditemukan pada model Material.');
    }

    private function restoreMaterialStock(Material $material, float $qty, string $note, Order $order): void
    {
        $qty = round(max($qty, 0), 4);

        if ($qty <= 0) {
            return;
        }

        if (method_exists($material, 'addStock')) {
            $material->addStock($qty, 0, $note, $order);
            return;
        }

        if (method_exists($material, 'increaseStock')) {
            $material->increaseStock($qty, $note, $order);
            return;
        }

        if (method_exists($material, 'returnStock')) {
            $material->returnStock($qty, $note, $order);
            return;
        }

        foreach (['stock', 'stock_qty', 'quantity', 'qty', 'current_stock', 'available_stock'] as $column) {
            if (Schema::hasColumn($material->getTable(), $column)) {
                $material->increment($column, $qty);
                return;
            }
        }

        throw new \RuntimeException('Method tambah stok tidak ditemukan pada model Material. Tambahkan addStock()/increaseStock() atau pastikan kolom stok tersedia.');
    }
}