from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
import random
from database.models import User
from aiogram.types import User as TelegramUser
from database.base import async_session
from sqlalchemy import select, func, text
from sqlalchemy.orm import joinedload, selectinload
from database.models import VPNSubscription, VPNPlan, Payment, Order
from typing import Optional
import asyncio
from sqlalchemy.exc import OperationalError, IntegrityError
import os
import logging
logger = logging.getLogger(__name__)

# USERS
async def get_or_create_user(tg_user: TelegramUser, referal_id: Optional[int] = None) -> User:
    async with async_session() as session:
        # Try to find existing user
        query = select(User).where(User.telegram_id == tg_user.id)
        result = await session.execute(query)
        user = result.scalar_one_or_none()
        
        if not user:
            # Create new user if not exists
            user = User(
                telegram_id=tg_user.id,
                username=tg_user.username,
                ref_id=referal_id
            )
            session.add(user)
            await session.commit()
            await session.refresh(user)
        
        return user

async def search_user(value: str) -> Optional[User]:
    async with async_session() as session:
        try:
            # Try to convert value to int for telegram_id search
            user_id = int(value)
            query = (select(User)
                    .options(selectinload(User.subscriptions).selectinload(VPNSubscription.plan))
                    .where((User.telegram_id == user_id) | (User.username == value)))
        except ValueError:
            # If conversion fails, search by username
            query = (select(User)
                    .options(selectinload(User.subscriptions).selectinload(VPNSubscription.plan))
                    .where(User.username == value))
        result = await session.execute(query)
        return result.scalar_one_or_none()

async def get_users_ids() -> list[int]:
    async with async_session() as session:
        query = select(User.telegram_id)
        result = await session.execute(query)
        return result.scalars().all()

async def get_users_count() -> int:
    async with async_session() as session:
        query = select(func.count(User.telegram_id))
        result = await session.scalar(query)
        return result

async def update_user_ban(user_id: int, ban: bool):
    async with async_session() as session:
        query = select(User).where(User.telegram_id == user_id)
        result = await session.execute(query)
        user = result.scalar_one()
        user.ban = ban
        session.add(user)
        await session.commit()
        await session.refresh(user)
        return user

# SUBSCRIPTIONS
async def get_user_subscriptions(user_id: int) -> list[VPNSubscription]:
    async with async_session() as session:
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.plan))
                .options(joinedload(VPNSubscription.user))
                .where(VPNSubscription.user_id == user_id))
        result = await session.execute(query)
        return result.scalars().all()

async def add_subscription(user_id: int, plan_id: int):
    async with async_session() as session:
        # Load plan directly in this query to avoid N+1
        query = select(VPNPlan).where(VPNPlan.id == plan_id)
        result = await session.execute(query)
        plan = result.scalar_one()
        
        subscription = VPNSubscription(
            user_id=user_id,
            plan_id=plan_id,
            end_date=datetime.now(ZoneInfo("Europe/Moscow")) + timedelta(days=plan.duration_days),
        )
        session.add(subscription)
        await session.commit()
        await session.refresh(subscription)
        return subscription

async def extend_subscription(subscription_id: int):
    async with async_session() as session:
        # Load subscription with plan in a single query
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.plan))
                .where(VPNSubscription.id == subscription_id))
        result = await session.execute(query)
        subscription = result.scalar_one()
        
        # Ensure current end_date is timezone-aware
        current_end_date = subscription.end_date
        if current_end_date.tzinfo is None:
            current_end_date = current_end_date.replace(tzinfo=ZoneInfo("Europe/Moscow"))
        
        subscription.end_date = current_end_date + timedelta(days=subscription.plan.duration_days)
        session.add(subscription)
        await session.commit()
        await session.refresh(subscription)
        return subscription

async def add_subscription_links(subscription_id: int, links: list[str]):
    async with async_session() as session:
        # Direct query instead of calling get_subscription to avoid extra query
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.plan))
                .options(joinedload(VPNSubscription.user))
                .where(VPNSubscription.id == subscription_id))
        result = await session.execute(query)
        subscription = result.scalar_one()
        
        if isinstance(links, str):
            links = [links]
        subscription.config_data = '\n'.join(links)
        session.add(subscription)
        await session.commit()
        await session.refresh(subscription)
        return subscription

async def get_subscription(subscription_id: int) -> VPNSubscription:
    async with async_session() as session:
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.plan))
                .options(joinedload(VPNSubscription.user))
                .where(VPNSubscription.id == subscription_id))
        result = await session.execute(query)
        return result.scalar_one()

async def get_user_active_subscriptions(user_id: int) -> list[VPNSubscription]:
    async with async_session() as session:
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.plan))
                .options(joinedload(VPNSubscription.user))
                .where(VPNSubscription.user_id == user_id, 
                       VPNSubscription.end_date > datetime.now(ZoneInfo("Europe/Moscow"))))
        result = await session.execute(query)
        return result.scalars().all()

async def get_active_subscriptions() -> list[VPNSubscription]:
    async with async_session() as session:
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.user))
                .options(joinedload(VPNSubscription.plan))
                .where(VPNSubscription.end_date > datetime.now(ZoneInfo("Europe/Moscow"))))
        result = await session.execute(query)
        return result.scalars().all()

async def get_user_subscriptions_paginated(user_id: int, page: int = 1, per_page: int = 5) -> tuple[list[VPNSubscription], int]:
    async with async_session() as session:
        # Get total count
        count_query = select(func.count(VPNSubscription.id)).where(VPNSubscription.user_id == user_id)
        total = await session.scalar(count_query)
        
        # Get paginated subscriptions with related data
        query = (select(VPNSubscription)
                .options(joinedload(VPNSubscription.plan))
                .options(joinedload(VPNSubscription.user))
                .where(VPNSubscription.user_id == user_id)
                .order_by(VPNSubscription.id.desc())
                .offset((page - 1) * per_page)
                .limit(per_page))
        
        result = await session.execute(query)
        subscriptions = result.scalars().all()
        
        return subscriptions, total

async def get_active_subscriptions_count() -> int:
    async with async_session() as session:
        query = select(func.count(VPNSubscription.id)).where(VPNSubscription.end_date > datetime.now(ZoneInfo("Europe/Moscow")))
        result = await session.scalar(query)
        return result

# PLANS
async def create_plan(name: str, duration_days: int, price: int):
    async with async_session() as session:
        plan = VPNPlan(
            name=name,
            duration_days=duration_days,
            price=price,
            description=""
        )
        session.add(plan)
        await session.commit()
        await session.refresh(plan)
        return plan

async def get_plans() -> list[VPNPlan]:
    async with async_session() as session:
        query = select(VPNPlan)
        result = await session.execute(query)
        return result.scalars().all()

async def get_plan(plan_id: int) -> VPNPlan:
    async with async_session() as session:
        query = select(VPNPlan).where(VPNPlan.id == plan_id)
        result = await session.execute(query)
        return result.scalar_one()


# PAYMENTS
async def create_payment(user_id: int, amount: float, provider: str) -> Payment:
    # if insert error payment_id already exists, generate new payment_id
    while True:
        try:
            async with async_session() as session:
                payment = Payment(
                    user_id=user_id,
                    amount=amount,
                    status="pending",
                    provider=provider
                )
                session.add(payment)
                await session.commit()
                await session.refresh(payment)
                return payment
        except Exception as e:
            print(e)

async def set_invoice_id(id: int, invoice_id: str) -> Payment:
    async with async_session() as session:
        query = select(Payment).where(Payment.id == id)
        result = await session.execute(query)
        payment = result.scalar_one()
        payment.payment_id = invoice_id
        session.add(payment)
        await session.commit()
        await session.refresh(payment)
        return payment

async def update_payment_status(payment_id: str, status: str, paid_at: Optional[datetime] = None) -> Payment:
    async with async_session() as session:
        query = select(Payment).where(Payment.id == int(payment_id))
        result = await session.execute(query)
        payment = result.scalar_one()
        
        payment.status = status
        if paid_at:
            if isinstance(paid_at, str):
                # Replace Z timezone with +00:00 for ISO format compatibility
                paid_at = paid_at.replace('Z', '+00:00')
                paid_at = datetime.fromisoformat(paid_at)
            payment.paid_at = paid_at
        
        await session.commit()
        await session.refresh(payment)
        return payment

async def get_payment(payment_id: str) -> Optional[Payment]:
    async with async_session() as session:
        query = (select(Payment)
                .options(joinedload(Payment.user))
                .where(Payment.id == int(payment_id))) 
        result = await session.execute(query)
        return result.scalar_one_or_none()

async def get_payments_amount() -> float:
    async with async_session() as session:
        query = select(func.coalesce(func.sum(Payment.amount), 0)).where(Payment.status == "completed")
        result = await session.scalar(query)
        return result or 0.0

# Balance
async def add_balance(user_id: int, amount: float) -> float:
    """
    Adds amount to user's balance with proper transaction handling.
    Uses row-level locking to prevent race conditions.
    
    Args:
        user_id: Telegram ID of the user
        amount: Amount to add to balance
        
    Returns:
        float: New balance after addition
        
    Raises:
        ValueError: If user not found
        TimeoutError: If the lock cannot be acquired within the timeout period
    Example usage:
        try:
            new_balance = await add_balance(user.telegram_id, 1000)  # Add 1000 to balance
            # Success handling
        except ValueError as e:
            # Handle user not found error
        except TimeoutError as e:
            # Handle locking/timeout errors
        except Exception as e:
            # Handle other unexpected errors
    """
    MAX_RETRY = 3
    RETRY_DELAY = 0.5  # seconds
    
    for attempt in range(MAX_RETRY):
        try:
            async with async_session() as session:
                # Begin transaction explicitly
                async with session.begin():
                    session.connection(execution_options={"isolation_level": "SERIALIZABLE"})
                    # Lock the user row with nowait to avoid deadlocks
                    query = select(User).where(User.telegram_id == user_id).with_for_update(nowait=True)
                    result = await session.execute(query)
                    user = result.scalar_one_or_none()
                    
                    if not user:
                        raise ValueError(f"Пользователь с ID {user_id} не найден")
                    
                    # Add to balance
                    user.balance += amount
                    
                    # Add object to the session
                    session.add(user)
                
                # Log successful transaction
                logger.info(f"Successfully added {amount} to user {user_id}'s balance. New balance: {user.balance}")
                
                return user.balance
                
        except (OperationalError, IntegrityError) as db_err:
            # These are typically lock-related errors
            if attempt == MAX_RETRY - 1:
                raise TimeoutError(f"Ошибка при пополнении баланса: {str(db_err)}") from db_err
            
            # Log retry attempt
            logger.error(f"Lock contention detected, retrying ({attempt+1}/{MAX_RETRY}): {str(db_err)}")
            
            # Wait with exponential backoff before retrying
            backoff_time = RETRY_DELAY * (2 ** attempt)
            await asyncio.sleep(backoff_time)
            
        except ValueError as value_err:
            # User not found - raise immediately
            raise value_err
            
        except Exception as e:
            # Other unexpected errors
            logger.error(f"Unexpected error during balance update: {str(e)}")
            if attempt == MAX_RETRY - 1:
                raise TimeoutError(f"Ошибка при пополнении баланса: {str(e)}") from e
            
            await asyncio.sleep(RETRY_DELAY)

# Orders
async def create_order(user_id: int, amount: float, product: str, product_info: str = None) -> tuple[Order, float]:
    """
    Creates an order and deducts the amount from user's balance.
    Uses row-level locking to prevent race conditions.
    
    Args:
        user_id: Telegram ID of the user
        amount: Amount to deduct from balance and set as order amount
        product: Type of product being ordered
        product_info: Additional product information as JSON string
        
    Returns:
        tuple: (created order, new balance)
        
    Raises:
        ValueError: If user has insufficient balance
        TimeoutError: If the lock cannot be acquired within the timeout period
    """
    MAX_RETRY = 3
    RETRY_DELAY = 0.5  # seconds
    
    for attempt in range(MAX_RETRY):
        try:
            async with async_session() as session:
                async with session.begin():
                    session.connection(execution_options={"isolation_level": "SERIALIZABLE"})
                    # Begin transaction explicitly
                    # Lock the user row with nowait to avoid deadlocks
                    # This ensures we fail fast if lock can't be acquired
                    query = select(User).where(User.telegram_id == user_id).with_for_update(nowait=True)
                    result = await session.execute(query)
                    user = result.scalar_one_or_none()
                    
                    if not user:
                        raise ValueError(f"User with telegram_id {user_id} not found")
                    
                    # Check if user has enough balance
                    if user.balance < amount:
                        raise ValueError(f"Недостаточно средств: {user.balance} < {amount}")
                    
                    # Decrease balance
                    user.balance -= amount
                    

                    # Create order with timestamp
                    order = Order(
                        user_id=user_id,
                        amount=amount,
                        product=product,
                        type_of_order="buy",
                        product_info=product_info,
                        created_at=datetime.now(ZoneInfo("Europe/Moscow"))
                    )
                    
                    # Add both objects to the session
                    session.add(user)
                    session.add(order)
                
                # Log successful transaction
                logger.info(f"Successfully created order {order.id} and decreased balance to {user.balance}")
                
                return order, user.balance
                
        except (OperationalError, IntegrityError) as db_err:
            # These are typically lock-related errors or constraint violations
            if attempt == MAX_RETRY - 1:
                raise TimeoutError(f"Ошибка при создании заказа: {str(db_err)}") from db_err
            
            # Log retry attempt
            logger.error(f"Lock contention detected, retrying ({attempt+1}/{MAX_RETRY}): {str(db_err)}")
            
            # Wait with exponential backoff before retrying
            backoff_time = RETRY_DELAY * (2 ** attempt)  # Exponential backoff
            await asyncio.sleep(backoff_time)
            
        except ValueError as value_err:
            # Business rule violations should be raised immediately
            raise value_err
            
        except Exception as e:
            # Other unexpected errors
            logger.error(f"Unexpected error during order creation: {str(e)}")
            if attempt == MAX_RETRY - 1:
                raise TimeoutError(f"Ошибка при создании заказа: {str(e)}") from e
            
            await asyncio.sleep(RETRY_DELAY)

async def get_referral_earnings(user_id: int) -> int:
    async with async_session() as session:
        query = select(func.coalesce(func.sum(Order.amount), 0)).where(Order.user_id == user_id, Order.type_of_order == "referal_payment")
        result = await session.scalar(query)
        return int(result) if result else 0
