<?php

namespace Modules\Inventory\Services;

use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Modules\Accounts\Traits\HasFiscalContext;
use Modules\Approval\Services\ApprovalSubmissionService;
use Modules\Approval\Support\ApprovalRelations;
use Modules\Core\Exceptions\ApiValidationException;
use Modules\Core\Exceptions\BusinessException;
use Modules\Inventory\DTO\PurchaseInvoiceListFilter;
use Modules\Inventory\Enums\PurchaseStatusEnum;
use Modules\Inventory\Models\PurchaseInvoice;
use Modules\Inventory\Models\PurchaseRequisition;
use Modules\Inventory\Models\Traits\GeneratesPurchaseInvoiceTrait;
use Modules\Inventory\Repositories\PurchaseRepository;
use Modules\Inventory\Repositories\PurchaseRequisitionRepository;

class PurchaseService
{
    use HasFiscalContext, GeneratesPurchaseInvoiceTrait;

    public function __construct(
        protected ApprovalSubmissionService $approvalSubmissionService,
        protected PurchaseCalculationService $calculator,
        protected PurchaseRepository $purchaseRepo,
        protected PurchaseRequisitionRepository $purchaseReqRepo,
    ) { }

    public function list(PurchaseInvoiceListFilter $filters): LengthAwarePaginator
    {
        $columns = [
            'id',
            'invoice_no',
            'supplier_id',
            'invoice_date',
            'subtotal',
            'total_discount',
            'taxable_amount',
            'total_vat',
            'grand_total',
            'paid_amount',
            'due_amount',
            'payment_status',
            'status'
        ];

        return $this->purchaseRepo->getPurchaseInvoices(
            filters: $filters,
            columns: $columns,
            relations: [
                'supplier:id,name'
            ]
        );
    }

    public function find(int $id)
    {
        return $this->purchaseRepo->find($id);
    }

    public function show(int $id)
    {
        return $this->purchaseRepo->findById(
            $id,
            relations: array_merge([
                'supplier:id,name,phone,address,account_id',
                'supplier.account:id,account_name,account_code',
                'purchaseRequisition:id,requisition_no,requisition_date,status',
                'items.product:id,sku,name',
                'items.unit:id,name',
                'items.purchaseRequisitionItem:id,purchase_requisition_id'
            ], ApprovalRelations::approvalRequest())
        );
    }

    public function storeFromRequisition(PurchaseRequisition $requisition, array $data)
    {
        return DB::transaction(function () use ($requisition, $data) {
            $requisition = $this->purchaseReqRepo->findForUpdate(
                $requisition->id,
                relations: [
                    'purchaseInvoice:id,purchase_requisition_id',
                    'items:id,purchase_requisition_id,product_id,purchased_qty'
                ]
            );

            if (!$requisition->isEligibleForPurchaseInvoice()) {
                BusinessException::message(
                    'Purchase invoice cannot be created from this requisition.'
                );
            }

            $this->validatePurchaseItems($requisition, $data['items']);

            $calculated = $this->calculator->calculate($data['items']);
            $data = $this->attachFiscalContext($data, 'invoice_date');

            $invoicePayload = [
                ...Arr::except($data, ['items']),
                ...Arr::except($calculated, ['items']),
                'purchase_requisition_id' =>$requisition->id,
                'invoice_no' => $this->generateInvoiceNo(),
                'status' => PurchaseStatusEnum::DRAFT->value,
            ];

            $invoice = $this->purchaseRepo->createInvoice($invoicePayload);

            $itemPayloads = $this->prepareItemPayloads(
                $invoice,
                $calculated['items']
            );

            $this->purchaseRepo->createItems($itemPayloads);

            $this->purchaseReqRepo->increasePurchasedQty(
                $requisition,
                $calculated['items']
            );

            return $invoice;
        });
    }

    private function validatePurchaseItems(PurchaseRequisition $requisition, array $items): void
    {
        $requisitionItems = $requisition->items->keyBy('id');

        $errors = [];

        foreach ($items as $index => $item) {
            $requisitionItem = $requisitionItems->get($item['purchase_requisition_item_id']);

            if (!$requisitionItem) {
                $errors["items.$index.purchase_requisition_item_id"][] = 'Invalid purchase requisition item.';

                continue;
            }

            if ($requisitionItem->product_id != $item['product_id']) {
                $errors["items.$index.product_id"][] = 'Product does not match the purchase requisition.';
            }

        }

        if ($errors) {
            ApiValidationException::errors($errors);
        }

        $submittedIds = collect($items)
            ->pluck('purchase_requisition_item_id')
            ->sort()
            ->values()
            ->all();

        $expectedIds = $requisition->items
            ->pluck('id')
            ->sort()
            ->values()
            ->all();

        if ($submittedIds !== $expectedIds) {
            BusinessException::message('All purchase requisition items must be included.');
        }
    }

    public function store(array $data)
    {
        return DB::transaction(function () use ($data) {
            $calculated = $this->calculator->calculate($data['items']);
            $data = $this->attachFiscalContext($data, 'invoice_date');

            $invoicePayload = [
                ...Arr::except($data, ['items']),
                ...Arr::except($calculated, ['items']),

                'invoice_no' => $this->generateInvoiceNo(),
                'status' => PurchaseStatusEnum::DRAFT->value,
            ];

            $invoice = $this->purchaseRepo->createInvoice($invoicePayload);

            $itemPayloads = $this->prepareItemPayloads(
                $invoice,
                $calculated['items']
            );

            $this->purchaseRepo->createItems($itemPayloads);

            return $invoice;
        });
    }

    private function prepareItemPayloads(PurchaseInvoice $invoice, array $items): array
    {
        return collect($items)
            ->map(fn ($item) => [
                'purchase_invoice_id' => $invoice->id,
                'purchase_requisition_item_id' => $item['purchase_requisition_item_id'],
                'product_id' => $item['product_id'],
                'unit_id' => $item['unit_id'],
                'qty' => $item['qty'],
                'base_qty' => $item['base_qty'],
                'unit_price' => $item['unit_price'],
                'vat_price_type' => $item['vat_price_type'],
                'line_total' => $item['line_total'],
                'discount_type' => $item['discount_type'],
                'discount_amount' => $item['discount_amount'],
                'taxable_amount' => $item['taxable_amount'],
                'vat_type' => $item['vat_type'],
                'vat_rate' => $item['vat_rate'],
                'vat_amount' => $item['vat_amount'],
                'net_amount' => $item['net_amount'],
                'cost_price' => $item['cost_price'],
                'total' => $item['total'],
                'batch_no' => $item['batch_no'],
                'expiry_date' => $item['expiry_date'],
                'created_at' => now(),
                'updated_at' => now(),
            ])
            ->toArray();
    }

    public function update(PurchaseInvoice $invoice, array $data)
    {
        return DB::transaction(function () use ($invoice, $data) {
            $this->validateUpdate($invoice);

            $calculated = $this->calculator->calculate($data['items']);
            $data = $this->attachFiscalContext($data, 'invoice_date');

            $invoicePayload = [
                ...Arr::except($data, ['items']),
                ...Arr::except($calculated, ['items']),
            ];

            $this->purchaseRepo->updateInvoice(
                $invoice,
                $invoicePayload
            );

            $this->purchaseRepo->deleteItems($invoice);

            $itemPayloads = $this->prepareItemPayloads(
                $invoice,
                $calculated['items']
            );

            $this->purchaseRepo->createItems($itemPayloads);

            return $invoice->fresh();
        });
    }

    private function validateUpdate(PurchaseInvoice $invoice, string $action = 'updated'): void
    {
        if ($invoice->status !== PurchaseStatusEnum::DRAFT->value) {
            BusinessException::message(
                "Only draft purchase invoices can be {$action}."
            );
        }

        if ($invoice->is_posted) {
            BusinessException::message(
                "Posted purchase invoices cannot be {$action}."
            );
        }
    }

    public function submit(PurchaseInvoice $invoice): PurchaseInvoice
    {
        return DB::transaction(function () use ($invoice) {
            $this->validateUpdate($invoice, 'submitted');

            $this->approvalSubmissionService->submit($invoice);

            $this->purchaseRepo->updateInvoice(
                invoice: $invoice,
                data: [
                    'status' => PurchaseStatusEnum::SUBMITTED->value,
                    'submitted_at' => now(),
                ]
            );

            return $invoice->fresh();
        });
    }

    public function delete(PurchaseInvoice $invoice): void
    {
        $this->validateUpdate($invoice, 'deleted');

        DB::transaction(function () use ($invoice) {
            $invoice->delete();
        });
    }
}
