<?php

namespace App\Services\AgentSettlement;

use Carbon\Carbon;
use App\Models\AdminUser;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Query\Builder;
use App\Support\AdminAgentSettlementScope;

/** 结算中心收付 + 调账合并列表（DB 分页）。 */
final class SettlementOperationsListService
{
    /**
     * @return array{
     *   items: list<array<string, mixed>>,
     *   total: int,
     *   page: int,
     *   per_page: int,
     * }
     */
    public function list(
        AdminUser $admin,
        int $page,
        int $perPage,
        SettlementOperationsListFilters $filters,
    ): array {
        $stubQueries = [];

        if ($filters->includesPayments()) {
            $paymentStub = $this->paymentStubQuery($admin, $filters);
            if ($paymentStub !== null) {
                $stubQueries[] = $paymentStub;
            }
        }

        if ($filters->includesAdjustments()) {
            $adjustmentStub = $this->adjustmentStubQuery($admin, $filters);
            if ($adjustmentStub !== null) {
                $stubQueries[] = $adjustmentStub;
            }
        }

        if ($stubQueries === []) {
            return [
                'items' => [],
                'total' => 0,
                'page' => $page,
                'per_page' => $perPage,
            ];
        }

        $offset = max(0, ($page - 1) * $perPage);

        if (count($stubQueries) === 1) {
            $base = $stubQueries[0];
            $total = (int) (clone $base)->count();
            $stubs = (clone $base)
                ->orderByDesc('sort_at')
                ->orderByDesc('record_id')
                ->offset($offset)
                ->limit($perPage)
                ->get();
        } else {
            $union = null;
            foreach ($stubQueries as $stubQuery) {
                $union = $union === null ? $stubQuery : $union->unionAll($stubQuery);
            }
            $wrapped = DB::query()->fromSub($union, 'operations_page');
            $total = (int) (clone $wrapped)->count();
            $stubs = $wrapped
                ->orderByDesc('sort_at')
                ->orderByDesc('record_id')
                ->offset($offset)
                ->limit($perPage)
                ->get();
        }

        return [
            'items' => $this->hydrateStubs($stubs),
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
        ];
    }

    private function paymentStubQuery(AdminUser $admin, SettlementOperationsListFilters $filters): ?Builder
    {
        $query = DB::table('payment_records as pr')
            ->join('settlement_bills as sb', 'sb.id', '=', 'pr.settlement_bill_id')
            ->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
            ->selectRaw("'payment' as op_kind")
            ->selectRaw('pr.id as record_id')
            ->selectRaw('pr.settlement_bill_id as bill_id')
            ->selectRaw('COALESCE(pr.confirmed_at, pr.created_at) as sort_at');

        $this->applySiteAndPeriodFilters($query, $admin, $filters, 'sp', 'sb.settlement_period_id');
        AdminAgentSettlementScope::applyDirectEdgeScopeToBillsQuery($query, $admin, 'sb');

        if ($filters->billId !== null) {
            $query->where('pr.settlement_bill_id', $filters->billId);
        }

        $this->applyPaymentKeywordFilter($query, $filters->keyword);

        return $query;
    }

    private function adjustmentStubQuery(AdminUser $admin, SettlementOperationsListFilters $filters): ?Builder
    {
        $query = DB::table('settlement_adjustments as sa')
            ->leftJoin('settlement_periods as sp', 'sp.id', '=', 'sa.settlement_period_id')
            ->leftJoin('settlement_bills as sb', 'sb.id', '=', 'sa.original_bill_id')
            ->selectRaw("CASE sa.adjustment_type WHEN 'bad_debt' THEN 'bad_debt' WHEN 'reversal' THEN 'reversal' ELSE 'adjustment' END as op_kind")
            ->selectRaw('sa.id as record_id')
            ->selectRaw('sa.original_bill_id as bill_id')
            ->selectRaw('sa.created_at as sort_at');

        $this->applySiteAndPeriodFilters($query, $admin, $filters, 'sp', 'sa.settlement_period_id');

        if ($filters->billId !== null) {
            $query->where('sa.original_bill_id', $filters->billId);
        }

        if ($filters->operationType === 'adjustment') {
            $query->where('sa.adjustment_type', 'adjustment');
        } elseif ($filters->operationType === 'reversal') {
            $query->where('sa.adjustment_type', 'reversal');
        } elseif ($filters->operationType === 'bad_debt') {
            $query->where('sa.adjustment_type', 'bad_debt');
        }

        $actorId = AdminAgentSettlementScope::boundAgentNodeId($admin);
        if ($actorId !== null) {
            $query->where(function (Builder $outer) use ($admin): void {
                $outer->whereNull('sa.original_bill_id')
                    ->orWhereExists(function (Builder $exists) use ($admin): void {
                        $exists->selectRaw('1')
                            ->from('settlement_bills as sb')
                            ->whereColumn('sb.id', 'sa.original_bill_id');
                        AdminAgentSettlementScope::applyDirectEdgeScopeToBillsQuery($exists, $admin, 'sb');
                    });
            });
        }

        $this->applyAdjustmentKeywordFilter($query, $filters->keyword);

        return $query;
    }

    private function applySiteAndPeriodFilters(
        Builder $query,
        AdminUser $admin,
        SettlementOperationsListFilters $filters,
        string $periodsAlias,
        string $periodColumn,
    ): void {
        if ($filters->settlementPeriodId !== null) {
            $query->where($periodColumn, $filters->settlementPeriodId);
        }

        if ($filters->adminSiteId !== null) {
            $query->where($periodsAlias.'.admin_site_id', $filters->adminSiteId);
        }

        $siteIds = $admin->accessibleAdminSiteIds();
        if ($siteIds !== null) {
            if ($siteIds === []) {
                $query->whereRaw('0 = 1');

                return;
            }
            $query->whereIn($periodsAlias.'.admin_site_id', $siteIds);
        }
    }

    private function applyPaymentKeywordFilter(Builder $query, ?string $keyword): void
    {
        if ($keyword === null) {
            return;
        }

        $like = '%'.addcslashes(strtolower($keyword), '%_\\').'%';
        $query->where(function (Builder $match) use ($like, $keyword): void {
            $match->whereRaw('LOWER(COALESCE(pr.method, \'\')) LIKE ?', [$like])
                ->orWhereRaw('LOWER(COALESCE(pr.proof, \'\')) LIKE ?', [$like])
                ->orWhereRaw('LOWER(COALESCE(pr.remark, \'\')) LIKE ?', [$like])
                ->orWhereRaw('LOWER(CONCAT(pr.payer_type, \'#\', pr.payer_id, \' \', pr.payee_type, \'#\', pr.payee_id)) LIKE ?', [$like])
                ->orWhereRaw('LOWER(CONCAT(\'payment\')) LIKE ?', [$like]);

            if (ctype_digit($keyword)) {
                $match->orWhere('pr.id', (int) $keyword)
                    ->orWhere('pr.settlement_bill_id', (int) $keyword);
            }
        });
    }

    private function applyAdjustmentKeywordFilter(Builder $query, ?string $keyword): void
    {
        if ($keyword === null) {
            return;
        }

        $like = '%'.addcslashes(strtolower($keyword), '%_\\').'%';
        $query->where(function (Builder $match) use ($like, $keyword): void {
            $match->whereRaw('LOWER(COALESCE(sa.reason, \'\')) LIKE ?', [$like])
                ->orWhereRaw('LOWER(sa.adjustment_type) LIKE ?', [$like]);

            if (ctype_digit($keyword)) {
                $match->orWhere('sa.id', (int) $keyword)
                    ->orWhere('sa.original_bill_id', (int) $keyword);
            }
        });
    }

    /**
     * @param  Collection<int, object>  $stubs
     * @return list<array<string, mixed>>
     */
    private function hydrateStubs(Collection $stubs): array
    {
        if ($stubs->isEmpty()) {
            return [];
        }

        $paymentIds = [];
        $adjustmentIds = [];
        foreach ($stubs as $stub) {
            $kind = (string) $stub->op_kind;
            $id = (int) $stub->record_id;
            if ($kind === 'payment') {
                $paymentIds[] = $id;
            } else {
                $adjustmentIds[] = $id;
            }
        }

        $payments = $paymentIds === []
            ? collect()
            : DB::table('payment_records')->whereIn('id', $paymentIds)->get()->keyBy('id');
        $adjustments = $adjustmentIds === []
            ? collect()
            : DB::table('settlement_adjustments')->whereIn('id', $adjustmentIds)->get()->keyBy('id');

        $items = [];
        foreach ($stubs as $stub) {
            $kind = (string) $stub->op_kind;
            $id = (int) $stub->record_id;
            if ($kind === 'payment') {
                $row = $payments->get($id);
                if ($row === null) {
                    continue;
                }
                $items[] = $this->formatPaymentRow($row);
            } else {
                $row = $adjustments->get($id);
                if ($row === null) {
                    continue;
                }
                $items[] = $this->formatAdjustmentRow($row, $kind);
            }
        }

        return $items;
    }

    /**
     * @return array<string, mixed>
     */
    private function formatPaymentRow(object $row): array
    {
        $payer = (string) $row->payer_type === 'platform'
            ? 'platform'
            : (string) $row->payer_type.'#'.(int) $row->payer_id;
        $payee = (string) $row->payee_type === 'platform'
            ? 'platform'
            : (string) $row->payee_type.'#'.(int) $row->payee_id;
        $detailParts = [$payer.' → '.$payee];
        if (trim((string) ($row->proof ?? '')) !== '') {
            $detailParts[] = (string) $row->proof;
        }
        if (trim((string) ($row->remark ?? '')) !== '') {
            $detailParts[] = (string) $row->remark;
        }

        $sortAt = $row->confirmed_at ?? $row->created_at;

        return [
            'kind' => 'payment',
            'record_id' => (int) $row->id,
            'bill_id' => (int) $row->settlement_bill_id,
            'amount' => (int) $row->amount,
            'summary' => trim((string) ($row->method ?? '')) !== '' ? (string) $row->method : '—',
            'detail' => implode(' · ', $detailParts),
            'sort_at' => $sortAt !== null ? Carbon::parse($sortAt)->toIso8601String() : null,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function formatAdjustmentRow(object $row, string $kind): array
    {
        return [
            'kind' => $kind,
            'record_id' => (int) $row->id,
            'bill_id' => (int) ($row->original_bill_id ?? 0),
            'amount' => (int) $row->amount,
            'summary' => trim((string) ($row->reason ?? '')) !== '' ? (string) $row->reason : '—',
            'detail' => null,
            'sort_at' => $row->created_at !== null ? Carbon::parse($row->created_at)->toIso8601String() : null,
        ];
    }
}
