<?php

namespace App\Services\Manage;

use App\Enums\CuttingStatus;
use App\Enums\RawMaterialUnit;
use App\Models\Cutting;
use App\Models\CuttingMaterial;
use App\Models\CuttingMaterialCombination;
use App\Models\CuttingResult;
use App\Models\Product;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\User;
use App\Services\Concerns\CachesQuery;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
use App\Support\Media\MediaPresenter;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class CuttingService
{
    use CachesQuery, RunsInTransaction;

    public function __construct(
        private readonly PushNotificationService $pushNotificationService,
        private readonly MediaService $mediaService,
    ) {}

    public function paginateForIndex(array $tableQuery, User $user): LengthAwarePaginator
    {
        $query = Cutting::query()
            ->with([
                'media',
                'createdBy.profile',
                'rejection.rejectedBy.profile',
                'materials.rawMaterialPrice.rawMaterial:id,name,unit',
                'materials.rawMaterialPrice.media',
                'materials.combination',
            ])
            ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
                $search = $tableQuery['search'];
                $query->where(function (Builder $query) use ($search): void {
                    $query->where('description', 'like', "%{$search}%")
                        ->orWhereHas('materials.rawMaterialPrice', function (Builder $query) use ($search): void {
                            $query->where('variant', 'like', "%{$search}%")
                                ->orWhereHas('rawMaterial', fn (Builder $query) => $query->where('name', 'like', "%{$search}%"));
                        })
                        ->orWhereHas('results', function (Builder $query) use ($search): void {
                            $query->where('product_name', 'like', "%{$search}%");
                        });
                });
            });

        $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']);

        return $query
            ->paginate(25)
            ->withQueryString()
            ->through(function (Cutting $cutting) use ($user) {
                $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
                $cutting->setAttribute('is_editable', $cutting->status->isEditable());
                $this->appendCostPreview($cutting);
                $this->appendImages($cutting);
                $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));

                return $cutting;
            });
    }

    public function getInProgressCuttings(User $user): Collection
    {
        return Cutting::query()
            ->with([
                'media',
                'createdBy.profile',
                'rejection.rejectedBy.profile',
                'materials.rawMaterialPrice.rawMaterial:id,name,unit',
                'materials.rawMaterialPrice.media',
                'materials.combination',
            ])
            ->inProgress()
            ->latest()
            ->get()
            ->each(function (Cutting $cutting) use ($user): void {
                $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
                $cutting->setAttribute('is_editable', $cutting->status->isEditable());
                $this->appendCostPreview($cutting);
                $this->appendImages($cutting);
                $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
            });
    }

    public function getCompletedCuttings(User $user): Collection
    {
        return Cutting::query()
            ->with([
                'media',
                'createdBy.profile',
                'rejection.rejectedBy.profile',
                'materials.rawMaterialPrice.rawMaterial:id,name,unit',
                'materials.rawMaterialPrice.media',
                'materials.combination',
            ])
            ->completed()
            ->latest()
            ->get()
            ->each(function (Cutting $cutting) use ($user): void {
                $cutting->setAttribute('available_actions', $this->filterActionsForUser($cutting, $user));
                $cutting->setAttribute('is_editable', $cutting->status->isEditable());
                $this->appendCostPreview($cutting);
                $this->appendImages($cutting);
                $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));
            });
    }

    public function rawMaterialCatalog(?Cutting $cutting = null, ?User $user = null): Collection
    {
        $selectedPriceIds = $cutting
            ? $cutting->materials()->pluck('raw_material_price_id')->all()
            : ($user ? $this->draftMaterialsQuery($user)->pluck('raw_material_price_id')->all() : []);

        return RawMaterial::query()
            ->with([
                'prices' => fn ($query) => $query
                    ->orderBy('created_at')
                    ->with('media')
                    ->where(fn ($q) => $q->where('stock', '>', 0)->orWhereIn('id', $selectedPriceIds)),
            ])
            ->where(function (Builder $query) use ($selectedPriceIds): void {
                $query->active();

                if ($selectedPriceIds !== []) {
                    $query->orWhereHas(
                        'prices',
                        fn (Builder $query) => $query->whereIn('id', $selectedPriceIds),
                    );
                }
            })
            ->orderBy('name')
            ->get()
            ->each(function (RawMaterial $rawMaterial): void {
                $rawMaterial->prices->each(function (RawMaterialPrice $price) use ($rawMaterial): void {
                    $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
                    $price->setAttribute('unit_abbreviation', $rawMaterial->unit->abbreviation());
                    $price->unsetRelation('rawMaterial');
                });
            });
    }

    public function findForEdit(Cutting $cutting): Cutting
    {
        $cutting->load([
            'media',
            'materials.rawMaterialPrice.rawMaterial:id,name,unit',
            'materials.rawMaterialPrice.media',
            'materials.combination',
            'rejection.rejectedBy.profile',
        ]);

        $this->appendImages($cutting);
        $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));

        return $cutting;
    }

    public function findForShare(Cutting $cutting): Cutting
    {
        $cutting->load([
            'media',
            'createdBy.profile',
            'materials.rawMaterialPrice.rawMaterial:id,name,unit',
            'materials.rawMaterialPrice.media',
            'materials.combination',
        ]);

        $this->appendCostPreview($cutting);
        $this->appendImages($cutting);
        $cutting->materials->each(fn (CuttingMaterial $m) => $this->breakMaterialCircularReference($m));

        return $cutting;
    }

    private function appendImages(Cutting $cutting): void
    {
        $cutting->setAttribute('images', MediaPresenter::collection($cutting, 'images'));

        $cutting->materials->each(function (CuttingMaterial $material): void {
            $price = $material->rawMaterialPrice;

            if ($price) {
                $price->setAttribute('images', MediaPresenter::collection($price, 'images'));
            }
        });

        $cutting->results->each(function (CuttingResult $result): void {
            $result->setAttribute('product_name', $result->product_name ?? '');
        });
    }

    public function draftMaterialsForUser(User $user): array
    {
        return $this->draftMaterialsQuery($user)
            ->with([
                'rawMaterialPrice.rawMaterial:id,name,unit',
                'rawMaterialPrice.media',
                'combination',
            ])
            ->get()
            ->map(function (CuttingMaterial $item) {
                $result = $this->presentDraftMaterial($item);
                $this->breakMaterialCircularReference($item);

                return $result;
            })
            ->values()
            ->all();
    }

    public function draftResultsForUser(User $user): array
    {
        return $this->draftResultsQuery($user)
            ->get()
            ->map(fn (CuttingResult $item) => $this->presentDraftResult($item))
            ->values()
            ->all();
    }

    public function syncDraftMaterial(array $validated, User $user): array
    {
        $price = RawMaterialPrice::query()
            ->with('rawMaterial')
            ->findOrFail($validated['raw_material_price_id']);

        $materialUsage = round((float) $validated['material_usage'], 2);

        if ($materialUsage <= 0) {
            throw ValidationException::withMessages([
                'material_usage' => 'Pemakaian bahan harus lebih dari 0.',
            ]);
        }

        $materialResult = array_key_exists('material_result', $validated) && $validated['material_result'] !== null
            ? (int) $validated['material_result']
            : null;

        $item = CuttingMaterial::query()->updateOrCreate(
            [
                'user_id' => $user->id,
                'raw_material_price_id' => $price->id,
                'cutting_id' => null,
            ],
            [
                'material_usage' => $materialUsage,
                'material_result' => $materialResult,
            ],
        );

        $item->load([
            'rawMaterialPrice.rawMaterial:id,name,unit',
            'rawMaterialPrice.media',
        ]);

        $result = $this->presentDraftMaterial($item);
        $this->breakMaterialCircularReference($item);

        return $result;
    }

    public function syncDraftResult(array $validated, User $user): array
    {
        $productName = $validated['product_name'] ?? null;

        $cuttingResult = array_key_exists('cutting_result', $validated) && $validated['cutting_result'] !== null
            ? (int) $validated['cutting_result']
            : null;
        $sample = array_key_exists('sample', $validated) && $validated['sample'] !== null
            ? (int) $validated['sample']
            : null;
        $originalOutsideSample = array_key_exists('original_outside_sample', $validated) && $validated['original_outside_sample'] !== null
            ? (int) $validated['original_outside_sample']
            : null;

        if ($cuttingResult !== null) {
            if ($cuttingResult < 1) {
                throw ValidationException::withMessages([
                    'cutting_result' => 'Hasil cutting minimal 1 pcs.',
                ]);
            }

            if ($sample !== null && $sample < 0) {
                throw ValidationException::withMessages([
                    'sample' => 'sample tidak boleh negatif.',
                ]);
            }

            if ($originalOutsideSample !== null && $originalOutsideSample < 0) {
                throw ValidationException::withMessages([
                    'original_outside_sample' => 'Hasil cutting diluar sample tidak boleh negatif.',
                ]);
            }

            if ($sample !== null && $originalOutsideSample !== null && ($sample + $originalOutsideSample) !== $cuttingResult) {
                throw ValidationException::withMessages([
                    'cutting_result' => 'Hasil cutting harus sama dengan sample ditambah hasil cutting diluar sample.',
                ]);
            }
        }

        $item = CuttingResult::query()->create([
            'user_id' => $user->id,
            'cutting_id' => null,
            'product_name' => $productName,
            'cutting_result' => $cuttingResult,
            'sample' => $sample,
            'original_outside_sample' => $originalOutsideSample,
        ]);

        return $this->presentDraftResult($item);
    }

    public function removeDraftMaterial(User $user, RawMaterialPrice $rawMaterialPrice): void
    {
        CuttingMaterial::query()
            ->whereNull('cutting_id')
            ->where('user_id', $user->id)
            ->where('raw_material_price_id', $rawMaterialPrice->id)
            ->delete();
    }

    public function removeDraftResult(User $user, CuttingResult $cuttingResult): void
    {
        if ($cuttingResult->cutting_id !== null || $cuttingResult->user_id !== $user->id) {
            return;
        }

        $cuttingResult->delete();
    }

    /**
     * @return list<array>
     */
    public function syncDraftCombination(array $validated, User $user): array
    {
        $materialResult = array_key_exists('material_result', $validated) && $validated['material_result'] !== null
            ? (int) $validated['material_result']
            : null;

        $combination = CuttingMaterialCombination::create([
            'user_id' => $user->id,
            'cutting_id' => null,
            'material_result' => $materialResult,
        ]);

        $items = [];

        foreach ($validated['materials'] as $index => $materialData) {
            $price = RawMaterialPrice::query()
                ->with('rawMaterial')
                ->findOrFail($materialData['raw_material_price_id']);

            $materialUsage = round((float) $materialData['material_usage'], 2);

            if ($materialUsage <= 0) {
                throw ValidationException::withMessages([
                    "materials.{$index}.material_usage" => 'Pemakaian bahan harus lebih dari 0.',
                ]);
            }

            $item = CuttingMaterial::create([
                'user_id' => $user->id,
                'cutting_id' => null,
                'raw_material_price_id' => $price->id,
                'combination_id' => $combination->id,
                'material_usage' => $materialUsage,
            ]);

            $item->load([
                'rawMaterialPrice.rawMaterial:id,name,unit',
                'rawMaterialPrice.media',
            ]);

            $items[] = $this->presentDraftMaterial($item);
            $this->breakMaterialCircularReference($item);
        }

        return $items;
    }

    public function removeDraftCombination(User $user, int $combinationId): void
    {
        $combination = CuttingMaterialCombination::query()
            ->whereNull('cutting_id')
            ->where('user_id', $user->id)
            ->where('id', $combinationId)
            ->first();

        if ($combination === null) {
            return;
        }

        // Hard delete associated materials first (cascadeOnDelete only works with hard delete)
        CuttingMaterial::query()
            ->where('combination_id', $combination->id)
            ->forceDelete();

        $combination->forceDelete();
    }

    public function create(array $validated, User $user): Cutting
    {
        $cutting = $this->runInTransaction(
            function () use ($validated, $user): Cutting {

                $draftMaterials = $this->draftMaterialsQuery($user)
                    ->with('rawMaterialPrice.rawMaterial')
                    ->lockForUpdate()
                    ->get();

                if ($draftMaterials->isEmpty()) {
                    throw ValidationException::withMessages([
                        'materials' => 'Tambahkan minimal satu bahan baku.',
                    ]);
                }

                $cutting = Cutting::create([
                    'status' => CuttingStatus::IN_PROGRESS,
                    'description' => $validated['description'] ?? null,
                    'sewing_cost' => (int) ($validated['sewing_cost'] ?? 0),
                    'other_cost' => (int) ($validated['other_cost'] ?? 0),
                    'created_by_id' => $user->id,
                ]);

                $this->syncImages($cutting, $validated);

                // Transfer draft combinations to cutting
                $draftCombinations = CuttingMaterialCombination::query()
                    ->whereNull('cutting_id')
                    ->where('user_id', $user->id)
                    ->lockForUpdate()
                    ->get();

                foreach ($draftCombinations as $combination) {
                    $combination->cutting_id = $cutting->id;
                    $combination->user_id = null;
                    $combination->save();
                }

                foreach ($draftMaterials as $material) {
                    $material->cutting_id = $cutting->id;
                    $material->user_id = null;
                    $material->save();
                }

                $results = $this->buildResults($validated['results']);
                foreach ($results as $resultData) {
                    $cutting->results()->create($resultData);
                }

                $cutting->load('materials.rawMaterialPrice.rawMaterial');
                $this->deductMaterialStock($cutting);

                return $cutting;
            },
            'Gagal membuat proses cutting',
        );

        $this->pushNotificationService->sendToRoles(
            '✂️ Proses Cutting Baru',
            "Proses cutting dengan deskripsi ({$cutting->description}) telah dimulai oleh {$user->profile?->full_name}.",
            ['owner', 'developer', 'direktur'],
            route('admin.manage.cuttings.index'),
        );

        $this->cacheForgetByPattern('manage:cuttings:*');

        return $cutting;
    }

    public function update(Cutting $cutting, array $validated, User $user): void
    {
        if (! $cutting->status->isEditable()) {
            throw ValidationException::withMessages([
                'status' => 'Proses cutting tidak dapat diubah.',
            ]);
        }

        $this->runInTransaction(
            function () use ($cutting, $validated): void {
                $cutting->load(['materials.rawMaterialPrice.rawMaterial', 'results', 'materials.combination']);

                if ($cutting->status === CuttingStatus::IN_PROGRESS) {
                    $this->reverseTotalMaterialStock($cutting);
                }

                $cutting->materials()->delete();
                $cutting->results()->delete();
                $cutting->combinations()->delete();

                $materials = $this->buildMaterials($validated['materials']);
                $results = $this->buildResults($validated['results']);

                $cutting->description = $validated['description'] ?? null;
                $cutting->sewing_cost = (int) ($validated['sewing_cost'] ?? 0);
                $cutting->other_cost = (int) ($validated['other_cost'] ?? 0);
                $cutting->save();

                $this->syncImages($cutting, $validated);

                // Group materials by combination_id to create combinations
                $combinationGroups = [];
                foreach ($materials as $materialData) {
                    $combinationId = $materialData['combination_id'];
                    if ($combinationId !== null) {
                        if (! isset($combinationGroups[$combinationId])) {
                            $combinationGroups[$combinationId] = [
                                'materials' => [],
                                'material_result' => $materialData['combination_material_result'] ?? null,
                            ];
                        }
                        $combinationGroups[$combinationId]['materials'][] = $materialData;
                    }
                }

                // Create combinations and update combination_id for materials
                $combinationIdMap = [];
                foreach ($combinationGroups as $oldCombinationId => $group) {
                    $combination = $cutting->combinations()->create([
                        'material_result' => $group['material_result'],
                    ]);
                    $combinationIdMap[$oldCombinationId] = $combination->id;
                }

                foreach ($materials as $materialData) {
                    $newCombinationId = $materialData['combination_id'] !== null
                        ? $combinationIdMap[$materialData['combination_id']] ?? null
                        : null;

                    $cutting->materials()->create([
                        'raw_material_price_id' => $materialData['raw_material_price_id'],
                        'material_usage' => $materialData['material_usage'],
                        'material_result' => $materialData['material_result'],
                        'combination_id' => $newCombinationId,
                    ]);
                }

                foreach ($results as $resultData) {
                    $cutting->results()->create($resultData);
                }

                if ($cutting->status === CuttingStatus::IN_PROGRESS) {
                    $cutting->load('materials.rawMaterialPrice.rawMaterial');
                    $this->deductMaterialStock($cutting);
                }
            },
            'Gagal memperbarui proses cutting',
        );

        $description = $cutting->description ?? '-';
        $this->pushNotificationService->sendToRoles(
            '✏️ Proses Cutting Diperbarui',
            "Proses cutting dengan deskripsi '{$description}' telah diperbarui oleh {$user->profile?->full_name}.",
            ['owner', 'developer', 'direktur'],
            route('admin.manage.cuttings.index'),
        );

        $this->cacheForgetByPattern('manage:cuttings:*');
    }

    public function delete(Cutting $cutting, User $user): void
    {
        if ($cutting->status !== CuttingStatus::IN_PROGRESS) {
            throw ValidationException::withMessages([
                'status' => 'Proses cutting hanya dapat dihapus saat masih proses.',
            ]);
        }

        $description = $cutting->description ?? '-';

        $this->runInTransaction(
            function () use ($cutting): void {
                $cutting->load(['materials.rawMaterialPrice.rawMaterial']);

                $this->reverseTotalMaterialStock($cutting);

                $cutting->materials()->delete();
                $cutting->results()->delete();
                $cutting->delete();
            },
            'Gagal menghapus proses cutting',
        );

        $this->pushNotificationService->sendToRoles(
            '🗑️ Proses Cutting Dihapus',
            "Proses cutting dengan deskripsi {$description} telah dihapus oleh {$user->profile?->full_name}.",
            ['owner', 'developer', 'direktur'],
            route('admin.manage.cuttings.index'),
        );

        $this->cacheForgetByPattern('manage:cuttings:*');
    }

    public function transitionStatus(
        Cutting $cutting,
        CuttingStatus $status,
        User $user,
        ?string $reason = null,
    ): void {
        if (! $cutting->status->canTransitionTo($status)) {
            throw ValidationException::withMessages([
                'status' => 'Status proses cutting tidak dapat diubah.',
            ]);
        }

        $this->runInTransaction(
            function () use ($cutting, $status): void {
                $cutting->load(['materials.rawMaterialPrice', 'results']);

                if ($status === CuttingStatus::COMPLETED) {
                    $cutting->total_material_cost = $this->calculateTotalMaterialCost($cutting);
                    $cutting->cost_per_unit = $this->calculateCostPerUnit($cutting);
                }

                $cutting->status = $status;
                $cutting->save();
            },
            'Gagal mengubah status proses cutting',
        );

        $description = $cutting->description ?? '-';
        $message = match ($status) {
            CuttingStatus::COMPLETED => "Proses cutting dengan deskripsi '{$description}' telah selesai oleh {$user->profile?->full_name}.",
            CuttingStatus::IN_PROGRESS => "Proses cutting dengan deskripsi '{$description}' dikembalikan ke proses oleh {$user->profile?->full_name}.",
            default => "Status proses cutting dengan deskripsi '{$description}' telah diperbarui ke: {$status->label()} oleh {$user->profile?->full_name}.",
        };

        $title = match ($status) {
            CuttingStatus::COMPLETED => '✂️ Proses Cutting Selesai',
            CuttingStatus::IN_PROGRESS => '✂️ Proses Cutting Dikembalikan',
            default => '✂️ Proses Cutting Diperbarui',
        };

        $this->pushNotificationService->sendToRoles(
            $title,
            $message,
            ['owner', 'developer', 'direktur'],
            route('admin.manage.cuttings.index'),
        );

        $this->cacheForgetByPattern('manage:cuttings:*');
        $this->cacheForgetByPattern('prices:*');
    }

    private function buildMaterials(array $materials): array
    {
        return collect($materials)
            ->map(function (array $itemData, int $index) {
                $price = RawMaterialPrice::query()
                    ->with('rawMaterial')
                    ->find($itemData['raw_material_price_id']);

                if ($price === null) {
                    throw ValidationException::withMessages([
                        "materials.{$index}.raw_material_price_id" => 'Bahan baku tidak ditemukan.',
                    ]);
                }

                $materialUsage = round((float) $itemData['material_usage'], 2);

                if ($materialUsage <= 0) {
                    throw ValidationException::withMessages([
                        "materials.{$index}.material_usage" => 'Pemakaian bahan harus lebih dari 0.',
                    ]);
                }

                $materialResult = array_key_exists('material_result', $itemData) && $itemData['material_result'] !== null
                    ? (int) $itemData['material_result']
                    : null;

                return [
                    'raw_material_price_id' => $price->id,
                    'material_usage' => $materialUsage,
                    'material_result' => $materialResult,
                    'combination_id' => $itemData['combination_id'] ?? null,
                    'combination_material_result' => $itemData['combination_material_result'] ?? null,
                ];
            })
            ->all();
    }

    private function buildResults(array $results): array
    {
        return collect($results)
            ->map(function (array $itemData, int $index) {
                $productName = $itemData['product_name'] ?? null;

                $cuttingResult = (int) $itemData['cutting_result'];
                $sample = (int) $itemData['sample'];
                $originalOutsideSample = (int) ($itemData['original_outside_sample'] ?? 0);

                if ($cuttingResult < 1) {
                    throw ValidationException::withMessages([
                        "results.{$index}.cutting_result" => 'Hasil cutting minimal 1 pcs.',
                    ]);
                }

                if ($sample < 0) {
                    throw ValidationException::withMessages([
                        "results.{$index}.sample" => 'sample tidak boleh negatif.',
                    ]);
                }

                if ($originalOutsideSample < 0) {
                    throw ValidationException::withMessages([
                        "results.{$index}.original_outside_sample" => 'Hasil cutting diluar sample tidak boleh negatif.',
                    ]);
                }

                if (($sample + $originalOutsideSample) !== $cuttingResult) {
                    throw ValidationException::withMessages([
                        "results.{$index}.cutting_result" => 'Hasil cutting harus sama dengan sample ditambah hasil cutting diluar sample.',
                    ]);
                }

                return [
                    'product_name' => $productName,
                    'cutting_result' => $cuttingResult,
                    'sample' => $sample,
                    'original_outside_sample' => $originalOutsideSample,
                ];
            })
            ->all();
    }

    private function deductMaterialStock(Cutting $cutting): void
    {
        foreach ($cutting->materials as $material) {
            $totalTaken = (float) $material->material_usage;
            $price = RawMaterialPrice::query()->lockForUpdate()->find($material->raw_material_price_id);

            if ($price === null) {
                throw ValidationException::withMessages([
                    'materials' => 'Bahan baku tidak ditemukan.',
                ]);
            }

            if ((float) $price->stock < $totalTaken) {
                throw ValidationException::withMessages([
                    'materials' => "Stok {$price->rawMaterial?->name} ({$price->variant}) tidak mencukupi.",
                ]);
            }

            $price->decrement('stock', $totalTaken);
        }
    }

    private function reverseTotalMaterialStock(Cutting $cutting): void
    {
        foreach ($cutting->materials as $material) {
            RawMaterialPrice::query()
                ->whereKey($material->raw_material_price_id)
                ->increment('stock', (float) $material->material_usage);
        }
    }

    private function breakMaterialCircularReference(CuttingMaterial $material): void
    {
        $price = $material->rawMaterialPrice;

        // Always set these attributes regardless of price
        $material->setAttribute('material_result', $material->material_result);
        $material->setAttribute('material_result_input', $material->material_result);
        $material->setAttribute('combination_id', $material->combination_id);
        $material->setAttribute('combination_material_result', $material->combination?->material_result);

        if ($price) {
            $rawMaterial = $price->rawMaterial;

            $material->setAttribute('variant', $price->variant);
            $material->setAttribute('stock_input', $price->stock_input);
            $material->setAttribute('images', $price->getAttribute('images') ?? []);

            if ($rawMaterial) {
                $unitAbbreviation = $rawMaterial->unit->abbreviation();
                $price->setAttribute('unit_abbreviation', $unitAbbreviation);
                $material->setAttribute('unit_abbreviation', $unitAbbreviation);
                $material->setAttribute('unit', $rawMaterial->unit->value);
                $material->setAttribute('raw_material_id', $rawMaterial->id);
                $material->setAttribute('raw_material_name', $rawMaterial->name);
                $material->setAttribute('raw_material_unit_label', $rawMaterial->unit->label());
            }

            $price->unsetRelation('rawMaterial');
        }

        $material->unsetRelation('rawMaterialPrice');
        $material->unsetRelation('combination');
    }

    private function applySorting(Builder $query, string $sort, string $direction): void
    {
        if (in_array($sort, ['created_at', 'status'], true)) {
            $query->orderBy($sort, $direction);

            return;
        }

        $query->latest();
    }

    private function draftMaterialsQuery(User $user): Builder
    {
        return CuttingMaterial::query()
            ->whereNull('cutting_id')
            ->where('user_id', $user->id);
    }

    private function draftResultsQuery(User $user): Builder
    {
        return CuttingResult::query()
            ->whereNull('cutting_id')
            ->where('user_id', $user->id);
    }

    private function presentDraftMaterial(CuttingMaterial $item): array
    {
        $price = $item->rawMaterialPrice;
        $rawMaterial = $price?->rawMaterial;
        $combination = $item->combination;

        return [
            'raw_material_price_id' => $item->raw_material_price_id,
            'raw_material_name' => $rawMaterial?->name ?? '',
            'variant' => $price?->variant ?? '',
            'unit' => $rawMaterial?->unit?->value ?? '',
            'unit_abbreviation' => $rawMaterial?->unit?->abbreviation() ?? '',
            'stock_input' => $price?->stock_input ?? '',
            'material_usage' => $this->formatQuantityInput((float) $item->material_usage),
            'material_result' => $item->material_result !== null ? (int) $item->material_result : null,
            'images' => $price ? MediaPresenter::collection($price, 'images') : [],
            'combination_id' => $item->combination_id,
            'combination_material_result' => $combination?->material_result,
        ];
    }

    private function presentDraftResult(CuttingResult $item): array
    {
        return [
            'product_name' => $item->product_name ?? '',
            'cutting_result' => $item->cutting_result !== null ? (string) $item->cutting_result : null,
            'sample' => $item->sample !== null ? (string) $item->sample : null,
            'original_outside_sample' => $item->original_outside_sample !== null ? (string) $item->original_outside_sample : null,
        ];
    }

    private function formatQuantityInput(float $value): string
    {
        return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
    }

    private function formatMaterialUsageSummary(Cutting $cutting): string
    {
        $groups = [];

        foreach ($cutting->materials as $material) {
            $unit = $material->rawMaterialPrice?->rawMaterial?->unit?->abbreviation() ?? '';
            $groups[$unit] = ($groups[$unit] ?? 0) + (float) $material->material_usage;
        }

        return collect($groups)
            ->filter(fn (float $total) => $total > 0)
            ->map(function (float $total, string $unit) {
                $formatted = rtrim(rtrim(number_format($total, 2, ',', '.'), '0'), ',');

                return "{$formatted} {$unit}";
            })
            ->join(', ');
    }

    /**
     * @return list<array{status: string, label: string, destructive: bool, permission: string, icon_only: bool}>
     */
    private function filterActionsForUser(Cutting $cutting, User $user): array
    {
        return collect($cutting->status->availableActions())
            ->filter(fn (array $action) => $user->can($action['permission']))
            ->values()
            ->all();
    }

    private function appendCostPreview(Cutting $cutting): void
    {
        $totalResultPieces = (int) $cutting->results->sum('cutting_result');
        $cutting->setAttribute('total_result_pieces', $totalResultPieces);
        $cutting->setAttribute('total_material_usage', (float) $cutting->materials->sum(fn (CuttingMaterial $material) => (float) $material->material_usage));
        $cutting->setAttribute('total_material_usage_summary_formatted', $this->formatMaterialUsageSummary($cutting));

        $totalMaterialCost = $cutting->total_material_cost ?? $this->calculateTotalMaterialCost($cutting);
        $sewingCost = (int) ($cutting->sewing_cost ?? 0);
        $otherCost = (int) ($cutting->other_cost ?? 0);
        $totalProductionCost = $totalMaterialCost + $sewingCost + $otherCost;
        $costPerUnit = $cutting->cost_per_unit ?? $this->calculateCostPerUnit($cutting);
        $materialCostPerProduct = $totalResultPieces > 0 ? (int) round($totalMaterialCost / $totalResultPieces) : 0;

        $cutting->setAttribute('total_material_cost', $totalMaterialCost);
        $cutting->setAttribute('total_material_cost_formatted', 'Rp '.number_format($totalMaterialCost, 0, ',', '.'));
        $cutting->setAttribute('material_cost_per_product', $materialCostPerProduct);
        $cutting->setAttribute('material_cost_per_product_formatted', 'Rp '.number_format($materialCostPerProduct, 0, ',', '.'));
        $cutting->setAttribute('sewing_cost', $sewingCost);
        $cutting->setAttribute('sewing_cost_formatted', 'Rp '.number_format($sewingCost, 0, ',', '.'));
        $cutting->setAttribute('other_cost', $otherCost);
        $cutting->setAttribute('other_cost_formatted', 'Rp '.number_format($otherCost, 0, ',', '.'));
        $cutting->setAttribute('total_production_cost', $totalProductionCost);
        $cutting->setAttribute('total_production_cost_formatted', 'Rp '.number_format($totalProductionCost, 0, ',', '.'));
        $cutting->setAttribute('estimated_cost_per_unit', $costPerUnit);
        $cutting->setAttribute('estimated_cost_per_unit_formatted', 'Rp '.number_format($costPerUnit, 0, ',', '.'));
    }

    public function calculateTotalMaterialCost(Cutting $cutting): int
    {
        return (int) $cutting->materials->sum(fn (CuttingMaterial $material) => $material->materialCost());
    }

    public function calculateTotalProductionCost(Cutting $cutting): int
    {
        return $this->calculateTotalMaterialCost($cutting)
            + (int) ($cutting->sewing_cost ?? 0)
            + (int) ($cutting->other_cost ?? 0);
    }

    public function calculateCostPerUnit(Cutting $cutting): int
    {
        $totalPieces = (int) $cutting->results->sum('cutting_result');

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

        return (int) round($this->calculateTotalProductionCost($cutting) / $totalPieces);
    }

    /**
     * Quick-create a product with variants and prices, bypassing owner verification.
     */
    public function quickCreateProduct(array $validated): array
    {
        $product = Product::create([
            'name' => $validated['name'],
            'description' => $validated['description'] ?? null,
            'is_active' => true,
        ]);

        if (! empty($validated['category_ids'])) {
            $product->categories()->sync($validated['category_ids']);
        }

        $createdVariants = [];
        $maxVariantImages = 5;

        foreach ($validated['variants'] as $index => $variantData) {
            $variant = $product->variants()->create([
                'name' => $variantData['name'],
                'stock' => $variantData['stock'] ?? 0,
                'retail_stock' => $variantData['retail_stock'] ?? 0,
            ]);

            if (! empty($variantData['prices'])) {
                foreach ($variantData['prices'] as $type => $priceValue) {
                    $variant->prices()->create([
                        'type' => $type,
                        'price' => (int) $priceValue,
                    ]);
                }
            }

            $this->mediaService->syncCollection(
                $variant,
                'images',
                $variantData['images'] ?? null,
                null,
                $maxVariantImages,
                required: false,
                errorKey: "variants.{$index}.s3_keys",
                s3Keys: $variantData['s3_keys'] ?? null,
            );

            $variant->refresh();

            $createdVariants[] = [
                'id' => $variant->id,
                'name' => $variant->name,
                'stock' => $variant->stock,
                'images' => MediaPresenter::collection($variant, 'images'),
            ];
        }

        $this->cacheForgetByPattern('manage:cuttings:*');

        return [
            'id' => $product->id,
            'name' => $product->name,
            'is_active' => true,
            'variants' => $createdVariants,
        ];
    }

    private function formatStockForUnit(float $stock, RawMaterialUnit $unit): string
    {
        $formatted = rtrim(rtrim(number_format($stock, 2, ',', '.'), '0'), ',');

        return "{$formatted} {$unit->abbreviation()}";
    }

    private function syncImages(Cutting $cutting, array $validated): void
    {
        $this->mediaService->syncCollection(
            $cutting,
            'images',
            $validated['images'] ?? null,
            $validated['remove_media_ids'] ?? null,
            10,
            required: false,
            errorKey: 'images',
            s3Keys: $validated['s3_keys'] ?? null,
        );
    }
}
