<?php

declare(strict_types=1);

namespace App\Services;

use App\Enums\ContributionType;
use App\Enums\ExpenseCategory;
use App\Enums\IncomeCategory;
use App\Enums\OwnershipModel;
use App\Enums\ProfitDistributionStatus;
use App\Enums\WithdrawalType;
use App\Models\CapitalContribution;
use App\Models\Company;
use App\Models\Expense;
use App\Models\Income;
use App\Models\Partner;
use App\Models\PartnerWithdrawal;
use App\Models\ProfitDistribution;
use App\Models\ProfitDistributionLine;
use App\Services\Dashboard\DashboardCacheService;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * Servicio central para gestionar inversiones de socios.
 *
 * PRINCIPIOS:
 * - Toda operacion financiera usa DB::transaction para atomicidad.
 * - Todo calculo monetario usa Money VO (BC Math) — cero floats.
 * - Los aportes son EQUITY (patrimonio), NO ingresos operativos.
 * - Los retiros reducen equity o utilidades retenidas.
 * - La distribucion de utilidades usa allocateByRatios() para precision exacta
 *   (la ultima linea absorbe residuos de redondeo).
 * - SELECT ... FOR UPDATE se usa donde hay riesgo de race conditions.
 */
class PartnerInvestmentService
{
    public function __construct(
        private DashboardCacheService $dashboardCache
    ) {}

    // ═══════════════════════════════════════════════════════════════════════
    // CONTRIBUTIONS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Register a capital contribution from a partner.
     *
     * Creates:
     * 1. CapitalContribution record
     * 2. Income record (category: capital_injection or investor_contribution)
     *
     * The Income ensures the contribution appears in the company's cash flow
     * and capital tracking (affects_capital=true, affects_profit=false).
     */
    public function registerContribution(
        Partner $partner,
        string $amount,
        ContributionType $type,
        Carbon $operationDate,
        ?string $method = null,
        ?string $reference = null,
        ?string $notes = null,
        ?array $metadata = null,
    ): CapitalContribution {
        $amountMoney = Money::of($amount);

        if ($amountMoney->isZero() || $amountMoney->isNegative()) {
            throw new RuntimeException('El monto del aporte debe ser positivo.');
        }

        if (! $partner->isActive()) {
            throw new RuntimeException('No se puede registrar un aporte para un socio inactivo.');
        }

        $contribution = DB::transaction(function () use (
            $partner, $amountMoney, $type, $operationDate, $method, $reference, $notes, $metadata
        ) {
            // Lock the partner row to prevent concurrent modifications
            Partner::where('id', $partner->id)->lockForUpdate()->first();

            // 1. Create the Income record (EQUITY entry)
            $income = Income::query()->create([
                'company_id' => $partner->company_id,
                'amount' => $amountMoney->toString(),
                'category' => IncomeCategory::CAPITAL_INJECTION->value,
                'method' => $method,
                'operation_date' => $operationDate,
                'reference' => $reference,
                'notes' => $notes
                    ? "Aporte de {$partner->name}: {$notes}"
                    : "Aporte de capital - {$partner->name}",
                'metadata' => array_merge($metadata ?? [], [
                    'partner_id' => $partner->id,
                    'partner_name' => $partner->name,
                    'contribution_type' => $type->value,
                ]),
                'user_id' => auth()->id(),
                'affects_capital' => true,
                'affects_profit' => false,
            ]);

            // 2. Create the CapitalContribution record
            return CapitalContribution::create([
                'company_id' => $partner->company_id,
                'partner_id' => $partner->id,
                'amount' => $amountMoney->toString(),
                'type' => $type,
                'method' => $method,
                'operation_date' => $operationDate,
                'reference' => $reference,
                'notes' => $notes,
                'metadata' => $metadata,
                'income_id' => $income->id,
                'user_id' => auth()->id(),
            ]);
        });

        $this->dashboardCache->invalidateFinancialRelated($partner->company_id);

        return $contribution;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // WITHDRAWALS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Register a withdrawal request from a partner.
     *
     * Withdrawal is created with approval_status='pending'.
     * The Expense record is only created when the withdrawal is approved.
     *
     * Validations:
     * - For capital_return: amount <= net capital of the partner
     * - For profit_withdrawal/dividend: no validation against balance
     *   (business decision, may distribute future profits)
     */
    public function registerWithdrawal(
        Partner $partner,
        string $amount,
        WithdrawalType $type,
        Carbon $operationDate,
        ?string $method = null,
        ?string $reference = null,
        ?string $notes = null,
        ?array $metadata = null,
    ): PartnerWithdrawal {
        $amountMoney = Money::of($amount);

        if ($amountMoney->isZero() || $amountMoney->isNegative()) {
            throw new RuntimeException('El monto del retiro debe ser positivo.');
        }

        if (! $partner->isActive()) {
            throw new RuntimeException('No se puede registrar un retiro para un socio inactivo.');
        }

        return DB::transaction(function () use (
            $partner, $amountMoney, $type, $operationDate, $method, $reference, $notes, $metadata
        ) {
            // Lock partner to prevent concurrent modifications
            Partner::where('id', $partner->id)->lockForUpdate()->first();

            // Validate capital return doesn't exceed net capital
            if ($type === WithdrawalType::CAPITAL_RETURN) {
                $netCapital = $partner->getNetCapital();
                if ($amountMoney->greaterThan($netCapital)) {
                    throw new RuntimeException(
                        "El retiro de capital ({$amountMoney->formatWithCurrency()}) "
                        ."excede el capital neto del socio ({$netCapital->formatWithCurrency()})."
                    );
                }
            }

            return PartnerWithdrawal::create([
                'company_id' => $partner->company_id,
                'partner_id' => $partner->id,
                'amount' => $amountMoney->toString(),
                'type' => $type,
                'method' => $method,
                'operation_date' => $operationDate,
                'reference' => $reference,
                'notes' => $notes,
                'metadata' => $metadata,
                'approval_status' => 'pending',
                'user_id' => auth()->id(),
            ]);
        });
    }

    /**
     * Approve a pending withdrawal and create the corresponding Expense.
     */
    public function approveWithdrawal(
        PartnerWithdrawal $withdrawal,
        ?string $approvalNotes = null,
    ): PartnerWithdrawal {
        if (! $withdrawal->isPending()) {
            throw new RuntimeException('Solo se pueden aprobar retiros pendientes.');
        }

        $companyId = $withdrawal->company_id;

        $result = DB::transaction(function () use ($withdrawal, $approvalNotes) {
            // Lock the withdrawal row
            $withdrawal = PartnerWithdrawal::where('id', $withdrawal->id)
                ->lockForUpdate()
                ->firstOrFail();

            $partner = $withdrawal->partner;
            $type = $withdrawal->type instanceof WithdrawalType
                ? $withdrawal->type
                : WithdrawalType::from($withdrawal->type);

            // Create the Expense record.
            // Mark as pre-approved: approval already happened at the PartnerWithdrawal level,
            // so the creating hook must not override approval_status to 'pending'.
            $expense = new Expense([
                'company_id' => $withdrawal->company_id,
                'amount' => $withdrawal->amount,
                'category' => $type->toExpenseCategory()->value,
                'expense_type' => $type->value,
                'method' => $withdrawal->method,
                'operation_date' => $withdrawal->operation_date,
                'reference' => $withdrawal->reference,
                'notes' => "Retiro de {$partner->name}: ".($withdrawal->notes ?? $type->getLabel()),
                'metadata' => array_merge($withdrawal->metadata ?? [], [
                    'partner_id' => $partner->id,
                    'partner_name' => $partner->name,
                    'withdrawal_id' => $withdrawal->id,
                    'withdrawal_type' => $type->value,
                ]),
                'user_id' => auth()->id(),
                'affects_cash' => true,
                'affects_profit' => $type->affectsProfit(),
                'reduces_capital' => $type->reducesCapital(),
                'requires_approval' => false,
                'approval_status' => 'approved',
                'approved_by_user_id' => auth()->id(),
                'approved_at' => now(),
                'approval_notes' => $approvalNotes,
            ]);
            $expense->preApproved = true;
            $expense->save();

            // Update the withdrawal
            $withdrawal->update([
                'expense_id' => $expense->id,
                'approval_status' => 'approved',
                'approved_by_user_id' => auth()->id(),
                'approved_at' => now(),
                'approval_notes' => $approvalNotes,
            ]);

            return $withdrawal->fresh();
        });

        $this->dashboardCache->invalidateFinancialRelated($companyId);

        return $result;
    }

    /**
     * Reject a pending withdrawal.
     */
    public function rejectWithdrawal(
        PartnerWithdrawal $withdrawal,
        string $reason,
    ): PartnerWithdrawal {
        if (! $withdrawal->isPending()) {
            throw new RuntimeException('Solo se pueden rechazar retiros pendientes.');
        }

        $withdrawal->update([
            'approval_status' => 'rejected',
            'approved_by_user_id' => auth()->id(),
            'approved_at' => now(),
            'approval_notes' => $reason,
        ]);

        return $withdrawal->fresh();
    }

    // ═══════════════════════════════════════════════════════════════════════
    // OWNERSHIP CALCULATIONS
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Calculate ownership percentages for all active partners in a company.
     *
     * For FIXED model partners: uses their fixed_ownership_percentage.
     * For DYNAMIC model partners: calculates based on net capital proportions.
     *
     * Returns an array of [partner_id => percentage_string].
     * All percentages use BC Math with 4 decimal precision.
     * The sum of percentages always equals exactly '100.0000'.
     */
    public function calculateOwnershipPercentages(int $companyId): array
    {
        $partners = Partner::where('company_id', $companyId)
            ->where('status', 'active')
            ->get();

        if ($partners->isEmpty()) {
            return [];
        }

        $result = [];
        $fixedTotal = '0';
        $dynamicPartners = [];
        $dynamicCapitals = [];

        // First pass: separate fixed vs dynamic
        foreach ($partners as $partner) {
            $model = $partner->ownership_model instanceof OwnershipModel
                ? $partner->ownership_model
                : OwnershipModel::from($partner->ownership_model);

            if ($model === OwnershipModel::FIXED && $partner->fixed_ownership_percentage !== null) {
                $pct = bcadd($partner->fixed_ownership_percentage, '0', 4);
                $result[$partner->id] = $pct;
                $fixedTotal = bcadd($fixedTotal, $pct, 4);
            } else {
                $dynamicPartners[] = $partner;
                $netCapital = $partner->getNetCapital()->toString();
                $dynamicCapitals[$partner->id] = $netCapital;
            }
        }

        // Remaining percentage for dynamic partners
        $remainingPercentage = bcsub('100.0000', $fixedTotal, 4);

        if (bccomp($remainingPercentage, '0', 4) < 0) {
            throw new RuntimeException(
                'Los porcentajes fijos suman mas del 100%. '
                ."Total fijo: {$fixedTotal}%"
            );
        }

        // Second pass: distribute remaining among dynamic partners
        if (! empty($dynamicPartners)) {
            $totalDynamicCapital = '0';
            foreach ($dynamicCapitals as $capital) {
                $totalDynamicCapital = bcadd($totalDynamicCapital, $capital, 8);
            }

            if (bccomp($totalDynamicCapital, '0', 8) <= 0) {
                // No capital yet — distribute equally
                $count = count($dynamicPartners);
                $equalShare = bcdiv($remainingPercentage, (string) $count, 4);
                $allocated = '0';

                foreach ($dynamicPartners as $i => $partner) {
                    if ($i === $count - 1) {
                        // Last partner gets remainder to avoid rounding errors
                        $result[$partner->id] = bcsub($remainingPercentage, $allocated, 4);
                    } else {
                        $result[$partner->id] = $equalShare;
                        $allocated = bcadd($allocated, $equalShare, 4);
                    }
                }
            } else {
                // Proportional to capital
                $allocated = '0';
                $lastIndex = count($dynamicPartners) - 1;

                foreach ($dynamicPartners as $i => $partner) {
                    $capital = $dynamicCapitals[$partner->id];

                    if ($i === $lastIndex) {
                        // Last partner absorbs rounding residue
                        $result[$partner->id] = bcsub($remainingPercentage, $allocated, 4);
                    } else {
                        $ratio = bcdiv($capital, $totalDynamicCapital, 8);
                        $pct = bcmul($remainingPercentage, $ratio, 4);
                        $result[$partner->id] = $pct;
                        $allocated = bcadd($allocated, $pct, 4);
                    }
                }
            }
        }

        return $result;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // PROFIT DISTRIBUTION
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Create a draft profit distribution for a period.
     *
     * Calculates net profit from Income/Expense records that affect profit,
     * then allocates to partners based on ownership percentages.
     *
     * Uses Money::allocateByRatios() for exact distribution without
     * floating point errors — the last partner absorbs any rounding residue.
     */
    public function distributeProfits(
        int $companyId,
        Carbon $periodStart,
        Carbon $periodEnd,
        string $retainedPercentage = '0',
        ?string $notes = null,
    ): ProfitDistribution {
        return DB::transaction(function () use (
            $companyId, $periodStart, $periodEnd, $retainedPercentage, $notes
        ) {
            // 1. Calculate period financials
            $totalRevenue = Money::of(
                Income::where('company_id', $companyId)
                    ->where('affects_profit', true)
                    ->whereBetween('operation_date', [$periodStart, $periodEnd])
                    ->sum('amount')
            );

            // ->effective(): egresos pendientes/rechazados no descuentan caja/utilidad
            // hasta que el admin los aprueba (misma regla canónica que getCashBase/
            // getCashPosition, ver CLAUDE.md). Sin este scope, un egreso pendiente
            // de un cobrador reduciría la utilidad distribuible antes de aprobarse.
            $totalExpenses = Money::of(
                Expense::where('company_id', $companyId)
                    ->where('affects_profit', true)
                    ->whereBetween('operation_date', [$periodStart, $periodEnd])
                    ->effective()
                    ->sum('amount')
            );

            $netProfit = $totalRevenue->subtract($totalExpenses);

            if ($netProfit->isZeroOrNegative()) {
                throw new RuntimeException(
                    'No hay utilidades para distribuir en el periodo seleccionado. '
                    ."Ingresos: {$totalRevenue->formatWithCurrency()}, "
                    ."Egresos: {$totalExpenses->formatWithCurrency()}, "
                    ."Resultado: {$netProfit->formatWithCurrency()}"
                );
            }

            // 2. Calculate retained vs distributable
            $retainedAmount = $netProfit->percentage($retainedPercentage);
            $distributableProfit = $netProfit->subtract($retainedAmount);

            // 3. Get ownership percentages
            $percentages = $this->calculateOwnershipPercentages($companyId);

            if (empty($percentages)) {
                throw new RuntimeException('No hay socios activos para distribuir utilidades.');
            }

            // 4. Create distribution header
            $distribution = ProfitDistribution::create([
                'company_id' => $companyId,
                'period_start' => $periodStart,
                'period_end' => $periodEnd,
                'total_revenue' => $totalRevenue->toString(),
                'total_expenses' => $totalExpenses->toString(),
                'net_profit' => $netProfit->toString(),
                'distributable_profit' => $distributableProfit->toString(),
                'retained_amount' => $retainedAmount->toString(),
                'status' => ProfitDistributionStatus::DRAFT->value,
                'user_id' => auth()->id(),
                'notes' => $notes,
                'metadata' => [
                    'retained_percentage' => $retainedPercentage,
                    'partner_count' => count($percentages),
                ],
            ]);

            // 5. Allocate using Money VO for exact distribution
            $ratios = array_values($percentages);
            $partnerIds = array_keys($percentages);
            $allocations = $distributableProfit->allocateByRatios($ratios);

            // 6. Create distribution lines
            foreach ($partnerIds as $index => $partnerId) {
                $partner = Partner::find($partnerId);
                $lineAmount = $allocations[$index];

                ProfitDistributionLine::create([
                    'profit_distribution_id' => $distribution->id,
                    'partner_id' => $partnerId,
                    'ownership_percentage' => $percentages[$partnerId],
                    'capital_at_distribution' => $partner->getNetCapital()->toString(),
                    'gross_amount' => $lineAmount->toString(),
                    'retained_amount' => '0.00',
                    'net_amount' => $lineAmount->toString(),
                ]);
            }

            return $distribution->load('lines.partner');
        });
    }

    /**
     * Execute an approved distribution: create withdrawals + expenses for each line.
     */
    public function executeDistribution(ProfitDistribution $distribution): ProfitDistribution
    {
        if ($distribution->status !== ProfitDistributionStatus::APPROVED) {
            throw new RuntimeException('Solo se pueden ejecutar distribuciones aprobadas.');
        }

        $companyId = $distribution->company_id;

        $result = DB::transaction(function () use ($distribution) {
            $distribution = ProfitDistribution::where('id', $distribution->id)
                ->lockForUpdate()
                ->firstOrFail();

            foreach ($distribution->lines as $line) {
                if ($line->is_paid) {
                    continue;
                }

                $netAmount = Money::of($line->net_amount);
                if ($netAmount->isZero()) {
                    continue;
                }

                $partner = $line->partner;

                // Create withdrawal (auto-approved since distribution is approved)
                $withdrawal = PartnerWithdrawal::create([
                    'company_id' => $distribution->company_id,
                    'partner_id' => $partner->id,
                    'amount' => $netAmount->toString(),
                    'type' => WithdrawalType::DIVIDEND_PAYMENT->value,
                    'operation_date' => Carbon::today(),
                    'notes' => "Distribucion #{$distribution->id} - Periodo {$distribution->period_start->format('Y-m-d')} a {$distribution->period_end->format('Y-m-d')}",
                    'approval_status' => 'approved',
                    'approved_by_user_id' => auth()->id(),
                    'approved_at' => now(),
                    'user_id' => auth()->id(),
                ]);

                // Create expense — pre-approved because the distribution itself was approved.
                $expense = new Expense([
                    'company_id' => $distribution->company_id,
                    'amount' => $netAmount->toString(),
                    'category' => ExpenseCategory::DIVIDEND_PAYMENT->value,
                    'expense_type' => 'dividend_payment',
                    'operation_date' => Carbon::today(),
                    'notes' => "Utilidades - {$partner->name} - Periodo {$distribution->period_start->format('Y-m-d')} a {$distribution->period_end->format('Y-m-d')}",
                    'metadata' => [
                        'partner_id' => $partner->id,
                        'distribution_id' => $distribution->id,
                        'ownership_percentage' => $line->ownership_percentage,
                    ],
                    'user_id' => auth()->id(),
                    'affects_cash' => true,
                    'affects_profit' => true,
                    'reduces_capital' => false,
                    'requires_approval' => false,
                    'approval_status' => 'approved',
                ]);
                $expense->preApproved = true;
                $expense->save();

                $withdrawal->update(['expense_id' => $expense->id]);

                $line->update([
                    'is_paid' => true,
                    'paid_at' => Carbon::today(),
                    'withdrawal_id' => $withdrawal->id,
                ]);
            }

            $distribution->update([
                'status' => ProfitDistributionStatus::DISTRIBUTED->value,
                'distributed_at' => now(),
            ]);

            return $distribution->fresh(['lines.partner', 'lines.withdrawal']);
        });

        $this->dashboardCache->invalidateFinancialRelated($companyId);

        return $result;
    }

    // ═══════════════════════════════════════════════════════════════════════
    // BALANCE & REPORTING
    // ═══════════════════════════════════════════════════════════════════════

    /**
     * Get comprehensive financial summary for a partner.
     */
    public function getPartnerBalance(Partner $partner): array
    {
        $totalContributed = $partner->getTotalContributed();
        $totalCapitalReturned = $partner->getTotalCapitalReturned();
        $netCapital = $partner->getNetCapital();
        $totalProfitsReceived = $partner->getTotalProfitsReceived();
        $totalWithdrawn = $partner->getTotalWithdrawn();
        $currentBalance = $partner->getCurrentBalance();
        $roi = $partner->getReturnOnInvestment();

        return [
            'partner_id' => $partner->id,
            'partner_name' => $partner->name,
            'total_contributed' => $totalContributed->toString(),
            'total_contributed_formatted' => $totalContributed->formatWithCurrency(),
            'total_capital_returned' => $totalCapitalReturned->toString(),
            'total_capital_returned_formatted' => $totalCapitalReturned->formatWithCurrency(),
            'net_capital' => $netCapital->toString(),
            'net_capital_formatted' => $netCapital->formatWithCurrency(),
            'total_profits_received' => $totalProfitsReceived->toString(),
            'total_profits_received_formatted' => $totalProfitsReceived->formatWithCurrency(),
            'total_withdrawn' => $totalWithdrawn->toString(),
            'total_withdrawn_formatted' => $totalWithdrawn->formatWithCurrency(),
            'current_balance' => $currentBalance->toString(),
            'current_balance_formatted' => $currentBalance->formatWithCurrency(),
            'roi_percentage' => $roi,
            'contribution_count' => $partner->contributions()->count(),
            'withdrawal_count' => $partner->withdrawals()->approved()->count(),
        ];
    }

    /**
     * Get total capital invested across all active partners for a company.
     */
    public function getTotalCapitalInvested(int $companyId): Money
    {
        $sum = CapitalContribution::where('company_id', $companyId)->sum('amount');

        return Money::of($sum);
    }

    /**
     * Get summary of all partner balances for a company.
     */
    public function getCompanyPartnerSummary(int $companyId): array
    {
        $partners = Partner::where('company_id', $companyId)
            ->where('status', 'active')
            ->get();

        $percentages = $this->calculateOwnershipPercentages($companyId);
        $totalCapital = $this->getTotalCapitalInvested($companyId);

        $partnerSummaries = [];
        foreach ($partners as $partner) {
            $balance = $this->getPartnerBalance($partner);
            $balance['ownership_percentage'] = $percentages[$partner->id] ?? '0.0000';
            $partnerSummaries[] = $balance;
        }

        return [
            'total_capital_invested' => $totalCapital->toString(),
            'total_capital_invested_formatted' => $totalCapital->formatWithCurrency(),
            'partner_count' => $partners->count(),
            'partners' => $partnerSummaries,
        ];
    }
}
