<?php

namespace App\Services\Admin\Manage;

use App\Enums\Role;
use App\Models\Purchase;
use App\Models\PurchaseItem;
use App\Models\RawMaterial;
use App\Models\RawMaterialPrice;
use App\Models\Supplier;
use App\Services\Concerns\RegistersMedia;
use App\Services\NotificationService;
use App\Services\S3PresignedService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;

class PurchaseService
{
    use RegistersMedia;

    public function __construct(
        private S3PresignedService $s3Service,
    ) {}

    public function paginated(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator
    {
        $paginator = Purchase::query()
            ->select(['id', 'supplier_id', 'created_by_id', 'subtotal', 'discount', 'shipping_cost', 'total', 'notes', 'created_at'])
            ->with([
                'supplier:id,name',
                'createdBy:id',
                'createdBy.userProfile:id,user_id,full_name',
                'purchaseItems' => fn ($q) => $q
                    ->select(['id', 'purchase_id', 'raw_material_price_id', 'quantity', 'unit_price', 'subtotal'])
                    ->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'),
                'purchaseItems.rawMaterialPrice:id,raw_material_id,variant,price,stock',
                'purchaseItems.rawMaterialPrice.rawMaterial:id,name,unit',
            ])
            ->when($search, function ($q) use ($search) {
                $q->whereHas('supplier', fn ($sq) => $sq->where('name', 'like', "%{$search}%"))
                    ->orWhere('notes', 'like', "%{$search}%");
            })
            ->when($filters['supplier_id'] ?? null, fn ($q, $supplierId) => $q->where('supplier_id', $supplierId))
            ->orderBy($sort, $direction)
            ->paginate($perPage);

        $paginator->getCollection()->each(function (Purchase $purchase) {
            $purchaseMedia = $purchase->getFirstMedia('photos');
            $purchase->photo_url = $purchaseMedia
                ? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath())
                : null;
            $purchase->photo_conversion_url = $purchaseMedia
                ? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath('thumb'))
                : null;

            $purchase->purchaseItems->each(function (PurchaseItem $item) {
                if (! $item->rawMaterialPrice) {
                    return;
                }

                $media = $item->rawMaterialPrice->getFirstMedia('images');
                $item->rawMaterialPrice->photo_url = $media
                    ? $this->s3Service->getTemporaryUrl($media->getPath())
                    : null;
                $item->rawMaterialPrice->photo_conversion_url = $media
                    ? $this->s3Service->getTemporaryUrl($media->getPath('thumb'))
                    : null;
            });
        });

        return $paginator;
    }

    public function getForCreate(): array
    {
        return [
            'suppliers' => Supplier::select(['id', 'name'])->latest()->get(),
            'rawMaterials' => RawMaterial::query()
                ->select(['id', 'name', 'unit', 'is_active'])
                ->with([
                    'rawMaterialPrices:id,raw_material_id,variant,price,stock',
                ])
                ->orderBy('name')
                ->get()
                ->each(function (RawMaterial $rawMaterial) {
                    $rawMaterial->rawMaterialPrices->each(function (RawMaterialPrice $price) {
                        $media = $price->getFirstMedia('images');
                        $price->photo_url = $media
                            ? $this->s3Service->getTemporaryUrl($media->getPath())
                            : null;
                    });
                }),
        ];
    }

    public function getForEdit(Purchase $purchase): array
    {
        $purchase->load([
            'purchaseItems' => fn ($q) => $q
                ->orderByRaw('(SELECT variant FROM raw_material_prices WHERE raw_material_prices.id = purchase_items.raw_material_price_id)'),
            'purchaseItems.rawMaterialPrice.rawMaterial',
            'supplier',
        ]);

        $rawMaterial = $purchase->purchaseItems->first()?->rawMaterialPrice?->rawMaterial;

        $variants = $purchase->purchaseItems->map(function (PurchaseItem $item) {
            if (! $item->rawMaterialPrice) {
                return null;
            }

            $media = $item->rawMaterialPrice->getFirstMedia('images');

            return [
                'id' => $item->rawMaterialPrice->id,
                'variant' => $item->rawMaterialPrice->variant,
                'price' => $item->unit_price,
                'stock' => $item->quantity,
                'photo_key' => $media?->getCustomProperty('s3_key') ?? $media?->file_name,
                'photo_url' => $media ? $this->s3Service->getTemporaryUrl($media->getPath()) : null,
            ];
        })->filter()->values();

        $items = $purchase->purchaseItems;
        $items = $items->filter(fn (PurchaseItem $item) => $item->rawMaterialPrice !== null);

        $materials = $items
            ->map(fn (PurchaseItem $item) => $item->rawMaterialPrice->rawMaterial)
            ->filter()
            ->unique(fn (RawMaterial $material) => $material->id);

        $singleMaterial = $materials->count() === 1;
        $sharedWithOther = PurchaseItem::where('purchase_id', '!=', $purchase->id)
            ->whereIn('raw_material_price_id', $items->pluck('raw_material_price_id'))
            ->exists();

        $purchaseMedia = $purchase->getFirstMedia('photos');
        $purchasePhotoKey = $purchaseMedia?->getCustomProperty('s3_key') ?? $purchaseMedia?->file_name;
        $purchasePhotoUrl = $purchaseMedia
            ? $this->s3Service->getTemporaryUrl($purchaseMedia->getPath())
            : null;

        return [
            'id' => $purchase->id,
            'name' => $rawMaterial?->name ?? '',
            'unit' => $rawMaterial?->unit->value ?? 'kg',
            'supplier_id' => $purchase->supplier_id,
            'discount' => $purchase->discount,
            'shipping_cost' => $purchase->shipping_cost,
            'notes' => $purchase->notes,
            'photo_key' => $purchasePhotoKey,
            'photo_url' => $purchasePhotoUrl,
            'variants' => $variants,
            'default_mode' => $singleMaterial && ! $sharedWithOther ? 'new' : 'existing',
            'existing_material_name' => $singleMaterial ? $materials->first()->name : null,
            'existing_quantities' => $singleMaterial
                ? $items->mapWithKeys(fn (PurchaseItem $item) => [
                    (int) $item->raw_material_price_id => (int) $item->quantity,
                ])->all()
                : [],
        ];
    }

    public function store(array $data): Purchase
    {
        if (($data['mode'] ?? 'new') === 'existing') {
            return $this->storeFromExisting($data);
        }

        return $this->storeNew($data);
    }

    private function storeFromExisting(array $data): Purchase
    {
        return DB::transaction(function () use ($data) {
            $now = now();
            $subtotal = 0;

            $itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) {
                $itemSubtotal = (int) ($item['unit_price'] * $item['quantity']);
                $subtotal += $itemSubtotal;

                return [
                    'purchase_id' => null,
                    'raw_material_price_id' => $item['raw_material_price_id'],
                    'user_id' => auth()->id(),
                    'quantity' => $item['quantity'],
                    'unit_price' => $item['unit_price'],
                    'subtotal' => $itemSubtotal,
                    'created_at' => $now,
                    'updated_at' => $now,
                ];
            })->toArray();

            $discount = $data['discount'] ?? 0;
            $shippingCost = $data['shipping_cost'] ?? 0;
            $total = $subtotal - $discount + $shippingCost;

            $purchase = Purchase::create([
                'supplier_id' => $data['supplier_id'],
                'created_by_id' => auth()->id(),
                'subtotal' => $subtotal,
                'discount' => $discount,
                'shipping_cost' => $shippingCost,
                'total' => $total,
                'notes' => $data['notes'] ?? null,
            ]);

            foreach ($itemRows as &$row) {
                $row['purchase_id'] = $purchase->id;
            }
            DB::table('purchase_items')->insert($itemRows);

            foreach ($data['existing_items'] as $item) {
                RawMaterialPrice::whereKey($item['raw_material_price_id'])
                    ->increment('stock', (int) $item['quantity']);
            }

            if (! empty($data['photo_key'])) {
                $this->registerMedia(
                    model: $purchase,
                    s3Key: $data['photo_key'],
                    collectionName: 'photos',
                    orderColumn: 1,
                );
            }

            NotificationService::notify(
                roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
                title: 'Belanja Baru',
                body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
                url: route('admin.manage.purchases.index'),
            );

            return $purchase;
        });
    }

    private function storeNew(array $data): Purchase
    {
        return DB::transaction(function () use ($data) {
            $rawMaterial = RawMaterial::create([
                'name' => $data['name'],
                'unit' => $data['unit'],
                'is_active' => true,
            ]);

            $subtotal = 0;
            $now = now();
            $priceRows = collect($data['variants'])->map(function ($v) use ($rawMaterial, $now, &$subtotal) {
                $itemSubtotal = (int) ($v['price'] * $v['stock']);
                $subtotal += $itemSubtotal;

                return [
                    'raw_material_id' => $rawMaterial->id,
                    'variant' => $v['variant'],
                    'price' => $v['price'],
                    'stock' => $v['stock'],
                    'created_at' => $now,
                    'updated_at' => $now,
                ];
            })->toArray();

            DB::table('raw_material_prices')->insert($priceRows);

            $insertedPrices = RawMaterialPrice::where('raw_material_id', $rawMaterial->id)->get();
            $variantMap = $insertedPrices->mapWithKeys(fn ($p) => [$p->variant => $p->id]);

            foreach ($data['variants'] as $variantData) {
                if (! empty($variantData['photo_key'])) {
                    $priceId = $variantMap[$variantData['variant']];
                    $priceModel = RawMaterialPrice::find($priceId);
                    $this->registerMedia(
                        model: $priceModel,
                        s3Key: $variantData['photo_key'],
                        collectionName: 'images',
                        orderColumn: 1,
                    );
                }
            }

            $discount = $data['discount'] ?? 0;
            $shippingCost = $data['shipping_cost'] ?? 0;
            $total = $subtotal - $discount + $shippingCost;

            $purchase = Purchase::create([
                'supplier_id' => $data['supplier_id'],
                'created_by_id' => auth()->id(),
                'subtotal' => $subtotal,
                'discount' => $discount,
                'shipping_cost' => $shippingCost,
                'total' => $total,
                'notes' => $data['notes'] ?? null,
            ]);

            $purchaseItems = $insertedPrices->map(fn ($price) => [
                'purchase_id' => $purchase->id,
                'raw_material_price_id' => $price->id,
                'user_id' => auth()->id(),
                'quantity' => $price->stock,
                'unit_price' => $price->price,
                'subtotal' => (int) ($price->price * $price->stock),
                'created_at' => $now,
                'updated_at' => $now,
            ])->toArray();

            DB::table('purchase_items')->insert($purchaseItems);

            if (! empty($data['photo_key'])) {
                $this->registerMedia(
                    model: $purchase,
                    s3Key: $data['photo_key'],
                    collectionName: 'photos',
                    orderColumn: 1,
                );
            }

            NotificationService::notify(
                roles: [Role::OWNER, Role::DEVELOPER, Role::ADMIN_BAHAN_BAKU],
                title: 'Belanja Baru',
                body: 'Belanja bahan baku sebesar Rp '.number_format($total, 0, ',', '.').' berhasil dicatat oleh '.auth()->user()->full_name.'.',
                url: route('admin.manage.purchases.index'),
            );

            return $purchase;
        });
    }

    public function update(Purchase $purchase, array $data): Purchase
    {
        return DB::transaction(function () use ($purchase, $data) {
            $purchase->load('purchaseItems.rawMaterialPrice.rawMaterial');

            $oldItems = $purchase->purchaseItems;

            // 1. Reverse the stock increments of the old items, so prices
            //    get adjusted by the difference instead of being reset.
            $oldItems->each(function (PurchaseItem $item) {
                if ($item->rawMaterialPrice) {
                    $item->rawMaterialPrice->decrement('stock', (int) $item->quantity);
                }
            });

            $oldMaterial = $oldItems
                ->map(fn (PurchaseItem $item) => $item->rawMaterialPrice?->rawMaterial)
                ->filter()
                ->unique(fn (RawMaterial $material) => $material->id)
                ->first();

            $oldItems->each->delete();

            $now = now();
            $subtotal = 0;

            if (($data['mode'] ?? 'new') === 'existing') {
                // 2a. Reference existing prices and add their new stock.
                $itemRows = collect($data['existing_items'])->map(function ($item) use ($now, &$subtotal) {
                    $itemSubtotal = (int) ($item['unit_price'] * $item['quantity']);
                    $subtotal += $itemSubtotal;

                    return [
                        'purchase_id' => null,
                        'raw_material_price_id' => $item['raw_material_price_id'],
                        'user_id' => auth()->id(),
                        'quantity' => $item['quantity'],
                        'unit_price' => $item['unit_price'],
                        'subtotal' => $itemSubtotal,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ];
                })->toArray();

                foreach ($data['existing_items'] as $item) {
                    RawMaterialPrice::whereKey($item['raw_material_price_id'])
                        ->increment('stock', (int) $item['quantity']);
                }
            } else {
                // 2a. Always reuse the purchase's existing material in place;
                //     a fresh material is only created when the purchase has
                //     no items yet.
                if ($oldMaterial) {
                    $rawMaterial = $oldMaterial;
                    $rawMaterial->update([
                        'name' => $data['name'],
                        'is_active' => true,
                    ]);
                } else {
                    $rawMaterial = RawMaterial::create([
                        'name' => $data['name'],
                        'unit' => $data['unit'] ?? 'kg',
                        'is_active' => true,
                    ]);
                }

                // 2b. Adjust the stock of existing variant prices, create
                //     prices for new variants, but never delete variants.
                $itemRows = collect($data['variants'])->map(function ($v) use ($purchase, $rawMaterial, $now, &$subtotal) {
                    $price = null;

                    if (! empty($v['id'])) {
                        $price = RawMaterialPrice::withTrashed()->find($v['id']);
                    }

                    if (! $price) {
                        $price = $rawMaterial->rawMaterialPrices()
                            ->where('variant', $v['variant'])
                            ->first();
                    }

                    if ($price) {
                        $price->increment('stock', (int) $v['stock']);
                        $price->update(['price' => $v['price']]);
                    } else {
                        $price = $rawMaterial->rawMaterialPrices()->create([
                            'variant' => $v['variant'],
                            'price' => $v['price'],
                            'stock' => $v['stock'],
                        ]);
                    }

                    if (! empty($v['photo_key']) && $price->getFirstMedia('images')?->getCustomProperty('s3_key') !== $v['photo_key']) {
                        $price->clearMediaCollection('images');
                        $this->registerMedia(
                            model: $price,
                            s3Key: $v['photo_key'],
                            collectionName: 'images',
                            orderColumn: 1,
                        );
                    }

                    $itemSubtotal = (int) ($v['price'] * $v['stock']);
                    $subtotal += $itemSubtotal;

                    return [
                        'purchase_id' => $purchase->id,
                        'raw_material_price_id' => $price->id,
                        'user_id' => auth()->id(),
                        'quantity' => $v['stock'],
                        'unit_price' => $v['price'],
                        'subtotal' => $itemSubtotal,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ];
                })->toArray();
            }

            $discount = $data['discount'] ?? 0;
            $shippingCost = $data['shipping_cost'] ?? 0;
            $total = $subtotal - $discount + $shippingCost;

            $purchase->update([
                'supplier_id' => $data['supplier_id'],
                'subtotal' => $subtotal,
                'discount' => $discount,
                'shipping_cost' => $shippingCost,
                'total' => $total,
                'notes' => $data['notes'] ?? null,
            ]);

            foreach ($itemRows as &$row) {
                $row['purchase_id'] = $purchase->id;
            }
            DB::table('purchase_items')->insert($itemRows);

            $this->syncPhoto($purchase, $data);

            return $purchase;
        });
    }

    public function destroy(Purchase $purchase): bool
    {
        return DB::transaction(function () use ($purchase) {
            $purchase->load('purchaseItems.rawMaterialPrice');

            // Remove the stock the purchase added, keep the variants.
            $purchase->purchaseItems->each(function (PurchaseItem $item) {
                if ($item->rawMaterialPrice) {
                    $item->rawMaterialPrice->decrement('stock', (int) $item->quantity);
                }
            });

            $purchase->purchaseItems()->delete();
            $purchase->delete();

            return true;
        });
    }
}
