"""
Public Checkout endpoints — no merchant JWT required.
Paths: /public/checkout/{token}/... and /public/payer/...
Registered in v1.py with prefix="" so paths are used as-is.
"""
from __future__ import annotations

import logging
from typing import Any, Dict, Optional

import random
import string

from fastapi import APIRouter, Depends, Request, status
from sqlalchemy.orm import Session

from slowapi import Limiter
from slowapi.util import get_remote_address

from src.core.database import get_db
from src.core.exceptions import NotFoundError, APIException
from src.apps.checkouts import crud as checkout_crud
from src.apps.checkouts import services as checkout_services
from src.apps.checkouts.helpers.payer_auth import (
    get_current_payer,
    create_payer_token,
    create_payer_refresh_token,
    verify_payer_refresh_token,
)
from src.apps.checkouts.schemas.checkout_schemas import (
    CheckoutEmailCheckRequest,
    CheckoutEmailCheckResponse,
    CheckoutLoginRequest,
    CheckoutRegisterRequest,
    CheckoutSubmitRequest,
    ForgotPasswordRequest,
    PayerTokenResponse,
    ResetPasswordConfirmRequest,
    PayerProfileUpdate,
    ChangePasswordRequest,
)

router = APIRouter()
logger = logging.getLogger(__name__)
_limiter = Limiter(key_func=get_remote_address)


# ─── Public: Load checkout ────────────────────────────────────────────────────

@router.get("/public/checkout/{token}")
async def load_public_checkout(token: str, request: Request, db: Session = Depends(get_db)):
    """Validate token, increment click count, return checkout display data."""
    result = checkout_services.get_public_checkout(db, token)
    db.commit()

    checkout = result["checkout"]
    link_list = checkout.line_items or []
    line_items = [
        {"description": li.description, "quantity": li.quantity, "unit_price": li.unit_price, "discount_amount": li.discount_amount}
        for li in link_list
    ]

    # Load the merchant's active provider config for PayFields initialization
    provider_config = None
    try:
        from src.apps.payment_providers.models.merchant_provider_config import MerchantProviderConfig
        from sqlalchemy import select as sa_select_pc
        pc_stmt = sa_select_pc(MerchantProviderConfig).where(
            MerchantProviderConfig.merchant_id == checkout.merchant_id,
            MerchantProviderConfig.is_active == True,
        )
        merchant_pc = db.execute(pc_stmt).scalar_one_or_none()
        if merchant_pc and merchant_pc.config_data:
            cfg = merchant_pc.config_data
            provider_config = {
                "api_key": cfg.get("api_key"),
                "merchant_id": cfg.get("merchant_id"),
                "mode": cfg.get("mode"),
            }
    except Exception as exc:
        logger.warning("Could not load provider_config for checkout merchant %s: %s", checkout.merchant_id, exc)

    return {
        "token": token,
        "title": checkout.title,
        "description": checkout.description,
        "checkout_type": checkout.checkout_type,
        "amount": checkout.amount,
        "currency": checkout.currency,
        "min_amount": checkout.min_amount,
        "max_amount": checkout.max_amount,
        "suggested_amounts": checkout.suggested_amounts,
        "default_amount": checkout.default_amount,
        "tip_enabled": checkout.tip_enabled,
        "tip_type": checkout.tip_type,
        "tip_percentages": checkout.tip_percentages,
        "tip_allow_custom": checkout.tip_allow_custom,
        "payment_frequency": checkout.payment_frequency,
        "authorization_type": checkout.authorization_type,
        "authorization_method": checkout.authorization_method,
        "require_billing_address": checkout.require_billing_address,
        "require_cvv": checkout.require_cvv,
        "split_config": checkout.split_config,
        "recurring_config": checkout.recurring_config,
        "require_payer_verification": checkout.require_payer_verification,
        "payer_verification_type": checkout.payer_verification_type,
        "thank_you_message": checkout.thank_you_message,
        "redirect_url": checkout.redirect_url,
        "expires_at": checkout.expires_at,
        "line_items": line_items,
        "adjustment_config": checkout.adjustment_config,
        "merchant_branding": result["merchant_branding"],
        "provider_config": provider_config,
    }


# ─── Check email ──────────────────────────────────────────────────────────────

@router.post("/public/checkout/{token}/check-email", response_model=CheckoutEmailCheckResponse)
async def check_email(token: str, payload: CheckoutEmailCheckRequest, db: Session = Depends(get_db)):
    exists = checkout_services.check_payer_email(db, token, payload.email)
    return {"exists": exists}


# ─── Register ─────────────────────────────────────────────────────────────────

@router.post("/public/checkout/{token}/register", status_code=201)
async def register_payer(token: str, payload: CheckoutRegisterRequest, db: Session = Depends(get_db)):
    data = payload.model_dump()
    result = checkout_services.register_payer(db, token, data)
    db.commit()
    return result


# ─── Login ────────────────────────────────────────────────────────────────────

@router.post("/public/checkout/{token}/login")
async def login_payer(token: str, payload: CheckoutLoginRequest, db: Session = Depends(get_db)):
    result = checkout_services.login_payer(db, token, payload.email, payload.password)
    return result


# ─── Submit payment ───────────────────────────────────────────────────────────

@router.post("/public/checkout/{token}/submit")
async def submit_payment(
    token: str,
    payload: CheckoutSubmitRequest,
    payer: dict = Depends(get_current_payer),
    db: Session = Depends(get_db),
):
    """
    Creates a PaymentRequest with checkout_id FK set.
    Reuses existing payment processing flow via Kafka event dispatcher.
    """
    link = checkout_crud.get_link_by_token(db, token)
    if link is None or link.status.upper() != "ACTIVE":
        raise NotFoundError(message="Checkout link not found or inactive.")

    checkout = link.checkout
    if checkout.status.upper() != "ACTIVE":
        raise APIException(message="This checkout is no longer accepting payments.", status_code=410)

    merchant_id = checkout.merchant_id
    payer_user_id = int(payer["sub"])
    customer_id = payload.customer_id or payer.get("customer_id")
    if not customer_id:
        raise APIException(message="Payer account has no customer profile linked.", status_code=400)

    # Resolve the payment amount
    if checkout.checkout_type.upper() == "MERCHANT_DEFINED":
        # Use checkout's fixed amount — ignore whatever client sent
        resolved_amount = checkout.amount
        if resolved_amount is None or resolved_amount <= 0:
            raise APIException(message="Checkout has no amount configured.", status_code=400)
    else:
        # Payer-defined (PAYER_DEFINED): amount must be provided by client
        if payload.amount is None:
            raise APIException(message="Amount is required for this checkout.", status_code=422)
        resolved_amount = payload.amount
        if checkout.min_amount is not None and resolved_amount < checkout.min_amount:
            raise APIException(message=f"Amount must be at least {checkout.min_amount}.", status_code=400)
        if checkout.max_amount is not None and resolved_amount > checkout.max_amount:
            raise APIException(message=f"Amount cannot exceed {checkout.max_amount}.", status_code=400)

    # Enforce billing address requirement
    if checkout.require_billing_address and not payload.billing_info:
        raise APIException(message="Billing address is required for this checkout.", status_code=422)

    # Map checkout payment_frequency → PaymentRequest payment_frequency
    _freq_map = {
        "SINGLE": "one_time",
        "RECURRING": "recurring",
        "SPLIT": "split",
    }
    pr_frequency = _freq_map.get(
        (checkout.payment_frequency or "SINGLE").upper(), "one_time"
    )

    # Create PaymentRequest with payment_request_type=CHECKOUT, checkout_id FK, scoped to merchant
    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.apps.payment_requests.models.payment_request_customer import PaymentRequestCustomer
    from src.apps.base.utils.functions import generate_secure_id
    from src.core.utils.enums import PaymentRequestType

    pr = PaymentRequest(
        merchant_id=merchant_id,
        payment_request_id=generate_secure_id(),
        payment_request_type=PaymentRequestType.CHECKOUT.value,
        checkout_id=checkout.id,
        amount=resolved_amount,
        currency=checkout.currency,
        status=1,
        created_by_id=payer_user_id,
        # Propagate checkout metadata into the payment request
        title=checkout.title,
        payment_frequency=pr_frequency,
        allow_tip=bool(checkout.tip_enabled),
        authorization_type=checkout.authorization_type,
        require_billing_address=bool(checkout.require_billing_address),
        require_cvv=bool(checkout.require_cvv),
    )
    db.add(pr)
    db.flush()

    # Link payer customer to the payment request so invoice creation can resolve
    # customer_id downstream (prepare_invoice_from_payment_request requires it).
    pr_customer = PaymentRequestCustomer(
        payment_request_id=pr.id,
        customer_id=customer_id,
        payer_email_request_enabled=False,
        payer_email_receipt_enabled=False,
        payer_sms_request_enabled=False,
        payer_sms_receipt_enabled=False,
        approver_email_receipt_enabled=False,
        approver_sms_receipt_enabled=False,
        is_approver_approved=True,
    )
    db.add(pr_customer)
    db.flush()

    # Create the Transaction record so analytics queries have data to aggregate.
    # The total amount includes any tip the payer added.
    from src.apps.transactions.models.transactions import Transactions
    from src.apps.transactions.services import generate_txn_literal
    from src.apps.customers.models.customer import Customer
    from src.core.utils.enums import (
        TransactionStatusTypes,
        TransactionCategories,
        TransactionTypes,
        TransactionSourceTypes,
    )
    import uuid as _uuid

    # Resolve billing_name from the payer's customer record
    customer_obj = db.get(Customer, customer_id)
    if customer_obj:
        billing_name = (
            f"{customer_obj.first_name or ''} {customer_obj.last_name or ''}".strip()
            or customer_obj.business_legal_name
            or None
        )
    else:
        billing_name = None

    total_amount = float(resolved_amount) + float(payload.tip_amount or 0)
    txn_literal = generate_txn_literal(db)
    mock_txn_id = f"chk_{_uuid.uuid4().hex[:12]}"

    transaction = Transactions(
        txn_amount=total_amount,
        txn_type="charge",
        txn_status=TransactionStatusTypes.PAID,
        txn_id=mock_txn_id,
        txn_literal=txn_literal,
        currency=checkout.currency or "USD",
        description=f"Checkout payment for {checkout.title or checkout.checkout_literal}",
        category=TransactionCategories.CHARGE,
        transaction_type=TransactionTypes.CHECKOUT,
        txn_source=TransactionSourceTypes.CHECKOUT,
        payment_request_id=pr.id,
        merchant_id=merchant_id,
        customer_id=customer_id,
        billing_name=billing_name,
    )
    db.add(transaction)
    db.flush()

    # HPMNTP-962: create an authorization record when the checkout requires one.
    # This populates the Associations > Authorizations section in the merchant portal.
    if checkout.authorization_type:
        from datetime import datetime, timezone
        from src.apps.payment_requests.models.payment_request_authorizations import PaymentRequestAuthorizations
        from src.apps.base.utils.functions import generate_secure_id
        from src.core.utils.enums import AuthorizationStatus

        auth_id = generate_secure_id(prepend="auth", length=20)
        auth_literal = f"AUTH-{generate_secure_id(length=8).upper()}"

        auth_data = payload.authorization_data or {}
        signing_name = auth_data.get("signing_name")
        if not signing_name and customer_obj:
            signing_name = (
                f"{customer_obj.first_name or ''} {customer_obj.last_name or ''}".strip()
                or getattr(customer_obj, "business_legal_name", None)
                or None
            )

        checkout_auth = PaymentRequestAuthorizations(
            authorization_id=auth_id,
            authorization_literal=auth_literal,
            authorization_type=checkout.authorization_type,
            authorization_date=datetime.now(timezone.utc),
            is_verified=True,
            status=AuthorizationStatus.ACTIVE.value,
            signing_name=signing_name,
            auth_metadata={
                "txn_source": "CHECKOUT",
                "checkout_literal": checkout.checkout_literal,
            },
            payment_request_id=pr.id,
            merchant_id=merchant_id,
            customer_id=customer_id,
        )
        db.add(checkout_auth)
        db.flush()

    checkout_crud.create_checkout_activity(
        db, checkout.id, merchant_id,
        "checkout.payment_submitted",
        f"Payment of {resolved_amount} {checkout.currency} submitted.",
        actor_customer_id=customer_id,
    )

    db.commit()

    # Dispatch transaction.completed so downstream listeners (invoices, notifications) fire.
    try:
        from src.events.dispatcher import EventDispatcher
        from src.events.base import BaseEvent
        await EventDispatcher.dispatch(
            BaseEvent(
                event_type="transaction.completed",
                data={
                    "transaction_id": transaction.id,
                    "txn_id": mock_txn_id,
                    "txn_amount": total_amount,
                    "currency": checkout.currency,
                    "payment_request_id": pr.id,
                    "merchant_id": merchant_id,
                    "customer_id": customer_id,
                    "checkout_id": checkout.id,
                    "correlation_id": pr.payment_request_id,
                },
            )
        )
    except Exception as exc:
        logger.error("submit_payment: failed to dispatch transaction.completed: %s", exc)

    return {
        "payment_request_id": pr.payment_request_id,
        "transaction_id": transaction.txn_literal,
        "status": "PAID",
        "amount": total_amount,
        "checkout_literal": checkout.checkout_literal,
    }


# ─── Forgot / reset password ──────────────────────────────────────────────────

@router.post("/public/checkout/{token}/forgot-password")
async def forgot_password(token: str, payload: ForgotPasswordRequest, db: Session = Depends(get_db)):
    """
    Send a password reset email to the payer if the account exists.
    Uses constant-time response to prevent email enumeration.
    """
    link = checkout_crud.get_link_by_token(db, token)
    if link is None:
        raise NotFoundError(message="Checkout not found.")

    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    from src.core.config import settings
    from src.events.dispatcher import EventDispatcher
    from src.events.base import BaseEvent
    import jwt as pyjwt
    from datetime import datetime, timedelta, timezone

    merchant_id = link.checkout.merchant_id
    user = db.execute(
        sa_select(User).where(
            User.email == payload.email,
            User.merchant_id == merchant_id,
            User.user_type == "customer",
            User.deleted_at == None,
        )
    ).scalar_one_or_none()

    # Always return success to prevent email enumeration
    if user:
        import uuid
        import redis as _redis
        expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
        jti = str(uuid.uuid4())
        reset_payload = {
            "sub": str(user.id),
            "email": user.email,
            "purpose": "password_reset",
            "checkout_token": token,
            "jti": jti,
            "exp": expires_at,
        }
        reset_token = pyjwt.encode(
            reset_payload,
            settings.JWT_SECRET_KEY,
            algorithm=settings.JWT_ALGORITHM,
        )
        # Store jti in Redis so we can enforce single-use on confirm
        try:
            redis_url = getattr(settings, "REDIS_URL", None) or getattr(settings, "CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
            r = _redis.from_url(redis_url, decode_responses=True)
            r.setex(f"payer:reset_jti:{jti}", 3600, "1")
        except Exception as exc:
            logger.error("forgot_password: failed to store reset jti in Redis: %s", exc)
        # URL format per PRD: /c/reset-password?reset_token={t}  (checkout_token embedded in JWT)
        reset_link = f"{settings.hpp_frontend_base_url}/c/reset-password?reset_token={reset_token}"
        try:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.email_requested",
                    data={
                        "to_email": user.email,
                        "template_key": "password_reset",
                        "template_vars": {
                            "reset_link": reset_link,
                            "expires_in_minutes": 60,
                        },
                        "merchant_id": merchant_id,
                    },
                )
            )
        except Exception as exc:
            logger.error("forgot_password: failed to dispatch reset email: %s", exc)

    return {"message": "If that email is registered, a password reset link has been sent."}


@router.get("/public/reset-password/validate")
async def validate_reset_token(reset_token: str, db: Session = Depends(get_db)):
    """Validate that a password reset token is still valid and has not been used."""
    import jwt as pyjwt
    import redis as _redis
    from src.core.config import settings

    try:
        payload = pyjwt.decode(
            reset_token,
            settings.JWT_SECRET_KEY,
            algorithms=[settings.JWT_ALGORITHM],
        )
        if payload.get("purpose") != "password_reset":
            return {"valid": False, "checkout_token": None}
        # Check that the jti is still present in Redis (single-use guard)
        jti = payload.get("jti")
        if jti:
            try:
                redis_url = getattr(settings, "REDIS_URL", None) or getattr(settings, "CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
                r = _redis.from_url(redis_url, decode_responses=True)
                if not r.exists(f"payer:reset_jti:{jti}"):
                    return {"valid": False, "checkout_token": None}
            except Exception as exc:
                logger.warning("validate_reset_token: Redis check failed, allowing: %s", exc)
        return {"valid": True, "checkout_token": payload.get("checkout_token")}
    except pyjwt.ExpiredSignatureError:
        return {"valid": False, "checkout_token": None}
    except pyjwt.InvalidTokenError:
        return {"valid": False, "checkout_token": None}


@router.post("/public/reset-password/confirm")
async def confirm_password_reset(payload: ResetPasswordConfirmRequest, db: Session = Depends(get_db)):
    """Validate reset token, update payer password, and invalidate all existing payer sessions."""
    import jwt as pyjwt
    import bcrypt as _bcrypt
    import redis as _redis
    from datetime import datetime, timezone
    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    from src.core.config import settings
    from src.core.exceptions import UnauthorizedError

    try:
        decoded = pyjwt.decode(
            payload.reset_token,
            settings.JWT_SECRET_KEY,
            algorithms=[settings.JWT_ALGORITHM],
        )
        if decoded.get("purpose") != "password_reset":
            raise UnauthorizedError(message="Invalid reset token.")
    except pyjwt.ExpiredSignatureError:
        raise APIException(message="Reset token has expired.", status_code=400)
    except pyjwt.InvalidTokenError:
        raise UnauthorizedError(message="Invalid reset token.")

    # Enforce single-use: verify jti still exists in Redis
    jti = decoded.get("jti")
    redis_client = None
    try:
        redis_url = getattr(settings, "REDIS_URL", None) or getattr(settings, "CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
        redis_client = _redis.from_url(redis_url, decode_responses=True)
        if jti and not redis_client.exists(f"payer:reset_jti:{jti}"):
            raise APIException(message="Reset token has already been used.", status_code=400)
    except APIException:
        raise
    except Exception as exc:
        logger.warning("confirm_password_reset: Redis check failed, proceeding: %s", exc)

    user = db.execute(
        sa_select(User).where(User.id == int(decoded["sub"]), User.deleted_at == None)
    ).scalar_one_or_none()
    if not user:
        raise NotFoundError(message="User not found.")

    user.hashed_password = _bcrypt.hashpw(
        payload.new_password.encode("utf-8"), _bcrypt.gensalt()
    ).decode("utf-8")
    now = datetime.now(timezone.utc)
    if hasattr(user, "last_password_changed_at"):
        user.last_password_changed_at = now
    db.commit()

    # Single-use: delete the jti so this token cannot be replayed
    # Session invalidation: record timestamp so any payer JWT issued before now is rejected
    if redis_client:
        try:
            if jti:
                redis_client.delete(f"payer:reset_jti:{jti}")
            # TTL of 25 hours covers the maximum payer token lifetime (24 h) with 1 h buffer
            redis_client.setex(f"payer:invalidate_before:{user.id}", 90000, str(now.timestamp()))
        except Exception as exc:
            logger.error("confirm_password_reset: Redis cleanup failed: %s", exc)

    return {"message": "Password updated. Please log in.", "checkout_token": decoded.get("checkout_token")}


# ─── OTP ─────────────────────────────────────────────────────────────────────

_OTP_TTL_SECONDS = 300  # 5 minutes


def _get_otp_redis_key(token: str, email: str) -> str:
    return f"checkout_otp:{token}:{email}"


@router.post("/public/checkout/{token}/otp/send")
async def otp_send(token: str, request: Request, db: Session = Depends(get_db)):
    """
    Generate a 6-digit OTP for the authenticated payer and send it via email/SMS.
    Body: {"email": "...", "phone": "..."}
    """
    from src.apps.checkouts.helpers.payer_auth import get_current_payer as _get_payer
    from src.events.dispatcher import EventDispatcher
    from src.events.base import BaseEvent

    link = checkout_crud.get_link_by_token(db, token)
    if link is None:
        raise NotFoundError(message="Checkout not found.")

    body = {}
    try:
        body = await request.json()
    except Exception:
        pass

    email = body.get("email")
    phone = body.get("phone")

    if not email and not phone:
        raise APIException(message="email or phone is required.", status_code=422)

    otp_code = "".join(random.choices(string.digits, k=6))

    # Store in Redis with TTL
    try:
        import redis as _redis
        from src.core.config import settings
        redis_url = getattr(settings, "REDIS_URL", None) or getattr(settings, "CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
        r = _redis.from_url(redis_url, decode_responses=True)
        otp_key = _get_otp_redis_key(token, email or phone)
        r.setex(otp_key, _OTP_TTL_SECONDS, otp_code)
    except Exception as exc:
        logger.error("OTP: Redis store failed: %s", exc)
        # Fallback: proceed but OTP won't be verifiable
        raise APIException(message="OTP service unavailable.", status_code=503)

    merchant_id = link.checkout.merchant_id
    checkout_title = link.checkout.title or "Payment"

    if email:
        try:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.email_requested",
                    data={
                        "to_email": email,
                        "template_key": "checkout_otp",
                        "template_vars": {
                            "otp_code": otp_code,
                            "checkout_title": checkout_title,
                            "expires_in_minutes": _OTP_TTL_SECONDS // 60,
                        },
                        "merchant_id": merchant_id,
                    },
                )
            )
        except Exception as exc:
            logger.error("OTP email dispatch failed: %s", exc)

    if phone:
        try:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.sms_requested",
                    data={
                        "to_phone": phone,
                        "template_key": "checkout_otp",
                        "template_vars": {
                            "otp_code": otp_code,
                            "checkout_title": checkout_title,
                        },
                        "merchant_id": merchant_id,
                    },
                )
            )
        except Exception as exc:
            logger.error("OTP SMS dispatch failed: %s", exc)

    return {"message": "OTP sent.", "expires_in": _OTP_TTL_SECONDS}


@router.post("/public/checkout/{token}/otp/verify")
async def otp_verify(token: str, request: Request, db: Session = Depends(get_db)):
    """
    Verify OTP code submitted by payer.
    Body: {"email": "...", "otp": "123456"}
    """
    link = checkout_crud.get_link_by_token(db, token)
    if link is None:
        raise NotFoundError(message="Checkout not found.")

    body = {}
    try:
        body = await request.json()
    except Exception:
        pass

    email = body.get("email") or body.get("phone")
    otp_submitted = str(body.get("otp", "")).strip()

    if not email or not otp_submitted:
        raise APIException(message="email and otp are required.", status_code=422)

    try:
        import redis as _redis
        from src.core.config import settings
        redis_url = getattr(settings, "REDIS_URL", None) or getattr(settings, "CELERY_RESULT_BACKEND", "redis://localhost:6379/0")
        r = _redis.from_url(redis_url, decode_responses=True)
        otp_key = _get_otp_redis_key(token, email)
        stored_otp = r.get(otp_key)
    except Exception as exc:
        logger.error("OTP: Redis lookup failed: %s", exc)
        raise APIException(message="OTP service unavailable.", status_code=503)

    if not stored_otp:
        raise APIException(message="OTP has expired or was never sent.", status_code=400)

    if stored_otp != otp_submitted:
        raise APIException(message="Invalid OTP.", status_code=400)

    # Delete OTP after successful verification (one-time use)
    try:
        r.delete(otp_key)
    except Exception:
        pass

    return {"verified": True}


@router.get("/public/checkout/{token}/verify-email")
async def verify_email(token: str, verify_token: str, db: Session = Depends(get_db)):
    """Verify payer's email address using the JWT emailed during registration."""
    import jwt as pyjwt
    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    from src.core.config import settings
    from src.core.exceptions import UnauthorizedError

    try:
        payload = pyjwt.decode(
            verify_token,
            settings.JWT_SECRET_KEY,
            algorithms=[settings.JWT_ALGORITHM],
        )
        if payload.get("purpose") != "email_verify":
            raise UnauthorizedError(message="Invalid verification token.")
    except pyjwt.ExpiredSignatureError:
        raise APIException(message="Verification link has expired.", status_code=400)
    except pyjwt.InvalidTokenError:
        raise UnauthorizedError(message="Invalid verification token.")

    user = db.execute(
        sa_select(User).where(User.id == int(payload["sub"]), User.deleted_at == None)
    ).scalar_one_or_none()
    if not user:
        raise NotFoundError(message="User not found.")

    user.is_verified = True if hasattr(user, "is_verified") else None
    user.is_active = True
    db.commit()
    return {"message": "Email verified. You can now log in."}


# ─── Payer portal ─────────────────────────────────────────────────────────────

@router.post("/public/payer/login")
async def payer_login(payload: CheckoutLoginRequest, db: Session = Depends(get_db)):
    """Authenticate payer without a specific checkout token (for payer portal)."""
    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    import bcrypt as _bcrypt

    user = db.execute(
        sa_select(User).where(User.email == payload.email, User.deleted_at == None)
    ).scalar_one_or_none()

    _DUMMY = "$2b$12$LQv3c1yqBwEHmBZAFHMpBOeRrigUlpjeA5wQvN2VDW5AMNNuqW1uW"
    if not user:
        _bcrypt.checkpw(payload.password.encode("utf-8"), _DUMMY.encode("utf-8"))
        from src.core.exceptions import UnauthorizedError
        raise UnauthorizedError(message="Invalid email or password.")

    if not _bcrypt.checkpw(payload.password.encode("utf-8"), user.hashed_password.encode("utf-8")):
        from src.core.exceptions import UnauthorizedError
        raise UnauthorizedError(message="Invalid email or password.")

    merchant_id = getattr(user, "merchant_id", 0)
    customer_id = getattr(user, "customer_id", None)
    access_token = create_payer_token(user.id, merchant_id, customer_id)
    refresh_token = create_payer_refresh_token(user.id, merchant_id, customer_id)
    return {
        "access_token": access_token,
        "refresh_token": refresh_token,
        "token_type": "bearer",
        "expires_in": 86400,
        "refresh_expires_in": 604800,
    }


@router.post("/public/payer/refresh")
async def payer_refresh_token(request: Request):
    """
    Exchange a valid payer refresh token for a new access token (and rotated refresh token).
    Body: {"refresh_token": "<token>"}
    """
    body = {}
    try:
        body = await request.json()
    except Exception:
        pass

    raw_refresh = body.get("refresh_token")
    if not raw_refresh:
        raise APIException(message="refresh_token is required.", status_code=422)

    payload = verify_payer_refresh_token(raw_refresh)

    user_id = int(payload["sub"])
    merchant_id = payload.get("merchant_id", 0)
    customer_id = payload.get("customer_id")

    new_access_token = create_payer_token(user_id, merchant_id, customer_id)
    new_refresh_token = create_payer_refresh_token(user_id, merchant_id, customer_id)

    return {
        "access_token": new_access_token,
        "refresh_token": new_refresh_token,
        "token_type": "bearer",
        "expires_in": 86400,
        "refresh_expires_in": 604800,
    }


@router.get("/public/payer/me")
async def payer_me(payer: dict = Depends(get_current_payer), db: Session = Depends(get_db)):
    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    user = db.execute(
        sa_select(User).where(User.id == int(payer["sub"]), User.deleted_at == None)
    ).scalar_one_or_none()
    if not user:
        raise NotFoundError(message="User not found.")
    return {
        "user_id": user.id,
        "email": user.email,
        "first_name": getattr(user, "first_name", None),
        "last_name": getattr(user, "last_name", None),
        "customer_id": payer.get("customer_id"),
    }


@router.get("/public/payer/transactions")
async def payer_transactions(
    payer: dict = Depends(get_current_payer),
    db: Session = Depends(get_db),
    page: int = 1,
    page_size: int = 20,
):
    """Return paginated list of payment requests (transactions) made by this payer."""
    from sqlalchemy import select as sa_select
    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.apps.checkouts.models.checkout import Checkout

    payer_user_id = int(payer["sub"])
    offset = (page - 1) * page_size

    stmt = (
        sa_select(PaymentRequest)
        .where(
            PaymentRequest.created_by_id == payer_user_id,
            PaymentRequest.deleted_at.is_(None),
        )
        .order_by(PaymentRequest.created_at.desc())
        .offset(offset)
        .limit(page_size)
    )
    rows = db.execute(stmt).scalars().all()

    count_stmt = sa_select(PaymentRequest).where(
        PaymentRequest.created_by_id == payer_user_id,
        PaymentRequest.deleted_at.is_(None),
    )
    total = len(db.execute(count_stmt).scalars().all())

    items = []
    for pr in rows:
        checkout_title = None
        if pr.checkout_id:
            chk = db.execute(
                sa_select(Checkout).where(Checkout.id == pr.checkout_id)
            ).scalar_one_or_none()
            checkout_title = chk.title if chk else None
        items.append({
            "payment_request_id": pr.payment_request_id,
            "amount": float(pr.amount or 0),
            "currency": pr.currency,
            "status": pr.status,
            "checkout_title": checkout_title,
            "created_at": pr.created_at.isoformat() if pr.created_at else None,
        })

    return {"items": items, "total": total, "page": page, "page_size": page_size}


@router.get("/public/payer/payment-methods")
async def payer_payment_methods(
    payer: dict = Depends(get_current_payer),
    db: Session = Depends(get_db),
):
    """Return saved payment methods for this payer."""
    from sqlalchemy import select as sa_select
    from src.apps.payment_methods.models.payment_methods import PaymentMethod

    customer_id = payer.get("customer_id")
    if not customer_id:
        return {"items": [], "total": 0}

    stmt = sa_select(PaymentMethod).where(
        PaymentMethod.customer_id == customer_id,
        PaymentMethod.deleted_at.is_(None),
    ).order_by(PaymentMethod.created_at.desc())

    rows = db.execute(stmt).scalars().all()
    items = [
        {
            "id": pm.id,
            "payment_method_type": pm.payment_method_type,
            "card_last_four": getattr(pm, "card_last_four", None),
            "card_brand": getattr(pm, "card_brand", None),
            "is_default": getattr(pm, "is_default", False),
            "created_at": pm.created_at.isoformat() if pm.created_at else None,
        }
        for pm in rows
    ]
    return {"items": items, "total": len(items)}


@router.delete("/public/payer/payment-methods/{payment_method_id}", status_code=204)
async def delete_payer_payment_method(
    payment_method_id: int,
    payer: dict = Depends(get_current_payer),
    db: Session = Depends(get_db),
):
    """Soft-delete a payer's saved payment method."""
    from sqlalchemy import select as sa_select
    from src.apps.payment_methods.models.payment_methods import PaymentMethod
    from datetime import datetime, timezone

    customer_id = payer.get("customer_id")
    pm = db.execute(
        sa_select(PaymentMethod).where(
            PaymentMethod.id == payment_method_id,
            PaymentMethod.customer_id == customer_id,
            PaymentMethod.deleted_at.is_(None),
        )
    ).scalar_one_or_none()

    if pm is None:
        raise NotFoundError(message="Payment method not found.")

    pm.deleted_at = datetime.now(timezone.utc)
    db.commit()


@router.put("/public/payer/profile")
async def update_payer_profile(
    payload: PayerProfileUpdate,
    payer: dict = Depends(get_current_payer),
    db: Session = Depends(get_db),
):
    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    user = db.execute(
        sa_select(User).where(User.id == int(payer["sub"]), User.deleted_at == None)
    ).scalar_one_or_none()
    if not user:
        raise NotFoundError(message="User not found.")
    if payload.first_name is not None:
        user.first_name = payload.first_name
    if payload.last_name is not None:
        user.last_name = payload.last_name
    if payload.phone is not None:
        user.phone = payload.phone
    db.commit()
    return {"message": "Profile updated."}


@router.post("/public/payer/change-password")
async def change_payer_password(
    payload: ChangePasswordRequest,
    payer: dict = Depends(get_current_payer),
    db: Session = Depends(get_db),
):
    import bcrypt as _bcrypt
    from sqlalchemy import select as sa_select
    from src.apps.users.models.user import User
    from src.core.exceptions import UnauthorizedError

    user = db.execute(
        sa_select(User).where(User.id == int(payer["sub"]), User.deleted_at == None)
    ).scalar_one_or_none()
    if not user:
        raise NotFoundError(message="User not found.")
    if not _bcrypt.checkpw(payload.current_password.encode("utf-8"), user.hashed_password.encode("utf-8")):
        raise UnauthorizedError(message="Current password is incorrect.")
    user.hashed_password = _bcrypt.hashpw(payload.new_password.encode("utf-8"), _bcrypt.gensalt()).decode("utf-8")
    db.commit()
    return {"message": "Password changed."}
