<?php

declare(strict_types=1);

namespace App\Business\Settlement;

use App\Models\DberpPayableRecord;
use App\Models\DberpPaymentOrder;
use App\Models\DberpReceiptOrder;
use App\Models\DberpReceivableRecord;
use App\Models\DberpSettlementAdjustment;
use App\Models\DberpSettlementWriteOff;
use App\Support\DberpNumberFormatter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Cache;

final class SettlementReminderService
{
    /**
     * @var array<string, string|int>|null
     */
    private ?array $cachedOverviewMetrics = null;

    /**
     * @return array<string, string|int>
     */
    public function overviewMetrics(): array
    {
        if ($this->cachedOverviewMetrics !== null) {
            return $this->cachedOverviewMetrics;
        }

        $cacheSeconds = max(0, (int) config('dberp.workbench.metrics_cache_seconds', 120));

        if ($cacheSeconds > 0) {
            /** @var array<string, string|int> $metrics */
            $metrics = Cache::remember(
                'dberp.workbench.settlement_metrics',
                now()->addSeconds($cacheSeconds),
                fn (): array => $this->computeOverviewMetrics(),
            );

            return $this->cachedOverviewMetrics = $metrics;
        }

        return $this->cachedOverviewMetrics = $this->computeOverviewMetrics();
    }

    /**
     * @return list<array<string, string|int>>
     */
    public function reminderItems(): array
    {
        $metrics = $this->overviewMetrics();

        return [
            [
                'key' => 'due_today_receivable',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_due_today_receivable'),
                'count' => (int) $metrics['due_today_receivable_count'],
                'amount' => (string) $metrics['due_today_receivable_amount'],
            ],
            [
                'key' => 'overdue_receivable',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_overdue_receivable'),
                'count' => (int) $metrics['overdue_receivable_count'],
                'amount' => (string) $metrics['overdue_receivable_amount'],
            ],
            [
                'key' => 'due_soon_payable',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_due_soon_payable'),
                'count' => (int) $metrics['due_soon_payable_count'],
                'amount' => (string) $metrics['due_soon_payable_amount'],
            ],
            [
                'key' => 'large_unwritten',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_large_unwritten'),
                'count' => (int) $metrics['large_unwritten_count'],
                'amount' => '',
            ],
            [
                'key' => 'long_unwritten',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_long_unwritten'),
                'count' => (int) $metrics['long_unwritten_count'],
                'amount' => '',
            ],
            [
                'key' => 'reversed_write_off',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_reversed_write_off'),
                'count' => (int) $metrics['reversed_write_off_count'],
                'amount' => '',
            ],
            [
                'key' => 'recent_adjustment',
                'title' => (string) __('dberp.widgets.settlement_workbench.reminder_recent_adjustment'),
                'count' => (int) $metrics['recent_adjustment_count'],
                'amount' => '',
            ],
        ];
    }

    /**
     * @return array<string, string|int>
     */
    private function computeOverviewMetrics(): array
    {
        $today = now()->startOfDay();
        $monthStart = $today->copy()->startOfMonth();
        $dueTodayReceivable = $this->aggregateCountAndSum($this->dueTodayReceivableQuery());
        $overdueReceivable = $this->aggregateCountAndSum($this->overdueReceivableQuery());
        $dueSoonPayable = $this->aggregateCountAndSum($this->dueSoonPayableQuery());

        return [
            'receivable_total' => $this->decimalString($this->openReceivableQuery()->sum('remaining_amount')),
            'payable_total' => $this->decimalString($this->openPayableQuery()->sum('remaining_amount')),
            'due_today_receivable_count' => $dueTodayReceivable['count'],
            'due_today_receivable_amount' => $this->decimalString($dueTodayReceivable['sum']),
            'overdue_receivable_count' => $overdueReceivable['count'],
            'overdue_receivable_amount' => $this->decimalString($overdueReceivable['sum']),
            'due_soon_payable_count' => $dueSoonPayable['count'],
            'due_soon_payable_amount' => $this->decimalString($dueSoonPayable['sum']),
            'large_unwritten_count' => $this->largeUnwrittenReceiptQuery()->count() + $this->largeUnwrittenPaymentQuery()->count(),
            'long_unwritten_count' => $this->longUnwrittenReceiptQuery()->count() + $this->longUnwrittenPaymentQuery()->count(),
            'reversed_write_off_count' => $this->reversedWriteOffQuery()->count(),
            'recent_adjustment_count' => $this->recentAdjustmentQuery()->count(),
            'month_receipt_amount' => $this->decimalString(DberpReceiptOrder::query()
                ->where('status', 'effective')
                ->whereBetween('effective_at', [$monthStart, now()])
                ->sum('amount')),
            'month_payment_amount' => $this->decimalString(DberpPaymentOrder::query()
                ->where('status', 'effective')
                ->whereBetween('effective_at', [$monthStart, now()])
                ->sum('amount')),
            'overdue_customer_count' => $this->overdueReceivableQuery()->distinct('customer_id')->count('customer_id'),
            'overdue_supplier_count' => $this->overduePayableQuery()->distinct('supplier_id')->count('supplier_id'),
        ];
    }

    /**
     * @return array{count: int, sum: float}
     */
    private function aggregateCountAndSum(Builder $query, string $sumColumn = 'remaining_amount'): array
    {
        $table = $query->getModel()->getTable();
        $aggregate = (clone $query)
            ->selectRaw('COUNT(*) as aggregate_count, COALESCE(SUM('.$table.'.'.$sumColumn.'), 0) as aggregate_sum')
            ->first();

        return [
            'count' => (int) ($aggregate->aggregate_count ?? 0),
            'sum' => (float) ($aggregate->aggregate_sum ?? 0),
        ];
    }

    private function openReceivableQuery(): Builder
    {
        return DberpReceivableRecord::query()
            ->where('remaining_amount', '>', 0)
            ->whereNot('record_status', 'voided');
    }

    private function openPayableQuery(): Builder
    {
        return DberpPayableRecord::query()
            ->where('remaining_amount', '>', 0)
            ->whereNot('record_status', 'voided');
    }

    private function dueTodayReceivableQuery(): Builder
    {
        $today = now()->toDateString();

        return $this->openReceivableQuery()->where(function ($query) use ($today): void {
            $query->whereDate('due_date', $today)
                ->orWhere(function ($subQuery) use ($today): void {
                    $subQuery->whereNull('due_date')->whereDate('business_date', $today);
                });
        });
    }

    private function overdueReceivableQuery(): Builder
    {
        $today = now()->toDateString();

        return $this->openReceivableQuery()->where(function ($query) use ($today): void {
            $query->whereDate('due_date', '<', $today)
                ->orWhere(function ($subQuery) use ($today): void {
                    $subQuery->whereNull('due_date')->whereDate('business_date', '<', $today);
                });
        });
    }

    private function dueSoonPayableQuery(): Builder
    {
        $today = now()->toDateString();
        $dueSoonDate = now()->addDays($this->dueSoonDays())->toDateString();

        return $this->openPayableQuery()->where(function ($query) use ($dueSoonDate, $today): void {
            $query->whereBetween('due_date', [$today, $dueSoonDate])
                ->orWhere(function ($subQuery) use ($dueSoonDate, $today): void {
                    $subQuery->whereNull('due_date')->whereBetween('business_date', [$today, $dueSoonDate]);
                });
        });
    }

    private function overduePayableQuery(): Builder
    {
        $today = now()->toDateString();

        return $this->openPayableQuery()->where(function ($query) use ($today): void {
            $query->whereDate('due_date', '<', $today)
                ->orWhere(function ($subQuery) use ($today): void {
                    $subQuery->whereNull('due_date')->whereDate('business_date', '<', $today);
                });
        });
    }

    private function largeUnwrittenReceiptQuery(): Builder
    {
        return DberpReceiptOrder::query()
            ->where('status', 'effective')
            ->where('available_amount', '>=', $this->largeUnwrittenAmount());
    }

    private function largeUnwrittenPaymentQuery(): Builder
    {
        return DberpPaymentOrder::query()
            ->where('status', 'effective')
            ->where('available_amount', '>=', $this->largeUnwrittenAmount());
    }

    private function longUnwrittenReceiptQuery(): Builder
    {
        return DberpReceiptOrder::query()
            ->where('status', 'effective')
            ->where('available_amount', '>', 0)
            ->whereDate('business_time', '<=', now()->subDays($this->longUnwrittenDays())->toDateString());
    }

    private function longUnwrittenPaymentQuery(): Builder
    {
        return DberpPaymentOrder::query()
            ->where('status', 'effective')
            ->where('available_amount', '>', 0)
            ->whereDate('business_time', '<=', now()->subDays($this->longUnwrittenDays())->toDateString());
    }

    private function reversedWriteOffQuery(): Builder
    {
        return DberpSettlementWriteOff::query()
            ->whereIn('write_off_status', ['partially_reversed', 'reversed'])
            ->whereDate('reversed_at', '>=', now()->subDays($this->followUpDays())->toDateString());
    }

    private function recentAdjustmentQuery(): Builder
    {
        return DberpSettlementAdjustment::query()
            ->whereIn('status', ['effective', 'voided'])
            ->where(function ($query): void {
                $query->whereDate('effective_at', '>=', now()->subDays($this->followUpDays())->toDateString())
                    ->orWhereDate('voided_at', '>=', now()->subDays($this->followUpDays())->toDateString());
            });
    }

    private function largeUnwrittenAmount(): float
    {
        return (float) config('dberp.settlement.reminders.large_unwritten_amount', 10000);
    }

    private function longUnwrittenDays(): int
    {
        return (int) config('dberp.settlement.reminders.long_unwritten_days', 30);
    }

    private function dueSoonDays(): int
    {
        return (int) config('dberp.settlement.reminders.due_soon_days', 7);
    }

    private function followUpDays(): int
    {
        return (int) config('dberp.settlement.reminders.follow_up_days', 7);
    }

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