<?php

declare(strict_types=1);

require_once __DIR__ . '/SubscriptionAccessService.php';

final class WalletSubscriptionService
{
    public function __construct(private PDO $pdo)
    {
    }

    public function getCurrency(): string
    {
        try {
            $stmt = $this->pdo->prepare(
                "SELECT valor FROM configuracion_sistema
                 WHERE clave IN ('moneda', 'currency')
                 ORDER BY FIELD(clave, 'moneda', 'currency')
                 LIMIT 1"
            );
            $stmt->execute();
            $currency = strtoupper(trim((string) $stmt->fetchColumn()));
            return $currency !== '' ? $currency : 'PEN';
        } catch (Throwable) {
            return 'PEN';
        }
    }

    public function getCurrencySymbol(): string
    {
        try {
            $stmt = $this->pdo->prepare("SELECT valor FROM configuracion_sistema WHERE clave = 'moneda_simbolo' LIMIT 1");
            $stmt->execute();
            $configured = trim((string) $stmt->fetchColumn());
            if ($configured !== '') {
                return $configured;
            }
        } catch (Throwable) {
            // Se utiliza el símbolo estándar del código ISO.
        }

        $currency = $this->getCurrency();
        return match ($currency) {
            'PEN' => 'S/',
            'USD' => '$',
            'EUR' => '€',
            default => $currency,
        };
    }

    public function getBalance(int $userId): float
    {
        if ($userId <= 0) {
            return 0.0;
        }
        $stmt = $this->pdo->prepare('SELECT saldo FROM monederos WHERE id_usuario = ? LIMIT 1');
        $stmt->execute([$userId]);
        return round((float) ($stmt->fetchColumn() ?: 0), 2);
    }

    /**
     * Agrega crédito de forma auditable. Puede ejecutarse dentro de una transacción existente.
     */
    public function addCredit(
        int $userId,
        float $amount,
        string $type,
        string $description,
        ?int $createdBy = null,
        ?int $relatedUserId = null
    ): int {
        if ($userId <= 0 || $amount <= 0) {
            throw new InvalidArgumentException('El usuario y el importe deben ser válidos.');
        }
        $allowed = ['recarga', 'bono_referido', 'ajuste', 'reembolso'];
        if (!in_array($type, $allowed, true)) {
            throw new InvalidArgumentException('El tipo de movimiento no es válido para una recarga.');
        }

        $this->pdo->prepare(
            'INSERT INTO monederos (id_usuario, saldo) VALUES (?, 0)
             ON DUPLICATE KEY UPDATE id_usuario = VALUES(id_usuario)'
        )->execute([$userId]);

        $wallet = $this->pdo->prepare('SELECT id, saldo FROM monederos WHERE id_usuario = ? LIMIT 1 FOR UPDATE');
        $wallet->execute([$userId]);
        $row = $wallet->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            throw new RuntimeException('No fue posible preparar el monedero del usuario.');
        }

        $newBalance = round((float) $row['saldo'] + $amount, 2);
        $update = $this->pdo->prepare('UPDATE monederos SET saldo = ?, fecha_actualizacion = NOW() WHERE id = ?');
        $update->execute([$newBalance, (int) $row['id']]);

        $movement = $this->pdo->prepare(
            "INSERT INTO movimientos_credito
                (id_usuario, tipo, monto, descripcion, creado_por, id_usuario_relacionado, fecha_creacion)
             VALUES (?, ?, ?, ?, ?, ?, NOW())"
        );
        $movement->execute([
            $userId,
            $type,
            round($amount, 2),
            function_exists('mb_substr') ? mb_substr(trim($description), 0, 255) : substr(trim($description), 0, 255),
            $createdBy ?: null,
            $relatedUserId ?: null,
        ]);

        return (int) $this->pdo->lastInsertId();
    }

    /**
     * Transfiere saldo desde un Admin hacia una cuenta Admin referida directamente por él.
     * El importe se descuenta del monedero del referente y se acredita al referido.
     *
     * @return array{sender_balance:float,recipient_balance:float,sender_movement_id:int,recipient_movement_id:int}
     */
    public function transferCreditToReferral(
        int $referrerId,
        int $referredId,
        float $amount,
        string $description = ''
    ): array {
        $amount = round($amount, 2);
        if ($referrerId <= 0 || $referredId <= 0 || $referrerId === $referredId || $amount <= 0) {
            throw new InvalidArgumentException('Los datos de la recarga no son válidos.');
        }

        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) {
            $this->pdo->beginTransaction();
        }

        try {
            $recipientStmt = $this->pdo->prepare(
                "SELECT u.id, u.nombre_usuario, u.correo, u.id_referidor, r.nombre_rol
"
                . "FROM usuarios u
"
                . "JOIN roles r ON r.id = u.id_rol
"
                . "WHERE u.id = ? LIMIT 1 FOR UPDATE"
            );
            $recipientStmt->execute([$referredId]);
            $recipient = $recipientStmt->fetch(PDO::FETCH_ASSOC);
            if (!$recipient || mb_strtolower((string) ($recipient['nombre_rol'] ?? ''), 'UTF-8') !== 'admin') {
                throw new RuntimeException('La cuenta seleccionada no corresponde a un Admin.');
            }
            if ((int) ($recipient['id_referidor'] ?? 0) !== $referrerId) {
                throw new RuntimeException('Solo puedes recargar saldo a cuentas Admin referidas directamente por ti.');
            }

            $senderStmt = $this->pdo->prepare(
                "SELECT u.id, u.nombre_usuario, r.nombre_rol
"
                . "FROM usuarios u JOIN roles r ON r.id = u.id_rol
"
                . "WHERE u.id = ? LIMIT 1 FOR UPDATE"
            );
            $senderStmt->execute([$referrerId]);
            $sender = $senderStmt->fetch(PDO::FETCH_ASSOC);
            if (!$sender || mb_strtolower((string) ($sender['nombre_rol'] ?? ''), 'UTF-8') !== 'admin') {
                throw new RuntimeException('La cuenta que realiza la recarga no corresponde a un Admin.');
            }

            $this->pdo->prepare(
                'INSERT INTO monederos (id_usuario, saldo) VALUES (?, 0), (?, 0)
'
                . 'ON DUPLICATE KEY UPDATE id_usuario = VALUES(id_usuario)'
            )->execute([$referrerId, $referredId]);

            $walletStmt = $this->pdo->prepare(
                'SELECT id, id_usuario, saldo FROM monederos WHERE id_usuario IN (?, ?) ORDER BY id_usuario FOR UPDATE'
            );
            $walletStmt->execute([$referrerId, $referredId]);
            $wallets = [];
            foreach ($walletStmt->fetchAll(PDO::FETCH_ASSOC) as $wallet) {
                $wallets[(int) $wallet['id_usuario']] = $wallet;
            }
            if (!isset($wallets[$referrerId], $wallets[$referredId])) {
                throw new RuntimeException('No fue posible preparar los monederos para la recarga.');
            }

            $senderBalance = round((float) $wallets[$referrerId]['saldo'], 2);
            if ($senderBalance + 0.0001 < $amount) {
                throw new RuntimeException(
                    'Saldo insuficiente. Tu monedero dispone de '
                    . $this->getCurrencySymbol() . ' ' . number_format($senderBalance, 2) . '.'
                );
            }

            $recipientBalance = round((float) $wallets[$referredId]['saldo'], 2);
            $newSenderBalance = round($senderBalance - $amount, 2);
            $newRecipientBalance = round($recipientBalance + $amount, 2);

            $updateWallet = $this->pdo->prepare('UPDATE monederos SET saldo = ?, fecha_actualizacion = NOW() WHERE id = ?');
            $updateWallet->execute([$newSenderBalance, (int) $wallets[$referrerId]['id']]);
            $updateWallet->execute([$newRecipientBalance, (int) $wallets[$referredId]['id']]);

            $detail = trim($description);
            $senderDescription = 'Transferencia de saldo a ' . (string) $recipient['nombre_usuario'] . '.';
            $recipientDescription = 'Recarga recibida de ' . (string) $sender['nombre_usuario'] . '.';
            if ($detail !== '') {
                $shortDetail = function_exists('mb_substr') ? mb_substr($detail, 0, 150) : substr($detail, 0, 150);
                $senderDescription .= ' Detalle: ' . $shortDetail;
                $recipientDescription .= ' Detalle: ' . $shortDetail;
            }

            $movementStmt = $this->pdo->prepare(
                "INSERT INTO movimientos_credito
"
                . "(id_usuario, tipo, monto, descripcion, creado_por, id_usuario_relacionado, fecha_creacion)
"
                . "VALUES (?, ?, ?, ?, ?, ?, NOW())"
            );
            $movementStmt->execute([
                $referrerId,
                'debito',
                -$amount,
                function_exists('mb_substr') ? mb_substr($senderDescription, 0, 255) : substr($senderDescription, 0, 255),
                $referrerId,
                $referredId,
            ]);
            $senderMovementId = (int) $this->pdo->lastInsertId();

            $movementStmt->execute([
                $referredId,
                'recarga',
                $amount,
                function_exists('mb_substr') ? mb_substr($recipientDescription, 0, 255) : substr($recipientDescription, 0, 255),
                $referrerId,
                $referrerId,
            ]);
            $recipientMovementId = (int) $this->pdo->lastInsertId();

            if ($ownsTransaction) {
                $this->pdo->commit();
            }

            return [
                'sender_balance' => $newSenderBalance,
                'recipient_balance' => $newRecipientBalance,
                'sender_movement_id' => $senderMovementId,
                'recipient_movement_id' => $recipientMovementId,
            ];
        } catch (Throwable $e) {
            if ($ownsTransaction && $this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }
    }

    /** @return array<int,array<string,mixed>> */
    public function getAvailablePlans(): array
    {
        $stmt = $this->pdo->query(
            "SELECT id, nombre_plan, descripcion, precio_mensual, precio_anual,
                    permite_mensual, permite_anual, limite_buzones, limite_dominios,
                    limite_cuentas, limite_usuarios, limite_consultas, orden_publico
             FROM planes
             WHERE visible_publico = 1
               AND activo = 1
               AND es_interno = 0
               AND nombre_plan <> 'Gratis'
               AND (
                    (permite_mensual = 1 AND COALESCE(precio_mensual, 0) > 0)
                    OR (permite_anual = 1 AND COALESCE(precio_anual, 0) > 0)
               )
             ORDER BY orden_publico ASC, COALESCE(NULLIF(precio_mensual, 0), NULLIF(precio_anual, 0), 999999), id"
        );
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /** @return array<string,mixed>|null */
    public function getCurrentSubscription(int $adminId): ?array
    {
        $stmt = $this->pdo->prepare(
            "SELECT s.id, s.id_admin, s.id_plan, s.ciclo_facturacion, s.fecha_inicio, s.fecha_fin, s.estado,
                    p.nombre_plan, p.precio_mensual, p.precio_anual
             FROM suscripciones s
             JOIN planes p ON p.id = s.id_plan
             WHERE s.id_admin = ?
             ORDER BY s.fecha_fin DESC, s.id DESC
             LIMIT 1"
        );
        $stmt->execute([$adminId]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        return $row ?: null;
    }

    /** @return array<int,array<string,mixed>> */
    public function getReferrals(int $referrerId): array
    {
        $stmt = $this->pdo->prepare(
            "SELECT u.id, u.nombre_usuario, u.correo, u.telefono,
                    s.id AS subscription_id, s.estado, s.fecha_fin, s.id_plan,
                    p.nombre_plan
             FROM usuarios u
             JOIN roles r ON r.id = u.id_rol AND r.nombre_rol IN ('Admin', 'Ayudante de Superadmin')
             LEFT JOIN suscripciones s ON s.id = (
                 SELECT s2.id FROM suscripciones s2
                 WHERE s2.id_admin = u.id
                 ORDER BY s2.fecha_fin DESC, s2.id DESC
                 LIMIT 1
             )
             LEFT JOIN planes p ON p.id = s.id_plan
             WHERE u.id_referidor = ?
             ORDER BY u.nombre_usuario ASC, u.id DESC"
        );
        $stmt->execute([$referrerId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * Renueva únicamente la suscripción del propio Admin usando su saldo.
     *
     * @return array{renewal_id:int,movement_id:int,new_end_date:string,amount:float,currency:string,balance:float}
     */
    public function renewWithCredit(
        int $payerAdminId,
        int $beneficiaryAdminId,
        int $planId,
        string $cycle,
        string $idempotencyKey
    ): array {
        if ($payerAdminId <= 0 || $beneficiaryAdminId <= 0 || $planId <= 0) {
            throw new InvalidArgumentException('Los datos de renovación no son válidos.');
        }
        $cycle = mb_strtolower(trim($cycle), 'UTF-8');
        if (!in_array($cycle, ['mensual', 'anual'], true)) {
            throw new InvalidArgumentException('Selecciona una modalidad mensual o anual.');
        }
        if (!preg_match('/^[A-Za-z0-9_-]{16,64}$/', $idempotencyKey)) {
            throw new InvalidArgumentException('La clave de seguridad de la renovación no es válida.');
        }

        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) {
            $this->pdo->beginTransaction();
        }

        try {
            $duplicate = $this->pdo->prepare('SELECT id FROM renovaciones_credito WHERE clave_idempotencia = ? LIMIT 1 FOR UPDATE');
            $duplicate->execute([$idempotencyKey]);
            if ($duplicate->fetchColumn()) {
                throw new RuntimeException('Esta renovación ya fue procesada. Actualiza la página para ver el resultado.');
            }

            $authorization = $this->pdo->prepare(
                "SELECT u.id, u.nombre_usuario, u.id_referidor, r.nombre_rol
                 FROM usuarios u
                 JOIN roles r ON r.id = u.id_rol
                 WHERE u.id = ?
                 LIMIT 1
                 FOR UPDATE"
            );
            $authorization->execute([$beneficiaryAdminId]);
            $beneficiary = $authorization->fetch(PDO::FETCH_ASSOC);
            if (!$beneficiary || !in_array((string) ($beneficiary['nombre_rol'] ?? ''), ['Admin', 'Ayudante de Superadmin'], true)) {
                throw new RuntimeException('La cuenta beneficiaria no corresponde a un Admin o Ayudante operativo.');
            }
            if ($beneficiaryAdminId !== $payerAdminId) {
                throw new RuntimeException('El saldo solo puede utilizarse para renovar tu propia cuenta.');
            }

            $planStmt = $this->pdo->prepare(
                "SELECT id, nombre_plan, precio_mensual, precio_anual, permite_mensual, permite_anual,
                        visible_publico, activo, es_interno
                 FROM planes
                 WHERE id = ?
                 LIMIT 1
                 FOR UPDATE"
            );
            $planStmt->execute([$planId]);
            $plan = $planStmt->fetch(PDO::FETCH_ASSOC);
            if (!$plan
                || (int) ($plan['visible_publico'] ?? 0) !== 1
                || (int) ($plan['activo'] ?? 0) !== 1
                || (int) ($plan['es_interno'] ?? 0) === 1
            ) {
                throw new RuntimeException('El plan seleccionado no está disponible para renovación con crédito.');
            }
            $cycleAllowed = $cycle === 'anual'
                ? (int) ($plan['permite_anual'] ?? 0) === 1
                : (int) ($plan['permite_mensual'] ?? 0) === 1;
            if (!$cycleAllowed) {
                throw new RuntimeException('La modalidad seleccionada no está habilitada para este plan.');
            }

            $amount = $cycle === 'anual'
                ? round((float) ($plan['precio_anual'] ?? 0), 2)
                : round((float) ($plan['precio_mensual'] ?? 0), 2);
            if ($amount <= 0) {
                throw new RuntimeException('El plan no tiene un precio válido para la modalidad seleccionada.');
            }

            $this->pdo->prepare(
                'INSERT INTO monederos (id_usuario, saldo) VALUES (?, 0)
                 ON DUPLICATE KEY UPDATE id_usuario = VALUES(id_usuario)'
            )->execute([$payerAdminId]);
            $walletStmt = $this->pdo->prepare('SELECT id, saldo FROM monederos WHERE id_usuario = ? LIMIT 1 FOR UPDATE');
            $walletStmt->execute([$payerAdminId]);
            $wallet = $walletStmt->fetch(PDO::FETCH_ASSOC);
            $balance = round((float) ($wallet['saldo'] ?? 0), 2);
            if ($balance + 0.0001 < $amount) {
                throw new RuntimeException('Saldo insuficiente. Necesitas ' . $this->getCurrencySymbol() . ' ' . number_format($amount, 2) . ' para completar la renovación.');
            }

            $subscriptionStmt = $this->pdo->prepare(
                "SELECT id, fecha_fin, estado
                 FROM suscripciones
                 WHERE id_admin = ?
                 ORDER BY fecha_fin DESC, id DESC
                 LIMIT 1
                 FOR UPDATE"
            );
            $subscriptionStmt->execute([$beneficiaryAdminId]);
            $subscription = $subscriptionStmt->fetch(PDO::FETCH_ASSOC);

            $now = new DateTimeImmutable('now');
            $previousEnd = null;
            if ($subscription && !empty($subscription['fecha_fin']) && strtotime((string) $subscription['fecha_fin']) !== false) {
                $previousEnd = new DateTimeImmutable((string) $subscription['fecha_fin']);
            }
            $base = $previousEnd && $previousEnd > $now ? $previousEnd : $now;
            $newEnd = $cycle === 'anual'
                ? $this->addCalendarYears($base, 1)
                : $this->addCalendarMonths($base, 1);

            if ($subscription) {
                $updateSubscription = $this->pdo->prepare(
                    "UPDATE suscripciones
                     SET id_plan = ?, ciclo_facturacion = ?, fecha_fin = ?, estado = 'activa', fecha_actualizacion = NOW()
                     WHERE id = ?"
                );
                $updateSubscription->execute([$planId, $cycle, $newEnd->format('Y-m-d H:i:s'), (int) $subscription['id']]);
                $subscriptionId = (int) $subscription['id'];
            } else {
                $insertSubscription = $this->pdo->prepare(
                    "INSERT INTO suscripciones
                        (id_admin, id_plan, ciclo_facturacion, fecha_inicio, fecha_fin, estado, fecha_actualizacion)
                     VALUES (?, ?, ?, NOW(), ?, 'activa', NOW())"
                );
                $insertSubscription->execute([$beneficiaryAdminId, $planId, $cycle, $newEnd->format('Y-m-d H:i:s')]);
                $subscriptionId = (int) $this->pdo->lastInsertId();
            }

            $this->pdo->prepare('UPDATE usuarios SET id_plan = ? WHERE id = ?')->execute([$planId, $beneficiaryAdminId]);

            $newBalance = round($balance - $amount, 2);
            $this->pdo->prepare('UPDATE monederos SET saldo = ?, fecha_actualizacion = NOW() WHERE id = ?')
                ->execute([$newBalance, (int) $wallet['id']]);

            $description = sprintf(
                'Renovación %s del plan %s para %s.',
                $cycle,
                (string) $plan['nombre_plan'],
                (string) $beneficiary['nombre_usuario']
            );
            $movementStmt = $this->pdo->prepare(
                "INSERT INTO movimientos_credito
                    (id_usuario, tipo, monto, descripcion, creado_por, id_usuario_relacionado, fecha_creacion)
                 VALUES (?, 'debito', ?, ?, ?, ?, NOW())"
            );
            $movementStmt->execute([
                $payerAdminId,
                -$amount,
                function_exists('mb_substr') ? mb_substr($description, 0, 255) : substr($description, 0, 255),
                $payerAdminId,
                $beneficiaryAdminId,
            ]);
            $movementId = (int) $this->pdo->lastInsertId();

            $renewalStmt = $this->pdo->prepare(
                "INSERT INTO renovaciones_credito
                    (id_pagador, id_beneficiario, id_suscripcion, id_plan, ciclo, monto, moneda,
                     fecha_fin_anterior, fecha_fin_nueva, id_movimiento_credito, clave_idempotencia, fecha_creacion)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())"
            );
            $renewalStmt->execute([
                $payerAdminId,
                $beneficiaryAdminId,
                $subscriptionId,
                $planId,
                $cycle,
                $amount,
                $this->getCurrency(),
                $previousEnd?->format('Y-m-d H:i:s'),
                $newEnd->format('Y-m-d H:i:s'),
                $movementId,
                $idempotencyKey,
            ]);
            $renewalId = (int) $this->pdo->lastInsertId();

            if ($ownsTransaction) {
                $this->pdo->commit();
            }

            return [
                'renewal_id' => $renewalId,
                'movement_id' => $movementId,
                'new_end_date' => $newEnd->format('Y-m-d H:i:s'),
                'amount' => $amount,
                'currency' => $this->getCurrency(),
                'balance' => $newBalance,
            ];
        } catch (Throwable $e) {
            if ($ownsTransaction && $this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            throw $e;
        }
    }

    private function addCalendarMonths(DateTimeImmutable $date, int $months): DateTimeImmutable
    {
        $day = (int) $date->format('d');
        $targetFirst = $date->modify('first day of this month')->modify('+' . $months . ' month');
        $lastDay = (int) $targetFirst->format('t');
        return $targetFirst
            ->setDate((int) $targetFirst->format('Y'), (int) $targetFirst->format('m'), min($day, $lastDay))
            ->setTime((int) $date->format('H'), (int) $date->format('i'), (int) $date->format('s'));
    }

    private function addCalendarYears(DateTimeImmutable $date, int $years): DateTimeImmutable
    {
        $targetYear = (int) $date->format('Y') + $years;
        $month = (int) $date->format('m');
        $day = (int) $date->format('d');
        $first = $date->setDate($targetYear, $month, 1);
        $lastDay = (int) $first->format('t');
        return $first
            ->setDate($targetYear, $month, min($day, $lastDay))
            ->setTime((int) $date->format('H'), (int) $date->format('i'), (int) $date->format('s'));
    }

}
