from __future__ import annotations

from aiogram import F, Router
from aiogram.types import CallbackQuery, Message
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.common import get_or_create_user, money, notify_admins
from app.config import get_settings
from app.db import get_session
from app.keyboards import (
    categories_kb,
    delivery_kb,
    item_card_kb,
    item_reviews_back_kb,
    items_kb,
)
from app.models import Category, Item, Purchase, Review, Stock, User

router = Router()


async def _stock_count(session: AsyncSession, item_id: int) -> int:
    result = await session.execute(
        select(func.count())
        .select_from(Stock)
        .where(Stock.item_id == item_id, Stock.is_sold.is_(False))
    )
    return int(result.scalar_one())


async def _in_stock_ids(session: AsyncSession, item_ids: list[int]) -> set[int]:
    """Item ids (from the given list) that have at least one unsold stock position."""
    if not item_ids:
        return set()
    result = await session.execute(
        select(Stock.item_id)
        .where(Stock.item_id.in_(item_ids), Stock.is_sold.is_(False))
        .group_by(Stock.item_id)
    )
    return {row[0] for row in result.all()}


async def _rating(session: AsyncSession, item_id: int) -> tuple[float, int]:
    result = await session.execute(
        select(func.avg(Review.rating), func.count())
        .select_from(Review)
        .where(Review.item_id == item_id)
    )
    avg, count = result.one()
    return float(avg or 0.0), int(count or 0)


def _stars(rating: float) -> str:
    full = int(round(rating))
    return "★" * full + "☆" * (5 - full)


def _reviewer(username: str | None, user_id: int) -> str:
    """Real reviewer identity — @username (if any) plus the numeric id, unmasked."""
    handle = f"@{username}" if username else "user"
    return f"{handle} · id{user_id}"


async def _render_categories(session: AsyncSession) -> tuple[str, object] | None:
    result = await session.execute(
        select(Category).order_by(Category.sort_order, Category.id)
    )
    categories = result.scalars().all()
    if not categories:
        return None
    return "🛍 <b>Catalog</b>\n\nChoose a category:", categories_kb(categories)


@router.message(F.text == "◈ Catalog")
async def catalog_message(message: Message) -> None:
    async with get_session() as session:
        rendered = await _render_categories(session)
    if rendered is None:
        await message.answer("😕 The catalog is empty for now. Come back later.")
        return
    text, kb = rendered
    await message.answer(text, reply_markup=kb)


@router.callback_query(F.data == "catalog")
async def catalog_back(callback: CallbackQuery) -> None:
    async with get_session() as session:
        rendered = await _render_categories(session)
    if rendered is None:
        await callback.message.edit_text("😕 The catalog is empty.")
        await callback.answer()
        return
    text, kb = rendered
    await callback.message.edit_text(text, reply_markup=kb)
    await callback.answer()


@router.callback_query(F.data.startswith("cat:"))
async def open_category(callback: CallbackQuery) -> None:
    settings = get_settings()
    category_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        category = await session.get(Category, category_id)
        if category is None:
            await callback.answer("Category not found", show_alert=True)
            return
        result = await session.execute(
            select(Item)
            .where(Item.category_id == category_id, Item.is_active.is_(True))
            .order_by(Item.sort_order, Item.id)
        )
        items = result.scalars().all()
        stock_ids = await _in_stock_ids(session, [i.id for i in items])
    if not items:
        await callback.message.edit_text(
            f"📂 <b>{category.name}</b>\n\nNo items in this category yet.",
            reply_markup=items_kb([], settings.currency),
        )
        await callback.answer()
        return
    await callback.message.edit_text(
        f"📂 <b>{category.name}</b>\n\n🟢 in stock · 🔴 out of stock\n\nChoose an item:",
        reply_markup=items_kb(items, settings.currency, stock_ids),
    )
    await callback.answer()


# NOTE: colored inline buttons use the Bot API `style` field
# (success = green, danger = red, primary = blue). See items_kb / item_card_kb.


async def build_item_card(
    session: AsyncSession, item_id: int, currency: str
) -> tuple[str, object] | None:
    """Render an item card (text + inline keyboard) or None if the item is unavailable."""
    item = await session.get(Item, item_id)
    if item is None or not item.is_active:
        return None
    available = await _stock_count(session, item_id)
    has_2fa = bool(
        await session.scalar(
            select(Stock.id)
            .where(
                Stock.item_id == item_id,
                Stock.is_sold.is_(False),
                Stock.totp_secret != "",
            )
            .limit(1)
        )
    )
    avg, review_count = await _rating(session, item_id)
    desc = f"\n\n{item.description}" if item.description else ""
    stock_line = (
        f"📦 In stock: <b>{available} pcs.</b>"
        if available
        else "❌ <b>Out of stock</b>"
    )
    twofa_line = "\n🔐 Google Authenticator (2FA) — codes right in the bot" if has_2fa else ""
    rating_line = (
        f"\n⭐ Rating: {_stars(avg)} {avg:.1f} ({review_count})" if review_count else ""
    )
    text = (
        f"🛒 <b>{item.name}</b>{desc}\n\n"
        f"💵 Price: <b>{money(item.price)} {currency}</b>\n"
        f"{stock_line}{twofa_line}{rating_line}"
    )
    kb = item_card_kb(item.id, item.category_id, available > 0, review_count)
    return text, kb


@router.callback_query(F.data.startswith("item:"))
async def open_item(callback: CallbackQuery) -> None:
    settings = get_settings()
    item_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        card = await build_item_card(session, item_id, settings.currency)
    if card is None:
        await callback.answer("Item unavailable", show_alert=True)
        return
    text, kb = card
    await callback.message.edit_text(text, reply_markup=kb)
    await callback.answer()


@router.callback_query(F.data.startswith("revs:"))
async def show_reviews(callback: CallbackQuery) -> None:
    # Reviews are shown across ALL lots, not tied to a single item_id (item ids
    # get reused after deletion, which used to make a review surface on the wrong
    # lot). We keep the opened item_id only so the "back" button returns to it.
    item_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        avg, count = (
            await session.execute(
                select(func.avg(Review.rating), func.count()).select_from(Review)
            )
        ).one()
        result = await session.execute(
            select(Review, User.username)
            .outerjoin(User, User.user_id == Review.user_id)
            .order_by(Review.created_at.desc())
            .limit(20)
        )
        rows = result.all()
    if not rows:
        await callback.message.edit_text(
            "⭐ <b>Reviews — all lots</b>\n\nNo reviews yet.",
            reply_markup=item_reviews_back_kb(item_id),
        )
        await callback.answer()
        return
    avg = float(avg or 0.0)
    lines = [
        "⭐ <b>Reviews — all lots</b>",
        f"Average: {_stars(avg)} {avg:.1f} ({int(count or 0)})\n",
    ]
    for i, (r, username) in enumerate(rows, 1):
        who = _reviewer(username, r.user_id)
        date = r.created_at.strftime("%d.%m.%Y")
        lot = f" · {r.item_name}" if r.item_name else ""
        body = f"\n{r.text}" if r.text else ""
        lines.append(
            f"<b>Review {i}</b> — {_stars(r.rating)} · {who}{lot} · {date}{body}"
        )
    await callback.message.edit_text(
        "\n\n".join(lines), reply_markup=item_reviews_back_kb(item_id)
    )
    await callback.answer()


@router.callback_query(F.data.startswith("buy:"))
async def buy_item(callback: CallbackQuery) -> None:
    settings = get_settings()
    item_id = int(callback.data.split(":")[1])
    async with get_session() as session:
        user = await get_or_create_user(session, callback.from_user)
        item = await session.get(Item, item_id)
        if item is None or not item.is_active:
            await callback.answer("Item unavailable", show_alert=True)
            return
        if user.balance < item.price:
            need = item.price - user.balance
            await callback.answer(
                f"Not enough funds. You need {money(need)} {settings.currency} more. "
                "Please top up your balance.",
                show_alert=True,
            )
            return
        # take the first free stock position (locked within the transaction)
        result = await session.execute(
            select(Stock)
            .where(Stock.item_id == item_id, Stock.is_sold.is_(False))
            .order_by(Stock.id)
            .limit(1)
            .with_for_update()
        )
        stock = result.scalar_one_or_none()
        if stock is None:
            await callback.answer("The item just went out of stock 😔", show_alert=True)
            return
        stock.is_sold = True
        user.balance -= item.price
        user.purchases_count += 1
        user.total_spent += item.price
        user.weekly_spent += item.price
        purchase = Purchase(
            user_id=user.user_id,
            item_id=item.id,
            item_name=item.name,
            price=item.price,
            content=stock.content,
            totp_secret=stock.totp_secret,
        )
        session.add(purchase)
        await session.commit()
        content = stock.content
        item_name = item.name
        price = item.price
        new_balance = user.balance
        has_2fa = bool(stock.totp_secret)
        purchase_id = purchase.id

    twofa_note = "\n\n🔐 This item has 2FA attached — tap the button below for a code." if has_2fa else ""
    await callback.message.answer(
        f"✅ <b>Purchase successful!</b>\n\n"
        f"🛒 {item_name}\n"
        f"💵 Charged: {money(price)} {settings.currency}\n"
        f"💰 Balance left: {money(new_balance)} {settings.currency}\n\n"
        f"🎁 Your item:\n<code>{content}</code>{twofa_note}\n\n"
        "⭐ Enjoyed it? Leave a review below!",
        reply_markup=delivery_kb(purchase_id, has_2fa),
    )
    await callback.answer("Done! ✅")
    await notify_admins(
        callback.bot,
        "🛒 <b>New purchase</b>\n\n"
        f"👤 <code>{callback.from_user.id}</code>\n"
        f"Item: {item_name}\n"
        f"Amount: {money(price)} {settings.currency}",
    )
