<?php

namespace Database\Seeders\Demo;

use App\Models\Branch;
use App\Models\BranchStock;
use App\Models\Catalog\Product;
use App\Models\Customers\Customer;
use App\Models\Inventory\StockAdjustment;
use App\Models\Inventory\StockTransfer;
use App\Models\Lookups\AdjustmentReason;
use App\Models\Lookups\MovementType;
use App\Models\Lookups\ReturnReason;
use App\Models\Lookups\StockTransferReason;
use App\Models\Purchasing\Supplier;
use App\Services\AuditLog;
use App\Services\BatchService;
use App\Services\CreditNoteService;
use App\Services\GrnService;
use App\Services\InvoiceService;
use App\Services\PurchaseOrderService;
use App\Services\ReturnService;
use App\Services\StockMovementService;
use Database\Seeders\Demo\Concerns\GeneratesDemoData;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Auth;

/**
 * A handful of self-contained "create then reverse" fixtures so the audit
 * trail and reversal code paths (invoice/return/GRN delete, PO cancel,
 * credit note void, transfer/adjustment reversal) have real examples,
 * without disturbing anything the other Demo\* seeders built.
 */
class ReversalSeeder extends Seeder
{
    use GeneratesDemoData;

    public function run(): void
    {
        $branch = Branch::first();
        $branch2 = Branch::where('id', '!=', $branch->id)->first();
        $customer = Customer::inRandomOrder()->first();
        $supplier = Supplier::where('is_blacklisted', false)->inRandomOrder()->first();
        $product = Product::whereDoesntHave('serials')->whereHas('lots', fn ($q) => $q->active())->inRandomOrder()->first();
        if (! $product) {
            $this->command?->warn('ReversalSeeder: no serial-free product with an active lot — skipping reversals.');

            return;
        }
        $adminId = (int) Auth::id();

        $this->reverseInvoice($branch, $customer, $product);
        $this->reversePurchaseOrder($branch, $supplier, $product);
        $this->reverseCreditNote($customer);
        $this->reverseReturn($branch, $customer, $product);
        $this->reverseGrn($branch, $product, $adminId);
        $this->reverseTransfer($branch, $branch2, $product);
        $this->reverseAdjustment($branch, $product, $adminId);
    }

    private function reverseInvoice(Branch $branch, Customer $customer, Product $product): void
    {
        app(StockMovementService::class)->record(
            branchId: $branch->id, productId: $product->id,
            movementTypeCode: MovementType::ADJUSTMENT, quantityChange: 10,
            note: 'Reversal-demo stock top-up.',
        );

        $lot = app(BatchService::class)->sellableFor($product->id, $branch->id)->first();
        if (! $lot) {
            return;
        }
        $unitPrice = (float) $lot->sale_price;

        $invoice = app(InvoiceService::class)->create(
            header: ['channel' => 'sales', 'branch_id' => $branch->id, 'customer_id' => $customer->id],
            lines: [[
                'product_id' => $product->id, 'product_name' => $product->name,
                'quantity' => 1, 'unit_price' => $unitPrice, 'batch_id' => $lot->id,
                'vat_rate' => 0, 'vat_amount' => 0, 'line_total' => $unitPrice,
            ]],
            splits: [['method' => 'cash', 'amount' => $unitPrice]],
        );

        app(InvoiceService::class)->delete($invoice->fresh());
    }

    private function reversePurchaseOrder(Branch $branch, Supplier $supplier, Product $product): void
    {
        $po = app(PurchaseOrderService::class)->create(
            header: ['supplier_id' => $supplier->id, 'branch_id' => $branch->id],
            lines: [['product_id' => $product->id, 'quantity' => 5, 'unit_cost' => $this->purchasePriceFor($product)]],
        );
        app(PurchaseOrderService::class)->send($po);
        app(PurchaseOrderService::class)->cancel($po, 'Demo: supplier unable to fulfil, order withdrawn.');
    }

    private function reverseCreditNote(Customer $customer): void
    {
        $cn = app(CreditNoteService::class)->createManualDraft($customer->id, 300, 'Demo credit note — created in error.');
        app(CreditNoteService::class)->void($cn, 'Duplicate entry, voided same day.');
    }

    private function reverseReturn(Branch $branch, Customer $customer, Product $product): void
    {
        app(StockMovementService::class)->record(
            branchId: $branch->id, productId: $product->id,
            movementTypeCode: MovementType::ADJUSTMENT, quantityChange: 5,
            note: 'Reversal-demo stock top-up.',
        );

        $lot = app(BatchService::class)->sellableFor($product->id, $branch->id)->first();
        if (! $lot) {
            return;
        }
        $unitPrice = (float) $lot->sale_price;

        $invoice = app(InvoiceService::class)->create(
            header: ['channel' => 'sales', 'branch_id' => $branch->id, 'customer_id' => $customer->id],
            lines: [[
                'product_id' => $product->id, 'product_name' => $product->name,
                'quantity' => 1, 'unit_price' => $unitPrice, 'batch_id' => $lot->id,
                'vat_rate' => 0, 'vat_amount' => 0, 'line_total' => $unitPrice,
            ]],
            splits: [['method' => 'cash', 'amount' => $unitPrice]],
        );

        $return = app(ReturnService::class)->create(
            header: ['customer_id' => $customer->id, 'branch_id' => $branch->id, 'refund_method' => 'cash'],
            lines: [[
                'invoice_id' => $invoice->id, 'invoice_item_id' => $invoice->items()->first()->id,
                'product_id' => $product->id, 'return_reason_code' => ReturnReason::WRONG_ITEM,
                'quantity' => 1, 'unit_price' => $unitPrice, 'is_add_back_to_stock' => true,
            ]],
        );

        app(ReturnService::class)->delete($return->fresh());
    }

    private function reverseGrn(Branch $branch, Product $product, int $adminId): void
    {
        // Deliberately no purchase_order_id — GrnService::delete() doesn't
        // reverse purchase_order_items.received_quantity, so keep this demo
        // fixture unlinked to a PO to avoid leaving one in an inconsistent state.
        $grn = app(GrnService::class)->create(
            header: ['branch_id' => $branch->id],
            lines: [['product_id' => $product->id, 'quantity' => 8, 'unit_cost' => $this->purchasePriceFor($product), 'sale_price' => $this->salePriceFor($product)]],
        );
        app(GrnService::class)->confirm($grn, $adminId);
        app(GrnService::class)->delete($grn->fresh(), $adminId);
    }

    private function reverseTransfer(Branch $fromBranch, Branch $toBranch, Product $product): void
    {
        $stock = app(StockMovementService::class);
        $batches = app(BatchService::class);

        $stock->record(branchId: $fromBranch->id, productId: $product->id, movementTypeCode: MovementType::ADJUSTMENT, quantityChange: 10, note: 'Reversal-demo stock top-up.');

        $transfer = StockTransfer::create([
            'from_branch_id' => $fromBranch->id,
            'to_branch_id' => $toBranch->id,
            'product_id' => $product->id,
            'reason_id' => StockTransferReason::where('code', StockTransferReason::INTERNAL)->value('id'),
            'quantity' => 4,
            'note' => 'Demo transfer — reversed same day.',
        ]);
        $reference = 'TRF-DEMO-'.$transfer->id;
        $stock->record(branchId: $fromBranch->id, productId: $product->id, movementTypeCode: MovementType::TRANSFER_OUT, quantityChange: -4, referenceType: $transfer, referenceNumber: $reference);
        $stock->record(branchId: $toBranch->id, productId: $product->id, movementTypeCode: MovementType::TRANSFER_IN, quantityChange: 4, referenceType: $transfer, referenceNumber: $reference);

        // Reverse it — mirrors StockTransferController::destroy().
        $stock->record(branchId: $fromBranch->id, productId: $product->id, movementTypeCode: MovementType::TRANSFER_REVERSAL, quantityChange: 4, referenceType: $transfer, referenceNumber: $reference, note: 'Transfer deletion reversal');
        $stock->record(branchId: $toBranch->id, productId: $product->id, movementTypeCode: MovementType::TRANSFER_REVERSAL, quantityChange: -4, referenceType: $transfer, referenceNumber: $reference, note: 'Transfer deletion reversal');

        AuditLog::record('stock_transfer.deleted', $transfer, [
            'from_branch_id' => $fromBranch->id, 'to_branch_id' => $toBranch->id,
            'product_id' => $product->id, 'quantity_reversed' => '4',
        ]);
        $transfer->delete();
    }

    private function reverseAdjustment(Branch $branch, Product $product, int $adminId): void
    {
        $stock = app(StockMovementService::class);
        $stock->record(branchId: $branch->id, productId: $product->id, movementTypeCode: MovementType::ADJUSTMENT, quantityChange: 20, note: 'Reversal-demo stock top-up.');

        $before = BranchStock::quantityFor($branch->id, $product->id);
        $diff = -3.0;

        $adjustment = StockAdjustment::create([
            'branch_id' => $branch->id,
            'product_id' => $product->id,
            'adjustment_reason_id' => AdjustmentReason::where('code', AdjustmentReason::COUNT_ERROR)->value('id'),
            'before_stock' => $before,
            'new_stock' => $before + $diff,
            'diff' => $diff,
            'note' => 'Demo adjustment — reversed same day.',
            'adjusted_by' => $adminId,
        ]);
        $stock->record(branchId: $branch->id, productId: $product->id, movementTypeCode: MovementType::ADJUSTMENT, quantityChange: $diff, note: 'Demo adjustment.');

        // Reverse it — mirrors StockAdjustmentController::destroy().
        $stock->record(branchId: $branch->id, productId: $product->id, movementTypeCode: MovementType::ADJUSTMENT_REVERSAL, quantityChange: -$diff, referenceType: $adjustment, note: 'Adjustment deletion reversal');

        AuditLog::record('stock_adjustment.deleted', $adjustment, [
            'branch_id' => $branch->id, 'product_id' => $product->id, 'diff_reversed' => (string) $diff,
        ]);
        $adjustment->delete();
    }

    /**
     * The product's "typical" prices from CatalogSeeder's in-process price
     * book, falling back to freshly faked ones for a standalone run — the
     * product row itself no longer carries prices, so nothing here may read
     * them from the model.
     */
    private function salePriceFor(Product $product): float
    {
        return CatalogSeeder::$priceBook[$product->id]['sale'] ?? round(fake()->randomFloat(2, 90, 349999), 2);
    }

    private function purchasePriceFor(Product $product): float
    {
        return CatalogSeeder::$priceBook[$product->id]['purchase'] ?? round($this->salePriceFor($product) * fake()->randomFloat(2, 0.55, 0.75), 2);
    }
}
