<?php

namespace App\Http\Controllers\Admin;

use App\CentralLogics\Helpers;
use App\Http\Controllers\Controller;
use App\Models\Courier;
use App\Models\CourierLedgerEntry;
use App\Models\CourierReconciliationImport;
use App\Models\CourierSettlement;
use App\Models\OrderCourierShipment;
use App\Models\OrderCourierShipmentLog;
use App\Services\Courier\CourierLedgerService;
use App\Services\Courier\CourierReconciliationService;
use App\Services\Courier\CourierSettlementService;
use Brian2694\Toastr\Facades\Toastr;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\StreamedResponse;

class CourierAccountingController extends Controller
{
    public function __construct(
        private readonly CourierLedgerService $ledgerService,
        private readonly CourierSettlementService $settlementService,
        private readonly CourierReconciliationService $reconciliationService
    ) {
    }

    public function accounts(Request $request)
    {
        $this->ledgerService->syncLegacyCouriersFromShipments();

        $couriers = Courier::query()
            ->when($request->search, function ($query) use ($request) {
                $key = trim($request->search);
                $query->where(function ($subQuery) use ($key) {
                    $subQuery->where('name', 'like', "%{$key}%")
                        ->orWhere('provider_key', 'like', "%{$key}%")
                        ->orWhere('api_type', 'like', "%{$key}%");
                });
            })
            ->latest('id')
            ->paginate(config('default_pagination'))
            ->withQueryString();

        return view('admin-views.courier.accounts', compact('couriers'));
    }

    public function updateAccount(Request $request, Courier $courier)
    {
        $this->permission('courier_adjustment');

        $data = $request->validate([
            'name' => 'required|string|max:191',
            'api_type' => 'nullable|string|max:80',
            'receivable_account_name' => 'required|string|max:191',
            'charge_expense_account_name' => 'required|string|max:191',
            'return_charge_expense_account_name' => 'required|string|max:191',
            'cod_charge_expense_account_name' => 'required|string|max:191',
            'default_settlement_account_name' => 'required|string|max:191',
            'opening_balance' => 'nullable|numeric',
            'opening_balance_direction' => 'nullable|in:receivable,payable',
            'opening_balance_date' => 'nullable|date',
            'opening_balance_reference' => 'nullable|string|max:191',
            'opening_balance_note' => 'nullable|string|max:1000',
            'is_active' => 'nullable|boolean',
        ]);

        $data = $this->normaliseCourierAccountData($request, $data);
        $data['is_active'] = $request->boolean('is_active');
        $courier->update($data);
        $this->ledgerService->syncOpeningBalance($courier->fresh(), auth('admin')->id());

        Toastr::success(translate('Courier account updated successfully.'));
        return back();
    }

    public function orders(Request $request)
    {
        $couriers = Courier::orderBy('name')->get();
        $queryError = null;
        $summary = $this->emptyShipmentSummary();

        try {
            $query = $this->shipmentQuery($request);
            $summary = $this->shipmentSummary(clone $query);
            $shipments = $query->latest('id')->paginate(config('default_pagination'))->withQueryString();
        } catch (\Throwable $throwable) {
            Log::error('Courier orders report failed.', [
                'message' => $throwable->getMessage(),
                'trace' => $throwable->getTraceAsString(),
            ]);
            $queryError = $throwable->getMessage();
            $shipments = $this->emptyPaginator($request);
        }

        $mode = 'orders';

        return $this->renderOrdersView(compact('shipments', 'couriers', 'summary', 'mode', 'queryError'));
    }

    public function ledger(Request $request)
    {
        $couriers = Courier::orderBy('name')->get();
        $query = CourierLedgerEntry::query()
            ->with(['courier', 'order', 'shipment', 'settlement'])
            ->when($request->courier_id, fn ($q) => $q->where('courier_id', $request->courier_id))
            ->when($request->event_type && $request->event_type !== 'all', fn ($q) => $q->where('event_type', $request->event_type))
            ->when($request->status && $request->status !== 'all', fn ($q) => $q->where('status', $request->status))
            ->when($request->parcel_id, fn ($q) => $q->where('parcel_id', 'like', '%' . trim($request->parcel_id) . '%'))
            ->when($request->invoice_no, fn ($q) => $q->where('invoice_no', 'like', '%' . trim($request->invoice_no) . '%'))
            ->when($request->filled('from'), fn ($q) => $q->whereDate('entry_date', '>=', $request->from))
            ->when($request->filled('to'), fn ($q) => $q->whereDate('entry_date', '<=', $request->to));

        $summary = $this->ledgerSummary(clone $query);
        $entries = $query->oldest('entry_date')->oldest('id')->paginate(config('default_pagination'))->withQueryString();
        $eventTypes = collect([
            CourierLedgerEntry::EVENT_OPENING_BALANCE,
            CourierLedgerEntry::EVENT_HANDOVER,
            CourierLedgerEntry::EVENT_COD_COLLECTION,
            CourierLedgerEntry::EVENT_DELIVERY_CHARGE,
            CourierLedgerEntry::EVENT_COD_CHARGE,
            CourierLedgerEntry::EVENT_RETURN_CHARGE,
            CourierLedgerEntry::EVENT_SETTLEMENT,
            CourierLedgerEntry::EVENT_ADJUSTMENT,
            CourierLedgerEntry::EVENT_REVERSAL,
        ])->merge(CourierLedgerEntry::query()->select('event_type')->distinct()->pluck('event_type'))->unique()->values();

        return view('admin-views.courier.ledger', compact('entries', 'couriers', 'summary', 'eventTypes'));
    }

    public function backfillLedger(Request $request)
    {
        $this->permission('courier_adjustment');

        $query = OrderCourierShipment::query()
            ->with('order')
            ->whereNotNull('provider')
            ->when($request->courier_id, fn ($q) => $q->where('courier_id', $request->courier_id));
        $processed = 0;
        $openingSynced = 0;

        Courier::query()
            ->when($request->courier_id, fn ($q) => $q->where('id', $request->courier_id))
            ->each(function (Courier $courier) use (&$openingSynced) {
                if ($this->ledgerService->syncOpeningBalance($courier, auth('admin')->id())) {
                    $openingSynced++;
                }
            });

        $query->chunkById(100, function ($shipments) use (&$processed) {
            foreach ($shipments as $shipment) {
                if (! $shipment->order) {
                    continue;
                }

                $this->ledgerService->recordShipmentAccounting(
                    order: $shipment->order,
                    shipment: $shipment,
                    payload: ['legacy_backfill' => true],
                    adminId: auth('admin')->id(),
                    source: 'legacy_backfill'
                );
                $processed++;
            }
        });

        Toastr::success(translate('Courier ledger backfill completed for') . ' ' . $processed . ' ' . translate('parcels') . '. ' . translate('Opening balances synced') . ': ' . $openingSynced . '.');
        return back();
    }

    public function storeAdjustment(Request $request)
    {
        $this->permission('courier_adjustment');

        $data = $request->validate([
            'courier_id' => 'required|exists:couriers,id',
            'entry_side' => 'required|in:debit,credit',
            'amount' => 'required|numeric|min:0.01',
            'particulars' => 'required|string|max:191',
        ]);

        $courier = Courier::findOrFail($data['courier_id']);
        $this->ledgerService->postManualAdjustment(
            courier: $courier,
            debit: $data['entry_side'] === 'debit' ? (float) $data['amount'] : 0,
            credit: $data['entry_side'] === 'credit' ? (float) $data['amount'] : 0,
            particulars: $data['particulars'],
            adminId: auth('admin')->id()
        );

        Toastr::success(translate('Courier manual adjustment posted successfully.'));
        return back();
    }

    public function outstanding(Request $request)
    {
        $couriers = Courier::orderBy('name')->get();
        $queryError = null;
        $summary = $this->emptyShipmentSummary();

        try {
            $query = $this->shipmentQuery($request)
                ->where(function ($subQuery) {
                    $subQuery->whereNull('settlement_status')
                        ->orWhereNotIn('settlement_status', ['settled', 'paid']);
                });
            $summary = $this->shipmentSummary(clone $query);
            $shipments = $query->latest('id')->paginate(config('default_pagination'))->withQueryString();
        } catch (\Throwable $throwable) {
            Log::error('Courier outstanding report failed.', [
                'message' => $throwable->getMessage(),
                'trace' => $throwable->getTraceAsString(),
            ]);
            $queryError = $throwable->getMessage();
            $shipments = $this->emptyPaginator($request);
        }

        $mode = 'outstanding';

        return $this->renderOrdersView(compact('shipments', 'couriers', 'summary', 'mode', 'queryError'));
    }

    public function settlements(Request $request)
    {
        $couriers = Courier::orderBy('name')->get();
        $settlements = CourierSettlement::query()
            ->with('courier')
            ->when($request->courier_id, fn ($q) => $q->where('courier_id', $request->courier_id))
            ->when($request->status && $request->status !== 'all', fn ($q) => $q->where('status', $request->status))
            ->when($request->search, function ($query) use ($request) {
                $key = trim($request->search);
                $query->where(function ($subQuery) use ($key) {
                    $subQuery->where('voucher_no', 'like', "%{$key}%")
                        ->orWhere('reference_no', 'like', "%{$key}%")
                        ->orWhere('note', 'like', "%{$key}%");
                });
            })
            ->when($request->filled('from'), fn ($q) => $q->whereDate('settlement_date', '>=', $request->from))
            ->when($request->filled('to'), fn ($q) => $q->whereDate('settlement_date', '<=', $request->to))
            ->latest('settlement_date')
            ->latest('id')
            ->paginate(config('default_pagination'))
            ->withQueryString();

        return view('admin-views.courier.settlements.index', compact('settlements', 'couriers'));
    }

    public function createSettlement(Request $request)
    {
        $this->permission('courier_settlement');

        $couriers = Courier::where('is_active', true)->orderBy('name')->get();
        $shipments = collect();

        if ($request->courier_id) {
            $shipments = $this->shipmentQuery($request)
                ->where('courier_id', $request->courier_id)
                ->where(function ($query) {
                    $query->whereNull('settlement_status')
                        ->orWhereNotIn('settlement_status', ['settled', 'paid']);
                })
                ->latest('id')
                ->paginate(100)
                ->withQueryString();
        }

        return view('admin-views.courier.settlements.create', compact('couriers', 'shipments'));
    }

    public function storeSettlement(Request $request)
    {
        $this->permission('courier_settlement');

        $data = $request->validate([
            'courier_id' => 'required|exists:couriers,id',
            'settlement_date' => 'required|date',
            'reference_no' => 'nullable|string|max:191',
            'payment_method' => 'nullable|string|max:80',
            'settlement_account' => 'nullable|string|max:191',
            'net_received_amount' => 'nullable|numeric|min:0',
            'adjustment_amount' => 'nullable|numeric',
            'note' => 'nullable|string',
            'shipment_ids' => 'required|array|min:1',
            'shipment_ids.*' => 'integer|exists:order_courier_shipments,id',
        ]);

        try {
            $settlement = $this->settlementService->create($data, auth('admin')->id());
            Toastr::success(translate('Courier settlement posted successfully.'));
            return redirect()->route('admin.courier.settlements.show', $settlement->id);
        } catch (\Throwable $throwable) {
            Toastr::error($throwable->getMessage());
            return back()->withInput();
        }
    }

    public function showSettlement(CourierSettlement $settlement)
    {
        $settlement->load(['courier', 'items.shipment.order.customer', 'ledgerEntries']);

        return view('admin-views.courier.settlements.show', compact('settlement'));
    }

    public function reverseSettlement(Request $request, CourierSettlement $settlement)
    {
        $this->permission('courier_adjustment');

        $request->validate(['reverse_reason' => 'required|string|max:191']);
        $this->settlementService->reverse($settlement, $request->reverse_reason, auth('admin')->id());

        Toastr::success(translate('Courier settlement reversed with audit voucher.'));
        return back();
    }

    public function charges(Request $request)
    {
        $couriers = Courier::orderBy('name')->get();
        $shipments = $this->shipmentQuery($request)
            ->where(function ($query) {
                $query->where('actual_delivery_cost', '>', 0)
                    ->orWhere('actual_cod_charge', '>', 0)
                    ->orWhere('actual_return_cost', '>', 0);
            })
            ->latest('id')
            ->paginate(config('default_pagination'))
            ->withQueryString();

        return view('admin-views.courier.charges', compact('shipments', 'couriers'));
    }

    public function reconciliation(Request $request)
    {
        $couriers = Courier::orderBy('name')->get();
        $imports = CourierReconciliationImport::query()
            ->with('courier')
            ->when($request->courier_id, fn ($q) => $q->where('courier_id', $request->courier_id))
            ->latest('id')
            ->paginate(config('default_pagination'))
            ->withQueryString();

        return view('admin-views.courier.reconciliation', compact('imports', 'couriers'));
    }

    public function importReconciliation(Request $request)
    {
        $this->permission('courier_adjustment');

        $data = $request->validate([
            'courier_id' => 'required|exists:couriers,id',
            'import_date' => 'required|date',
            'reference_no' => 'nullable|string|max:191',
            'statement_file' => 'required|file|mimes:csv,txt|max:5120',
        ]);

        $courier = Courier::findOrFail($data['courier_id']);
        $this->reconciliationService->importCsv($courier, $request->file('statement_file'), $data, auth('admin')->id());

        Toastr::success(translate('Courier reconciliation statement imported successfully.'));
        return back();
    }

    public function logs(Request $request)
    {
        $this->permission('courier_api_log');

        $logs = OrderCourierShipmentLog::query()
            ->with(['shipment.courier', 'order'])
            ->when($request->provider, fn ($q) => $q->where('provider', $request->provider))
            ->when($request->action, fn ($q) => $q->where('action', $request->action))
            ->when($request->result === 'success', fn ($q) => $q->where('success', true))
            ->when($request->result === 'failed', fn ($q) => $q->where('success', false))
            ->when($request->filled('from'), fn ($q) => $q->whereDate('created_at', '>=', $request->from))
            ->when($request->filled('to'), fn ($q) => $q->whereDate('created_at', '<=', $request->to))
            ->latest('id')
            ->paginate(config('default_pagination'))
            ->withQueryString();
        $providers = OrderCourierShipmentLog::query()->whereNotNull('provider')->select('provider')->distinct()->pluck('provider');
        $actions = OrderCourierShipmentLog::query()->select('action')->distinct()->pluck('action');

        return view('admin-views.courier.logs', compact('logs', 'providers', 'actions'));
    }

    public function export(Request $request, string $report, string $type = 'csv'): StreamedResponse
    {
        $this->permission('courier_export');

        return match ($report) {
            'orders' => $this->exportShipments($request, 'courier-orders.csv'),
            'outstanding' => $this->exportShipments($request, 'courier-outstanding.csv', true),
            'ledger' => $this->exportLedger($request),
            default => abort(404),
        };
    }

    private function shipmentQuery(Request $request)
    {
        return OrderCourierShipment::query()
            ->with(['courier', 'order.customer'])
            ->when($request->courier_id, fn ($q) => $q->where('courier_id', $request->courier_id))
            ->when($request->courier_status, fn ($q) => $q->where('courier_status', 'like', '%' . trim($request->courier_status) . '%'))
            ->when($request->collection_status && $request->collection_status !== 'all', fn ($q) => $q->where('cod_collection_status', $request->collection_status))
            ->when($request->settlement_status && $request->settlement_status !== 'all', fn ($q) => $q->where('settlement_status', $request->settlement_status))
            ->when($request->parcel_id, function ($query) use ($request) {
                $key = trim($request->parcel_id);
                $query->where(function ($subQuery) use ($key) {
                    $subQuery->where('courier_parcel_id', 'like', "%{$key}%")
                        ->orWhere('courier_consignment_id', 'like', "%{$key}%")
                        ->orWhere('courier_tracking_id', 'like', "%{$key}%");
                });
            })
            ->when($request->invoice_no, fn ($q) => is_numeric($request->invoice_no) ? $q->where('order_id', $request->invoice_no) : $q)
            ->when($request->mobile, function ($query) use ($request) {
                $key = trim($request->mobile);
                $query->whereHas('order.customer', fn ($subQuery) => $subQuery->where('phone', 'like', "%{$key}%"));
            })
            ->when($request->order_status, fn ($q) => $q->whereHas('order', fn ($subQuery) => $subQuery->where('order_status', $request->order_status)))
            ->when($request->payment_status, fn ($q) => $q->whereHas('order', fn ($subQuery) => $subQuery->where('payment_status', $request->payment_status)))
            ->when($request->filled('from'), fn ($q) => $q->whereDate('created_at', '>=', $request->from))
            ->when($request->filled('to'), fn ($q) => $q->whereDate('created_at', '<=', $request->to));
    }

    private function renderOrdersView(array $data)
    {
        try {
            return response(view('admin-views.courier.orders', $data)->render());
        } catch (\Throwable $throwable) {
            Log::error('Courier orders/outstanding view failed.', [
                'mode' => $data['mode'] ?? null,
                'message' => $throwable->getMessage(),
                'trace' => $throwable->getTraceAsString(),
            ]);

            return response($this->fallbackCourierReportHtml($data['mode'] ?? 'orders', $throwable->getMessage()));
        }
    }

    private function fallbackCourierReportHtml(string $mode, string $error): string
    {
        $title = $mode === 'outstanding' ? 'Courier Outstanding Report' : 'Courier Orders Report';
        $safeError = e($error);
        $accountsUrl = e(route('admin.courier.accounts'));
        $ledgerUrl = e(route('admin.courier.ledger'));
        $ordersUrl = e(route('admin.courier.orders'));
        $outstandingUrl = e(route('admin.courier.outstanding'));

        return <<<HTML
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{$title}</title>
    <style>
        body{font-family:Arial,sans-serif;background:#f7f8fb;color:#1f2937;margin:0;padding:32px}
        .box{max-width:960px;margin:0 auto;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:24px}
        .warning{background:#fff7ed;border:1px solid #fdba74;color:#9a3412;border-radius:6px;padding:14px;margin:16px 0}
        .links a{display:inline-block;margin:6px 8px 0 0;padding:8px 12px;border:1px solid #0f766e;color:#0f766e;text-decoration:none;border-radius:4px}
        code{white-space:pre-wrap;word-break:break-word}
    </style>
</head>
<body>
    <div class="box">
        <h1>{$title}</h1>
        <p>The courier report route is reachable, but the full table view could not render. Existing live data was not changed.</p>
        <div class="warning"><strong>Render warning:</strong><br><code>{$safeError}</code></div>
        <p>Please send the latest <code>storage/logs/laravel.log</code> lines so the exact field/view mismatch can be fixed.</p>
        <div class="links">
            <a href="{$accountsUrl}">Courier Accounts</a>
            <a href="{$ledgerUrl}">Courier Ledger</a>
            <a href="{$ordersUrl}">Courier Orders</a>
            <a href="{$outstandingUrl}">Outstanding</a>
        </div>
    </div>
</body>
</html>
HTML;
    }

    private function normaliseCourierAccountData(Request $request, array $data): array
    {
        $openingBalance = abs((float) ($data['opening_balance'] ?? 0));
        $direction = $request->input('opening_balance_direction');

        if (! in_array($direction, ['receivable', 'payable'], true)) {
            $direction = ((float) ($data['opening_balance'] ?? 0)) < 0 ? 'payable' : 'receivable';
        }

        $data['opening_balance'] = $direction === 'payable' ? -$openingBalance : $openingBalance;
        unset($data['opening_balance_direction']);

        foreach (['opening_balance_date', 'opening_balance_reference', 'opening_balance_note'] as $column) {
            if (! Schema::hasColumn('couriers', $column)) {
                unset($data[$column]);
            }
        }

        return $data;
    }

    private function ledgerSummary($query): array
    {
        return [
            'opening_balance' => $this->eventNet(clone $query, CourierLedgerEntry::EVENT_OPENING_BALANCE),
            'cod_collection' => $this->eventDebit(clone $query, CourierLedgerEntry::EVENT_COD_COLLECTION),
            'delivery_charge' => $this->eventCredit(clone $query, CourierLedgerEntry::EVENT_DELIVERY_CHARGE),
            'cod_charge' => $this->eventCredit(clone $query, CourierLedgerEntry::EVENT_COD_CHARGE),
            'return_charge' => $this->eventCredit(clone $query, CourierLedgerEntry::EVENT_RETURN_CHARGE),
            'payment_received' => $this->eventCredit(clone $query, CourierLedgerEntry::EVENT_SETTLEMENT),
            'debit' => (float) (clone $query)->sum('debit'),
            'credit' => (float) (clone $query)->sum('credit'),
            'balance' => (float) ((clone $query)->latest('entry_date')->latest('id')->value('balance') ?? 0),
        ];
    }

    private function eventDebit($query, string $eventType): float
    {
        return (float) $query->where('event_type', $eventType)->sum('debit');
    }

    private function eventCredit($query, string $eventType): float
    {
        return (float) $query->where('event_type', $eventType)->sum('credit');
    }

    private function eventNet($query, string $eventType): float
    {
        return $this->eventDebit(clone $query, $eventType) - $this->eventCredit(clone $query, $eventType);
    }

    private function shipmentSummary($query): array
    {
        $codAmount = $this->safeShipmentSum(clone $query, 'collectable_amount');
        $deliveryCharge = $this->safeShipmentSum(clone $query, 'actual_delivery_cost');
        $codCharge = $this->safeShipmentSum(clone $query, 'actual_cod_charge');
        $returnCharge = $this->safeShipmentSum(clone $query, 'actual_return_cost');
        $netReceived = $this->safeShipmentSum(clone $query, 'settled_amount');

        return [
            'cod_amount' => $codAmount,
            'delivery_charge' => $deliveryCharge,
            'cod_charge' => $codCharge,
            'return_charge' => $returnCharge,
            'net_received' => $netReceived,
            'balance' => max($codAmount - $deliveryCharge - $codCharge - $returnCharge - $netReceived, 0),
        ];
    }

    private function emptyShipmentSummary(): array
    {
        return [
            'cod_amount' => 0,
            'delivery_charge' => 0,
            'cod_charge' => 0,
            'return_charge' => 0,
            'net_received' => 0,
            'balance' => 0,
        ];
    }

    private function emptyPaginator(Request $request): LengthAwarePaginator
    {
        return new LengthAwarePaginator(
            [],
            0,
            config('default_pagination') ?: 25,
            LengthAwarePaginator::resolveCurrentPage(),
            [
                'path' => $request->url(),
                'query' => $request->query(),
            ]
        );
    }

    private function safeShipmentSum($query, string $column): float
    {
        if (! Schema::hasColumn('order_courier_shipments', $column)) {
            return 0;
        }

        return (float) $query->sum($column);
    }

    private function exportShipments(Request $request, string $filename, bool $outstandingOnly = false): StreamedResponse
    {
        $query = $this->shipmentQuery($request);

        if ($outstandingOnly) {
            $query->where(function ($subQuery) {
                $subQuery->whereNull('settlement_status')
                    ->orWhereNotIn('settlement_status', ['settled', 'paid']);
            });
        }

        return $this->csv($filename, [
            'Date', 'Order', 'Customer', 'Mobile', 'Courier', 'Parcel ID', 'COD Amount', 'Courier Status',
            'Collection Status', 'Settlement Status', 'Courier Charge', 'Return Charge', 'Net Receivable',
            'Net Received', 'Balance', 'Delivered Date', 'Settlement Date',
        ], function () use ($query) {
            $query->chunkById(200, function ($shipments) {
                foreach ($shipments as $shipment) {
                    $order = $shipment->order;
                    $charges = (float) $shipment->actual_delivery_cost + (float) $shipment->actual_cod_charge + (float) $shipment->actual_return_cost;
                    $netReceivable = $shipment->net_receivable_amount ?? max((float) $shipment->collectable_amount - $charges, 0);

                    echo $this->csvLine([
                        $shipment->created_at?->toDateString(),
                        $order?->id,
                        $order?->customer?->f_name . ' ' . $order?->customer?->l_name,
                        $order?->customer?->phone,
                        $shipment->courier?->name ?: $shipment->provider,
                        $shipment->courier_consignment_id ?: $shipment->courier_parcel_id ?: $shipment->courier_tracking_id,
                        $shipment->collectable_amount,
                        $shipment->courier_status,
                        $shipment->cod_collection_status,
                        $shipment->settlement_status,
                        $shipment->actual_delivery_cost + $shipment->actual_cod_charge,
                        $shipment->actual_return_cost,
                        $netReceivable,
                        $shipment->settled_amount,
                        $shipment->balance_amount ?? ($netReceivable - (float) $shipment->settled_amount),
                        $shipment->delivered_at?->toDateString(),
                        $shipment->settlement_paid_at?->toDateString(),
                    ]);
                }
            });
        });
    }

    private function exportLedger(Request $request): StreamedResponse
    {
        $query = CourierLedgerEntry::query()
            ->with('courier')
            ->when($request->courier_id, fn ($q) => $q->where('courier_id', $request->courier_id))
            ->when($request->filled('from'), fn ($q) => $q->whereDate('entry_date', '>=', $request->from))
            ->when($request->filled('to'), fn ($q) => $q->whereDate('entry_date', '<=', $request->to))
            ->oldest('entry_date')
            ->oldest('id');

        return $this->csv('courier-ledger.csv', [
            'Date', 'Voucher No', 'Voucher Type', 'Courier', 'Order', 'Parcel ID', 'Particulars', 'Debit', 'Credit', 'Balance', 'Status',
        ], function () use ($query) {
            $query->chunkById(200, function ($entries) {
                foreach ($entries as $entry) {
                    echo $this->csvLine([
                        $entry->entry_date?->toDateString(),
                        $entry->voucher_no,
                        $entry->event_type,
                        $entry->courier?->name,
                        $entry->invoice_no,
                        $entry->parcel_id,
                        $entry->particulars,
                        $entry->debit,
                        $entry->credit,
                        $entry->balance,
                        $entry->status,
                    ]);
                }
            });
        });
    }

    private function csv(string $filename, array $headers, callable $rows): StreamedResponse
    {
        return response()->streamDownload(function () use ($headers, $rows) {
            echo $this->csvLine($headers);
            $rows();
        }, $filename, ['Content-Type' => 'text/csv']);
    }

    private function csvLine(array $row): string
    {
        $handle = fopen('php://temp', 'r+');
        fputcsv($handle, $row);
        rewind($handle);
        $line = stream_get_contents($handle);
        fclose($handle);

        return $line;
    }

    private function permission(string $module): void
    {
        if (! Helpers::module_permission_check($module)) {
            abort(403);
        }
    }
}
