<?php

namespace App\Services\Master;

use App\Enums\CuttingStatus;
use App\Enums\OwnerVerificationAction;
use App\Enums\RawMaterialUnit;
use App\Models\CuttingMaterial;
use App\Models\OwnerVerificationRequest;
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\Manage\StockMutationService;
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\Validation\ValidationException;

class RawMaterialService
{
    use CachesQuery, RunsInTransaction;

    private const MAX_VARIANT_IMAGES = 5;

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

    public function paginateForIndex(array $tableQuery, string $isActive, string $stockStatus = '', string $rawMaterialId = ''): LengthAwarePaginator
    {
        $query = RawMaterial::query()
            ->with([
                'prices' => fn ($query) => $query
                    ->orderBy('created_at')
                    ->with('media')
                    ->when($tableQuery['search'] !== '', function ($query) use ($tableQuery): void {
                        $query->where('variant', 'like', "%{$tableQuery['search']}%");
                    })
                    ->when($stockStatus === 'out_of_stock', fn ($q) => $q->where('stock', '<=', 0))
                    ->when($stockStatus === 'low_stock', function ($q): void {
                        $q->where('stock', '>', 0)
                            ->where(function ($q): void {
                                $q->orWhere(function ($q): void {
                                    $q->whereHas('rawMaterial', fn ($q) => $q->where('unit', RawMaterialUnit::YARD->value))
                                        ->where('stock', '<', 20);
                                })
                                    ->orWhere(function ($q): void {
                                        $q->whereHas('rawMaterial', fn ($q) => $q->where('unit', RawMaterialUnit::METER->value))
                                            ->where('stock', '<', 10);
                                    })
                                    ->orWhere(function ($q): void {
                                        $q->whereHas('rawMaterial', fn ($q) => $q->where('unit', RawMaterialUnit::KILOGRAM->value))
                                            ->where('stock', '<', 5);
                                    });
                            });
                    }),
            ])
            ->when(($tableQuery['search_id'] ?? '') !== '', function (Builder $query) use ($tableQuery): void {
                $query->where('raw_materials.id', $tableQuery['search_id']);
            })
            ->when(($tableQuery['search_id'] ?? '') === '' && $tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void {
                $search = $tableQuery['search'];
                $query->whereHas('prices', fn (Builder $query) => $query->where('variant', 'like', "%{$search}%"));
            })
            ->when($rawMaterialId !== '', fn (Builder $query) => $query->where('id', $rawMaterialId))
            ->when($isActive !== '', fn (Builder $query) => $query->where('is_active', $isActive === '1'))
            ->when($stockStatus === 'out_of_stock', function (Builder $query): void {
                $query->whereHas('prices', fn (Builder $priceQuery) => $priceQuery->where('stock', '<=', 0));
            })
            ->when($stockStatus === 'low_stock', function (Builder $query): void {
                $query->where(function (Builder $query): void {
                    $query->where('unit', RawMaterialUnit::YARD->value)
                        ->whereHas('prices', fn (Builder $q) => $q->where('stock', '>', 0)->where('stock', '<', 20));
                })->orWhere(function (Builder $query): void {
                    $query->where('unit', RawMaterialUnit::METER->value)
                        ->whereHas('prices', fn (Builder $q) => $q->where('stock', '>', 0)->where('stock', '<', 10));
                })->orWhere(function (Builder $query): void {
                    $query->where('unit', RawMaterialUnit::KILOGRAM->value)
                        ->whereHas('prices', fn (Builder $q) => $q->where('stock', '>', 0)->where('stock', '<', 5));
                });
            });

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

        return $query
            ->paginate(25)
            ->withQueryString()
            ->through(function (RawMaterial $rawMaterial) {
                $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');
                });

                return $rawMaterial;
            });
    }

    public function findForEdit(RawMaterial $rawMaterial): RawMaterial
    {
        $rawMaterial->load([
            'prices' => fn ($query) => $query->orderBy('created_at')->with('media'),
        ]);

        $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');
        });

        return $rawMaterial;
    }

    public function create(array $validated, User $user): void
    {
        $rawMaterial = $this->runInTransaction(
            function () use ($validated): RawMaterial {
                $rawMaterial = RawMaterial::create([
                    'name' => $validated['name'],
                    'unit' => $validated['unit'],
                    'is_active' => true,
                ]);

                foreach ($validated['prices'] as $index => $priceData) {
                    $this->createPrice($rawMaterial, $priceData, $index);
                }

                return $rawMaterial;
            },
            'Gagal membuat bahan baku',
        );

        $this->cacheForgetByPattern('master:raw_materials:*');

        $this->notifyOwner(
            'Tambah Bahan Baku',
            "Bahan baku '{$validated['name']}' telah ditambahkan oleh {$user->profile?->full_name}.",
            route('admin.master.raw_materials.index', ['search_id' => $rawMaterial->id]),
        );
    }

    public function update(RawMaterial $rawMaterial, array $validated, User $user): void
    {
        $this->runInTransaction(
            function () use ($validated, $rawMaterial): void {
                $payload = $this->enrichPayload($this->buildPayloadFromValidated($validated));
                $this->applyPayloadToRawMaterial($rawMaterial, $payload, originalPrices: $validated['prices'] ?? []);
            },
            'Gagal memperbarui bahan baku',
        );

        $this->cacheForgetByPattern('master:raw_materials:*');

        $this->notifyOwner(
            'Ubah Bahan Baku',
            "Bahan baku '{$rawMaterial->name}' telah diperbarui oleh {$user->profile?->full_name}.",
            route('admin.master.raw_materials.index', ['search_id' => $rawMaterial->id]),
        );
    }

    public function delete(RawMaterial $rawMaterial, User $user): void
    {
        $name = $rawMaterial->name;

        $this->applyDeleteSubject($rawMaterial);
        $this->cacheForgetByPattern('master:raw_materials:*');

        $this->notifyOwner(
            'Hapus Bahan Baku',
            "Bahan baku '{$name}' telah dihapus oleh {$user->profile?->full_name}.",
            route('admin.master.raw_materials.index'),
        );
    }

    public function toggleStatus(RawMaterial $rawMaterial, array $validated, User $user): void
    {
        $isActive = $validated['status'] === 'active';

        $rawMaterial->update([
            'is_active' => $isActive,
        ]);

        $this->cacheForgetByPattern('master:raw_materials:*');

        $statusLabel = $isActive ? 'aktif' : 'nonaktif';

        $this->notifyOwner(
            'Ubah Status Bahan Baku',
            "Status bahan baku '{$rawMaterial->name}' telah diubah menjadi {$statusLabel} oleh {$user->profile?->full_name}.",
            route('admin.master.raw_materials.index', ['search_id' => $rawMaterial->id]),
        );
    }

    public function applyVerificationRequest(OwnerVerificationRequest $verificationRequest): void
    {
        match ($verificationRequest->action) {
            OwnerVerificationAction::CREATE => $this->applyCreate($verificationRequest),
            OwnerVerificationAction::UPDATE => $this->applyUpdate($verificationRequest),
            OwnerVerificationAction::DELETE => $this->applyDelete($verificationRequest),
            OwnerVerificationAction::TOGGLE_STATUS => $this->applyToggleStatus($verificationRequest),
            default => throw ValidationException::withMessages([
                'action' => 'Aksi verifikasi bahan baku tidak didukung.',
            ]),
        };

        $this->cacheForgetByPattern('master:raw_materials:*');
    }

    public function rejectVerificationRequest(OwnerVerificationRequest $verificationRequest): void
    {
        match ($verificationRequest->action) {
            OwnerVerificationAction::CREATE => $this->rejectCreate($verificationRequest),
            OwnerVerificationAction::UPDATE => $this->rollbackUpdate($verificationRequest),
            OwnerVerificationAction::DELETE, OwnerVerificationAction::TOGGLE_STATUS => null,
            default => throw ValidationException::withMessages([
                'action' => 'Aksi verifikasi bahan baku tidak didukung.',
            ]),
        };

        $this->cacheForgetByPattern('master:raw_materials:*');
    }

    public function clearVerificationRequestMedia(OwnerVerificationRequest $verificationRequest): void
    {
        $verificationRequest->load('media');

        $newPayload = $this->payloadNew($verificationRequest);

        foreach ($newPayload['prices'] ?? [] as $index => $_price) {
            $verificationRequest->clearMediaCollection($verificationRequest->priceImageCollection((int) $index));
        }
    }

    private function notifyOwner(string $typeLabel, string $body, string $url): void
    {
        $this->pushNotificationService->sendToRoles(
            "📦 {$typeLabel}",
            $body,
            ['owner', 'developer'],
            $url,
        );
    }

    public function applyCreate(OwnerVerificationRequest $verificationRequest): void
    {
        $rawMaterial = $verificationRequest->subject;

        if (! $rawMaterial instanceof RawMaterial) {
            throw ValidationException::withMessages([
                'raw_material' => 'Bahan baku tidak ditemukan.',
            ]);
        }

        $rawMaterial->update([
            'is_active' => true,
        ]);
    }

    public function applyUpdate(OwnerVerificationRequest $verificationRequest): void
    {
        $rawMaterial = $verificationRequest->subject;

        if (! $rawMaterial instanceof RawMaterial) {
            throw ValidationException::withMessages([
                'raw_material' => 'Bahan baku tidak ditemukan.',
            ]);
        }

        $this->applyPayloadToRawMaterial($rawMaterial, $this->payloadNew($verificationRequest), $verificationRequest);
    }

    public function applyDelete(OwnerVerificationRequest $verificationRequest): void
    {
        $rawMaterial = $verificationRequest->subject;

        if (! $rawMaterial instanceof RawMaterial) {
            throw ValidationException::withMessages([
                'raw_material' => 'Bahan baku tidak ditemukan.',
            ]);
        }

        $this->applyDeleteSubject($rawMaterial);
    }

    private function applyDeleteSubject(RawMaterial $rawMaterial): void
    {
        $this->ensureNotUsedInActiveCutting($rawMaterial);

        $this->runInTransaction(
            function () use ($rawMaterial): void {
                $rawMaterial->prices()->delete();
                $rawMaterial->delete();
            },
            'Gagal menghapus bahan baku',
        );
    }

    private function ensureNotUsedInActiveCutting(RawMaterial $rawMaterial): void
    {
        $usedInActiveCutting = CuttingMaterial::query()
            ->whereHas('rawMaterialPrice', fn ($q) => $q->where('raw_material_id', $rawMaterial->id))
            ->whereHas('cutting', fn ($q) => $q->where('status', '!=', CuttingStatus::COMPLETED))
            ->exists();

        if ($usedInActiveCutting) {
            throw ValidationException::withMessages([
                'raw_material' => 'Bahan baku tidak dapat dihapus karena masih digunakan dalam proses cutting yang belum selesai.',
            ]);
        }
    }

    private function ensurePricesNotUsedInActiveCutting(array $priceIds): void
    {
        $usedPriceIds = CuttingMaterial::query()
            ->whereIn('raw_material_price_id', $priceIds)
            ->whereHas('cutting', fn ($q) => $q->where('status', '!=', CuttingStatus::COMPLETED))
            ->pluck('raw_material_price_id')
            ->unique()
            ->toArray();

        if ($usedPriceIds !== []) {
            $variantNames = RawMaterialPrice::whereIn('id', $usedPriceIds)
                ->pluck('variant')
                ->unique()
                ->implode(', ');

            throw ValidationException::withMessages([
                'prices' => "Variant '{$variantNames}' tidak dapat dihapus karena masih digunakan dalam proses cutting yang belum selesai.",
            ]);
        }
    }

    public function applyToggleStatus(OwnerVerificationRequest $verificationRequest): void
    {
        $rawMaterial = $verificationRequest->subject;

        if (! $rawMaterial instanceof RawMaterial) {
            throw ValidationException::withMessages([
                'raw_material' => 'Bahan baku tidak ditemukan.',
            ]);
        }

        $newPayload = $this->payloadNew($verificationRequest);

        $rawMaterial->update([
            'is_active' => ($newPayload['status'] ?? 'inactive') === 'active',
        ]);
    }

    private function applyPayloadToRawMaterial(
        RawMaterial $rawMaterial,
        array $payload,
        ?OwnerVerificationRequest $verificationRequest = null,
        array $originalPrices = [],
    ): void {
        $rawMaterial->update([
            'name' => $payload['name'],
            'unit' => $payload['unit'] ?? $rawMaterial->unit,
        ]);

        if (array_key_exists('is_active', $payload)) {
            $rawMaterial->update([
                'is_active' => (bool) $payload['is_active'],
            ]);
        }

        $submittedPriceIds = collect($payload['prices'] ?? [])
            ->pluck('id')
            ->filter()
            ->map(fn ($id) => (int) $id)
            ->all();

        $deletingPriceIds = $rawMaterial->prices()
            ->whereNotIn('id', $submittedPriceIds)
            ->pluck('id')
            ->toArray();

        if ($deletingPriceIds !== []) {
            $this->ensurePricesNotUsedInActiveCutting($deletingPriceIds);
        }

        $rawMaterial->prices()
            ->whereNotIn('id', $submittedPriceIds)
            ->get()
            ->each(function (RawMaterialPrice $price): void {
                $price->delete();
            });

        foreach ($payload['prices'] ?? [] as $index => $priceData) {
            if (! empty($priceData['id'])) {
                $price = $rawMaterial->prices()->findOrFail($priceData['id']);
                $oldStock = (float) $price->stock;

                if ((float) $priceData['stock'] !== $oldStock) {
                    $this->stockMutationService->record(
                        stockable: $price,
                        type: 'adjustment',
                        quantity: (float) $priceData['stock'] - $oldStock,
                        stockBefore: $oldStock,
                        stockAfter: (float) $priceData['stock'],
                        description: 'Edit langsung dari master data',
                    );
                }

                $price->update([
                    'variant' => $priceData['variant'],
                    'price' => $priceData['price'],
                    'stock' => $priceData['stock'],
                ]);

                if ($verificationRequest !== null) {
                    $this->applyPriceImageChanges($verificationRequest, $price, $priceData, (int) $index);
                } elseif (isset($originalPrices[$index])) {
                    $this->syncPriceImages($price, $originalPrices[$index], $index);
                }

                continue;
            }

            $price = $rawMaterial->prices()->create([
                'variant' => $priceData['variant'],
                'price' => $priceData['price'],
                'stock' => $priceData['stock'],
            ]);

            $this->stockMutationService->record(
                stockable: $price,
                type: 'adjustment',
                quantity: (float) $priceData['stock'],
                stockBefore: 0,
                stockAfter: (float) $priceData['stock'],
                description: 'Stok awal dari master data',
            );

            if ($verificationRequest !== null) {
                $this->copyRequestPriceImages($verificationRequest, (int) $index, $price);
            } elseif (isset($originalPrices[$index])) {
                $this->syncPriceImages($price, $originalPrices[$index], $index);
            }
        }
    }

    private function rejectCreate(OwnerVerificationRequest $verificationRequest): void
    {
        $rawMaterial = $verificationRequest->subject;

        if (! $rawMaterial instanceof RawMaterial) {
            return;
        }

        $this->runInTransaction(
            function () use ($rawMaterial): void {
                $rawMaterial->prices()->delete();
                $rawMaterial->delete();
            },
            'Gagal menolak bahan baku',
        );
    }

    private function rollbackUpdate(OwnerVerificationRequest $verificationRequest): void
    {
        $rawMaterial = $verificationRequest->subject;

        if (! $rawMaterial instanceof RawMaterial) {
            throw ValidationException::withMessages([
                'raw_material' => 'Bahan baku tidak ditemukan.',
            ]);
        }

        $oldPayload = $this->payloadOld($verificationRequest);

        if ($oldPayload === []) {
            return;
        }

        $this->applyPayloadToRawMaterial($rawMaterial, $oldPayload);
    }

    private function payloadOld(OwnerVerificationRequest $verificationRequest): array
    {
        $payload = $verificationRequest->payload ?? [];

        if (array_key_exists('old', $payload)) {
            return is_array($payload['old']) ? $payload['old'] : [];
        }

        return [];
    }

    private function payloadNew(OwnerVerificationRequest $verificationRequest): array
    {
        $payload = $verificationRequest->payload ?? [];

        if (array_key_exists('new', $payload)) {
            return is_array($payload['new']) ? $payload['new'] : [];
        }

        return $payload;
    }

    private function snapshotRawMaterial(RawMaterial $rawMaterial): array
    {
        $rawMaterial->load(['prices']);

        return $this->enrichPayload([
            'name' => $rawMaterial->name,
            'unit' => $rawMaterial->unit->value,
            'is_active' => $rawMaterial->is_active,
            'prices' => $rawMaterial->prices
                ->map(fn (RawMaterialPrice $price) => [
                    'id' => $price->id,
                    'variant' => $price->variant,
                    'price' => $price->price,
                    'stock' => $price->stock,
                ])
                ->all(),
        ]);
    }

    private function enrichPayload(array $data): array
    {
        if (isset($data['unit'])) {
            $unit = $data['unit'] instanceof RawMaterialUnit
                ? $data['unit']
                : RawMaterialUnit::from($data['unit']);

            $data['unit'] = $unit->value;
            $data['unit_label'] = $unit->label();
        }

        return $data;
    }

    private function buildPayloadFromValidated(array $validated): array
    {
        return [
            'name' => $validated['name'],
            'unit' => $validated['unit'],
            'prices' => collect($validated['prices'] ?? [])
                ->map(fn (array $priceData) => [
                    'id' => $priceData['id'] ?? null,
                    'variant' => $priceData['variant'],
                    'price' => $priceData['price'],
                    'stock' => $priceData['stock'],
                    'remove_media_ids' => $priceData['remove_media_ids'] ?? [],
                ])
                ->all(),
        ];
    }

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

            return;
        }

        $query->latest();
    }

    private function createPrice(RawMaterial $rawMaterial, array $priceData, int $index): RawMaterialPrice
    {
        $price = $rawMaterial->prices()->create([
            'variant' => $priceData['variant'],
            'price' => $priceData['price'],
            'stock' => $priceData['stock'],
        ]);

        $this->stockMutationService->record(
            stockable: $price,
            type: 'adjustment',
            quantity: (float) $priceData['stock'],
            stockBefore: 0,
            stockAfter: (float) $priceData['stock'],
            description: 'Stok awal dari master data',
        );

        $this->syncPriceImages($price, $priceData, $index);

        return $price;
    }

    private function syncPriceImages(RawMaterialPrice $price, array $priceData, int $index): void
    {
        $this->mediaService->syncCollection(
            $price,
            'images',
            $priceData['images'] ?? null,
            $priceData['remove_media_ids'] ?? null,
            self::MAX_VARIANT_IMAGES,
            required: true,
            errorKey: "prices.{$index}.s3_keys",
            s3Keys: $priceData['s3_keys'] ?? null,
        );
    }

    private function syncRequestPriceImages(
        OwnerVerificationRequest $verificationRequest,
        array $priceData,
        int $index,
        bool $required = true,
    ): void {
        $collection = $verificationRequest->priceImageCollection($index);

        $this->mediaService->syncCollection(
            $verificationRequest,
            $collection,
            $priceData['images'] ?? null,
            $priceData['remove_media_ids'] ?? null,
            self::MAX_VARIANT_IMAGES,
            required: $required,
            errorKey: "prices.{$index}.s3_keys",
            s3Keys: $priceData['s3_keys'] ?? null,
        );
    }

    private function applyPriceImageChanges(
        OwnerVerificationRequest $verificationRequest,
        RawMaterialPrice $price,
        array $priceData,
        int $index,
    ): void {
        $removeIds = $priceData['remove_media_ids'] ?? [];

        if ($removeIds !== []) {
            $price->getMedia('images')
                ->whereIn('id', $removeIds)
                ->each->delete();
        }

        $this->copyRequestPriceImages($verificationRequest, $index, $price);

        if ($price->getMedia('images')->isEmpty()) {
            throw ValidationException::withMessages([
                "prices.{$index}.images" => 'Foto varian wajib diisi.',
            ]);
        }
    }

    private function copyRequestPriceImages(OwnerVerificationRequest $verificationRequest, int $index, RawMaterialPrice $price): void
    {
        $verificationRequest->getMedia($verificationRequest->priceImageCollection($index))
            ->each(fn ($media) => $media->copy($price, 'images'));
    }
}
