<?php

declare(strict_types=1);

namespace App\Business\Settlement;

use App\Business\Report\CsvExportService;
use App\Business\Settlement\Enums\SettlementStatementStatus;
use App\Models\DberpCustomer;
use App\Models\DberpSettlementEntry;
use App\Models\DberpSettlementStatement;
use App\Models\DberpSupplier;
use App\Models\User;
use App\Services\DberpAuditService;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\StreamedResponse;

final class SettlementStatementService
{
    public function __construct(
        private readonly CsvExportService $csvExportService,
        private readonly DberpAuditService $auditService,
        private readonly SettlementRuleService $ruleService,
        private readonly SettlementSourceTypeResolver $sourceTypeResolver,
    ) {}

    /**
     * @param  array<string, mixed>  $data
     */
    public function createDraft(array $data): DberpSettlementStatement
    {
        $settlementSide = (string) ($data['settlement_side'] ?? 'receivable');
        $counterpartyType = $settlementSide === 'payable' ? 'supplier' : 'customer';
        $counterpartyId = (int) ($data['counterparty_id'] ?? 0);
        $periodStartDate = (string) ($data['period_start_date'] ?? '');
        $periodEndDate = (string) ($data['period_end_date'] ?? '');

        if ($counterpartyId <= 0) {
            throw ValidationException::withMessages([
                'counterparty_id' => '请选择往来对象后再生成对账单。',
            ]);
        }

        if ($periodStartDate === '' || $periodEndDate === '' || $periodStartDate > $periodEndDate) {
            throw ValidationException::withMessages([
                'period_start_date' => '请填写合法的对账期间，且开始日期不能晚于结束日期。',
            ]);
        }

        $exists = DberpSettlementStatement::query()
            ->where('settlement_side', $settlementSide)
            ->where('counterparty_id', $counterpartyId)
            ->where('period_start_date', $periodStartDate)
            ->where('period_end_date', $periodEndDate)
            ->whereNot('statement_status', SettlementStatementStatus::Voided->value)
            ->exists();

        if ($exists) {
            throw ValidationException::withMessages([
                'period_start_date' => '相同往来对象与期间的有效对账单已存在，请先作废旧单或调整期间。',
            ]);
        }

        return DB::transaction(function () use ($data, $counterpartyId, $counterpartyType, $periodEndDate, $periodStartDate, $settlementSide): DberpSettlementStatement {
            $openingBalance = $this->resolveOpeningBalance($settlementSide, $counterpartyType, $counterpartyId, $periodStartDate);
            $entries = $this->settlementEntriesQuery($settlementSide, $counterpartyType, $counterpartyId)
                ->whereDate('business_time', '>=', $periodStartDate)
                ->whereDate('business_time', '<=', $periodEndDate)
                ->orderBy('business_time')
                ->orderBy('id')
                ->get();

            $counterpartyName = $this->resolveCounterpartyName($counterpartyType, $counterpartyId);
            $runningBalance = $openingBalance;
            $periodIncreaseAmount = 0.0;
            $periodDecreaseAmount = 0.0;
            $items = [[
                'line_no' => 1,
                'source_type' => 'opening_balance',
                'source_id' => null,
                'source_document_no' => null,
                'summary' => '期初余额',
                'business_time' => $periodStartDate.' 00:00:00',
                'debit_amount' => '0.0000',
                'credit_amount' => '0.0000',
                'net_change_amount' => '0.0000',
                'balance_after' => number_format($openingBalance, 4, '.', ''),
                'source_status' => null,
                'snapshot_payload' => [
                    'opening_balance' => number_format($openingBalance, 4, '.', ''),
                ],
                'remark' => null,
            ]];

            foreach ($entries as $index => $entry) {
                $changeAmount = (float) $entry->change_amount;
                $runningBalance += $changeAmount;

                if ($changeAmount >= 0) {
                    $periodIncreaseAmount += $changeAmount;
                } else {
                    $periodDecreaseAmount += abs($changeAmount);
                }

                $normalizedSourceType = $this->sourceTypeResolver->normalize($entry->source_type);

                $items[] = [
                    'line_no' => $index + 2,
                    'source_type' => $normalizedSourceType ?? (string) $entry->source_type,
                    'source_id' => $entry->source_id,
                    'source_document_no' => $entry->document_number,
                    'summary' => __('dberp.options.settlement_change_type')[$entry->change_type] ?? (string) $entry->change_type,
                    'business_time' => $entry->business_time,
                    'debit_amount' => number_format(max($changeAmount, 0), 4, '.', ''),
                    'credit_amount' => number_format(abs(min($changeAmount, 0)), 4, '.', ''),
                    'net_change_amount' => number_format($changeAmount, 4, '.', ''),
                    'balance_after' => number_format((float) $entry->balance_after, 4, '.', ''),
                    'source_status' => null,
                    'snapshot_payload' => [
                        'change_type' => $entry->change_type,
                        'source_type' => $normalizedSourceType ?? $entry->source_type,
                        'source_id' => $entry->source_id,
                        'document_number' => $entry->document_number,
                    ],
                    'remark' => $entry->remark,
                ];
            }

            /** @var DberpSettlementStatement $statement */
            $statement = DberpSettlementStatement::query()->create([
                'settlement_side' => $settlementSide,
                'counterparty_type' => $counterpartyType,
                'counterparty_id' => $counterpartyId,
                'counterparty_name' => $counterpartyName,
                'statement_date' => (string) ($data['statement_date'] ?? $periodEndDate),
                'period_start_date' => $periodStartDate,
                'period_end_date' => $periodEndDate,
                'opening_balance' => number_format($openingBalance, 4, '.', ''),
                'period_increase_amount' => number_format($periodIncreaseAmount, 4, '.', ''),
                'period_decrease_amount' => number_format($periodDecreaseAmount, 4, '.', ''),
                'closing_balance' => number_format($runningBalance, 4, '.', ''),
                'item_count' => count($items),
                'statement_status' => SettlementStatementStatus::Draft->value,
                'handler_employee_id' => $data['handler_employee_id'] ?? null,
                'source_system' => 'dberp',
                'remark' => $data['remark'] ?? null,
            ]);

            $statement->items()->createMany($items);

            return $statement->refresh();
        });
    }

    public function confirm(DberpSettlementStatement $statement): void
    {
        $this->transitionStatement($statement, SettlementStatementStatus::Draft, SettlementStatementStatus::Confirmed, 'dberp.settlement_statement.confirmed', 'confirmed_at');
    }

    public function void(DberpSettlementStatement $statement): void
    {
        if (! $this->ruleService->allowsSettlementVoid()) {
            throw ValidationException::withMessages([
                'statement_status' => __('dberp.errors.settlement_void_policy_blocked'),
            ]);
        }

        DB::transaction(function () use ($statement): void {
            /** @var DberpSettlementStatement $locked */
            $locked = DberpSettlementStatement::query()->lockForUpdate()->findOrFail($statement->getKey());

            if ($locked->statement_status === SettlementStatementStatus::Voided->value) {
                throw ValidationException::withMessages([
                    'statement_status' => '当前对账单已作废，无需重复操作。',
                ]);
            }

            $before = $this->auditService->snapshot($locked, $locked->getFillable());

            $this->auditService->logSemanticSave(
                'dberp.settlement_statement.voided',
                $locked,
                function () use ($locked): void {
                    $locked->forceFill([
                        'statement_status' => SettlementStatementStatus::Voided->value,
                        'voided_at' => now(),
                    ])->save();
                },
                $before,
                $this->auditService->snapshot($locked, $locked->getFillable()),
            );
        });
    }

    public function export(DberpSettlementStatement $statement): StreamedResponse
    {
        /** @var DberpSettlementStatement $fresh */
        $fresh = DberpSettlementStatement::query()->with('items')->findOrFail($statement->getKey());

        $this->markExported($fresh, Auth::user());

        return $this->csvExportService->download(
            $fresh->code.'-statement-'.now()->format('Ymd_His').'.csv',
            ['行号', '业务时间', '来源类型', '来源单号', '摘要', '增加金额', '减少金额', '净变动', '余额', '备注'],
            $fresh->items->sortBy('line_no')->map(fn ($item): array => [
                (string) $item->line_no,
                $item->business_time?->format('Y-m-d H:i:s'),
                __('dberp.options.settlement_source_type')[$item->source_type] ?? (string) $item->source_type,
                (string) $item->source_document_no,
                (string) $item->summary,
                (string) $item->debit_amount,
                (string) $item->credit_amount,
                (string) $item->net_change_amount,
                (string) $item->balance_after,
                (string) $item->remark,
            ])->all(),
        );
    }

    private function markExported(DberpSettlementStatement $statement, ?User $user): void
    {
        $before = $this->auditService->snapshot($statement, $statement->getFillable());

        $this->auditService->logSemanticSave(
            'dberp.settlement_statement.exported',
            $statement,
            function () use ($statement, $user): void {
                $statement->forceFill([
                    'last_exported_at' => now(),
                    'last_exported_by' => $user?->getKey(),
                ])->save();
            },
            $before,
            $this->auditService->snapshot($statement, $statement->getFillable()),
        );
    }

    private function resolveCounterpartyName(string $counterpartyType, int $counterpartyId): ?string
    {
        return $counterpartyType === 'supplier'
            ? DberpSupplier::query()->whereKey($counterpartyId)->value('name')
            : DberpCustomer::query()->whereKey($counterpartyId)->value('name');
    }

    private function resolveOpeningBalance(string $settlementSide, string $counterpartyType, int $counterpartyId, string $periodStartDate): float
    {
        $entry = $this->settlementEntriesQuery($settlementSide, $counterpartyType, $counterpartyId)
            ->whereDate('business_time', '<', $periodStartDate)
            ->orderByDesc('business_time')
            ->orderByDesc('id')
            ->first();

        return (float) ($entry?->balance_after ?? 0);
    }

    private function settlementEntriesQuery(string $settlementSide, string $counterpartyType, int $counterpartyId)
    {
        return DberpSettlementEntry::query()
            ->where('settlement_side', $settlementSide)
            ->where('counterparty_type', $counterpartyType)
            ->where('counterparty_id', $counterpartyId);
    }

    private function transitionStatement(
        DberpSettlementStatement $statement,
        SettlementStatementStatus $from,
        SettlementStatementStatus $to,
        string $auditAction,
        string $timeField,
    ): void {
        DB::transaction(function () use ($auditAction, $from, $statement, $timeField, $to): void {
            /** @var DberpSettlementStatement $locked */
            $locked = DberpSettlementStatement::query()->lockForUpdate()->findOrFail($statement->getKey());

            if ($locked->statement_status !== $from->value) {
                throw ValidationException::withMessages([
                    'statement_status' => '当前对账单状态不允许执行该操作。',
                ]);
            }

            $before = $this->auditService->snapshot($locked, $locked->getFillable());

            $this->auditService->logSemanticSave(
                $auditAction,
                $locked,
                function () use ($locked, $timeField, $to): void {
                    $locked->forceFill([
                        'statement_status' => $to->value,
                        $timeField => now(),
                    ])->save();
                },
                $before,
                $this->auditService->snapshot($locked, $locked->getFillable()),
            );
        });
    }
}
