<?php

declare(strict_types=1);

/**
 * 本文件属于 DBErp 进销存系统。
 *
 * 版权所有 © 2026 北京珑大钜商科技有限公司。保留所有权利。
 *
 * 未经官方授权，不得删除、篡改或隐藏本文件中的版权声明、产品标识与来源信息。
 * 商业授权、官方更新与技术支持服务，请以珑大官方支持中心记录为准。
 *
 * @link https://support.loongdom.cn
 * @see  https://www.dberp.com.cn
 */

namespace App\Business\Settlement;

use App\Business\Settlement\Enums\SettlementOverdueStatus;
use App\Business\Settlement\Enums\SettlementRecordStatus;
use App\Support\DberpNumberFormatter;
use App\Tools\Entity\EntityReference;
use App\Tools\Entity\EntityType;
use App\Tools\Support\ToolAmountRange;
use App\Tools\Support\ToolDateRange;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

/**
 * 应收/应付列表共用筛选与白名单映射。不复用 Filament Resource 查询。
 */
final class SettlementRecordSearchSupport
{
    /**
     * @param  array<string, mixed>  $input
     */
    public static function applyFilters(
        Builder $query,
        array $input,
        string $asOfDate,
        string $table,
        string $counterpartyRelation,
        string $counterpartyIdColumn,
    ): void {
        $businessDates = ToolDateRange::parseOptional(
            $input['date_from'] ?? null,
            $input['date_until'] ?? null,
            'date_from',
            'date_until',
        );
        $dueDates = ToolDateRange::parseOptional(
            $input['due_date_from'] ?? null,
            $input['due_date_until'] ?? null,
            'due_date_from',
            'due_date_until',
        );
        $amounts = ToolAmountRange::parseOptional(
            $input['amount_min'] ?? null,
            $input['amount_max'] ?? null,
            'amount_min',
            'amount_max',
        );

        $keyword = trim((string) ($input['q'] ?? ''));
        if ($keyword !== '') {
            $like = '%'.$keyword.'%';
            $query->where(function (Builder $builder) use ($like, $table, $counterpartyRelation): void {
                $builder
                    ->where($table.'.code', 'like', $like)
                    ->orWhere($table.'.source_document_no', 'like', $like)
                    ->orWhereHas($counterpartyRelation, function (Builder $counterparty) use ($like): void {
                        $counterparty
                            ->where('code', 'like', $like)
                            ->orWhere('name', 'like', $like);
                    });
            });
        }

        if (($input[$counterpartyIdColumn] ?? 0) > 0) {
            $query->where($table.'.'.$counterpartyIdColumn, (int) $input[$counterpartyIdColumn]);
        }

        $status = trim((string) ($input['status'] ?? ''));
        if ($status !== '') {
            $query->where($table.'.record_status', $status);
        }

        $overdue = SettlementOverdueStatus::tryFrom(trim((string) ($input['overdue_status'] ?? '')));
        if ($overdue instanceof SettlementOverdueStatus) {
            SettlementDueConstraint::apply($query, $overdue, $asOfDate);
        }

        if ($businessDates['from'] !== null) {
            $query->whereDate($table.'.business_date', '>=', $businessDates['from']);
        }

        if ($businessDates['until'] !== null) {
            $query->whereDate($table.'.business_date', '<=', $businessDates['until']);
        }

        if ($dueDates['from'] !== null) {
            $query->whereDate($table.'.due_date', '>=', $dueDates['from']);
        }

        if ($dueDates['until'] !== null) {
            $query->whereDate($table.'.due_date', '<=', $dueDates['until']);
        }

        if ($amounts['min'] !== null) {
            $query->where($table.'.remaining_amount', '>=', $amounts['min']);
        }

        if ($amounts['max'] !== null) {
            $query->where($table.'.remaining_amount', '<=', $amounts['max']);
        }
    }

    /**
     * @return list<string>
     */
    public static function statusValues(): array
    {
        return array_map(
            static fn (SettlementRecordStatus $status): string => $status->value,
            SettlementRecordStatus::cases(),
        );
    }

    /**
     * @return array<string, mixed>
     */
    public static function mapSummary(Model $record, string $asOfDate, string $counterpartyRelation): array
    {
        $counterparty = $record->{$counterpartyRelation};
        $overdueStatus = SettlementDueConstraint::overdueStatus(
            $record->getAttribute('remaining_amount'),
            $record->getAttribute('record_status'),
            $record->getAttribute('due_date'),
            $record->getAttribute('business_date'),
            $asOfDate,
        );

        $counterpartyType = $counterpartyRelation === 'supplier' ? EntityType::Supplier : EntityType::Customer;
        $recordType = $counterpartyRelation === 'supplier' ? EntityType::Payable : EntityType::Receivable;
        $counterpartyPayload = [
            'id' => $counterparty === null ? null : (int) $counterparty->getKey(),
            'code' => self::nullableString($counterparty?->code),
            'name' => (string) ($counterparty?->name ?? ''),
        ];

        return EntityReference::attach([
            'id' => (int) $record->getKey(),
            'code' => (string) $record->getAttribute('code'),
            'status' => (string) $record->getAttribute('record_status'),
            'overdue_status' => $overdueStatus,
            'is_overdue' => $overdueStatus === SettlementOverdueStatus::Overdue->value,
            'business_date' => self::isoDateTime($record->getAttribute('business_date')),
            'due_date' => self::dateOnly($record->getAttribute('due_date')),
            $counterpartyRelation => EntityReference::attachNested($counterpartyPayload, $counterpartyType),
            'source_document_no' => self::nullableString($record->getAttribute('source_document_no')),
            'amounts' => [
                'original_amount' => DberpNumberFormatter::money($record->getAttribute('original_amount')),
                'adjusted_amount' => DberpNumberFormatter::money($record->getAttribute('adjusted_amount')),
                'written_off_amount' => DberpNumberFormatter::money($record->getAttribute('written_off_amount')),
                'remaining_amount' => DberpNumberFormatter::money($record->getAttribute('remaining_amount')),
            ],
        ], $recordType, (int) $record->getKey(), (string) $record->getAttribute('code'));
    }

    /**
     * @return array<string, mixed>
     */
    public static function mapDetail(Model $record, string $asOfDate, string $counterpartyRelation): array
    {
        return [
            ...self::mapSummary($record, $asOfDate, $counterpartyRelation),
            'source_type' => (string) $record->getAttribute('source_type'),
            'source_document_status' => self::nullableString($record->getAttribute('source_document_status')),
            'formed_at' => self::isoDateTime($record->getAttribute('formed_at')),
            'last_write_off_at' => self::isoDateTime($record->getAttribute('last_write_off_at')),
            'closed_at' => self::isoDateTime($record->getAttribute('closed_at')),
        ];
    }

    private static function isoDateTime(mixed $value): ?string
    {
        if ($value instanceof \DateTimeInterface) {
            return $value->format(\DateTimeInterface::ATOM);
        }

        return self::nullableString($value);
    }

    private static function dateOnly(mixed $value): ?string
    {
        if ($value instanceof \DateTimeInterface) {
            return $value->format('Y-m-d');
        }

        $normalized = self::nullableString($value);

        return $normalized === null ? null : substr($normalized, 0, 10);
    }

    private static function nullableString(mixed $value): ?string
    {
        if ($value === null) {
            return null;
        }

        $normalized = trim((string) $value);

        return $normalized === '' ? null : $normalized;
    }
}
