<?php

namespace App\Support;

use App\Enums\InvestmentStatus;
use App\Enums\InvestmentTransactionType;
use App\Enums\WithdrawalRequestStatus;
use App\Models\Investment;
use App\Models\InvestmentSetting;
use App\Models\InvestmentTransaction;
use App\Models\User;
use App\Models\WithdrawalRequest;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;

class UserInvestmentPortfolio
{
    /**
     * Inversiones visibles para el inversionista (excluye contenedor ADMIN-ADJ).
     *
     * @return HasMany<Investment, User>
     */
    private static function investorVisibleInvestmentsQuery(User $user)
    {
        return $user->investments()
            ->where('txid', 'not like', InvestmentLedger::ADMIN_CREDIT_TXID_PREFIX.'%');
    }

    /**
     * Resumen financiero compacto para el dashboard (capital, contadores, movimientos recientes).
     *
     * @return array<string, mixed>
     */
    public static function summaryForDashboard(User $user): array
    {
        $totals = self::balanceTotalsForUser($user);

        $investmentsCount = self::investorVisibleInvestmentsQuery($user)->count();

        $activeCount = self::investorVisibleInvestmentsQuery($user)
            ->where('status', InvestmentStatus::Active)
            ->count();

        $pendingCount = self::investorVisibleInvestmentsQuery($user)
            ->where('status', InvestmentStatus::Pending)
            ->count();

        $walletCount = $user->wallets()->count();

        $portfolioWithdrawalPending = WithdrawalRequest::query()
            ->where('user_id', $user->id)
            ->whereIn('status', [WithdrawalRequestStatus::Pending, WithdrawalRequestStatus::PendingEmail])
            ->exists();

        return [
            ...$totals,
            'invested_any' => $investmentsCount > 0,
            'investments_count' => $investmentsCount,
            'active_investments_count' => $activeCount,
            'pending_investments_count' => $pendingCount,
            'wallet_count' => $walletCount,
            'portfolio_withdrawal_pending' => $portfolioWithdrawalPending,
            'recent_transactions' => self::recentLedgerPreviewForDashboard($user, 8),
        ];
    }

    /**
     * Totales de balance para el inversionista (incluyen contenedor ADMIN-ADJ en el total).
     *
     * @return array{
     *     balance_total: string,
     *     invested_active_total: string,
     *     withdrawable_total: string,
     *     referral_balance_total: string,
     *     invested_pending_total: string
     * }
     */
    private static function balanceTotalsForUser(User $user): array
    {
        $investedActiveTotal = (float) (self::investorVisibleInvestmentsQuery($user)
            ->where('status', InvestmentStatus::Active)
            ->sum(DB::raw('COALESCE(capital_balance, amount)')) ?: 0);

        $withdrawable = InvestmentLedger::totalWithdrawableForUser($user->id);
        $referralBalance = InvestmentLedger::referralBalanceForUser($user->id);

        $allCapital = (float) ($user->investments()
            ->whereIn('status', [InvestmentStatus::Active, InvestmentStatus::Completed])
            ->sum(DB::raw('COALESCE(capital_balance, amount)')) ?: 0);

        $investedPendingTotal = (float) (self::investorVisibleInvestmentsQuery($user)
            ->where('status', InvestmentStatus::Pending)
            ->sum('amount') ?: 0);

        return [
            'balance_total' => number_format(round(max(0, $allCapital), 2), 2, '.', ''),
            'invested_active_total' => number_format(round(max(0, $investedActiveTotal), 2), 2, '.', ''),
            'withdrawable_total' => number_format($withdrawable, 2, '.', ''),
            'referral_balance_total' => number_format($referralBalance, 2, '.', ''),
            'invested_pending_total' => number_format(round(max(0, $investedPendingTotal), 2), 2, '.', ''),
        ];
    }

    /**
     * @return list<array{id: int, type: string, amount_usd: string, investment_txid: string, created_at: string|null}>
     */
    private static function recentLedgerPreviewForDashboard(User $user, int $limit): array
    {
        return InvestmentTransaction::query()
            ->visibleInUserLedger($user->id)
            ->with(['investment:id,txid'])
            ->orderByDesc('id')
            ->limit($limit)
            ->get()
            ->map(fn (InvestmentTransaction $t): array => [
                'id' => $t->id,
                'type' => $t->type->value,
                'amount_usd' => number_format((float) $t->amount, 2, '.', ''),
                'investment_txid' => $t->investment?->txid ?? '—',
                'created_at' => $t->created_at?->toIso8601String(),
            ])
            ->values()
            ->all();
    }

    /**
     * Lista detallada para /mis-inversiones (capital vigente vs pactado + acreditaciones).
     *
     * @return array<string, mixed>
     */
    public static function detailPage(User $user): array
    {
        $investments = self::investorVisibleInvestmentsQuery($user)
            ->orderByRaw('CASE WHEN status = ? THEN 0 ELSE 1 END', [InvestmentStatus::Pending->value])
            ->orderByDesc('created_at')
            ->get([
                'id',
                'amount',
                'capital_balance',
                'months_credited',
                'duration_months',
                'monthly_return_snapshot',
                'txid',
                'status',
                'created_at',
                'confirmed_at',
                'maturity_at',
            ]);

        $totals = self::balanceTotalsForUser($user);

        return [
            'investments' => $investments->map(fn (Investment $investment): array => self::portfolioInvestmentPayload($investment))->values()->all(),
            ...$totals,
            'invested_any' => $investments->isNotEmpty(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private static function portfolioInvestmentPayload(Investment $investment): array
    {
        if (InvestmentStatusHelper::isActive($investment)) {
            self::syncContractMaturityExtension($investment);
            $investment->refresh();
        }

        $principal = (float) $investment->amount;
        $snapshotPct = (float) $investment->monthly_return_snapshot;
        $vigentePct = (float) InvestmentSetting::current()->monthly_return;

        $projectedMonthlyNominal = round($principal * ($snapshotPct / 100), 2);
        $projectedTotalGain = round($projectedMonthlyNominal * $investment->duration_months, 2);
        $projectedEnding = round($principal + $projectedTotalGain, 2);

        $daysInPlanMonth = Carbon::now()->daysInMonth;
        $projectedDailyNominal = $daysInPlanMonth > 0
            ? round($principal * ($snapshotPct / 100) / $daysInPlanMonth, 2)
            : 0.0;

        $currentCapital = InvestmentStatusHelper::isActiveOrCompleted($investment)
            ? (float) ($investment->capital_balance ?? $investment->amount)
            : $principal;

        $monthPerformance = [];

        if (InvestmentStatusHelper::isActiveOrCompleted($investment) && $investment->confirmed_at !== null) {
            foreach (self::contractMonthScheduleForInvestment($investment) as $segment) {
                /** @var CarbonInterface $start */
                /** @var CarbonInterface $endExclusive */
                ['month_index' => $segmentIndex, 'start' => $start, 'end' => $endExclusive] = $segment;

                $expected = self::expectedNominalGainBetween($start, $endExclusive, $principal, $snapshotPct);
                $actual = self::actualGainForPeriod($investment->id, $start, $endExclusive);
                $expectedPeriodPct = $principal > 0
                    ? round($expected / $principal * 100, 4)
                    : null;
                $accreditedPct = $actual >= 0.01 && $principal > 0
                    ? round($actual / $principal * 100, 4)
                    : null;

                $periodEndInclusive = $endExclusive->copy()->subDay();

                $monthPerformance[] = [
                    'month_index' => $segmentIndex,
                    'period_label' => self::formatContractMonthLabel($start, $endExclusive),
                    'period_end' => $periodEndInclusive->toDateString(),
                    'nominal_percent' => round($snapshotPct, 4),
                    'expected_period_percent' => $expectedPeriodPct,
                    'projected_gain_usd' => number_format($expected, 2, '.', ''),
                    'capital_base_usd' => number_format($principal, 2, '.', ''),
                    'accredited_percent' => $accreditedPct,
                    'actual_gain_usd' => $actual >= 0.01
                        ? number_format($actual, 2, '.', '')
                        : null,
                    'is_skipped' => (bool) ($segment['is_skipped'] ?? false),
                    'is_extension' => (bool) ($segment['is_extension'] ?? false),
                ];
            }
        }

        $displayMonthsCredited = self::accreditedContractMonths($investment);

        $statusEnum = InvestmentStatusHelper::coerce($investment);

        return [
            'id' => $investment->id,
            'amount' => number_format((float) $investment->amount, 2, '.', ''),
            'current_capital_usd' => number_format($currentCapital, 2, '.', ''),
            'withdrawable_usd' => number_format(InvestmentLedger::withdrawableFromInvestment($investment), 2, '.', ''),
            'months_credited' => $displayMonthsCredited,
            'duration_months' => $investment->duration_months,
            'monthly_return_snapshot' => number_format($snapshotPct, 2, '.', ''),
            'monthly_return_current' => number_format($vigentePct, 2, '.', ''),
            'txid' => $investment->txid,
            'status' => $statusEnum->value,
            'created_at' => $investment->created_at?->toIso8601String(),
            'confirmed_at' => $investment->confirmed_at?->toIso8601String(),
            'maturity_at' => $investment->maturity_at?->toIso8601String(),
            'month_performance' => $monthPerformance,
            'projected_monthly_gain' => number_format($projectedMonthlyNominal, 2, '.', ''),
            'projected_daily_gain' => number_format($projectedDailyNominal, 2, '.', ''),
            'projected_total_gain' => number_format($projectedTotalGain, 2, '.', ''),
            'projected_ending_balance' => number_format($projectedEnding, 2, '.', ''),
        ];
    }

    /**
     * Tramos pactados originales (sin extensiones por meses omitidos).
     *
     * @return list<array{month_index: int, start: CarbonInterface, end: CarbonInterface}>
     */
    public static function baseCalendarMonthIntervalsForInvestment(Investment $investment): array
    {
        $confirmed = $investment->confirmed_at?->copy()->startOfDay();
        $maturityStart = $investment->maturity_at?->copy()->startOfDay();

        if ($confirmed === null || $maturityStart === null) {
            return [];
        }

        $lastActiveDay = $maturityStart->copy()->subDay()->startOfDay();
        if ($lastActiveDay->lt($confirmed)) {
            return [];
        }

        $durationMonths = max(1, (int) $investment->duration_months);

        $intervals = [];
        $cursor = $confirmed->copy();
        $segmentIndex = 0;

        while ($cursor->lte($lastActiveDay) && $segmentIndex < $durationMonths) {
            $segment = self::buildCalendarMonthSegmentFromCursor($cursor, $lastActiveDay, $segmentIndex + 1);
            if ($segment === null) {
                break;
            }

            $intervals[] = $segment;
            $segmentIndex++;
            $cursor = $segment['end']->copy();
        }

        return $intervals;
    }

    /**
     * Calendario contractual con meses omitidos (tachados) y extensiones al final.
     *
     * @return list<array{
     *     month_index: int,
     *     start: CarbonInterface,
     *     end: CarbonInterface,
     *     is_extension: bool,
     *     is_skipped: bool,
     * }>
     */
    public static function contractMonthScheduleForInvestment(Investment $investment): array
    {
        $base = self::baseCalendarMonthIntervalsForInvestment($investment);
        if ($base === []) {
            return [];
        }

        if ($investment->id === null) {
            return array_map(static fn (array $segment): array => [
                ...$segment,
                'is_extension' => false,
                'is_skipped' => false,
            ], $base);
        }

        $gainsByIndex = [];
        foreach ($base as $segment) {
            $gainsByIndex[$segment['month_index']] = self::actualGainForPeriod(
                $investment->id,
                $segment['start'],
                $segment['end'],
            );
        }

        $schedule = [];
        $skippedCount = 0;

        foreach ($base as $segment) {
            $index = (int) $segment['month_index'];
            $actual = $gainsByIndex[$index] ?? 0.0;
            $isSkipped = false;

            if ($actual < 0.01) {
                foreach ($base as $later) {
                    if ($later['start']->lte($segment['start'])) {
                        continue;
                    }

                    if (self::actualGainForPeriod(
                        $investment->id,
                        $later['start'],
                        $later['end'],
                    ) >= 0.01) {
                        $isSkipped = true;
                        break;
                    }
                }
            }

            if ($isSkipped) {
                $skippedCount++;
            }

            $schedule[] = [
                ...$segment,
                'is_extension' => false,
                'is_skipped' => $isSkipped,
            ];
        }

        if ($skippedCount === 0) {
            return $schedule;
        }

        $lastBase = $base[array_key_last($base)];
        $cursor = $lastBase['end']->copy();
        $nextIndex = (int) $lastBase['month_index'];

        for ($extension = 0; $extension < $skippedCount; $extension++) {
            $nextIndex++;
            $segment = self::buildCalendarMonthSegmentFromCursor($cursor, null, $nextIndex);
            if ($segment === null) {
                break;
            }

            $schedule[] = [
                ...$segment,
                'is_extension' => true,
                'is_skipped' => false,
            ];

            $cursor = $segment['end']->copy();
        }

        return $schedule;
    }

    /**
     * Tramos por mes calendario (pactados + extensiones), para acreditaciones y cierre de contrato.
     *
     * @return list<array{month_index: int, start: CarbonInterface, end: CarbonInterface}>
     */
    public static function calendarMonthIntervalsForInvestment(Investment $investment): array
    {
        return array_map(
            static fn (array $segment): array => [
                'month_index' => $segment['month_index'],
                'start' => $segment['start'],
                'end' => $segment['end'],
            ],
            self::contractMonthScheduleForInvestment($investment),
        );
    }

    /**
     * @return array{month_index: int, start: CarbonInterface, end: CarbonInterface}|null
     */
    private static function buildCalendarMonthSegmentFromCursor(
        CarbonInterface $cursor,
        ?CarbonInterface $lastActiveDay,
        int $monthIndex,
    ): ?array {
        $periodStart = $cursor->copy()->startOfDay();
        $monthEnd = $periodStart->copy()->endOfMonth()->startOfDay();
        $periodEnd = $monthEnd;

        if ($lastActiveDay !== null && $monthEnd->gt($lastActiveDay)) {
            $periodEnd = $lastActiveDay->copy();
        }

        if ($periodStart->gt($periodEnd)) {
            return null;
        }

        return [
            'month_index' => $monthIndex,
            'start' => $periodStart,
            'end' => $periodEnd->copy()->addDay()->startOfDay(),
        ];
    }

    /**
     * Extiende vencimiento cuando hay meses pactados omitidos y meses adicionales al final.
     */
    public static function syncContractMaturityExtension(Investment $investment): void
    {
        if (! InvestmentStatusHelper::isActive($investment) || $investment->confirmed_at === null) {
            return;
        }

        $schedule = self::contractMonthScheduleForInvestment($investment);
        if ($schedule === []) {
            return;
        }

        $last = $schedule[array_key_last($schedule)];
        $newMaturity = $last['end']->copy();

        if ($investment->maturity_at === null || $newMaturity->gt($investment->maturity_at->copy()->startOfDay())) {
            $investment->update(['maturity_at' => $newMaturity]);
        }
    }

    /**
     * Tramos del plan con ganancia acreditada (≥ US$ 0,01), para «N de M acreditaciones».
     */
    public static function accreditedContractMonths(Investment $investment): int
    {
        $duration = max(1, (int) $investment->duration_months);

        if (InvestmentStatusHelper::isCompleted($investment)) {
            return $duration;
        }

        if (! InvestmentStatusHelper::isActive($investment)) {
            return max(0, min($duration, (int) $investment->months_credited));
        }

        $intervals = self::calendarMonthIntervalsForInvestment($investment);
        if ($intervals === []) {
            return max(0, min($duration, (int) $investment->months_credited));
        }

        $credited = 0;
        foreach ($intervals as $segment) {
            if (self::actualGainForPeriod(
                $investment->id,
                $segment['start'],
                $segment['end'],
            ) >= 0.01) {
                $credited++;
            }
        }

        return min($duration, $credited);
    }

    /**
     * Etiqueta del tramo (siempre un solo mes calendario), p. ej. «Mayo 31-31» o «Junio 1-30».
     */
    public static function formatContractMonthLabel(CarbonInterface $start, CarbonInterface $endExclusive): string
    {
        $periodStart = $start->copy()->startOfDay();
        $periodEnd = $endExclusive->copy()->subDay()->startOfDay();

        if ($periodEnd->lt($periodStart)) {
            return '';
        }

        return sprintf(
            '%s %d-%d',
            self::spanishMonthName($periodStart),
            $periodStart->day,
            $periodEnd->day,
        );
    }

    private static function spanishMonthName(CarbonInterface $date): string
    {
        static $names = [
            1 => 'Enero',
            2 => 'Febrero',
            3 => 'Marzo',
            4 => 'Abril',
            5 => 'Mayo',
            6 => 'Junio',
            7 => 'Julio',
            8 => 'Agosto',
            9 => 'Septiembre',
            10 => 'Octubre',
            11 => 'Noviembre',
            12 => 'Diciembre',
        ];

        return $names[(int) $date->month] ?? $date->format('F');
    }

    private static function expectedNominalGainBetween(
        CarbonInterface $start,
        CarbonInterface $endExclusive,
        float $principal,
        float $nominalPct,
    ): float {
        $total = 0.0;
        $d = $start->copy()->startOfDay();
        $cap = $endExclusive->copy()->startOfDay();

        while ($d->lt($cap)) {
            $dim = max(1, $d->daysInMonth);
            $total += $principal * ($nominalPct / 100) / $dim;
            $d = $d->addDay();
        }

        return round($total, 2);
    }

    private static function actualGainForPeriod(
        int $investmentId,
        CarbonInterface $start,
        CarbonInterface $endExclusive,
    ): float {
        $lastDay = $endExclusive->copy()->subDay();
        if ($lastDay->lt($start)) {
            return 0.0;
        }

        $d0 = $start->toDateString();
        $d1 = $lastDay->toDateString();

        $daily = (float) InvestmentTransaction::query()
            ->where('investment_id', $investmentId)
            ->where('type', InvestmentTransactionType::DailyGain)
            ->whereDate('accrual_on', '>=', $d0)
            ->whereDate('accrual_on', '<=', $d1)
            ->sum('amount');

        $contractStart = $start->copy()->startOfDay();
        $contractLast = $lastDay->copy()->startOfDay();

        $fromPeriod = 0.0;
        $lumps = InvestmentTransaction::query()
            ->where('investment_id', $investmentId)
            ->where('type', InvestmentTransactionType::PeriodAccrual)
            ->whereNotNull('gain_period_start')
            ->whereNotNull('gain_period_end')
            ->get(['amount', 'gain_period_start', 'gain_period_end']);

        foreach ($lumps as $row) {
            $p0 = Carbon::parse($row->gain_period_start)->startOfDay();
            $p1 = Carbon::parse($row->gain_period_end)->startOfDay();
            $overlapStart = $contractStart->gt($p0) ? $contractStart->copy() : $p0->copy();
            $overlapEnd = $contractLast->lt($p1) ? $contractLast->copy() : $p1->copy();

            if ($overlapStart->gt($overlapEnd)) {
                continue;
            }

            $overlapDays = $overlapStart->diffInDays($overlapEnd) + 1;
            $periodDays = $p0->diffInDays($p1) + 1;
            if ($periodDays < 1) {
                continue;
            }

            $fromPeriod += (float) $row->amount * ($overlapDays / $periodDays);
        }

        return round($daily + $fromPeriod, 2);
    }
}
