"""
Celery task: send a published invoice to its customer.

Task name: invoice.send_invoice
Queue:     default

Steps:
1. Fetch invoice from DB.
2. Guard: silently return if status is DRAFT (should never happen in production).
3. Generate PDF via InvoicePDFGenerator.
4. Resolve recipient contact details (email + phone).
5. Dispatch email and/or SMS based on the requested channels.
6. Write InvoiceActivity(INVOICE_SENT).
7. If invoice.status == CREATED: transition to PENDING.
"""

import asyncio
import logging
from datetime import datetime, timezone
from typing import List, Optional

from celery.utils.log import get_task_logger

from src.worker.celery_app import celery_app
from src.core.database import SessionCelery
from src.core.utils.enums import InvoiceStatusTypes, InvoiceActivityTypes
from src.apps.invoices import crud as invoice_crud

logger = get_task_logger(__name__)


@celery_app.task(
    name="invoice.send_invoice",
    bind=True,
    max_retries=3,
    default_retry_delay=60,
)
def send_invoice_task(
    self,
    invoice_id: int,
    channels: Optional[List[str]] = None,
    message: Optional[str] = None,
) -> dict:
    """
    Send a published invoice to its customer via email and/or SMS.

    Args:
        invoice_id: Integer PK of the Invoice record (not the literal).
        channels:   List of delivery channels — 'email', 'sms', or both.
                    Defaults to ['email'] when not supplied.
        message:    Optional custom message appended to the notification body.

    Returns:
        Dict with task result metadata.
    """
    if not channels:
        channels = ["email"]

    logger.info(
        "send_invoice_task starting for invoice_id=%s channels=%s",
        invoice_id,
        channels,
    )

    with SessionCelery() as db:
        from sqlalchemy import select
        from src.apps.invoices.models.invoice import Invoice
        from sqlalchemy.orm import joinedload, selectinload

        # 1. Fetch invoice with relationships
        stmt = (
            select(Invoice)
            .where(Invoice.id == invoice_id, Invoice.deleted_at == None)
            .options(
                joinedload(Invoice.customer),
                joinedload(Invoice.merchant),
                joinedload(Invoice.payer),
                selectinload(Invoice.invoice_line_items),
            )
        )
        invoice = db.execute(stmt).unique().scalar_one_or_none()

        if invoice is None:
            logger.warning("send_invoice_task: invoice_id=%s not found, skipping", invoice_id)
            return {"status": "skipped", "reason": "invoice not found"}

        # 2. Guard: skip DRAFT silently
        if invoice.status == InvoiceStatusTypes.DRAFT:
            logger.warning(
                "send_invoice_task: invoice_id=%s is DRAFT — aborting send (should never happen)",
                invoice_id,
            )
            return {"status": "skipped", "reason": "draft invoices cannot be sent"}

        merchant = invoice.merchant
        customer = invoice.customer
        line_items = invoice.invoice_line_items or []

        # 3. Generate PDF (email-only; reused across retries if needed)
        pdf_bytes: bytes | None = None
        if "email" in channels:
            try:
                from src.apps.invoices.helpers.pdf_generator import InvoicePDFGenerator
                gen = InvoicePDFGenerator()
                pdf_bytes = gen.generate_pdf(invoice, merchant, customer, line_items)
                logger.info(
                    "send_invoice_task: PDF generated (%d bytes) for invoice_id=%s",
                    len(pdf_bytes),
                    invoice_id,
                )
            except ImportError:
                logger.warning("WeasyPrint not available — sending invoice without PDF attachment")
            except Exception as exc:
                logger.error("PDF generation failed for invoice_id=%s: %s", invoice_id, exc)
                # Don't abort — email goes out without attachment

        # 4. Resolve recipient contact details
        recipient_email: str | None = None
        recipient_phone: str | None = None

        if customer:
            recipient_email = getattr(customer, "email", None) or None
            recipient_phone = getattr(customer, "phone", None) or None
        if not recipient_email and invoice.payer:
            recipient_email = getattr(invoice.payer, "email", None) or None
        if not recipient_phone and invoice.payer:
            recipient_phone = getattr(invoice.payer, "phone", None) or None

        # 5. Dispatch per channel
        send_error: Exception | None = None
        channels_sent: list[str] = []
        channels_failed: list[str] = []

        if "email" in channels:
            if not recipient_email:
                logger.warning(
                    "send_invoice_task: no recipient email for invoice_id=%s — skipping email",
                    invoice_id,
                )
                channels_failed.append("email")
            else:
                try:
                    _send_invoice_email(
                        db=db,
                        invoice=invoice,
                        merchant=merchant,
                        customer=customer,
                        recipient_email=recipient_email,
                        pdf_bytes=pdf_bytes,
                        custom_message=message,
                    )
                    channels_sent.append("email")
                except Exception as exc:
                    logger.error(
                        "send_invoice_task: email send failed for invoice_id=%s: %s",
                        invoice_id,
                        exc,
                    )
                    send_error = exc
                    channels_failed.append("email")

        if "sms" in channels:
            if not recipient_phone:
                logger.warning(
                    "send_invoice_task: no recipient phone for invoice_id=%s — skipping SMS",
                    invoice_id,
                )
                channels_failed.append("sms")
            else:
                try:
                    _send_invoice_sms(
                        db=db,
                        invoice=invoice,
                        merchant=merchant,
                        customer=customer,
                        recipient_phone=recipient_phone,
                        custom_message=message,
                    )
                    channels_sent.append("sms")
                except Exception as exc:
                    logger.error(
                        "send_invoice_task: SMS send failed for invoice_id=%s: %s",
                        invoice_id,
                        exc,
                    )
                    if send_error is None:
                        send_error = exc
                    channels_failed.append("sms")

        # Retry if ALL requested channels failed due to transient errors
        if send_error is not None and not channels_sent:
            raise self.retry(exc=send_error)

        # 6. Write activity only after at least one successful send
        if channels_sent:
            recipients_str = ", ".join(
                filter(None, [
                    recipient_email if "email" in channels_sent else None,
                    recipient_phone if "sms" in channels_sent else None,
                ])
            )
            invoice_crud.write_activity(
                db=db,
                invoice_id=invoice.id,
                activity_type=InvoiceActivityTypes.INVOICE_SENT,
                description=f"Invoice sent via {', '.join(channels_sent)} to {recipients_str}",
                actor_type="system",
                actor_id=None,
                metadata={
                    "channels_sent": channels_sent,
                    "channels_failed": channels_failed,
                    "recipient_email": recipient_email,
                    "recipient_phone": recipient_phone,
                },
            )

        # 7. Transition CREATED → PENDING
        if invoice.status == InvoiceStatusTypes.CREATED:
            invoice.status = InvoiceStatusTypes.PENDING
            invoice.updated_at = datetime.now(timezone.utc)
            db.flush()

        # SessionCelery auto-commits on exit
        logger.info(
            "send_invoice_task: completed for invoice_id=%s sent=%s failed=%s",
            invoice_id,
            channels_sent,
            channels_failed,
        )
        return {
            "status": "sent",
            "invoice_id": invoice_id,
            "channels_sent": channels_sent,
            "channels_failed": channels_failed,
            "recipient_email": recipient_email,
            "recipient_phone": recipient_phone,
        }


def _run_async(coro):
    """Run an async coroutine from sync context (Celery worker)."""
    try:
        return asyncio.run(coro)
    except RuntimeError:
        # asyncio.run() fails when a loop is already running (gevent/eventlet pools)
        loop = asyncio.new_event_loop()
        try:
            return loop.run_until_complete(coro)
        finally:
            loop.close()


def _send_invoice_email(
    db,
    invoice,
    merchant,
    customer,
    recipient_email: str,
    pdf_bytes: bytes | None,
    custom_message: str | None = None,
) -> None:
    """
    Send the invoice email using the site_templates table (key: 'invoice_email').

    Falls back to a plain-text body if the template is not found or inactive.
    Custom message, when provided, is appended to the body.
    """
    template_subject: str = f"Invoice {invoice.invoice_literal} from {getattr(merchant, 'name', 'us')}"
    html_body: str | None = None
    text_body: str = _default_invoice_email_body(invoice, merchant, custom_message)

    try:
        from src.apps.site_templates import crud as site_template_crud
        from src.apps.site_templates.services import render_template as site_template_render
        from src.apps.notifications.services import _get_merchant_branding

        tmpl = site_template_crud.get_template_by_key(db, "invoice_email")
        if tmpl and tmpl.is_active:
            merchant_id = getattr(merchant, "id", None)
            branding = _get_merchant_branding(db, merchant_id)
            context_vars = {
                "invoice_number": invoice.invoice_literal or "",
                "customer_name": (
                    f"{getattr(customer, 'first_name', '') or ''} {getattr(customer, 'last_name', '') or ''}".strip()
                    if customer else ""
                ),
                "merchant_name": getattr(merchant, "name", "") or "",
                "amount": f"${invoice.amount / 100:.2f}" if invoice.amount else "$0.00",
                "due_date": invoice.due_date.strftime("%B %d, %Y") if invoice.due_date else "",
                "pay_url": "",  # TODO: inject HPP payment URL
                "custom_message": custom_message or "",
            }
            rendered = site_template_render(tmpl, {**branding, **context_vars})
            if tmpl.subject:
                template_subject = tmpl.subject
            html_body = rendered.get("html")
            text_body = rendered.get("text") or text_body
    except Exception as exc:
        logger.warning(
            "_send_invoice_email: site_template lookup/render failed for invoice_id=%s, "
            "using plain-text fallback: %s",
            invoice.id,
            exc,
        )

    from src.apps.notifications.providers.sendgrid import SendGridProvider
    provider = SendGridProvider()
    _run_async(
        provider.send(
            recipient=recipient_email,
            subject=template_subject,
            body_text=text_body,
            body_html=html_body,
        )
    )


def _send_invoice_sms(
    db,
    invoice,
    merchant,
    customer,
    recipient_phone: str,
    custom_message: str | None = None,
) -> None:
    """
    Send the invoice SMS using the site_templates table (key: 'invoice_send_sms').

    Falls back to a plain-text body if the template is not found or inactive.
    """
    text_body: str = _default_invoice_sms_body(invoice, merchant, custom_message)

    try:
        from src.apps.site_templates import crud as site_template_crud
        from src.apps.site_templates.services import render_template as site_template_render
        from src.apps.notifications.services import _get_merchant_branding

        tmpl = site_template_crud.get_template_by_key(db, "invoice_send_sms")
        if tmpl and tmpl.is_active:
            merchant_id = getattr(merchant, "id", None)
            branding = _get_merchant_branding(db, merchant_id)
            context_vars = {
                "invoice_number": invoice.invoice_literal or "",
                "customer_name": (
                    f"{getattr(customer, 'first_name', '') or ''} {getattr(customer, 'last_name', '') or ''}".strip()
                    if customer else ""
                ),
                "merchant_name": getattr(merchant, "name", "") or "",
                "amount": f"${invoice.amount / 100:.2f}" if invoice.amount else "$0.00",
                "due_date": invoice.due_date.strftime("%B %d, %Y") if invoice.due_date else "",
                "pay_url": "",  # TODO: inject HPP payment URL
                "custom_message": custom_message or "",
            }
            rendered = site_template_render(tmpl, {**branding, **context_vars})
            text_body = rendered.get("text") or text_body
    except Exception as exc:
        logger.warning(
            "_send_invoice_sms: site_template lookup/render failed for invoice_id=%s, "
            "using plain-text fallback: %s",
            invoice.id,
            exc,
        )

    from src.apps.notifications.providers.twilio import TwilioProvider
    provider = TwilioProvider()
    _run_async(
        provider.send(
            recipient=recipient_phone,
            subject=None,
            body_text=text_body,
            body_html=None,
        )
    )


def _default_invoice_email_body(invoice, merchant, custom_message: str | None = None) -> str:
    merchant_name = getattr(merchant, "name", "your service provider")
    literal = invoice.invoice_literal or invoice.invoice_id
    amount = invoice.amount or 0
    due_str = ""
    if invoice.due_date:
        due_str = f" due on {invoice.due_date.strftime('%B %d, %Y')}"

    body = (
        f"Please find attached invoice {literal}{due_str} "
        f"for ${amount / 100:.2f} from {merchant_name}.\n\n"
        "Thank you for your business."
    )
    if custom_message:
        body += f"\n\n{custom_message}"
    return body


def _default_invoice_sms_body(invoice, merchant, custom_message: str | None = None) -> str:
    merchant_name = getattr(merchant, "name", "your service provider")
    literal = invoice.invoice_literal or invoice.invoice_id
    amount = invoice.amount or 0
    due_str = ""
    if invoice.due_date:
        due_str = f", due {invoice.due_date.strftime('%b %d, %Y')}"

    body = (
        f"{merchant_name}: Invoice {literal}{due_str} — "
        f"${amount / 100:.2f} due. "
        "Reply STOP to unsubscribe."
    )
    if custom_message:
        body = f"{custom_message}\n\n{body}"
    return body
