<?php

namespace App\Services;

use App\Models\ProductVariant;
use App\Models\StockCode;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use RuntimeException;

class StockCodeInventoryService
{
    public function importCodes(ProductVariant $product, string $rawCodes, array $metadata, ?User $actor = null): array
    {
        if ($product->delivery_method !== 'stored_code') {
            throw new RuntimeException('Stock codes can only be imported for stored-code products.');
        }

        $lines = preg_split('/\r\n|\r|\n/', $rawCodes) ?: [];
        $normalized = collect($lines)
            ->map(fn (string $code) => $this->normalize($code))
            ->filter()
            ->values();

        if ($normalized->isEmpty()) {
            throw new RuntimeException('No valid codes were provided.');
        }

        return DB::transaction(function () use ($product, $normalized, $metadata, $actor) {
            $product = ProductVariant::whereKey($product->id)->lockForUpdate()->firstOrFail();
            $seen = [];
            $created = 0;
            $duplicateInput = 0;
            $duplicateExisting = 0;

            foreach ($normalized as $plainCode) {
                $fingerprint = self::fingerprint($plainCode);

                if (isset($seen[$fingerprint])) {
                    $duplicateInput++;

                    continue;
                }

                $seen[$fingerprint] = true;

                $exists = StockCode::where('product_variant_id', $product->id)
                    ->where('code_fingerprint', $fingerprint)
                    ->exists();

                if ($exists) {
                    $duplicateExisting++;

                    continue;
                }

                StockCode::create([
                    'product_variant_id' => $product->id,
                    'batch_reference' => $metadata['batch_reference'] ?? null,
                    'encrypted_code' => Crypt::encryptString($plainCode),
                    'code_fingerprint' => $fingerprint,
                    'serial_number' => ($metadata['serial_prefix'] ?? null)
                        ? $metadata['serial_prefix'].'-'.str_pad((string) ($created + 1), 4, '0', STR_PAD_LEFT)
                        : null,
                    'cost_minor' => $metadata['cost_minor'] ?? 0,
                    'currency_code' => $metadata['currency_code'] ?? $product->currency_code,
                    'expires_at' => $metadata['expires_at'] ?? null,
                    'status' => 'available',
                    'imported_by' => $actor?->id,
                    'import_note' => $metadata['import_note'] ?? null,
                ]);

                $created++;
            }

            $this->syncProductStock($product);

            return [
                'imported' => $created,
                'duplicate_input' => $duplicateInput,
                'duplicate_existing' => $duplicateExisting,
                'available_stock' => $product->fresh()->stock_available,
            ];
        });
    }

    public function void(StockCode $code, User $actor, ?string $note = null): StockCode
    {
        return DB::transaction(function () use ($code, $actor, $note) {
            $code = StockCode::whereKey($code->id)->lockForUpdate()->firstOrFail();

            if ($code->status !== 'available') {
                throw new RuntimeException('Only available codes can be voided.');
            }

            $code->update([
                'status' => 'voided',
                'voided_at' => now(),
                'import_note' => trim(($code->import_note ? $code->import_note."\n" : '').'Voided by '.$actor->email.($note ? ': '.$note : '')),
            ]);

            $this->syncProductStock($code->productVariant);

            return $code;
        });
    }

    public function releaseReservedForOrder(int $orderId): void
    {
        DB::transaction(function () use ($orderId) {
            $codes = StockCode::where('reserved_order_id', $orderId)
                ->where('status', 'reserved')
                ->lockForUpdate()
                ->get();

            foreach ($codes as $code) {
                $code->update([
                    'status' => 'available',
                    'reserved_order_id' => null,
                    'sold_order_item_id' => null,
                    'reserved_at' => null,
                ]);

                $this->syncProductStock($code->productVariant);
            }
        });
    }

    public function syncProductStock(ProductVariant $product): void
    {
        if ($product->delivery_method !== 'stored_code') {
            return;
        }

        $product->update([
            'stock_available' => StockCode::where('product_variant_id', $product->id)
                ->where('status', 'available')
                ->count(),
        ]);
    }

    public static function fingerprint(string $plainCode): string
    {
        return hash_hmac('sha256', mb_strtolower(trim($plainCode)), config('app.key'));
    }

    private function normalize(string $code): string
    {
        return trim(preg_replace('/\s+/', ' ', $code) ?? '');
    }
}
