<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Models\Credit;
use App\Services\Metrics\InterestAttributionService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

/**
 * Concilia el patrimonio por dos caminos independientes y reporta la brecha.
 *
 * Camino contable (devengado):
 *     capital aportado - retiros + interes cobrado - gastos - capital condonado
 *     + interes capitalizado que sigue vivo
 *
 * Camino de activos (lo que realmente existe hoy):
 *     caja - saldos a favor de clientes + capital por recuperar
 *
 * Los dos deben dar lo mismo. Si se separan, algo se esta registrando mal.
 *
 * Nace de una auditoria en la que el puente no cerraba por $450.000. Las tres
 * causas resultaron ser legitimas pero invisibles, y las tres estan modeladas
 * aqui como componentes con nombre para que la proxima brecha SI signifique un
 * problema:
 *
 *   - Al refinanciar, el credito hijo nace con un capital que incluye el interes
 *     impago del padre. Ese capital existe sin que haya salido plata de la caja,
 *     asi que "capital vivo" nunca es igual a "desembolsado - recuperado".
 *   - Una condonacion puede perdonar capital, no solo interes. El enum la trata
 *     como si nunca tocara el capital (`ExpenseCategory::INTEREST_WAIVER` tiene
 *     `reducesCapital() === false`), asi que aqui se calcula el reparto real
 *     desde las cuotas, imputando interes-primero.
 *   - Un sobrepago entra a la caja pero es un saldo a favor del cliente, no
 *     patrimonio.
 *
 * Hermano de `credify:verify-interest`, que ancla la imputacion de intereses.
 */
class VerifyEquity extends Command
{
    protected $signature = 'credify:verify-equity
                            {--company=1 : Empresa a conciliar}
                            {--tolerance=50000 : Brecha maxima aceptable en pesos}';

    protected $description = 'Concilia el patrimonio por dos caminos independientes y reporta la brecha';

    public function handle(InterestAttributionService $interest): int
    {
        $companyId = (int) $this->option('company');
        $tolerance = (float) $this->option('tolerance');

        $aportes = $this->sumIncomes($companyId, ['capital_injection', 'investor_contribution']);
        $retiros = $this->sumExpenses($companyId, ['profit_withdrawal', 'capital_return']);
        $interesCobrado = $interest->interestTotal($companyId);
        $gastos = $this->sumExpenses($companyId, ['other_operational', 'office_supplies', 'admin_expense']);
        $capitalCondonado = $this->forgivenPrincipal($companyId);
        $capitalizado = $this->capitalizedInterestStillLive($companyId);

        $patrimonio = $aportes - $retiros + $interesCobrado - $gastos - $capitalCondonado + $capitalizado;

        $caja = $this->cashOnHand($companyId);
        $saldosAFavor = $this->sumIncomes($companyId, ['overpayment']);
        $capitalVivo = $this->outstandingPrincipal($companyId);

        $activo = $caja - $saldosAFavor + $capitalVivo;
        $brecha = $activo - $patrimonio;

        $this->table(['Patrimonio contable', 'Monto'], [
            ['(+) Capital aportado', $this->money($aportes)],
            ['(-) Retiros y devoluciones', $this->money(-$retiros)],
            ['(+) Interes cobrado', $this->money($interesCobrado)],
            ['(-) Gastos operativos', $this->money(-$gastos)],
            ['(-) Capital condonado', $this->money(-$capitalCondonado)],
            ['(+) Interes capitalizado vivo', $this->money($capitalizado)],
            ['= PATRIMONIO', $this->money($patrimonio)],
        ]);

        $this->table(['Activos reales', 'Monto'], [
            ['Caja', $this->money($caja)],
            ['(-) Saldos a favor de clientes', $this->money(-$saldosAFavor)],
            ['(+) Capital por recuperar', $this->money($capitalVivo)],
            ['= ACTIVO', $this->money($activo)],
        ]);

        $this->newLine();
        $this->line('  Brecha: '.$this->money($brecha).'  (tolerancia '.$this->money($tolerance).')');
        $this->newLine();

        $sospechas = $this->capitalAnomalies($companyId);

        if ($sospechas !== []) {
            $this->warn('  APORTES DE CAPITAL SOSPECHOSOS');
            foreach ($sospechas as $s) {
                $this->line('   · '.$s);
            }
            $this->newLine();
        }

        if (abs($brecha) > $tolerance) {
            $this->error('BRECHA FUERA DE TOLERANCIA. Algo se esta registrando mal: revisar condonaciones, refinanciaciones y sobrepagos recientes.');

            return self::FAILURE;
        }

        if ($sospechas !== []) {
            $this->error('El puente cuadra, pero hay aportes de capital que no cierran como movimientos reales.');

            return self::FAILURE;
        }

        $this->info('OK: los dos caminos cuadran dentro de la tolerancia.');

        return self::SUCCESS;
    }

    /**
     * Anomalias que el puente NO puede ver.
     *
     * Un aporte de capital duplicado infla los dos lados por igual —sube el
     * capital aportado Y sube la caja—, asi que la brecha sigue dando cero. En
     * produccion habia $1.500.000 registrados dos veces el mismo dia por el mismo
     * socio (una vez en el momento como `investor_contribution`, otra dos semanas
     * despues al cargar los aportes por el modulo de socios) y la conciliacion
     * cuadraba perfectamente con un millon y medio de mas en cada plato.
     *
     * @return list<string>
     */
    private function capitalAnomalies(int $companyId): array
    {
        $hallazgos = [];

        // 1) Mismo monto, misma fecha: la firma de un movimiento cargado dos veces.
        $duplicados = DB::table('incomes')
            ->where('company_id', $companyId)
            ->whereIn('category', ['capital_injection', 'investor_contribution'])
            ->select('operation_date', 'amount', DB::raw('COUNT(*) as n'), DB::raw('GROUP_CONCAT(id) as ids'))
            ->groupBy('operation_date', 'amount')
            ->havingRaw('COUNT(*) > 1')
            ->get();

        foreach ($duplicados as $d) {
            $hallazgos[] = sprintf(
                'posible duplicado: %s el %s, %d veces (ingresos %s)',
                $this->money((float) $d->amount), $d->operation_date, $d->n, $d->ids
            );
        }

        // 2) Plata que entra como capital sin socio detras. Todo aporte legitimo
        //    pasa por el modulo de socios y deja su fila en `capital_contributions`.
        $huerfanos = DB::table('incomes as i')
            ->where('i.company_id', $companyId)
            ->whereIn('i.category', ['capital_injection', 'investor_contribution'])
            ->whereNotExists(
                fn ($q) => $q->select(DB::raw(1))
                    ->from('capital_contributions as cc')
                    ->whereColumn('cc.income_id', 'i.id')
            )
            ->select('i.id', 'i.amount', 'i.operation_date', 'i.notes')
            ->get();

        foreach ($huerfanos as $h) {
            $hallazgos[] = sprintf(
                'aporte sin socio vinculado: ingreso #%d, %s el %s (%s)',
                $h->id, $this->money((float) $h->amount), $h->operation_date, $h->notes ?: 'sin notas'
            );
        }

        return $hallazgos;
    }

    /** @param  list<string>  $categories */
    private function sumIncomes(int $companyId, array $categories): float
    {
        return (float) DB::table('incomes')
            ->where('company_id', $companyId)
            ->whereIn('category', $categories)
            ->sum('amount');
    }

    /** Solo gastos aprobados: los pendientes todavia no salieron de la caja. */
    private function sumExpenses(int $companyId, array $categories): float
    {
        return (float) DB::table('expenses')
            ->where('company_id', $companyId)
            ->whereIn('category', $categories)
            ->where(fn ($q) => $q->where('approval_status', 'approved')->orWhere('requires_approval', 0))
            ->sum('amount');
    }

    private function cashOnHand(int $companyId): float
    {
        $in = (float) DB::table('incomes')->where('company_id', $companyId)->sum('amount');
        $out = (float) DB::table('expenses')
            ->where('company_id', $companyId)
            ->where('affects_cash', 1)
            ->where(fn ($q) => $q->where('approval_status', 'approved')->orWhere('requires_approval', 0))
            ->sum('amount');

        return $in - $out;
    }

    /**
     * Capital pendiente, imputando interes-primero: de lo ya abonado en una
     * cuota, el interes se cubre antes que el capital.
     *
     * Incluye los ARCHIVADOS a proposito. Archivar retira el credito de la
     * cartera OPERATIVA —el cobrador deja de verlo y el panel deja de contarlo—
     * pero no perdona la deuda: ese capital sigue siendo un activo. Si no se
     * contara aqui, archivar un credito abriria una brecha en la conciliacion
     * exactamente del tamano de su saldo, y el control nocturno empezaria a
     * fallar por una operacion perfectamente legitima.
     */
    private function outstandingPrincipal(int $companyId): float
    {
        $recuperables = [...Credit::ACTIVE_STATUSES, Credit::STATUS_ARCHIVED];

        return (float) DB::table('installments as i')
            ->join('credits as c', 'c.id', '=', 'i.credit_id')
            ->where('c.company_id', $companyId)
            ->whereIn('c.status', $recuperables)
            ->whereNotExists(fn ($q) => $q->select(DB::raw(1))->from('credits as ch')->whereColumn('ch.parent_credit_id', 'c.id'))
            ->whereIn('i.status', ['pending', 'partial_paid', 'overdue'])
            ->sum(DB::raw('GREATEST(0, i.principal_amount - GREATEST(0, i.amount_paid - i.interest_amount))'));
    }

    /**
     * Cuanto de lo condonado era CAPITAL.
     *
     * `amount_paid` incluye lo condonado, asi que el efectivo real es
     * `amount_paid - forgiven_amount`. Ese efectivo cubre primero el interes; lo
     * que la condonacion termina perdonando es el capital que el efectivo no
     * alcanzo a cubrir.
     */
    private function forgivenPrincipal(int $companyId): float
    {
        return (float) DB::table('installments as i')
            ->join('credits as c', 'c.id', '=', 'i.credit_id')
            ->where('c.company_id', $companyId)
            ->where('i.forgiven_amount', '>', 0)
            ->sum(DB::raw('
                GREATEST(0, i.principal_amount
                    - GREATEST(0, (i.amount_paid - i.forgiven_amount) - i.interest_amount))
            '));
    }

    /**
     * Interes que se volvio capital al refinanciar y que sigue vivo hoy.
     *
     * El hijo nace con un monto mayor al capital que le quedaba al padre: esa
     * diferencia es interes capitalizado. Solo cuenta si el hijo sigue vigente.
     */
    private function capitalizedInterestStillLive(int $companyId): float
    {
        $total = 0.0;

        $children = Credit::query()
            ->withoutGlobalScopes()
            ->where('company_id', $companyId)
            ->whereNotNull('parent_credit_id')
            ->activeLeafCredits()
            ->get(['id', 'amount', 'parent_credit_id']);

        foreach ($children as $child) {
            $parentPrincipal = (float) DB::table('installments')
                ->where('credit_id', $child->parent_credit_id)
                ->sum(DB::raw('GREATEST(0, principal_amount - GREATEST(0, amount_paid - interest_amount))'));

            $total += (float) $child->amount - $parentPrincipal;
        }

        return $total;
    }

    private function money(float $value): string
    {
        return '$'.number_format($value, 0, ',', '.');
    }
}
