<?php

declare(strict_types=1);

/**
 * Servicio de renovación comercial.
 *
 * La operación principal solo registra el pago y actualiza la vigencia. Los
 * beneficios de referidos se procesan en una petición independiente para no
 * retrasar la respuesta al Superadmin.
 */
final class SubscriptionService
{
    private const VALID_CYCLES = ['mensual', 'anual'];

    public function __construct(private PDO $pdo)
    {
    }

    /**
     * @return array{
     *   payment_id:int,
     *   duplicate:bool,
     *   new_end_date:string,
     *   expected_price:float,
     *   subscription_id:int,
     *   cycle:string,
     *   referral_job:?array{subscription_id:int,payment_id:int}
     * }
     */
    public function registerPaymentAndRenew(array $input, int $registeredBy): array
    {
        $subscriptionId = (int) ($input['id_suscripcion'] ?? $input['id_suscripcion_pago'] ?? 0);
        $planId = (int) ($input['id_plan'] ?? $input['new_plan_id_on_payment'] ?? 0);
        $cycle = mb_strtolower(trim((string) ($input['ciclo'] ?? $input['ciclo_pago'] ?? '')), 'UTF-8');
        $amountRaw = $input['monto'] ?? $input['monto_pago'] ?? 0;
        $amount = is_numeric($amountRaw) ? round((float) $amountRaw, 2) : 0.0;
        $reference = trim((string) ($input['referencia_transaccion'] ?? ''));
        $notes = trim((string) ($input['notas'] ?? ''));
        $idempotencyKey = trim((string) ($input['clave_idempotencia'] ?? $input['payment_key'] ?? ''));

        if ($subscriptionId <= 0 || $planId <= 0 || $registeredBy <= 0 || $amount <= 0) {
            throw new InvalidArgumentException('Completa el plan, la modalidad y el monto de la renovación.');
        }
        if (!in_array($cycle, self::VALID_CYCLES, 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 del pago no es válida.');
        }

        $existing = $this->findByIdempotency($idempotencyKey, $registeredBy);
        if ($existing !== null) {
            return $existing + ['duplicate' => true];
        }

        $this->configureFastFailSession();
        $hasIncomeType = $this->hasColumn('pagos', 'tipo_ingreso');
        $transactionStarted = false;
        $startedAt = microtime(true);
        $operation = substr(hash('sha256', $idempotencyKey), 0, 12);

        try {
            $this->pdo->beginTransaction();
            $transactionStarted = true;

            // El bloqueo de la fila de suscripción evita dos renovaciones simultáneas
            // sin depender de GET_LOCK/RELEASE_LOCK, que en algunos hostings puede
            // mantener la petición abierta después de confirmar el pago.
            $subscriptionStmt = $this->pdo->prepare(
                "SELECT s.id, s.id_admin, s.id_plan, s.fecha_fin, s.estado,
                        u.nombre_usuario, r.nombre_rol
                 FROM suscripciones s
                 JOIN usuarios u ON u.id = s.id_admin
                 JOIN roles r ON r.id = u.id_rol
                 WHERE s.id = ?
                 LIMIT 1
                 FOR UPDATE"
            );
            $subscriptionStmt->execute([$subscriptionId]);
            $subscription = $subscriptionStmt->fetch(PDO::FETCH_ASSOC);
            if (!$subscription) {
                throw new RuntimeException('La suscripción seleccionada ya no existe.');
            }
            if (mb_strtolower((string) ($subscription['nombre_rol'] ?? ''), 'UTF-8') === 'superadmin') {
                throw new RuntimeException('El plan interno del Superadmin no se renueva desde este módulo.');
            }

            // Segunda comprobación dentro de la transacción. La restricción UNIQUE
            // de clave_idempotencia continúa siendo la protección definitiva.
            $existing = $this->findByIdempotency($idempotencyKey, $registeredBy);
            if ($existing !== null) {
                $this->pdo->commit();
                $transactionStarted = false;
                return $existing + ['duplicate' => true];
            }

            $planStmt = $this->pdo->prepare(
                'SELECT id, nombre_plan, precio_mensual, precio_anual,
                        permite_mensual, permite_anual, activo, es_interno
                 FROM planes
                 WHERE id = ?
                 LIMIT 1'
            );
            $planStmt->execute([$planId]);
            $plan = $planStmt->fetch(PDO::FETCH_ASSOC);
            if (!$plan || (int) ($plan['activo'] ?? 0) !== 1 || (int) ($plan['es_interno'] ?? 0) === 1) {
                throw new RuntimeException('El plan seleccionado no está disponible para renovaciones comerciales.');
            }

            $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.');
            }

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

            $now = new DateTimeImmutable('now');
            $currentEnd = !empty($subscription['fecha_fin']) && strtotime((string) $subscription['fecha_fin']) !== false
                ? new DateTimeImmutable((string) $subscription['fecha_fin'])
                : $now;
            $baseDate = $currentEnd > $now ? $currentEnd : $now;
            $newEnd = $cycle === 'anual'
                ? $this->addCalendarYears($baseDate, 1)
                : $this->addCalendarMonths($baseDate, 1);

            $paymentNotes = $notes !== ''
                ? $notes
                : sprintf(
                    'Renovación %s del plan %s. Precio configurado: %.2f.',
                    $cycle,
                    (string) $plan['nombre_plan'],
                    $expectedPrice
                );

            if ($hasIncomeType) {
                $paymentStmt = $this->pdo->prepare(
                    "INSERT INTO pagos
                        (id_suscripcion, monto, ciclo, tipo_ingreso, moneda, fecha_pago, metodo_pago,
                         referencia_transaccion, notas, registrado_por_admin_id,
                         clave_idempotencia, estado_pago)
                     VALUES (?, ?, ?, 'renovacion', 'PEN', NOW(), 'renovacion', ?, ?, ?, ?, 'confirmado')"
                );
                $paymentStmt->execute([
                    $subscriptionId,
                    $amount,
                    $cycle,
                    $reference !== '' ? $reference : null,
                    $paymentNotes,
                    $registeredBy,
                    $idempotencyKey,
                ]);
            } else {
                $paymentStmt = $this->pdo->prepare(
                    "INSERT INTO pagos
                        (id_suscripcion, monto, ciclo, moneda, fecha_pago, metodo_pago,
                         referencia_transaccion, notas, registrado_por_admin_id,
                         clave_idempotencia, estado_pago)
                     VALUES (?, ?, ?, 'PEN', NOW(), 'renovacion', ?, ?, ?, ?, 'confirmado')"
                );
                $paymentStmt->execute([
                    $subscriptionId,
                    $amount,
                    $cycle,
                    $reference !== '' ? $reference : null,
                    $paymentNotes,
                    $registeredBy,
                    $idempotencyKey,
                ]);
            }
            $paymentId = (int) $this->pdo->lastInsertId();

            $updateStmt = $this->pdo->prepare(
                "UPDATE suscripciones
                 SET id_plan = ?, ciclo_facturacion = ?, fecha_fin = ?,
                     estado = 'activa', fecha_actualizacion = NOW()
                 WHERE id = ?"
            );
            $updateStmt->execute([
                $planId,
                $cycle,
                $newEnd->format('Y-m-d H:i:s'),
                $subscriptionId,
            ]);
            if ($updateStmt->rowCount() !== 1) {
                throw new RuntimeException('No fue posible actualizar la nueva vigencia de la suscripción.');
            }

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

            $this->pdo->commit();
            $transactionStarted = false;

            error_log(sprintf(
                '[Renovación %s] Confirmada en %.3f s. Suscripción %d, pago %d.',
                $operation,
                microtime(true) - $startedAt,
                $subscriptionId,
                $paymentId
            ));

            return [
                'payment_id' => $paymentId,
                'duplicate' => false,
                'new_end_date' => $newEnd->format('Y-m-d H:i:s'),
                'expected_price' => $expectedPrice,
                'subscription_id' => $subscriptionId,
                'cycle' => $cycle,
                'referral_job' => $cycle === 'mensual'
                    ? ['subscription_id' => $subscriptionId, 'payment_id' => $paymentId]
                    : null,
            ];
        } catch (PDOException $e) {
            if ($transactionStarted && $this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }

            if ((string) $e->getCode() === '23000') {
                $existing = $this->findByIdempotency($idempotencyKey, $registeredBy);
                if ($existing !== null) {
                    return $existing + ['duplicate' => true];
                }
            }

            error_log(sprintf('[Renovación %s] Error PDO: %s', $operation, $e->getMessage()));
            throw $e;
        } catch (Throwable $e) {
            if ($transactionStarted && $this->pdo->inTransaction()) {
                $this->pdo->rollBack();
            }
            error_log(sprintf('[Renovación %s] Error: %s', $operation, $e->getMessage()));
            throw $e;
        }
    }

    /**
     * @return array{
     *   payment_id:int,
     *   new_end_date:string,
     *   expected_price:float,
     *   subscription_id:int,
     *   cycle:string,
     *   referral_job:?array{subscription_id:int,payment_id:int}
     * }|null
     */
    public function findByIdempotency(string $idempotencyKey, int $registeredBy): ?array
    {
        if ($idempotencyKey === '' || $registeredBy <= 0) {
            return null;
        }

        $stmt = $this->pdo->prepare(
            "SELECT p.id AS payment_id, p.id_suscripcion, p.monto,
                    COALESCE(NULLIF(p.ciclo, ''), NULLIF(s.ciclo_facturacion, ''), 'manual') AS ciclo,
                    s.fecha_fin
             FROM pagos p
             JOIN suscripciones s ON s.id = p.id_suscripcion
             WHERE p.clave_idempotencia = ?
               AND p.registrado_por_admin_id = ?
             LIMIT 1"
        );
        $stmt->execute([$idempotencyKey, $registeredBy]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return null;
        }

        $cycle = mb_strtolower((string) ($row['ciclo'] ?? 'manual'), 'UTF-8');
        $paymentId = (int) $row['payment_id'];
        $subscriptionId = (int) $row['id_suscripcion'];

        return [
            'payment_id' => $paymentId,
            'new_end_date' => (string) ($row['fecha_fin'] ?? ''),
            'expected_price' => (float) ($row['monto'] ?? 0),
            'subscription_id' => $subscriptionId,
            'cycle' => $cycle,
            'referral_job' => $cycle === 'mensual'
                ? ['subscription_id' => $subscriptionId, 'payment_id' => $paymentId]
                : null,
        ];
    }

    private function configureFastFailSession(): void
    {
        foreach ([
            'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED',
            'SET SESSION innodb_lock_wait_timeout = 3',
            'SET SESSION lock_wait_timeout = 3',
        ] as $statement) {
            try {
                $this->pdo->exec($statement);
            } catch (Throwable $e) {
                error_log('Ajuste opcional de renovación omitido: ' . $e->getMessage());
            }
        }
    }

    private function hasColumn(string $table, string $column): bool
    {
        try {
            $stmt = $this->pdo->prepare(
                'SELECT COUNT(*) FROM information_schema.COLUMNS
                 WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?'
            );
            $stmt->execute([$table, $column]);
            return (int) $stmt->fetchColumn() > 0;
        } catch (Throwable) {
            return false;
        }
    }

    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'));
    }
}
