"""
Celery task: generate invoices for future-dated one-time and split payment requests.

Task name: payment_requests.generate_future_payment_invoices
Schedule:  daily at 01:30 UTC (Celery Beat)
Queue:     scheduler

For each one-time / split PR whose billing_date falls within the next
SCHEDULER_INVOICE_LOOKAHEAD_DAYS days (but after today), this task:
  1. Creates a PENDING Invoice linked to the PR.
  2. Creates a PENDING Transaction linked to the invoice.
  3. Schedules process_scheduled_payment via Celery ETA.
  4. Dispatches notification.email_requested + notification.sms_requested.

Each PR/split is processed in its own DB session (one failure never rolls back others).
Idempotent: skips if a non-cancelled invoice already exists for this PR/split.
"""
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import Optional

from celery.utils.log import get_task_logger

from src.worker.celery_app import celery_app
from src.core.database import SessionCelery

logger = get_task_logger(__name__)


@celery_app.task(
    bind=True,
    name="payment_requests.generate_future_payment_invoices",
    max_retries=3,
    default_retry_delay=300,
)
def generate_future_payment_invoices(self) -> dict:
    """
    Idempotent daily task at 01:30 UTC.
    Pre-generates invoices for one-time and split PRs due within the look-ahead window.
    """
    from src.core.config import settings
    from sqlalchemy import select, and_, not_, exists
    from sqlalchemy.orm import joinedload
    from src.apps.payment_requests.models.payment_request import PaymentRequest
    from src.apps.payment_requests.models.split_payment_requests import SplitPaymentRequests
    from src.apps.payment_requests.enums import PaymentRequestStatusTypes, PaymentFrequencies
    from src.apps.invoices.models.invoice import Invoice
    from src.core.utils.enums import InvoiceStatusTypes

    now = datetime.now(timezone.utc)
    today = now.date()
    horizon = now + timedelta(days=settings.SCHEDULER_INVOICE_LOOKAHEAD_DAYS)

    processed = 0
    invoices_created = 0
    notifications_sent = 0
    errors = 0

    logger.info(
        "generate_future_payment_invoices: today=%s horizon=%s",
        today,
        horizon.date(),
    )

    # ── Collect IDs in a lightweight read pass ──────────────────────────────
    try:
        with SessionCelery() as db:
            # Subquery: PR already has a non-cancelled invoice
            has_invoice_sub = exists().where(
                Invoice.payment_request_id == PaymentRequest.id,
                Invoice.status != InvoiceStatusTypes.CANCELLED,
                Invoice.deleted_at.is_(None),
            )

            due_pr_stmt = (
                select(PaymentRequest.id)
                .where(
                    PaymentRequest.status == PaymentRequestStatusTypes.PENDING,
                    PaymentRequest.deleted_at.is_(None),
                    PaymentRequest.payment_frequency.in_(
                        [PaymentFrequencies.ONE_TIME, None]
                    ) | PaymentRequest.payment_frequency.is_(None),
                    PaymentRequest.billing_date.isnot(None),
                    PaymentRequest.billing_date > now,
                    PaymentRequest.billing_date <= horizon,
                    ~has_invoice_sub,
                )
            )
            pr_ids = list(db.execute(due_pr_stmt).scalars().all())

            # Split installments — idempotency is per-installment via split_payment_request_id
            has_split_invoice_sub = exists().where(
                Invoice.split_payment_request_id == SplitPaymentRequests.id,
                Invoice.status != InvoiceStatusTypes.CANCELLED,
                Invoice.deleted_at.is_(None),
            )

            parent_pr_alias = PaymentRequest
            due_split_stmt = (
                select(SplitPaymentRequests.id)
                .join(parent_pr_alias, parent_pr_alias.id == SplitPaymentRequests.payment_request_id)
                .where(
                    parent_pr_alias.status == PaymentRequestStatusTypes.PENDING,
                    parent_pr_alias.deleted_at.is_(None),
                    SplitPaymentRequests.paid_date.is_(None),
                    SplitPaymentRequests.billing_date.isnot(None),
                    SplitPaymentRequests.billing_date > now,
                    SplitPaymentRequests.billing_date <= horizon,
                    ~has_split_invoice_sub,
                )
            )
            split_ids = list(db.execute(due_split_stmt).scalars().all())

    except Exception as exc:
        logger.error("generate_future_payment_invoices: fatal fetch error: %s", exc, exc_info=True)
        raise self.retry(exc=exc, countdown=300)

    # ── Process one-time PRs ─────────────────────────────────────────────────
    for pr_id in pr_ids:
        try:
            notif_payloads = []
            with SessionCelery() as db:
                from sqlalchemy import select as _sel
                from src.apps.payment_requests.models.payment_request import PaymentRequest as _PR
                pr = db.execute(
                    _sel(_PR).where(_PR.id == pr_id).options(joinedload(_PR.payment_methods))
                ).unique().scalar_one_or_none()
                if pr is None:
                    continue

                # Double-check idempotency
                existing = db.execute(
                    _sel(Invoice).where(
                        Invoice.payment_request_id == pr_id,
                        Invoice.status != InvoiceStatusTypes.CANCELLED,
                        Invoice.deleted_at.is_(None),
                    )
                ).scalar_one_or_none()
                if existing:
                    # Invoice was pre-created at PR creation time.
                    # Create a transaction + ETA if one does not exist yet.
                    from src.apps.transactions.models.transactions import Transactions as _Txn, transactions_invoices_map as _tim
                    from src.core.utils.enums import TransactionStatusTypes as _TS
                    _active_txn = db.execute(
                        _sel(_Txn)
                        .join(_tim, _tim.c.transaction_id == _Txn.id)
                        .where(
                            _tim.c.invoice_id == existing.id,
                            _Txn.txn_status.in_([_TS.PENDING, _TS.COMPLETED]),
                        )
                    ).scalar_one_or_none()
                    if _active_txn:
                        continue  # transaction already scheduled — nothing to do
                    # Create transaction + ETA for the existing invoice
                    _, notif_payloads = _create_invoice_and_transaction(
                        db, pr, pr.billing_date, float(pr.amount or 0), existing_invoice=existing
                    )
                    db.commit()
                    processed += 1
                else:
                    invoice, notif_payloads = _create_invoice_and_transaction(
                        db, pr, pr.billing_date, float(pr.amount or 0)
                    )
                    db.commit()
                    invoices_created += 1
                    processed += 1

            # Dispatch notifications outside DB session (covers both paths)
            for payload in notif_payloads:
                try:
                    _emit_notification(payload)
                    notifications_sent += 1
                except Exception as notif_err:
                    logger.warning("generate_future_payment_invoices: notification failed: %s", notif_err)

        except Exception as pr_err:
            logger.error("generate_future_payment_invoices: error for PR id=%s: %s", pr_id, pr_err, exc_info=True)
            errors += 1

    # ── Process split installments ───────────────────────────────────────────
    for split_id in split_ids:
        try:
            notif_payloads = []
            with SessionCelery() as db:
                from sqlalchemy import select as _sel
                from src.apps.payment_requests.models.split_payment_requests import SplitPaymentRequests as _Split
                from src.apps.payment_requests.models.payment_request import PaymentRequest as _PR

                split = db.execute(
                    _sel(_Split).where(_Split.id == split_id)
                ).scalar_one_or_none()
                if split is None:
                    continue

                pr = db.execute(
                    _sel(_PR).where(_PR.id == split.payment_request_id).options(joinedload(_PR.payment_methods))
                ).unique().scalar_one_or_none()
                if pr is None:
                    continue

                # Per-split secondary idempotency check
                existing = db.execute(
                    _sel(Invoice).where(
                        Invoice.split_payment_request_id == split_id,
                        Invoice.status != InvoiceStatusTypes.CANCELLED,
                        Invoice.deleted_at.is_(None),
                    )
                ).scalar_one_or_none()
                if existing:
                    continue

                # Compute split amount
                split_amount = _compute_split_amount(split, pr)
                billing_date = split.billing_date
                if billing_date and billing_date.tzinfo is None:
                    billing_date = billing_date.replace(tzinfo=timezone.utc)

                invoice, notif_payloads = _create_invoice_and_transaction(
                    db, pr, billing_date, split_amount, split_id=split_id
                )
                db.commit()
                invoices_created += 1
                processed += 1

            for payload in notif_payloads:
                try:
                    _emit_notification(payload)
                    notifications_sent += 1
                except Exception as notif_err:
                    logger.warning(
                        "generate_future_payment_invoices: split notification failed: %s", notif_err
                    )

        except Exception as split_err:
            logger.error(
                "generate_future_payment_invoices: error for split id=%s: %s",
                split_id, split_err, exc_info=True
            )
            errors += 1

    logger.info(
        "generate_future_payment_invoices: processed=%d invoices_created=%d "
        "notifications_sent=%d errors=%d",
        processed, invoices_created, notifications_sent, errors,
    )
    return {
        "processed": processed,
        "invoices_created": invoices_created,
        "notifications_sent": notifications_sent,
        "errors": errors,
    }


def _compute_split_amount(split, pr) -> float:
    """Derive the payment amount for a split installment."""
    split_type = getattr(split, "split_type", "amount")
    split_value = float(getattr(split, "split_value", 0) or 0)
    if split_type == "percent":
        return float(pr.amount or 0) * (split_value / 100.0)
    return split_value or float(pr.amount or 0)


def _create_invoice_and_transaction(db, pr, billing_date: datetime, amount: float, split_id: Optional[int] = None, existing_invoice=None):
    """
    Create a PENDING Invoice + PENDING Transaction for a future-dated PR.
    If existing_invoice is provided the invoice is reused and only the
    transaction + ETA task are created (invoice was pre-created at PR creation).
    Returns (invoice, notification_payloads_list).
    """
    import uuid as _uuid
    from src.apps.invoices.services.invoice_services import create_invoice
    from src.apps.transactions.models.transactions import Transactions, transactions_invoices_map
    from src.apps.transactions.services import generate_txn_literal
    from src.core.utils.enums import (
        InvoiceStatusTypes, TransactionStatusTypes, TransactionCategories, TransactionTypes
    )
    from src.apps.invoices import crud as invoice_crud
    from src.core.utils.enums import InvoiceActivityTypes

    now = datetime.now(timezone.utc)

    # Determine customer_id
    customer_id = None
    customer_email = None
    customer_phone = None
    for pr_customer in getattr(pr, "payment_request_customers", []):
        cust = getattr(pr_customer, "customer", None)
        if cust:
            customer_id = cust.id
            customer_email = getattr(cust, "email", None)
            customer_phone = getattr(cust, "phone", None)
            break

    if existing_invoice is None:
        invoice = create_invoice(
            db=db,
            payment_request=pr,
            merchant_id=pr.merchant_id,
            customer_id=customer_id,
            amount=amount,
            status=InvoiceStatusTypes.PENDING,
            due_date=billing_date,
            billing_date=now,
        )
        if split_id:
            invoice.split_payment_request_id = split_id
        db.flush()
        invoice_crud.write_activity(
            db=db,
            invoice_id=invoice.id,
            activity_type=InvoiceActivityTypes.INVOICE_CREATED,
            description="Invoice pre-generated for future-dated payment",
            actor_type="system",
            metadata={"billing_date": billing_date.isoformat() if billing_date else None},
        )
    else:
        invoice = existing_invoice

    # Payment method
    payment_method_id = None
    if pr.payment_methods:
        payment_method_id = pr.payment_methods[0].id

    pending_txn = Transactions(
        txn_id=f"sched_{pr.payment_request_id}_{_uuid.uuid4().hex[:8]}",
        txn_literal=generate_txn_literal(db),
        txn_amount=amount,
        txn_status=TransactionStatusTypes.PENDING,
        payment_request_id=pr.id,
        merchant_id=pr.merchant_id,
        customer_id=customer_id,
        payment_method_id=payment_method_id,
        transaction_type=TransactionTypes.PAYMENT_TERMINAL,
        category=TransactionCategories.CHARGE,
        txn_metadata={"invoice_id": invoice.invoice_id},
    )
    db.add(pending_txn)
    db.flush()

    db.execute(
        transactions_invoices_map.insert().values(
            transaction_id=pending_txn.id,
            invoice_id=invoice.id,
        )
    )
    db.flush()

    # Schedule ETA payment
    if payment_method_id and billing_date:
        try:
            from src.worker.hpp_tasks import process_scheduled_payment
            task = process_scheduled_payment.apply_async(
                args=[pending_txn.id, payment_method_id],
                eta=billing_date,
            )
            pending_txn.txn_metadata = {**(pending_txn.txn_metadata or {}), "celery_task_id": task.id}
            db.flush()
        except Exception as task_err:
            logger.error("_create_invoice_and_transaction: failed to schedule ETA task: %s", task_err)

    # Build notification payloads
    merchant = getattr(pr, "merchant", None)
    merchant_name = getattr(merchant, "name", "") if merchant else ""
    currency = getattr(pr, "currency", "USD") or "USD"
    notif_payloads = []

    if customer_email:
        notif_payloads.append({
            "event_type": "notification.email_requested",
            "to": customer_email,
            "template_key": "invoice_email",
            "template_vars": {
                "merchant_name": merchant_name,
                "invoice_id": invoice.invoice_literal or invoice.invoice_id,
                "amount": f"{amount:.2f}",
                "currency": currency,
                "due_date": billing_date.isoformat() if billing_date else "",
                "payment_link": "",
            },
            "merchant_id": pr.merchant_id,
        })

    if customer_phone:
        notif_payloads.append({
            "event_type": "notification.sms_requested",
            "to_phone": customer_phone,
            "template_key": "invoice_email",
            "template_vars": {
                "merchant_name": merchant_name,
                "invoice_id": invoice.invoice_literal or invoice.invoice_id,
                "amount": f"{amount:.2f}",
                "currency": currency,
                "due_date": billing_date.isoformat() if billing_date else "",
                "payment_link": "",
            },
            "merchant_id": pr.merchant_id,
        })

    return invoice, notif_payloads


def _emit_notification(payload: dict) -> None:
    """Dispatch a single notification event."""
    from src.events.base import BaseEvent
    from src.events.dispatcher import EventDispatcher

    # Make a copy so we don't mutate the caller's dict
    payload = dict(payload)
    event_type = payload.pop("event_type")
    to = payload.pop("to", None)
    to_phone = payload.pop("to_phone", None)

    async def _go():
        await EventDispatcher.dispatch(
            BaseEvent(
                event_type=event_type,
                data={
                    **({"to_email": to} if to else {}),
                    **({"to_phone": to_phone} if to_phone else {}),
                    "template_key": payload["template_key"],
                    "template_vars": payload["template_vars"],
                    "merchant_id": payload["merchant_id"],
                },
            )
        )

    loop = asyncio.new_event_loop()
    try:
        loop.run_until_complete(_go())
    finally:
        loop.close()
