from __future__ import annotations

import secrets
from datetime import timedelta
from typing import Any

import bcrypt
import logfire
from pydantic_ai import RunContext
from sqlalchemy import func
from sqlmodel import select

from app.agents.deps import AgentDeps
from app.db.base import utcnow
from app.email.scaleway import ScalewayEmailClient
from app.models.conversation import Conversation
from app.models.conversation_verification import ConversationVerification
from app.models.customer import Customer
from app.models.reservation import Reservation
from app.models.restaurant import Restaurant


async def send_verification_code_impl(
    ctx: RunContext[AgentDeps],
    email: str,
) -> dict[str, Any]:
    email = email.strip().lower()
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id
    conversation_id = ctx.deps.caller.conversation_id

    if not conversation_id:
        logfire.error(
            "verification_send_missing_conversation_id",
            restaurant_id=restaurant_id,
            caller_channel=ctx.deps.caller.channel,
        )
        raise RuntimeError("caller.conversation_id is required for verification")

    with logfire.span(
        "agents.verification.send_code",
        restaurant_id=restaurant_id,
        conversation_id=conversation_id,
        email=email,
    ):
        res_result = await session.execute(
            select(Reservation)
            .where(
                Reservation.restaurant_id == restaurant_id,
                Reservation.guest_email == email,
                Reservation.status.in_(["confirmed", "pending"]),  # type: ignore[attr-defined]
            )
            .limit(1)
        )
        if res_result.scalar_one_or_none() is None:
            logfire.info(
                "verification_send_rejected_no_reservations",
                restaurant_id=restaurant_id,
                conversation_id=conversation_id,
                email=email,
            )
            return {"error": "no_reservations_found"}

        one_hour_ago = utcnow() - timedelta(hours=1)
        count_result = await session.execute(
            select(func.count())
            .select_from(ConversationVerification)
            .where(
                ConversationVerification.conversation_id == conversation_id,
                ConversationVerification.created_at >= one_hour_ago,
            )
        )
        request_count = count_result.scalar() or 0
        if request_count >= 3:
            logfire.warning(
                "verification_send_rate_limited",
                restaurant_id=restaurant_id,
                conversation_id=conversation_id,
                email=email,
                requests_last_hour=request_count,
            )
            return {"error": "rate_limit_exceeded"}

        code = str(secrets.randbelow(900000) + 100000)
        code_hash = bcrypt.hashpw(code.encode(), bcrypt.gensalt()).decode()

        verification = ConversationVerification(
            conversation_id=conversation_id,
            email=email,
            code_hash=code_hash,
            expires_at=utcnow() + timedelta(minutes=10),
        )
        session.add(verification)
        await session.flush()

        rest_result = await session.execute(
            select(Restaurant).where(Restaurant.id == restaurant_id)
        )
        restaurant = rest_result.scalar_one_or_none()
        restaurant_name = restaurant.name if restaurant else "the restaurant"

        email_client = ScalewayEmailClient(client=ctx.deps.http_client)
        await email_client.send_verification_code(
            to_email=email,
            code=code,
            restaurant_name=restaurant_name,
        )

        logfire.info(
            "verification_code_sent",
            restaurant_id=restaurant_id,
            conversation_id=conversation_id,
            email=email,
            verification_id=verification.id,
        )
        return {"status": "code_sent", "email": email}


async def verify_code_impl(
    ctx: RunContext[AgentDeps],
    email: str,
    code: str,
) -> dict[str, Any]:
    email = email.strip().lower()
    session = ctx.deps.session
    restaurant_id = ctx.deps.restaurant_id
    conversation_id = ctx.deps.caller.conversation_id

    if not conversation_id:
        logfire.error(
            "verification_check_missing_conversation_id",
            restaurant_id=restaurant_id,
            caller_channel=ctx.deps.caller.channel,
        )
        raise RuntimeError("caller.conversation_id is required for verification")

    with logfire.span(
        "agents.verification.verify_code",
        restaurant_id=restaurant_id,
        conversation_id=conversation_id,
        email=email,
    ):
        result = await session.execute(
            select(ConversationVerification)
            .where(
                ConversationVerification.conversation_id == conversation_id,
                ConversationVerification.email == email,
                ConversationVerification.expires_at > utcnow(),
                ConversationVerification.verified_at.is_(None),  # type: ignore[union-attr]
            )
            .order_by(ConversationVerification.created_at.desc())  # type: ignore[attr-defined]
            .limit(1)
        )
        verification = result.scalar_one_or_none()
        if verification is None:
            logfire.info(
                "verification_check_missing_pending_code",
                restaurant_id=restaurant_id,
                conversation_id=conversation_id,
                email=email,
            )
            return {"error": "no_pending_code"}

        verification.attempts += 1
        if verification.attempts > 3:
            logfire.warning(
                "verification_check_max_attempts_exceeded",
                restaurant_id=restaurant_id,
                conversation_id=conversation_id,
                email=email,
                verification_id=verification.id,
                attempts=verification.attempts,
            )
            await session.flush()
            return {"error": "max_attempts_exceeded"}

        if not bcrypt.checkpw(code.encode(), verification.code_hash.encode()):
            attempts_remaining = 3 - verification.attempts
            logfire.info(
                "verification_check_invalid_code",
                restaurant_id=restaurant_id,
                conversation_id=conversation_id,
                email=email,
                verification_id=verification.id,
                attempts=verification.attempts,
                attempts_remaining=attempts_remaining,
            )
            await session.flush()
            return {"error": "invalid_code", "attempts_remaining": attempts_remaining}

        verification.verified_at = utcnow()

        customer_result = await session.execute(
            select(Customer).where(
                Customer.restaurant_id == restaurant_id,
                Customer.email == email,
            )
        )
        customer = customer_result.scalar_one_or_none()

        customer_linked = False
        if customer is not None:
            conversation_result = await session.execute(
                select(Conversation).where(Conversation.id == conversation_id)
            )
            conversation = conversation_result.scalar_one_or_none()
            if conversation is not None:
                conversation.customer_id = customer.id
            ctx.deps.caller.customer_id = customer.id
            ctx.deps.caller.verified = True
            customer_linked = True
        else:
            ctx.deps.caller.verified = True

        await session.flush()

        logfire.info(
            "verification_check_succeeded",
            restaurant_id=restaurant_id,
            conversation_id=conversation_id,
            email=email,
            verification_id=verification.id,
            customer_linked=customer_linked,
            customer_id=ctx.deps.caller.customer_id,
        )
        # The `next_action` hint is read by the LLM immediately after the
        # tool result, alongside the system prompt's post-verify steer.
        # We surface it inline because the model is much more reliable at
        # following tool-embedded instructions than recalling a distant
        # system rule — especially right after a tool-heavy verification
        # round-trip, when the original user intent may be several turns
        # back in the history window.
        return {
            "status": "verified",
            "customer_linked": customer_linked,
            "next_action": (
                "Resume the guest's ORIGINAL request from earlier in this "
                "conversation (cancel/modify/look up a reservation). Do NOT "
                "greet them again or ask what they need — they already told "
                "you. If you need a reservation_id, call find_reservation "
                "now (it is scoped to this verified guest). Acknowledge the "
                "verification in one short sentence, then act."
            ),
        }
