<?php

namespace App\Services\Courier;

use App\Models\Courier;
use App\Models\CourierAuditLog;
use App\Models\CourierLedgerEntry;
use App\Models\CourierSettlement;
use App\Models\Order;
use App\Models\OrderCourierShipment;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;

class CourierLedgerService
{
    public function __construct(private readonly CourierVoucherService $voucherService)
    {
    }

    public function syncLegacyCouriersFromShipments(): void
    {
        if (! Schema::hasTable('couriers') || ! Schema::hasTable('order_courier_shipments')) {
            return;
        }

        $providers = OrderCourierShipment::query()
            ->whereNotNull('provider')
            ->select('provider')
            ->distinct()
            ->pluck('provider');

        foreach ($providers as $provider) {
            $courier = $this->ensureCourier($provider);

            OrderCourierShipment::query()
                ->whereRaw('LOWER(provider) = ?', [$courier->provider_key])
                ->whereNull('courier_id')
                ->update(['courier_id' => $courier->id]);
        }
    }

    public function ensureCourier(?string $providerKey, ?string $name = null): Courier
    {
        $providerKey = Str::slug($providerKey ?: 'other', '_');

        return Courier::firstOrCreate(
            ['provider_key' => $providerKey],
            [
                'name' => $name ?: ucwords(str_replace(['_', '-'], ' ', $providerKey)),
                'api_type' => $providerKey,
                'is_active' => true,
            ]
        );
    }

    public function ensureCourierForShipment(OrderCourierShipment $shipment): Courier
    {
        if ($shipment->courier_id && $shipment->courier) {
            return $shipment->courier;
        }

        $courier = $this->ensureCourier($shipment->provider ?: 'other');
        $shipment->forceFill(['courier_id' => $courier->id])->save();

        return $courier;
    }

    public function recordHandover(Order $order, OrderCourierShipment $shipment, ?int $adminId = null, string $source = 'booking'): ?CourierLedgerEntry
    {
        if (! Schema::hasTable('courier_ledger_entries')) {
            return null;
        }

        $courier = $this->ensureCourierForShipment($shipment);
        $this->syncOpeningBalance($courier, $adminId);

        return $this->postEntry([
            'courier' => $courier,
            'order' => $order,
            'shipment' => $shipment,
            'event_type' => CourierLedgerEntry::EVENT_HANDOVER,
            'voucher_prefix' => 'CCL',
            'entry_date' => $shipment->sent_to_courier_at ?: now(),
            'particulars' => 'Parcel handed over to courier',
            'debit' => 0,
            'credit' => 0,
            'status' => $shipment->booking_status ?: 'booked',
            'source' => $source,
            'idempotency_key' => $this->key('handover', $shipment->id),
            'created_by_admin_id' => $adminId,
        ]);
    }

    public function recordShipmentAccounting(
        Order $order,
        OrderCourierShipment $shipment,
        array $payload = [],
        ?int $adminId = null,
        string $source = 'sync'
    ): array {
        if (! Schema::hasTable('courier_ledger_entries')) {
            return [];
        }

        $shipment->refresh();
        $order->refresh();
        $courier = $this->ensureCourierForShipment($shipment);
        $created = [];
        $collectable = (float) ($shipment->cod_collected_amount ?: $shipment->collectable_amount ?: 0);
        $deliveryCharge = (float) ($shipment->actual_delivery_cost ?: 0);
        $codCharge = (float) ($shipment->actual_cod_charge ?: 0);
        $returnCharge = (float) ($shipment->actual_return_cost ?: 0);
        $settledAmount = (float) ($shipment->settled_amount ?: 0);
        $isCollectionConfirmed = $this->isCollectionConfirmed($order, $shipment);
        $isSettled = $this->isSettled($shipment);
        $isReturned = $this->isReturned($order, $shipment);
        $eventDate = $shipment->cod_collected_at ?: $shipment->delivered_at ?: $shipment->courier_synced_at ?: now();

        $this->syncOpeningBalance($courier, $adminId);

        if ($isCollectionConfirmed && ! $shipment->delivered_at) {
            $shipment->forceFill(['delivered_at' => $eventDate])->save();
        }

        if ($collectable > 0 && $isCollectionConfirmed) {
            $created[] = $this->postEntry([
                'courier' => $courier,
                'order' => $order,
                'shipment' => $shipment,
                'event_type' => CourierLedgerEntry::EVENT_COD_COLLECTION,
                'voucher_prefix' => 'CCL',
                'entry_date' => $eventDate,
                'particulars' => 'COD collected by courier for order #' . $order->id,
                'debit' => $collectable,
                'credit' => 0,
                'status' => $shipment->cod_collection_status ?: 'collected',
                'source' => $source,
                'idempotency_key' => $this->key('collection', $shipment->id, $collectable),
                'payload' => $payload,
                'created_by_admin_id' => $adminId,
            ]);
        }

        if ($deliveryCharge > 0 && ($isCollectionConfirmed || $isSettled)) {
            $created[] = $this->postEntry([
                'courier' => $courier,
                'order' => $order,
                'shipment' => $shipment,
                'event_type' => CourierLedgerEntry::EVENT_DELIVERY_CHARGE,
                'voucher_prefix' => 'CCH',
                'entry_date' => $eventDate,
                'particulars' => 'Courier delivery charge deducted for order #' . $order->id,
                'debit' => 0,
                'credit' => $deliveryCharge,
                'status' => 'deducted',
                'source' => $source,
                'idempotency_key' => $this->key('delivery_charge', $shipment->id, $deliveryCharge),
                'payload' => $payload,
                'created_by_admin_id' => $adminId,
            ]);
        }

        if ($codCharge > 0 && ($isCollectionConfirmed || $isSettled)) {
            $created[] = $this->postEntry([
                'courier' => $courier,
                'order' => $order,
                'shipment' => $shipment,
                'event_type' => CourierLedgerEntry::EVENT_COD_CHARGE,
                'voucher_prefix' => 'CCH',
                'entry_date' => $eventDate,
                'particulars' => 'COD charge deducted for order #' . $order->id,
                'debit' => 0,
                'credit' => $codCharge,
                'status' => 'deducted',
                'source' => $source,
                'idempotency_key' => $this->key('cod_charge', $shipment->id, $codCharge),
                'payload' => $payload,
                'created_by_admin_id' => $adminId,
            ]);
        }

        if ($returnCharge > 0 && $isReturned) {
            $created[] = $this->postEntry([
                'courier' => $courier,
                'order' => $order,
                'shipment' => $shipment,
                'event_type' => CourierLedgerEntry::EVENT_RETURN_CHARGE,
                'voucher_prefix' => 'CRT',
                'entry_date' => $shipment->courier_synced_at ?: now(),
                'particulars' => 'Courier return charge applied for order #' . $order->id,
                'debit' => 0,
                'credit' => $returnCharge,
                'status' => 'deducted',
                'source' => $source,
                'idempotency_key' => $this->key('return_charge', $shipment->id, $returnCharge),
                'payload' => $payload,
                'created_by_admin_id' => $adminId,
            ]);
        }

        if ($settledAmount > 0 && $isSettled) {
            $created[] = $this->postEntry([
                'courier' => $courier,
                'order' => $order,
                'shipment' => $shipment,
                'event_type' => CourierLedgerEntry::EVENT_SETTLEMENT,
                'voucher_prefix' => 'CST',
                'entry_date' => $shipment->settlement_paid_at ?: now(),
                'particulars' => 'Courier settlement received for order #' . $order->id,
                'debit' => 0,
                'credit' => $settledAmount,
                'status' => $shipment->settlement_status ?: 'settled',
                'source' => $source,
                'idempotency_key' => $this->key('shipment_settlement', $shipment->id, $settledAmount, $shipment->settlement_reference ?: 'no-ref'),
                'payload' => $payload,
                'created_by_admin_id' => $adminId,
            ]);
        }

        $this->refreshShipmentMoneyStatus($shipment);

        return array_values(array_filter($created));
    }

    public function postSettlementReceipt(CourierSettlement $settlement, ?int $adminId = null): ?CourierLedgerEntry
    {
        $this->syncOpeningBalance($settlement->courier, $adminId);

        return $this->postEntry([
            'courier' => $settlement->courier,
            'settlement' => $settlement,
            'event_type' => CourierLedgerEntry::EVENT_SETTLEMENT,
            'voucher_prefix' => 'CST',
            'entry_date' => $settlement->settlement_date,
            'particulars' => 'Courier settlement received: ' . $settlement->voucher_no,
            'debit' => 0,
            'credit' => (float) $settlement->net_received_amount,
            'status' => $settlement->status,
            'source' => 'manual_settlement',
            'idempotency_key' => $this->key('manual_settlement', $settlement->id, $settlement->net_received_amount),
            'payload' => $settlement->payload ?: [],
            'created_by_admin_id' => $adminId,
        ]);
    }

    public function postManualAdjustment(
        Courier $courier,
        float $debit,
        float $credit,
        string $particulars,
        ?int $adminId = null
    ): ?CourierLedgerEntry {
        $this->syncOpeningBalance($courier, $adminId);

        return $this->postEntry([
            'courier' => $courier,
            'event_type' => CourierLedgerEntry::EVENT_ADJUSTMENT,
            'voucher_prefix' => 'CADJ',
            'entry_date' => now(),
            'particulars' => $particulars,
            'debit' => $debit,
            'credit' => $credit,
            'status' => 'posted',
            'source' => 'manual_adjustment',
            'idempotency_key' => $this->key('manual_adjustment', $courier->id, microtime(true)),
            'created_by_admin_id' => $adminId,
        ]);
    }

    public function syncOpeningBalance(Courier $courier, ?int $adminId = null): ?CourierLedgerEntry
    {
        if (! Schema::hasTable('courier_ledger_entries') || ! Schema::hasTable('couriers')) {
            return null;
        }

        return DB::transaction(function () use ($courier, $adminId) {
            /** @var Courier $courier */
            $courier = Courier::query()->lockForUpdate()->findOrFail($courier->id);
            $openingBalance = round((float) ($courier->opening_balance ?? 0), 8);
            $idempotencyKey = $this->key('opening_balance', $courier->id);

            $entry = CourierLedgerEntry::query()
                ->where('idempotency_key', $idempotencyKey)
                ->lockForUpdate()
                ->first();

            if (abs($openingBalance) < 0.00000001 && ! $entry) {
                $this->recalculateCourierBalanceLocked($courier);
                return null;
            }

            $debit = $openingBalance > 0 ? $openingBalance : 0;
            $credit = $openingBalance < 0 ? abs($openingBalance) : 0;
            $entryDate = $this->date($courier->opening_balance_date ?: $courier->created_at ?: now());
            $reference = $courier->opening_balance_reference ?: 'Opening balance';
            $particulars = $openingBalance < 0
                ? 'Courier Opening Balance - Payable to courier'
                : 'Courier Opening Balance - Receivable from courier';
            $payload = [
                'reference' => $reference,
                'note' => $courier->opening_balance_note,
                'signed_opening_balance' => $openingBalance,
            ];
            $oldValues = $entry?->toArray();

            if ($entry) {
                $entry->forceFill([
                    'event_type' => CourierLedgerEntry::EVENT_OPENING_BALANCE,
                    'entry_date' => $entryDate,
                    'parcel_id' => null,
                    'invoice_no' => null,
                    'particulars' => $particulars,
                    'debit' => $debit,
                    'credit' => $credit,
                    'status' => 'posted',
                    'source' => 'opening_balance',
                    'payload' => $payload,
                    'created_by_admin_id' => $entry->created_by_admin_id ?: $adminId,
                ])->save();
            } else {
                $entry = CourierLedgerEntry::create([
                    'courier_id' => $courier->id,
                    'voucher_no' => $this->voucherService->nextLedgerVoucher('COB'),
                    'event_type' => CourierLedgerEntry::EVENT_OPENING_BALANCE,
                    'entry_date' => $entryDate,
                    'particulars' => $particulars,
                    'debit' => $debit,
                    'credit' => $credit,
                    'balance' => 0,
                    'status' => 'posted',
                    'source' => 'opening_balance',
                    'idempotency_key' => $idempotencyKey,
                    'payload' => $payload,
                    'created_by_admin_id' => $adminId,
                ]);
            }

            $this->recalculateCourierBalanceLocked($courier);

            $courierUpdates = [];
            if (Schema::hasColumn('couriers', 'opening_balance_synced_at')) {
                $courierUpdates['opening_balance_synced_at'] = now();
            }

            if ($courierUpdates !== []) {
                $courier->forceFill($courierUpdates)->save();
            }

            CourierAuditLog::create([
                'courier_id' => $courier->id,
                'courier_ledger_entry_id' => $entry->id,
                'action' => 'opening_balance_synced',
                'auditable_type' => CourierLedgerEntry::class,
                'auditable_id' => $entry->id,
                'old_values' => $oldValues,
                'new_values' => $entry->fresh()->toArray(),
                'source' => 'admin',
                'created_by_admin_id' => $adminId,
            ]);

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

    public function reverseEntry(CourierLedgerEntry $entry, string $reason, ?int $adminId = null): ?CourierLedgerEntry
    {
        if ($entry->reversed_at) {
            return null;
        }

        $reversal = $this->postEntry([
            'courier' => $entry->courier,
            'order' => $entry->order,
            'shipment' => $entry->shipment,
            'settlement' => $entry->settlement,
            'event_type' => CourierLedgerEntry::EVENT_REVERSAL,
            'voucher_prefix' => 'CADJ',
            'entry_date' => now(),
            'particulars' => 'Reverse ' . $entry->voucher_no . ': ' . $reason,
            'debit' => (float) $entry->credit,
            'credit' => (float) $entry->debit,
            'status' => 'reversal',
            'source' => 'reverse_voucher',
            'idempotency_key' => $this->key('reverse', $entry->id),
            'created_by_admin_id' => $adminId,
        ]);

        $entry->forceFill([
            'reversed_at' => now(),
            'reverse_reason' => $reason,
        ])->save();

        return $reversal;
    }

    public function refreshShipmentMoneyStatus(OrderCourierShipment $shipment): void
    {
        $shipment->refresh();
        $collectable = (float) ($shipment->cod_collected_amount ?: $shipment->collectable_amount ?: 0);
        $charges = (float) ($shipment->actual_delivery_cost ?: 0)
            + (float) ($shipment->actual_cod_charge ?: 0)
            + (float) ($shipment->actual_return_cost ?: 0);
        $netReceivable = max($collectable - $charges, 0);
        $netReceived = (float) ($shipment->settled_amount ?: 0);
        $balance = $netReceivable - $netReceived;

        $shipment->forceFill([
            'customer_delivery_charge' => $shipment->customer_delivery_charge ?? $shipment->order?->delivery_charge,
            'net_receivable_amount' => $netReceivable,
            'net_received_amount' => $netReceived,
            'balance_amount' => $balance,
            'settlement_status' => $this->shipmentSettlementStatus($netReceivable, $netReceived, $shipment->settlement_status),
        ])->save();
    }

    private function postEntry(array $data): ?CourierLedgerEntry
    {
        return DB::transaction(function () use ($data) {
            $existing = CourierLedgerEntry::query()
                ->where('idempotency_key', $data['idempotency_key'])
                ->first();

            if ($existing) {
                return $existing;
            }

            /** @var Courier $courier */
            $courier = Courier::query()->lockForUpdate()->findOrFail($data['courier']->id);
            $lastBalance = CourierLedgerEntry::query()
                ->where('courier_id', $courier->id)
                ->lockForUpdate()
                ->latest('entry_date')
                ->latest('id')
                ->value('balance');
            $balance = (float) ($lastBalance ?? 0);
            $debit = round((float) ($data['debit'] ?? 0), 8);
            $credit = round((float) ($data['credit'] ?? 0), 8);
            $balance = round($balance + $debit - $credit, 8);
            $shipment = $data['shipment'] ?? null;
            $order = $data['order'] ?? null;
            $settlement = $data['settlement'] ?? null;

            $entry = CourierLedgerEntry::create([
                'courier_id' => $courier->id,
                'order_courier_shipment_id' => $shipment?->id,
                'order_id' => $order?->id,
                'courier_settlement_id' => $settlement?->id,
                'voucher_no' => $this->voucherService->nextLedgerVoucher($data['voucher_prefix']),
                'event_type' => $data['event_type'],
                'entry_date' => $this->date($data['entry_date']),
                'parcel_id' => $this->parcelId($shipment),
                'invoice_no' => $order?->id ? (string) $order->id : ($data['invoice_no'] ?? null),
                'particulars' => $data['particulars'],
                'debit' => $debit,
                'credit' => $credit,
                'balance' => $balance,
                'status' => $data['status'] ?? 'posted',
                'source' => $data['source'] ?? null,
                'source_type' => $data['source_type'] ?? null,
                'source_id' => $data['source_id'] ?? null,
                'idempotency_key' => $data['idempotency_key'],
                'payload' => $data['payload'] ?? null,
                'created_by_admin_id' => $data['created_by_admin_id'] ?? null,
                'reversed_entry_id' => $data['reversed_entry_id'] ?? null,
            ]);

            $courier->forceFill(['current_balance' => $balance])->save();

            CourierAuditLog::create([
                'courier_id' => $courier->id,
                'order_courier_shipment_id' => $shipment?->id,
                'courier_settlement_id' => $settlement?->id,
                'courier_ledger_entry_id' => $entry->id,
                'action' => 'ledger_entry_posted',
                'auditable_type' => CourierLedgerEntry::class,
                'auditable_id' => $entry->id,
                'new_values' => $entry->toArray(),
                'source' => $data['source'] ?? null,
                'created_by_admin_id' => $data['created_by_admin_id'] ?? null,
            ]);

            $this->recalculateCourierBalanceLocked($courier);

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

    private function recalculateCourierBalanceLocked(Courier $courier): void
    {
        $balance = 0;
        $entries = CourierLedgerEntry::query()
            ->where('courier_id', $courier->id)
            ->lockForUpdate()
            ->orderBy('entry_date')
            ->orderBy('id')
            ->get();

        foreach ($entries as $entry) {
            $balance = round($balance + (float) $entry->debit - (float) $entry->credit, 8);

            if (round((float) $entry->balance, 8) !== $balance) {
                $entry->forceFill(['balance' => $balance])->saveQuietly();
            }
        }

        $courier->forceFill(['current_balance' => $balance])->save();
    }

    private function isCollectionConfirmed(Order $order, OrderCourierShipment $shipment): bool
    {
        $haystack = strtolower(implode(' ', array_filter([
            $order->order_status,
            $shipment->courier_status,
            $shipment->cod_collection_status,
            $shipment->settlement_status,
        ])));

        return str_contains($haystack, 'delivered')
            || str_contains($haystack, 'collected')
            || str_contains($haystack, 'settled')
            || str_contains($haystack, 'paid')
            || $shipment->cod_collected_at !== null;
    }

    private function isSettled(OrderCourierShipment $shipment): bool
    {
        $status = strtolower((string) $shipment->settlement_status);

        if (in_array($status, ['collection_accounted', 'collected_unsettled', 'unsettled', 'pending', 'hold', 'disputed'], true)) {
            return false;
        }

        return $shipment->settlement_paid_at !== null
            || str_contains($status, 'settled')
            || str_contains($status, 'paid')
            || str_contains($status, 'received');
    }

    private function isReturned(Order $order, OrderCourierShipment $shipment): bool
    {
        $haystack = strtolower(implode(' ', array_filter([
            $order->order_status,
            $shipment->courier_status,
            $shipment->settlement_status,
        ])));

        return str_contains($haystack, 'return');
    }

    private function shipmentSettlementStatus(float $netReceivable, float $netReceived, ?string $currentStatus): ?string
    {
        if ($netReceivable <= 0 && $netReceived <= 0) {
            return $currentStatus;
        }

        if ($netReceived <= 0) {
            return $currentStatus ?: 'unsettled';
        }

        return $netReceived + 0.0001 >= $netReceivable ? 'settled' : 'partially_settled';
    }

    private function date(mixed $date): string
    {
        if ($date instanceof CarbonInterface) {
            return $date->toDateString();
        }

        return $date ? date('Y-m-d', strtotime((string) $date)) : now()->toDateString();
    }

    private function parcelId(?OrderCourierShipment $shipment): ?string
    {
        return $shipment?->courier_consignment_id ?: $shipment?->courier_parcel_id ?: $shipment?->courier_tracking_id;
    }

    private function key(mixed ...$parts): string
    {
        return sha1(implode('|', array_map(static fn ($part) => is_scalar($part) ? (string) $part : json_encode($part), $parts)));
    }
}
