"""
HPP (Hosted Payment Page) router.

Public endpoints (no JWT required) — authenticated by HPP token from DB:
  GET  /payment-requests/hpp/initiate?token={token}
  POST /payment-requests/hpp/submit
  GET  /payment-requests/hpp/retry/initiate?token={retry_token}
  POST /payment-requests/hpp/retry/submit
  POST /hpp/customer/register
  POST /hpp/customer/login
  GET  /hpp/customer/verify-email?token={verify_token}
  POST /hpp/otp/send       — rate limited: 10/minute per IP
  POST /hpp/otp/verify     — rate limited: 20/minute per IP

Merchant-authenticated endpoints (JWT required) are added to the existing
payment_requests router and transactions router (see their respective
router.py files).

Registration in v1.py:
  api_router.include_router(hpp_router, prefix="", tags=["HPP"])
  → Results in paths: /api/v1/payment-requests/hpp/... and /api/v1/hpp/...
"""

from io import BytesIO
from fastapi import APIRouter, Depends, Query, Request, status
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session

# C-2: slowapi rate limiting for OTP endpoints
from slowapi import Limiter
from slowapi.util import get_remote_address

from src.core.database import get_db
from src.apps.hpp import services
from src.apps.hpp.schemas.hpp_schemas import (
    HppInitiateResponseSchema,
    HppRetryInitiateResponseSchema,
    HppRetrySubmitResponseSchema,
    HppRetrySubmitSchema,
    HppSubmitResponseSchema,
    HppSubmitSchema,
    OtpSendResponseSchema,
    OtpSendSchema,
    OtpVerifyResponseSchema,
    OtpVerifySchema,
    CustomerRegisterSchema,
    CustomerRegisterResponseSchema,
    CustomerLoginSchema,
    CustomerLoginResponseSchema,
)
from src.apps.base.schemas.responses import BaseResponse

router = APIRouter()

# C-2: Per-router limiter keyed by client IP.
# The application-level Limiter instance (registered on the FastAPI app in
# main.py) handles the 429 error responses; this limiter shares the same
# Redis/memory backend via the default key-func.
_limiter = Limiter(key_func=get_remote_address)


# ─── HPP Initiate ─────────────────────────────────────────────────────────────

@router.get(
    "/payment-requests/hpp/initiate",
    name="HPP Initiate",
    summary="Validate HPP token and return page data",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
async def initiate_hpp(
    token: str,
    request: Request,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Validates the payment link token and returns all data needed to render
    the Hosted Payment Page.

    Error codes:
    - 404: Token not found
    - 409: Payment already completed
    - 410: Link expired or revoked
    """
    data = await services.initiate_hpp(db=db, token=token, request=request)
    return BaseResponse(data=data.model_dump(), message="Hosted Payment Page loaded")


# ─── HPP Submit ───────────────────────────────────────────────────────────────

@router.post(
    "/payment-requests/hpp/submit",
    name="HPP Submit",
    summary="Submit HPP payment",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
async def submit_hpp(
    payload: HppSubmitSchema,
    request: Request,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Processes the HPP payment submission:
    - Re-validates token
    - B-2: idempotency check — returns existing result on double-submit
    - B-1: server-side OTP re-validation (never trusts client payload)
    - Creates a Transaction record (mock payment)
    - Records authorization
    - Marks link USED (with SELECT FOR UPDATE lock)
    - Dispatches domain events
    """
    data = await services.submit_hpp(db=db, payload=payload, request=request)
    return BaseResponse(data=data.model_dump(), message="Payment submitted successfully")


# ─── Retry Initiate ───────────────────────────────────────────────────────────

@router.get(
    "/payment-requests/hpp/retry/initiate",
    name="HPP Retry Initiate",
    summary="Validate retry token and return retry page data",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
async def initiate_retry(
    token: str,
    request: Request,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Validates a retry token and returns context data for the retry payment page.

    Error codes:
    - 404: Retry token not found
    - 409: Already used
    - 410: Expired
    """
    data = await services.initiate_retry(db=db, token=token, request=request)
    return BaseResponse(data=data.model_dump(), message="Retry page loaded")


# ─── Retry Submit ─────────────────────────────────────────────────────────────

@router.post(
    "/payment-requests/hpp/retry/submit",
    name="HPP Retry Submit",
    summary="Submit retry payment",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
async def submit_retry(
    payload: HppRetrySubmitSchema,
    request: Request,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Processes a retry payment for a failed installment.
    C-10: Creates a new Transaction for the retry; preserves the original FAILED
    transaction with a metadata reference to the new transaction.
    """
    data = await services.submit_retry(db=db, payload=payload, request=request)
    return BaseResponse(data=data.model_dump(), message="Retry payment processed successfully")


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

@router.post(
    "/hpp/otp/send",
    name="HPP OTP Send",
    summary="Send OTP SMS for HPP authorization",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
@_limiter.limit("10/minute")  # C-2: max 10 OTP sends per IP per minute
async def send_otp(
    request: Request,
    payload: OtpSendSchema,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Generates and sends a 6-digit OTP via SMS for HPP SMS authorization.

    Limits:
    - Max 10 requests per IP per minute (rate-limited via slowapi)
    - Max 3 sends per payment request (429 if exceeded)
    - OTP expires in 10 minutes
    """
    data = await services.send_otp(db=db, token=payload.token)
    return BaseResponse(data=data.model_dump(), message="OTP sent successfully")


@router.post(
    "/hpp/otp/verify",
    name="HPP OTP Verify",
    summary="Verify OTP code entered by customer",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
@_limiter.limit("20/minute")  # C-2: max 20 OTP verify attempts per IP per minute
async def verify_otp(
    request: Request,
    payload: OtpVerifySchema,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Verifies the OTP code submitted by the customer.

    Limits:
    - Max 20 requests per IP per minute (rate-limited via slowapi)
    - Max 5 wrong attempts before lockout (429 if exceeded)
    """
    data = await services.verify_otp_code(db=db, token=payload.token, otp=payload.otp)
    return BaseResponse(data=data.model_dump(), message="OTP verified successfully")


# ─── Customer Account ─────────────────────────────────────────────────────────

@router.post(
    "/hpp/customer/register",
    name="HPP Customer Register",
    summary="Create a customer portal account via HPP",
    response_model=BaseResponse,
    status_code=status.HTTP_201_CREATED,
)
@_limiter.limit("5/minute")  # SEC-006: max 5 registration attempts per IP per minute
async def register_customer(
    request: Request,
    payload: CustomerRegisterSchema,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Creates a new customer portal User account scoped to the merchant derived
    from the HPP token.  The account starts as is_verified=False; a
    verification email is dispatched via the customer.account_created event.
    """
    data = await services.register_customer(
        db=db,
        token=payload.token,
        email=payload.email,
        password=payload.password,
    )
    return BaseResponse(
        data=data.model_dump(),
        message="Account created. Please verify your email.",
    )


@router.post(
    "/hpp/customer/login",
    name="HPP Customer Login",
    summary="Authenticate a customer portal account for HPP",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
@_limiter.limit("5/minute")  # SEC-006: max 5 login attempts per IP per minute
async def login_customer(
    request: Request,
    payload: CustomerLoginSchema,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Authenticates a customer and returns a short-lived HPP session JWT (30 min).
    C-6: Creates an AuthSession record for session revocation and audit logging.
    """
    data = await services.login_customer(
        db=db,
        token=payload.token,
        email=payload.email,
        password=payload.password,
        request=request,
    )
    return BaseResponse(data=data.model_dump(), message="Login successful")


@router.get(
    "/hpp/customer/verify-email",
    name="HPP Customer Verify Email",
    summary="Verify customer email address",
    response_model=BaseResponse,
    status_code=status.HTTP_200_OK,
)
async def verify_customer_email(
    token: str,
    db: Session = Depends(get_db),
) -> BaseResponse:
    """
    Verifies a customer's email address using the signed token sent by email.
    Sets user.is_verified = True.
    """
    data = await services.verify_customer_email(db=db, verify_token=token)
    return BaseResponse(data=data, message="Email verified successfully")


# ── Public PDF downloads (authenticated by payment-link token) ─────────────

@router.get(
    "/hpp/invoice-summary",
    tags=["HPP"],
    summary="Download invoice PDF before payment (public, token-authenticated)",
)
async def hpp_download_invoice_summary(
    token: str = Query(..., description="Payment link token from the HPP URL"),
    db: Session = Depends(get_db),
) -> StreamingResponse:
    """
    Public invoice PDF download available to HPP payers before payment.
    Looks up the invoice linked to the payment request for this token.
    """
    from src.apps.hpp import crud as hpp_crud
    from src.apps.invoices.models.invoice import Invoice
    from src.apps.invoices.helpers.pdf_generator import invoice_pdf_generator
    from src.apps.invoices.crud import get_invoice_by_literal
    from sqlalchemy import select as sa_select
    from src.core.exceptions import NotFoundError
    from fastapi import HTTPException
    import logging as _logging

    _logger = _logging.getLogger(__name__)

    link = hpp_crud.get_link_by_token(db, token)
    if not link:
        raise NotFoundError(message="Invalid or expired payment link.")

    # Find the first invoice linked to this payment request
    inv = db.execute(
        sa_select(Invoice)
        .where(
            Invoice.payment_request_id == link.payment_request_id,
            Invoice.deleted_at.is_(None),
        )
        .limit(1)
    ).scalar_one_or_none()

    if inv is None:
        raise NotFoundError(message="No invoice linked to this payment request.")

    # Load with full eager loading for the PDF generator
    from src.apps.merchants.models.merchant import Merchant
    full_invoice = get_invoice_by_literal(
        db=db,
        invoice_literal=inv.invoice_literal,
        merchant_id=inv.merchant_id,
    )
    if full_invoice is None:
        raise NotFoundError(message="Invoice not found.")

    try:
        pdf_bytes = invoice_pdf_generator.generate(full_invoice)
    except Exception as exc:
        _logger.exception("HPP invoice-summary PDF generation failed for payment_request_id=%s", link.payment_request_id)
        raise HTTPException(status_code=500, detail=f"PDF generation failed: {exc}")

    filename = f"invoice-{inv.invoice_literal}.pdf"
    return StreamingResponse(
        BytesIO(pdf_bytes),
        media_type="application/pdf",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"',
            "Content-Length": str(len(pdf_bytes)),
        },
    )


@router.get(
    "/hpp/receipt/{txn_literal}",
    tags=["HPP"],
    summary="Download receipt PDF (public, token-authenticated)",
)
async def hpp_download_receipt(
    txn_literal: str,
    token: str = Query(..., description="Payment link token from the HPP URL"),
    db: Session = Depends(get_db),
) -> StreamingResponse:
    """
    Public receipt PDF download for HPP customers after payment.
    Validates that the payment-link token belongs to the same payment request
    as the transaction — no merchant JWT required.
    """
    from src.apps.hpp import crud as hpp_crud
    from src.apps.transactions.models.transactions import Transactions
    from src.apps.payment_methods.models.payment_methods import PaymentMethod
    from sqlalchemy import select
    from sqlalchemy.orm import joinedload, selectinload
    from src.core.exceptions import NotFoundError, UnauthorizedError
    from fastapi import HTTPException
    import logging as _logging

    _logger = _logging.getLogger(__name__)

    # Validate token exists and is linked to a payment request (no status check —
    # the link is already USED after payment which is expected)
    link = hpp_crud.get_link_by_token(db, token)
    if not link:
        raise NotFoundError(message="Invalid or expired payment link.")

    # Load transaction with all relationships needed for the PDF
    stmt = (
        select(Transactions)
        .options(
            joinedload(Transactions.merchant),
            joinedload(Transactions.customer),
            joinedload(Transactions.payment_request),
            joinedload(Transactions.payment_method).joinedload(PaymentMethod.payer),
            selectinload(Transactions.invoices),
        )
        .where(Transactions.txn_literal == txn_literal)
    )
    txn = db.execute(stmt).unique().scalar_one_or_none()
    if txn is None:
        raise NotFoundError(message="Transaction not found.")

    # Security: ensure the transaction belongs to the payment request for this token
    if txn.payment_request_id != link.payment_request_id:
        raise UnauthorizedError(message="Token does not match this transaction.")

    from src.apps.transactions.helpers.receipt_pdf_generator import receipt_pdf_generator
    from src.apps.settings import services as settings_services

    receipt_settings = settings_services.get_receipt_settings(txn.merchant_id, db)

    try:
        pdf_bytes = receipt_pdf_generator.generate(txn, receipt_settings)
    except Exception as exc:
        _logger.exception("HPP receipt PDF generation failed for %s", txn_literal)
        raise HTTPException(status_code=500, detail=f"PDF generation failed: {exc}")

    filename = f"receipt-{txn_literal}.pdf"
    return StreamingResponse(
        BytesIO(pdf_bytes),
        media_type="application/pdf",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"',
            "Content-Length": str(len(pdf_bytes)),
        },
    )


@router.get(
    "/hpp/invoice/{txn_literal}",
    tags=["HPP"],
    summary="Download invoice PDF (public, token-authenticated)",
)
async def hpp_download_invoice(
    txn_literal: str,
    token: str = Query(..., description="Payment link token from the HPP URL"),
    db: Session = Depends(get_db),
) -> StreamingResponse:
    """
    Public invoice PDF download for HPP customers after payment.
    Finds the first invoice linked to the transaction and returns it as a PDF.
    """
    from src.apps.hpp import crud as hpp_crud
    from src.apps.transactions.models.transactions import Transactions, transactions_invoices_map
    from src.apps.invoices.models.invoice import Invoice
    from src.apps.invoices.helpers.pdf_generator import invoice_pdf_generator
    from src.apps.invoices.crud import get_invoice_by_literal
    from sqlalchemy import select as sa_select
    from sqlalchemy.orm import joinedload, selectinload
    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.apps.invoices.crud import get_invoice_by_literal
    from src.core.exceptions import NotFoundError, UnauthorizedError
    from fastapi import HTTPException
    import logging as _logging

    _logger = _logging.getLogger(__name__)

    link = hpp_crud.get_link_by_token(db, token)
    if not link:
        raise NotFoundError(message="Invalid or expired payment link.")

    # Find the transaction
    from src.apps.payment_methods.models.payment_methods import PaymentMethod
    stmt = (
        sa_select(Transactions)
        .where(Transactions.txn_literal == txn_literal)
    )
    txn = db.execute(stmt).scalar_one_or_none()
    if txn is None:
        raise NotFoundError(message="Transaction not found.")

    if txn.payment_request_id != link.payment_request_id:
        raise UnauthorizedError(message="Token does not match this transaction.")

    # Find the first invoice linked to this transaction
    inv_stmt = (
        sa_select(Invoice)
        .join(transactions_invoices_map, transactions_invoices_map.c.invoice_id == Invoice.id)
        .where(
            transactions_invoices_map.c.transaction_id == txn.id,
            Invoice.deleted_at.is_(None),
        )
        .limit(1)
    )
    invoice = db.execute(inv_stmt).scalar_one_or_none()
    if invoice is None:
        raise NotFoundError(message="No invoice linked to this transaction.")

    # Use the full eager-loaded invoice (same as merchant endpoint)
    from src.apps.invoices.crud import get_invoice_by_literal as _get_inv
    full_invoice = _get_inv(db=db, invoice_literal=invoice.invoice_literal, merchant_id=txn.merchant_id)
    if full_invoice is None:
        raise NotFoundError(message="Invoice not found.")

    try:
        pdf_bytes = invoice_pdf_generator.generate(full_invoice)
    except Exception as exc:
        _logger.exception("HPP invoice PDF generation failed for txn %s", txn_literal)
        raise HTTPException(status_code=500, detail=f"PDF generation failed: {exc}")

    filename = f"invoice-{invoice.invoice_literal}.pdf"
    return StreamingResponse(
        BytesIO(pdf_bytes),
        media_type="application/pdf",
        headers={
            "Content-Disposition": f'attachment; filename="{filename}"',
            "Content-Length": str(len(pdf_bytes)),
        },
    )
