<?php

namespace App\Services;

use App\Models\BinaryLedger;
use App\Models\BinaryMatchingIncome;
use App\Models\JoiningPairIncome;
use App\Models\User;
use App\Models\UserBinaryCarry;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class MatchingService
{
    public function __construct(private readonly WalletService $walletService, private readonly ?MlmLogService $mlmLogService = null)
    {
    }

    public function runJoiningMatching(Carbon $date): void
    {
        $pairValue = (float) setting('joining_pair_value', (float) setting('pair_income', 0));
        $binaryRate = $this->normalizePercentageRate((float) setting('joining_binary_percentage', 0));
        $capping = (float) setting('joining_daily_capping', (float) setting('daily_cap', 0));

        if ($pairValue <= 0 && $binaryRate <= 0) {
            return;
        }

        $this->processMatching($date, 'joining', $pairValue, $binaryRate, $capping);
    }

    public function runRepurchaseMatching(Carbon $date): void
    {
        $rate = $this->normalizePercentageRate((float) setting('pair_rate', (float) setting('binary_percentage', 0.10)));
        $capping = (float) setting('repurchase_daily_capping', 0);

        if ($rate <= 0) {
            return;
        }

        $this->processMatching($date, 'repurchase', 0, $rate, $capping);
    }

    private function processMatching(
        Carbon $date,
        string $type,
        float $defaultPairValue,
        float $defaultRate,
        float $defaultCapping
    ): void {
        $ledgerTypes = $type === 'joining'
            ? ['joining', 'reversal_joining']
            : ['repurchase', 'reversal_repurchase'];

        $endOfDay = $date->copy()->endOfDay();

        BinaryLedger::query()
            ->join('users', 'users.id', '=', 'binary_ledger.beneficiary_user_id')
            ->whereNull('binary_ledger.processed_at')
            ->whereIn('binary_ledger.ledger_type', $ledgerTypes)
            ->where('binary_ledger.posted_at', '<=', $endOfDay)
            ->where('users.role', 'user')
            ->where('users.status', 'active')
            ->select('binary_ledger.beneficiary_user_id')
            ->groupBy('binary_ledger.beneficiary_user_id')
            ->orderBy('binary_ledger.beneficiary_user_id')
            ->chunkById(200, function (Collection $rows) use ($date, $defaultPairValue, $defaultRate, $defaultCapping, $ledgerTypes, $type, $endOfDay): void {
                $userIds = $rows->pluck('beneficiary_user_id')->filter()->values();
                if ($userIds->isEmpty()) {
                    return;
                }

                $users = User::query()
                    ->whereIn('id', $userIds)
                    ->with(['currentRank', 'currentPlan'])
                    ->get()
                    ->keyBy('id');

                foreach ($userIds as $userId) {
                    $user = $users->get($userId);
                    if (! $user) {
                        continue;
                    }

                    DB::transaction(function () use ($user, $date, $defaultPairValue, $defaultRate, $defaultCapping, $ledgerTypes, $type, $endOfDay): void {
                        $carry = UserBinaryCarry::query()->firstOrCreate(
                            ['user_id' => $user->id],
                            [
                                'joining_left' => 0,
                                'joining_right' => 0,
                                'repurchase_left' => 0,
                                'repurchase_right' => 0,
                            ]
                        );

                        $ledgerRows = BinaryLedger::query()
                            ->where('beneficiary_user_id', $user->id)
                            ->whereNull('processed_at')
                            ->whereIn('ledger_type', $ledgerTypes)
                            ->where('posted_at', '<=', $endOfDay)
                            ->lockForUpdate()
                            ->get(['id', 'leg', 'direction', 'units']);

                        if ($ledgerRows->isEmpty()) {
                            return;
                        }
                        $ledgerIds = $ledgerRows->pluck('id')->map(fn ($id) => (int) $id)->sort()->values()->all();
                        $matchReference = $this->buildMatchReference($type, $date, $user->id, $ledgerIds);

                        $config = $this->resolveMatchingConfig($user, $type, $defaultPairValue, $defaultRate);
                        if ($config['pair_value'] <= 0 && $config['rate'] <= 0) {
                            return;
                        }

                        $leftDelta = 0.0;
                        $rightDelta = 0.0;
                        $leftMemberDelta = 0;
                        $rightMemberDelta = 0;
                        $leftAmountDelta = 0.0;
                        $rightAmountDelta = 0.0;

                        foreach ($ledgerRows as $row) {
                            $sign = $row->direction === 'credit' ? 1 : -1;
                            $signedUnits = $sign * (float) $row->units;

                            if ($row->leg === 'left') {
                                $leftAmountDelta += $signedUnits;
                                $leftMemberDelta += $sign;
                            } else {
                                $rightAmountDelta += $signedUnits;
                                $rightMemberDelta += $sign;
                            }

                            if ($type === 'joining' && $config['matching_type'] === 'member') {
                                $delta = (float) $sign;
                            } else {
                                $delta = $signedUnits;
                            }

                            if ($row->leg === 'left') {
                                $leftDelta += $delta;
                            } else {
                                $rightDelta += $delta;
                            }
                        }

                        if ($type === 'joining') {
                            $leftAvailable = max(0, (float) $carry->joining_left + $leftDelta);
                            $rightAvailable = max(0, (float) $carry->joining_right + $rightDelta);
                        } else {
                            $leftAvailable = max(0, (float) $carry->repurchase_left + $leftDelta);
                            $rightAvailable = max(0, (float) $carry->repurchase_right + $rightDelta);
                        }

                        $matched = min($leftAvailable, $rightAvailable);
                        if ($matched <= 0) {
                            $this->markProcessed($carry, $ledgerRows, $type, $leftAvailable, $rightAvailable, $config['carry_forward_enabled']);
                            return;
                        }

                        $matchedForPayout = $type === 'joining'
                            ? $this->applyPairLimits($user->id, $date, $matched, $config['matching_type'], $config['daily_pair_limit'], $config['weekly_pair_limit'])
                            : $matched;

                        $grossBeforeLimit = $this->calculateGross($type, $config['matching_type'], $matched, $config['pair_value'], $config['rate']);
                        $gross = $this->calculateGross($type, $config['matching_type'], $matchedForPayout, $config['pair_value'], $config['rate']);
                        $limitDeduction = round(max(0, $grossBeforeLimit - $gross), 2);

                        if ($type === 'joining') {
                            $todayNet = (float) JoiningPairIncome::query()
                                ->where('user_id', $user->id)
                                ->whereDate('run_date', $date->toDateString())
                                ->sum('net_income');
                        } else {
                            $todayNet = (float) BinaryMatchingIncome::query()
                                ->where('user_id', $user->id)
                                ->whereDate('run_date', $date->toDateString())
                                ->sum('net_income');
                        }

                        $rankCapping = (float) ($user->currentRank?->daily_capping ?? 0);
                        $effectiveCapping = $rankCapping > 0 ? $rankCapping : $defaultCapping;
                        $availableForToday = $effectiveCapping > 0 ? max(0, $effectiveCapping - $todayNet) : $gross;
                        $net = min($gross, $availableForToday);
                        $capDeduction = round(($gross - $net) + $limitDeduction, 2);

                        $displayReference = $type === 'joining'
                            ? 'JOIN-'.$date->format('Ymd')
                            : 'BV-'.$date->format('Ymd');

                        if ($matchedForPayout > 0) {
                            if ($type === 'joining') {
                                $income = JoiningPairIncome::query()->firstOrCreate(['reference' => $matchReference], [
                                    'user_id' => $user->id,
                                    'run_date' => $date->toDateString(),
                                    'matching_type' => $config['matching_type'],
                                    'left_units' => $leftAvailable,
                                    'right_units' => $rightAvailable,
                                    'matched_pairs' => $matchedForPayout,
                                    'pair_value' => $config['matching_type'] === 'member' ? $config['pair_value'] : $config['rate'],
                                    'gross_income' => $gross,
                                    'capping_deduction' => $capDeduction,
                                    'net_income' => $net,
                                    'status' => 'credited',
                                    'reference' => $matchReference,
                                ]);
                                $incomeNet = (float) $income->net_income;

                                if ($incomeNet > 0) {
                                    $this->walletService->credit(
                                        $user->id,
                                        'income',
                                        $incomeNet,
                                        'joining_matching',
                                        $income->id,
                                        [
                                            'reference' => 'JOIN-INC-'.$matchReference,
                                            'reference_type' => 'matching.joining.income',
                                            'reference_id' => $income->id,
                                            'display_reference' => $displayReference,
                                            'narration' => 'Joining pair income',
                                        ]
                                    );
                                    $this->walletService->credit(
                                        $user->id,
                                        'withdrawable',
                                        $incomeNet,
                                        'joining_matching',
                                        $income->id,
                                        [
                                            'reference' => 'JOIN-WDR-'.$matchReference,
                                            'reference_type' => 'matching.joining.withdrawable',
                                            'reference_id' => $income->id,
                                            'display_reference' => $displayReference,
                                            'narration' => 'Joining pair income',
                                        ]
                                    );
                                }
                            } else {
                                $income = BinaryMatchingIncome::query()->firstOrCreate(['reference' => $matchReference], [
                                    'user_id' => $user->id,
                                    'run_date' => $date->toDateString(),
                                    'left_bv' => $leftAvailable,
                                    'right_bv' => $rightAvailable,
                                    'matched_bv' => $matchedForPayout,
                                    'rate' => $config['rate'],
                                    'gross_income' => $gross,
                                    'capping_deduction' => $capDeduction,
                                    'net_income' => $net,
                                    'status' => 'credited',
                                    'reference' => $matchReference,
                                ]);
                                $incomeNet = (float) $income->net_income;

                                if ($incomeNet > 0) {
                                    $this->walletService->credit(
                                        $user->id,
                                        'income',
                                        $incomeNet,
                                        'binary_matching',
                                        $income->id,
                                        [
                                            'reference' => 'BV-INC-'.$matchReference,
                                            'reference_type' => 'matching.repurchase.income',
                                            'reference_id' => $income->id,
                                            'display_reference' => $displayReference,
                                            'narration' => 'Repurchase matching income',
                                        ]
                                    );
                                    $this->walletService->credit(
                                        $user->id,
                                        'withdrawable',
                                        $incomeNet,
                                        'binary_matching',
                                        $income->id,
                                        [
                                            'reference' => 'BV-WDR-'.$matchReference,
                                            'reference_type' => 'matching.repurchase.withdrawable',
                                            'reference_id' => $income->id,
                                            'display_reference' => $displayReference,
                                            'narration' => 'Repurchase matching income',
                                        ]
                                    );
                                }
                            }
                        }

                        if ($type === 'joining') {
                            $carry->joining_left = $config['carry_forward_enabled'] ? round($leftAvailable - $matchedForPayout, 4) : 0;
                            $carry->joining_right = $config['carry_forward_enabled'] ? round($rightAvailable - $matchedForPayout, 4) : 0;
                            $carry->last_joining_processed_at = now();

                            if ($config['matching_type'] === 'member') {
                                $user->left_count = max(0, (int) $user->left_count + $leftMemberDelta);
                                $user->right_count = max(0, (int) $user->right_count + $rightMemberDelta);
                            } else {
                                $user->left_business = round(max(0, (float) $user->left_business + $leftAmountDelta), 4);
                                $user->right_business = round(max(0, (float) $user->right_business + $rightAmountDelta), 4);
                            }

                            $user->carry_forward_left = (float) $carry->joining_left;
                            $user->carry_forward_right = (float) $carry->joining_right;
                            $user->save();
                        } else {
                            $carry->repurchase_left = $config['carry_forward_enabled'] ? round($leftAvailable - $matchedForPayout, 4) : 0;
                            $carry->repurchase_right = $config['carry_forward_enabled'] ? round($rightAvailable - $matchedForPayout, 4) : 0;
                            $carry->last_repurchase_processed_at = now();
                        }

                        $carry->last_processed_at = now();
                        $carry->save();

                        $this->markLedgerProcessed($ledgerRows->pluck('id')->all());

                        $this->mlmLogService?->log(
                            $type === 'joining' ? 'matching.joining.processed' : 'matching.repurchase.processed',
                            $user->id,
                            null,
                            null,
                            [
                                'run_date' => $date->toDateString(),
                                'left_available' => $leftAvailable,
                                'right_available' => $rightAvailable,
                                'matched' => $matchedForPayout,
                                'net_income' => $net ?? 0,
                            ]
                        );
                    });
                }
            }, 'beneficiary_user_id');
    }

    /**
     * @return array{
     *     matching_type:string,
     *     pair_value:float,
     *     rate:float,
     *     daily_pair_limit:int,
     *     weekly_pair_limit:int,
     *     carry_forward_enabled:bool
     * }
     */
    private function resolveMatchingConfig(User $user, string $type, float $defaultPairValue, float $defaultRate): array
    {
        if ($type === 'joining') {
            $matchingType = (string) ($user->currentPlan?->matching_type ?? setting('joining_matching_type', 'member'));
            if (! in_array($matchingType, ['member', 'amount'], true)) {
                $matchingType = 'member';
            }

            $pairValue = (float) ($user->currentPlan?->pair_income ?? 0);
            if ($pairValue <= 0) {
                $pairValue = $defaultPairValue;
            }

            $binaryPercentage = (float) ($user->currentPlan?->binary_percentage ?? 0);
            if ($binaryPercentage <= 0) {
                $binaryPercentage = (float) setting('joining_binary_percentage', 0);
            }

            return [
                'matching_type' => $matchingType,
                'pair_value' => max(0, $pairValue),
                'rate' => max(0, $this->normalizePercentageRate($binaryPercentage)),
                'daily_pair_limit' => (int) ($user->currentPlan?->daily_pair_limit ?? setting('joining_daily_pair_limit', 0)),
                'weekly_pair_limit' => (int) ($user->currentPlan?->weekly_pair_limit ?? setting('joining_weekly_pair_limit', 0)),
                'carry_forward_enabled' => $user->currentPlan?->carry_forward_enabled !== null
                    ? (bool) $user->currentPlan->carry_forward_enabled
                    : $this->boolSetting('joining_carry_forward_enabled', true),
            ];
        }

        $binaryPercentage = (float) ($user->currentPlan?->binary_percentage ?? 0);
        $rate = $binaryPercentage > 0 ? $this->normalizePercentageRate($binaryPercentage) : $defaultRate;

        return [
            'matching_type' => 'amount',
            'pair_value' => 0.0,
            'rate' => max(0, $rate),
            'daily_pair_limit' => 0,
            'weekly_pair_limit' => 0,
            'carry_forward_enabled' => $this->boolSetting('repurchase_carry_forward_enabled', true),
        ];
    }

    private function calculateGross(string $type, string $matchingType, float $matched, float $pairValue, float $rate): float
    {
        if ($matched <= 0) {
            return 0.0;
        }

        if ($type === 'joining' && $matchingType === 'member') {
            return round($matched * $pairValue, 2);
        }

        return round($matched * $rate, 2);
    }

    private function applyPairLimits(
        int $userId,
        Carbon $date,
        float $matched,
        string $matchingType,
        int $dailyLimit,
        int $weeklyLimit
    ): float {
        if ($matchingType !== 'member') {
            return round(max(0, $matched), 4);
        }

        $allowed = $matched;

        if ($dailyLimit > 0) {
            $usedDaily = (float) JoiningPairIncome::query()
                ->where('user_id', $userId)
                ->where('matching_type', $matchingType)
                ->whereDate('run_date', $date->toDateString())
                ->sum('matched_pairs');

            $allowed = min($allowed, max(0, $dailyLimit - $usedDaily));
        }

        if ($weeklyLimit > 0) {
            $weekStart = $date->copy()->startOfWeek();
            $weekEnd = $date->copy()->endOfWeek();

            $usedWeekly = (float) JoiningPairIncome::query()
                ->where('user_id', $userId)
                ->where('matching_type', $matchingType)
                ->whereBetween('run_date', [$weekStart->toDateString(), $weekEnd->toDateString()])
                ->sum('matched_pairs');

            $allowed = min($allowed, max(0, $weeklyLimit - $usedWeekly));
        }

        return round(max(0, $allowed), 4);
    }

    private function normalizePercentageRate(float $value): float
    {
        if ($value <= 0) {
            return 0.0;
        }

        return $value > 1 ? round($value / 100, 6) : $value;
    }

    private function boolSetting(string $key, bool $default): bool
    {
        $raw = setting($key, $default ? '1' : '0');
        if (is_bool($raw)) {
            return $raw;
        }

        $value = strtolower(trim((string) $raw));

        return in_array($value, ['1', 'true', 'yes', 'on'], true);
    }

    private function markProcessed(
        UserBinaryCarry $carry,
        Collection $ledgerRows,
        string $type,
        float $leftAvailable,
        float $rightAvailable,
        bool $carryForwardEnabled
    ): void {
        if ($type === 'joining') {
            $carry->joining_left = $carryForwardEnabled ? round($leftAvailable, 4) : 0;
            $carry->joining_right = $carryForwardEnabled ? round($rightAvailable, 4) : 0;
            $carry->last_joining_processed_at = now();
        } else {
            $carry->repurchase_left = $carryForwardEnabled ? round($leftAvailable, 4) : 0;
            $carry->repurchase_right = $carryForwardEnabled ? round($rightAvailable, 4) : 0;
            $carry->last_repurchase_processed_at = now();
        }

        $carry->last_processed_at = now();
        $carry->save();

        $this->markLedgerProcessed($ledgerRows->pluck('id')->all());
    }

    /**
     * @param  array<int, int|string>  $ledgerIds
     */
    private function markLedgerProcessed(array $ledgerIds): void
    {
        if ($ledgerIds === []) {
            return;
        }

        DB::table('binary_ledger')
            ->whereIn('id', $ledgerIds)
            ->update([
                'processed_at' => now(),
                'updated_at' => now(),
            ]);
    }

    /**
     * @param  array<int, int>  $ledgerIds
     */
    private function buildMatchReference(string $type, Carbon $date, int $userId, array $ledgerIds): string
    {
        $prefix = $type === 'joining' ? 'JOIN' : 'BV';
        $digest = substr(sha1(implode(',', $ledgerIds)), 0, 12);

        return "{$prefix}-{$date->format('Ymd')}-{$userId}-{$digest}";
    }
}


