<?php

declare(strict_types=1);

namespace App\Business\Report;

use App\Business\Settlement\Enums\SettlementDocumentStatus;
use App\Business\Settlement\Enums\SettlementFundsUsageStatus;
use App\Business\Settlement\Enums\SettlementRecordStatus;
use App\Business\Settlement\Enums\SettlementStatementStatus;
use App\Business\Settlement\Enums\SettlementWriteOffStatus;
use App\Models\DberpCustomer;
use App\Models\DberpPayableRecord;
use App\Models\DberpPaymentOrder;
use App\Models\DberpReceiptOrder;
use App\Models\DberpReceivableRecord;
use App\Models\DberpSettlementStatement;
use App\Models\DberpSettlementWriteOff;
use App\Models\DberpSupplier;
use App\Support\DberpNumberFormatter;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

/**
 * 往来结算分析：余额为当前数据库结余快照；收付款趋势按登记单业务日期且已确认；核销按核销单业务日期；账龄按截止日与应收/应付到期口径。
 */
final class SettlementAnalysisReportService
{
    /**
     * @param  array<string, mixed>  $formState
     * @return array<string, mixed>
     */
    public function normalizeFilters(array $formState): array
    {
        $filters = [
            'business_time_from' => $formState['business_time_from'] ?? null,
            'business_time_until' => $formState['business_time_until'] ?? null,
            'as_of_date' => $formState['as_of_date'] ?? null,
            'customer_id' => (int) ($formState['customer_id'] ?? 0),
            'supplier_id' => (int) ($formState['supplier_id'] ?? 0),
        ];
        if (empty($filters['business_time_from'])) {
            unset($filters['business_time_from']);
        }
        if (empty($filters['business_time_until'])) {
            unset($filters['business_time_until']);
        }
        if (empty($filters['as_of_date'])) {
            unset($filters['as_of_date']);
        }
        if ($filters['customer_id'] <= 0) {
            unset($filters['customer_id']);
        }
        if ($filters['supplier_id'] <= 0) {
            unset($filters['supplier_id']);
        }

        return $filters;
    }

    public function asOfCarbon(array $filters): CarbonInterface
    {
        $raw = $filters['as_of_date'] ?? $filters['business_time_until'] ?? now()->toDateString();

        return now()->parse((string) $raw)->startOfDay();
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return array<string, int|string|float>
     */
    public function summary(array $filters): array
    {
        $asOf = $this->asOfCarbon($filters);

        $receivableBase = $this->receivableOpenBase($filters);
        $payableBase = $this->payableOpenBase($filters);

        $receiptBase = $this->effectiveReceiptQuery($filters);
        $paymentBase = $this->effectivePaymentQuery($filters);

        $writeOffBase = DberpSettlementWriteOff::query()
            ->where('write_off_status', SettlementWriteOffStatus::Confirmed->value);
        $this->applyWriteOffBusinessDateRange($writeOffBase, $filters);

        $statementBase = DberpSettlementStatement::query()
            ->where('statement_status', SettlementStatementStatus::Confirmed->value);
        $this->applyStatementDateRange($statementBase, $filters);

        $receiptUnwritten = (clone $receiptBase)->where('write_off_status', SettlementFundsUsageStatus::Unwritten->value)->count();
        $paymentUnwritten = (clone $paymentBase)->where('write_off_status', SettlementFundsUsageStatus::Unwritten->value)->count();

        $receivableOverdue = (int) (clone $receivableBase)->where(function ($q) use ($asOf): void {
            $q->whereDate('due_date', '<', $asOf->toDateString())
                ->orWhere(function ($sub) use ($asOf): void {
                    $sub->whereNull('due_date')->whereDate('business_date', '<', $asOf->toDateString());
                });
        })->count();

        $payableOverdue = (int) (clone $payableBase)->where(function ($q) use ($asOf): void {
            $q->whereDate('due_date', '<', $asOf->toDateString())
                ->orWhere(function ($sub) use ($asOf): void {
                    $sub->whereNull('due_date')->whereDate('business_date', '<', $asOf->toDateString());
                });
        })->count();

        return [
            'receivable_remaining_total' => $this->decimal((clone $receivableBase)->sum('remaining_amount')),
            'payable_remaining_total' => $this->decimal((clone $payableBase)->sum('remaining_amount')),
            'receipt_amount_period' => $this->decimal((clone $receiptBase)->sum('amount')),
            'payment_amount_period' => $this->decimal((clone $paymentBase)->sum('amount')),
            'write_off_confirmed_count' => (int) (clone $writeOffBase)->count(),
            'receipt_unwritten_count' => $receiptUnwritten,
            'payment_unwritten_count' => $paymentUnwritten,
            'statement_confirmed_count' => (int) (clone $statementBase)->count(),
            'receivable_overdue_open_count' => $receivableOverdue,
            'payable_overdue_open_count' => $payableOverdue,
        ];
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return list<array{date: string, amount: string}>
     */
    public function dailyReceiptTrend(array $filters): array
    {
        return $this->dailyAmountTrend($this->effectiveReceiptQuery($filters), 'amount');
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return list<array{date: string, amount: string}>
     */
    public function dailyPaymentTrend(array $filters): array
    {
        return $this->dailyAmountTrend($this->effectivePaymentQuery($filters), 'amount');
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return list<array{id: int, name: string, amount: string}>
     */
    public function topCustomersByRemaining(array $filters, int $limit = 8): array
    {
        $base = $this->receivableOpenBase($filters);

        $rows = (clone $base)
            ->select('customer_id', DB::raw('SUM(remaining_amount) as total_remaining'))
            ->groupBy('customer_id')
            ->orderByDesc('total_remaining')
            ->limit($limit)
            ->get();

        $names = DberpCustomer::query()->whereIn('id', $rows->pluck('customer_id'))->pluck('name', 'id');

        $out = [];
        foreach ($rows as $row) {
            $id = (int) $row->customer_id;
            $out[] = [
                'id' => $id,
                'name' => (string) ($names[$id] ?? (string) $id),
                'amount' => $this->decimal($row->total_remaining ?? 0),
            ];
        }

        return $out;
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return list<array{id: int, name: string, amount: string}>
     */
    public function topSuppliersByRemaining(array $filters, int $limit = 8): array
    {
        $base = $this->payableOpenBase($filters);

        $rows = (clone $base)
            ->select('supplier_id', DB::raw('SUM(remaining_amount) as total_remaining'))
            ->groupBy('supplier_id')
            ->orderByDesc('total_remaining')
            ->limit($limit)
            ->get();

        $names = DberpSupplier::query()->whereIn('id', $rows->pluck('supplier_id'))->pluck('name', 'id');

        $out = [];
        foreach ($rows as $row) {
            $id = (int) $row->supplier_id;
            $out[] = [
                'id' => $id,
                'name' => (string) ($names[$id] ?? (string) $id),
                'amount' => $this->decimal($row->total_remaining ?? 0),
            ];
        }

        return $out;
    }

    /**
     * 应收账龄分布（按剩余金额汇总到各账龄区间）。
     *
     * @param  array<string, mixed>  $filters
     * @return array<string, string>
     */
    public function receivableAgingBuckets(array $filters): array
    {
        return $this->agingBucketsForReceivable($this->receivableOpenBase($filters), $this->asOfCarbon($filters));
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return array<string, string>
     */
    public function payableAgingBuckets(array $filters): array
    {
        return $this->agingBucketsForPayable($this->payableOpenBase($filters), $this->asOfCarbon($filters));
    }

    /**
     * @param  Builder<DberpReceivableRecord>  $base
     * @return array<string, string>
     */
    private function agingBucketsForReceivable(Builder $base, CarbonInterface $asOf): array
    {
        $totals = [
            'current_amount' => 0.0,
            'bucket_1_30_amount' => 0.0,
            'bucket_31_60_amount' => 0.0,
            'bucket_61_90_amount' => 0.0,
            'bucket_91_plus_amount' => 0.0,
        ];

        foreach ((clone $base)->get() as $record) {
            $bucket = $this->resolveBucket($record->due_date ?? $record->business_date, $asOf);
            $totals[$bucket] += (float) $record->remaining_amount;
        }

        return array_map(fn (float $v): string => $this->decimal($v), $totals);
    }

    /**
     * @param  Builder<DberpPayableRecord>  $base
     * @return array<string, string>
     */
    private function agingBucketsForPayable(Builder $base, CarbonInterface $asOf): array
    {
        $totals = [
            'current_amount' => 0.0,
            'bucket_1_30_amount' => 0.0,
            'bucket_31_60_amount' => 0.0,
            'bucket_61_90_amount' => 0.0,
            'bucket_91_plus_amount' => 0.0,
        ];

        foreach ((clone $base)->get() as $record) {
            $bucket = $this->resolveBucket($record->due_date ?? $record->business_date, $asOf);
            $totals[$bucket] += (float) $record->remaining_amount;
        }

        return array_map(fn (float $v): string => $this->decimal($v), $totals);
    }

    private function resolveBucket(mixed $dueDate, CarbonInterface $asOfDate): string
    {
        $baseDate = $dueDate instanceof CarbonInterface ? $dueDate->copy()->startOfDay() : now()->parse((string) $dueDate)->startOfDay();

        if ($baseDate->greaterThanOrEqualTo($asOfDate)) {
            return 'current_amount';
        }

        $days = $baseDate->diffInDays($asOfDate);

        return match (true) {
            $days <= 30 => 'bucket_1_30_amount',
            $days <= 60 => 'bucket_31_60_amount',
            $days <= 90 => 'bucket_61_90_amount',
            default => 'bucket_91_plus_amount',
        };
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return Builder<DberpReceivableRecord>
     */
    private function receivableOpenBase(array $filters): Builder
    {
        return DberpReceivableRecord::query()
            ->where('remaining_amount', '>', 0)
            ->whereNot('record_status', SettlementRecordStatus::Voided->value)
            ->when($filters['customer_id'] ?? null, fn ($q, int $id) => $q->where('customer_id', $id));
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return Builder<DberpPayableRecord>
     */
    private function payableOpenBase(array $filters): Builder
    {
        return DberpPayableRecord::query()
            ->where('remaining_amount', '>', 0)
            ->whereNot('record_status', SettlementRecordStatus::Voided->value)
            ->when($filters['supplier_id'] ?? null, fn ($q, int $id) => $q->where('supplier_id', $id));
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return Builder<DberpReceiptOrder>
     */
    private function effectiveReceiptQuery(array $filters): Builder
    {
        return DberpReceiptOrder::query()
            ->where('status', SettlementDocumentStatus::Effective->value)
            ->when($filters['business_time_from'] ?? null, fn ($q, string $d) => $q->whereDate('business_time', '>=', $d))
            ->when($filters['business_time_until'] ?? null, fn ($q, string $d) => $q->whereDate('business_time', '<=', $d))
            ->when($filters['customer_id'] ?? null, fn ($q, int $id) => $q->where('customer_id', $id));
    }

    /**
     * @param  array<string, mixed>  $filters
     * @return Builder<DberpPaymentOrder>
     */
    private function effectivePaymentQuery(array $filters): Builder
    {
        return DberpPaymentOrder::query()
            ->where('status', SettlementDocumentStatus::Effective->value)
            ->when($filters['business_time_from'] ?? null, fn ($q, string $d) => $q->whereDate('business_time', '>=', $d))
            ->when($filters['business_time_until'] ?? null, fn ($q, string $d) => $q->whereDate('business_time', '<=', $d))
            ->when($filters['supplier_id'] ?? null, fn ($q, int $id) => $q->where('supplier_id', $id));
    }

    /**
     * @param  Builder<DberpSettlementWriteOff>  $query
     * @param  array<string, mixed>  $filters
     */
    private function applyWriteOffBusinessDateRange(Builder $query, array $filters): void
    {
        if (! empty($filters['business_time_from'])) {
            $query->whereDate('business_date', '>=', $filters['business_time_from']);
        }
        if (! empty($filters['business_time_until'])) {
            $query->whereDate('business_date', '<=', $filters['business_time_until']);
        }
    }

    /**
     * @param  Builder<DberpSettlementStatement>  $query
     * @param  array<string, mixed>  $filters
     */
    private function applyStatementDateRange(Builder $query, array $filters): void
    {
        if (! empty($filters['business_time_from'])) {
            $query->whereDate('statement_date', '>=', $filters['business_time_from']);
        }
        if (! empty($filters['business_time_until'])) {
            $query->whereDate('statement_date', '<=', $filters['business_time_until']);
        }
    }

    /**
     * @param  Builder<DberpReceiptOrder|DberpPaymentOrder>  $base
     * @return list<array{date: string, amount: string}>
     */
    private function dailyAmountTrend(Builder $base, string $column): array
    {
        $rows = (clone $base)
            ->selectRaw('DATE(business_time) as stat_date')
            ->selectRaw('SUM('.$column.') as total_amount')
            ->groupBy(DB::raw('DATE(business_time)'))
            ->orderBy('stat_date')
            ->get();

        $out = [];
        foreach ($rows as $row) {
            $out[] = [
                'date' => (string) $row->stat_date,
                'amount' => $this->decimal($row->total_amount ?? 0),
            ];
        }

        return $out;
    }

    private function decimal(mixed $value): string
    {
        return DberpNumberFormatter::money((float) $value);
    }
}
