"""
Transactions event listener.

Reacts to domain events and updates transaction/invoice state.

Events handled:
  transaction.completed → update related invoice status to PAID
  transaction.failed    → update PR status, generate retry token, dispatch
                          notification event with retry link (C-3)
"""

import logging
from datetime import datetime, timedelta, timezone
from typing import Any

from src.events.base import BaseEvent
from src.events.dispatcher import on_event_async

logger = logging.getLogger(__name__)

RETRY_TOKEN_EXPIRY_HOURS = 72


@on_event_async("transaction.completed")
async def handle_transaction_completed(event: BaseEvent) -> None:
    """
    When a transaction completes successfully, mark any linked invoices as PAID
    if all transactions on that invoice are now paid.

    Idempotent via ProcessedEvent guard — safe to receive the same event multiple
    times (e.g. Kafka at-least-once delivery).
    """
    from sqlalchemy import select

    from src.apps.invoices.models.invoice import Invoice
    from src.apps.transactions.models.transactions import Transactions
    from src.core.database import SessionCelery
    from src.core.utils.enums import InvoiceStatusTypes, TransactionStatusTypes
    from src.events.utils import is_event_processed, mark_event_processed

    data: Any = event.data
    transaction_id = data.get("transaction_id")

    if not transaction_id:
        return

    try:
        with SessionCelery() as db:
            if await is_event_processed(db, str(event.event_id), "handle_transaction_completed"):
                return

            stmt = select(Transactions).where(Transactions.id == transaction_id)
            txn = db.execute(stmt).scalar_one_or_none()
            if not txn:
                await mark_event_processed(
                    db, str(event.event_id), event.event_type, "handle_transaction_completed"
                )
                return

            for invoice in getattr(txn, "invoices", []):
                # Check if all transactions linked to this invoice are PAID
                all_paid = all(
                    t.txn_status == TransactionStatusTypes.PAID
                    for t in getattr(invoice, "transactions", [])
                )
                if all_paid and invoice.status != InvoiceStatusTypes.PAID:
                    invoice.status = InvoiceStatusTypes.PAID
                    logger.info("Invoice %s marked as PAID", invoice.id)

            await mark_event_processed(
                db, str(event.event_id), event.event_type, "handle_transaction_completed"
            )

    except Exception as exc:
        logger.error("handle_transaction_completed (transactions listener) failed: %s", exc)


@on_event_async("transaction.failed")
async def handle_transaction_failed(event: BaseEvent) -> None:
    """
    When a transaction fails:
      1. Update the payment request status to RETRYING (if not already terminal)
      2. Generate a retry token and store it
      3. C-3: Dispatch a NEW transaction.failed event that includes the retry_link
         so the notifications listener can send the failure email/SMS to the customer.

    Note: The incoming event is only processed here when it does NOT already
    carry a retry_token (which would mean it was merchant-triggered — the
    merchant flow creates the token itself before dispatching this event).
    """
    from sqlalchemy import select

    from src.apps.hpp import crud as hpp_crud
    from src.apps.hpp.helpers.token_helpers import generate_retry_token
    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.apps.payment_requests.enums import PaymentRequestStatusTypes
    from src.apps.transactions.models.transactions import Transactions
    from src.core.config import settings
    from src.core.database import SessionCelery
    from src.core.utils.enums import TransactionStatusTypes
    from src.events.dispatcher import EventDispatcher

    data: Any = event.data
    transaction_id = data.get("transaction_id")
    payment_request_id = data.get("payment_request_id")
    # Skip if retry token already generated (e.g. merchant-triggered retry)
    if data.get("retry_token"):
        return

    if not transaction_id or not payment_request_id:
        return

    retry_token_str: str | None = None
    txn_amount: float = float(data.get("txn_amount", 0))

    try:
        with SessionCelery() as db:
            stmt = select(Transactions).where(Transactions.id == transaction_id)
            txn = db.execute(stmt).scalar_one_or_none()
            if not txn:
                return

            txn_amount = float(txn.txn_amount or 0)

            # Update PR status to RETRYING
            pr_stmt = select(PaymentRequest).where(
                PaymentRequest.id == payment_request_id,
                PaymentRequest.deleted_at.is_(None),
            )
            pr = db.execute(pr_stmt).scalar_one_or_none()
            retrying_status = getattr(PaymentRequestStatusTypes, "RETRYING", None)
            if pr and retrying_status:
                pr.status = retrying_status.value

            # Generate retry token
            retry_token_str = generate_retry_token()
            expires_at = datetime.now(timezone.utc) + timedelta(hours=RETRY_TOKEN_EXPIRY_HOURS)
            retry_token_obj = hpp_crud.create_retry_token(
                db=db,
                transaction_id=transaction_id,
                payment_request_id=payment_request_id,
                token=retry_token_str,
                expires_at=expires_at,
            )

            logger.info(
                "Retry token created for transaction %s: id=%s",
                transaction_id,
                retry_token_obj.id,
            )

    except Exception as exc:
        logger.error("handle_transaction_failed (transactions listener) failed: %s", exc)
        return

    # C-3: Dispatch a second transaction.failed event that carries the retry_link.
    # dispatch_failure_notification (below) listens on "transaction.failed" and will
    # send the failure email/SMS to the customer when retry_link is present.
    if retry_token_str:
        retry_link = (
            f"{settings.hpp_frontend_base_url}/hpp/retry?token={retry_token_str}"
        )
        try:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="transaction.failed",
                    data={
                        "transaction_id": transaction_id,
                        "payment_request_id": payment_request_id,
                        "txn_amount": txn_amount,
                        "retry_token": retry_token_str,
                        "retry_link": retry_link,
                        "is_system_retry_dispatch": True,
                    },
                )
            )
            logger.info(
                "Dispatched transaction.failed notification event for transaction %s",
                transaction_id,
            )
        except Exception as exc:
            logger.error(
                "Failed to dispatch notification event after retry token creation "
                "for transaction %s: %s",
                transaction_id,
                exc,
            )


@on_event_async("transaction.completed")
async def dispatch_receipt_notification(event: BaseEvent) -> None:
    """
    Emit notification.email_requested for a payment receipt after a
    successful transaction.  Idempotent via ProcessedEvent guard.
    """
    from sqlalchemy import select

    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.core.database import SessionCelery
    from src.events.dispatcher import EventDispatcher
    from src.events.utils import is_event_processed, mark_event_processed

    data: Any = event.data
    payment_request_id = data.get("payment_request_id")
    txn_id = data.get("txn_id", "")
    txn_amount = float(data.get("txn_amount", 0))

    if not payment_request_id:
        return

    try:
        with SessionCelery() as db:
            if await is_event_processed(db, str(event.event_id), "dispatch_receipt_notification"):
                return

            stmt = select(PaymentRequest).where(
                PaymentRequest.id == payment_request_id,
                PaymentRequest.deleted_at.is_(None),
            )
            pr = db.execute(stmt).scalar_one_or_none()
            if not pr:
                await mark_event_processed(
                    db, str(event.event_id), event.event_type, "dispatch_receipt_notification"
                )
                return

            merchant = getattr(pr, "merchant", None)
            merchant_name = getattr(merchant, "name", "") if merchant else ""

            customer_email = None
            customer_phone = None
            for pr_customer in getattr(pr, "payment_request_customers", []):
                cust = getattr(pr_customer, "customer", None)
                if cust:
                    customer_email = getattr(cust, "email", None)
                    customer_phone = getattr(cust, "phone", None) or getattr(
                        cust, "phone_number", None
                    )
                    break

            template_vars = {
                "merchant_name": merchant_name,
                "amount": f"{txn_amount:.2f}",
                "currency": pr.currency or "USD",
                "transaction_id": txn_id,
            }
            merchant_id = pr.merchant_id

            await mark_event_processed(
                db, str(event.event_id), event.event_type, "dispatch_receipt_notification"
            )

        if customer_email:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.email_requested",
                    data={
                        "to_email": customer_email,
                        "template_key": "transaction_receipt_email",
                        "template_vars": template_vars,
                        "merchant_id": merchant_id,
                    },
                    correlation_id=event.correlation_id,
                )
            )

        if customer_phone:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.sms_requested",
                    data={
                        "to_phone": customer_phone,
                        "template_key": "transaction_receipt_email",
                        "template_vars": template_vars,
                        "merchant_id": merchant_id,
                    },
                    correlation_id=event.correlation_id,
                )
            )

    except Exception as exc:
        logger.error(
            "dispatch_receipt_notification failed for PR %s: %s",
            payment_request_id,
            exc,
        )


@on_event_async("transaction.refunded")
async def handle_transaction_refunded(event: BaseEvent) -> None:
    """
    Dispatch a refund notification email/SMS to the customer after a successful refund.

    Reads merchant and customer contact info from the original transaction's
    payment request and emits notification.email_requested / notification.sms_requested
    events.  Idempotent via ProcessedEvent guard.
    """
    from sqlalchemy import select

    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.core.database import SessionCelery
    from src.events.dispatcher import EventDispatcher
    from src.events.utils import is_event_processed, mark_event_processed

    data: Any = event.data
    transaction_id = data.get("transaction_id")
    payment_request_id = data.get("payment_request_id")
    refund_amount = float(data.get("refund_amount", 0))
    refund_literal = data.get("refund_literal", "")

    if not payment_request_id:
        return

    try:
        with SessionCelery() as db:
            if await is_event_processed(db, str(event.event_id), "handle_transaction_refunded"):
                return

            stmt = select(PaymentRequest).where(
                PaymentRequest.id == payment_request_id,
                PaymentRequest.deleted_at.is_(None),
            )
            pr = db.execute(stmt).scalar_one_or_none()
            if not pr:
                await mark_event_processed(
                    db, str(event.event_id), event.event_type, "handle_transaction_refunded"
                )
                return

            merchant = getattr(pr, "merchant", None)
            merchant_name = getattr(merchant, "name", "") if merchant else ""

            customer_email = None
            customer_phone = None
            for pr_customer in getattr(pr, "payment_request_customers", []):
                cust = getattr(pr_customer, "customer", None)
                if cust:
                    customer_email = getattr(cust, "email", None)
                    customer_phone = getattr(cust, "phone", None) or getattr(
                        cust, "phone_number", None
                    )
                    break

            template_vars = {
                "merchant_name": merchant_name,
                "amount": f"{refund_amount:.2f}",
                "currency": pr.currency or "USD",
                "transaction_id": refund_literal,
            }
            merchant_id = pr.merchant_id

            await mark_event_processed(
                db, str(event.event_id), event.event_type, "handle_transaction_refunded"
            )

        if customer_email:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.email_requested",
                    data={
                        "to_email": customer_email,
                        "template_key": "refund_processed",
                        "template_vars": template_vars,
                        "merchant_id": merchant_id,
                    },
                    correlation_id=event.correlation_id,
                )
            )

        if customer_phone:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.sms_requested",
                    data={
                        "to_phone": customer_phone,
                        "template_key": "refund_processed",
                        "template_vars": template_vars,
                        "merchant_id": merchant_id,
                    },
                    correlation_id=event.correlation_id,
                )
            )

    except Exception as exc:
        logger.error(
            "handle_transaction_refunded failed for transaction %s: %s",
            transaction_id,
            exc,
        )


@on_event_async("transaction.failed")
async def dispatch_failure_notification(event: BaseEvent) -> None:
    """
    Emit notification.email_requested for a payment failure + retry link.

    Only fires when the event data already contains a retry_link (set by
    handle_transaction_failed after creating the retry token).
    """
    from sqlalchemy import select

    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.core.database import SessionCelery
    from src.events.dispatcher import EventDispatcher

    data: Any = event.data
    payment_request_id = data.get("payment_request_id")
    retry_link = data.get("retry_link", "")
    txn_amount = float(data.get("txn_amount", 0))

    if not payment_request_id or not retry_link:
        # No retry_link means the retry token hasn't been generated yet —
        # a second event will arrive with the link once it is.
        return

    try:
        with SessionCelery() as db:
            stmt = select(PaymentRequest).where(
                PaymentRequest.id == payment_request_id,
                PaymentRequest.deleted_at.is_(None),
            )
            pr = db.execute(stmt).scalar_one_or_none()
            if not pr:
                return

            merchant = getattr(pr, "merchant", None)
            merchant_name = getattr(merchant, "name", "") if merchant else ""

            customer_email = None
            customer_phone = None
            for pr_customer in getattr(pr, "payment_request_customers", []):
                cust = getattr(pr_customer, "customer", None)
                if cust:
                    customer_email = getattr(cust, "email", None)
                    customer_phone = getattr(cust, "phone", None) or getattr(
                        cust, "phone_number", None
                    )
                    break

            due_date_str = pr.due_date.strftime("%Y-%m-%d") if pr.due_date else None
            template_vars = {
                "merchant_name": merchant_name,
                "amount": f"{txn_amount:.2f}",
                "currency": pr.currency or "USD",
                "due_date": due_date_str or "",
                "retry_link": retry_link,
            }
            merchant_id = pr.merchant_id

        if customer_email:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.email_requested",
                    data={
                        "to_email": customer_email,
                        "template_key": "payment_failed",
                        "template_vars": template_vars,
                        "merchant_id": merchant_id,
                    },
                    correlation_id=event.correlation_id,
                )
            )

        if customer_phone:
            await EventDispatcher.dispatch(
                BaseEvent(
                    event_type="notification.sms_requested",
                    data={
                        "to_phone": customer_phone,
                        "template_key": "payment_failed",
                        "template_vars": template_vars,
                        "merchant_id": merchant_id,
                    },
                    correlation_id=event.correlation_id,
                )
            )

    except Exception as exc:
        logger.error(
            "dispatch_failure_notification failed for PR %s: %s",
            payment_request_id,
            exc,
        )
