from __future__ import annotations

import asyncio
import logging
from datetime import date, datetime, timezone
from decimal import Decimal
from typing import Optional
from uuid import UUID

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload

from src.apps.payments.models.payment_event import PaymentEvent
from src.apps.sales.models.contract import Contract
from src.apps.sales.models.opportunity import Opportunity
from src.apps.tenants.models.account import Account
from src.core.config import settings

logger = logging.getLogger(__name__)


def _amount_due_now(contract: Contract) -> Decimal:
    """The amount actually payable today for this contract.

    A split payment plan (e.g. deposit_50) creates two invoice rows up
    front: the deposit, due now, and the remainder, not due until 30 days
    after the deposit's due date — see
    InvoiceService.create_contract_invoices. The online payment page/intent
    must only ever charge the currently-due invoice(s), never the full
    contract total, or a deposit-plan family would be charged in full
    before the remainder's due date arrives.
    """
    today = date.today()
    return sum(
        (
            inv.balance_due
            for inv in contract.invoices
            if inv.due_date is not None and inv.due_date <= today
        ),
        Decimal("0"),
    )


async def lookup_contract_by_token(
    db: AsyncSession,
    token: str,
    tenant_id: UUID,
) -> tuple[str, Optional[Contract]]:
    """
    Return (status, contract) where status is one of:
      'ok' | 'not_found' | 'already_used' | 'expired'
    """
    stmt = (
        select(Contract)
        .options(selectinload(Contract.plot), selectinload(Contract.invoices))
        .where(
            Contract.tenant_id == tenant_id,
            Contract.deleted_at.is_(None),
        )
    )
    # We deliberately do NOT filter by payment_token here so we can distinguish
    # "null token (already used)" from "token not found at all".
    result = await db.execute(stmt)
    contracts = result.scalars().all()

    # Find by token value
    match = next((c for c in contracts if c.payment_token == token), None)
    if match is None:
        return "not_found", None

    # Check expiry
    if match.token_expires_at and match.token_expires_at < datetime.now(timezone.utc):
        return "expired", match

    return "ok", match


async def lookup_contract_by_token_v2(
    db: AsyncSession,
    token: str,
    tenant_id: UUID,
) -> tuple[str, Optional[Contract]]:
    """
    Accurate lookup that searches all contracts for the token or its nulled slot.
    Returns (status, contract).
    """
    # First try active token
    stmt_active = (
        select(Contract)
        .options(selectinload(Contract.plot), selectinload(Contract.invoices))
        .where(
            Contract.tenant_id == tenant_id,
            Contract.payment_token == token,
            Contract.deleted_at.is_(None),
        )
    )
    result = await db.execute(stmt_active)
    contract = result.scalar_one_or_none()

    if contract is not None:
        # Found with active token — check expiry
        if contract.token_expires_at and contract.token_expires_at < datetime.now(timezone.utc):
            return "expired", contract
        return "ok", contract

    # Token not found as active — check if a payment_event already processed this token
    # (token is stored in payment event metadata via payment_link_url pattern)
    # Simple approach: token not in DB = not found (includes already-used case detected
    # separately via the payment_events table in create-intent).
    return "not_found", None


async def get_payment_page_data(
    db: AsyncSession,
    token: str,
    tenant_id: UUID,
) -> tuple[str, Optional[dict]]:
    """
    Returns (status, data_dict).
    status: 'ok' | 'not_found' | 'already_used' | 'expired'
    """
    status, contract = await lookup_contract_by_token_v2(db, token, tenant_id)
    if status != "ok" or contract is None:
        return status, None

    # Load account for cemetery name + logo
    account_result = await db.execute(
        select(Account)
        .options(selectinload(Account.branding))
        .where(Account.id == tenant_id)
    )
    account = account_result.scalar_one_or_none()
    cemetery_name = account.organization_name if account else ""
    logo_url: Optional[str] = None
    if account and account.branding:
        logo_url = account.branding.logo_url

    plot_ref: Optional[str] = None
    if contract.plot:
        plot_ref = contract.plot.plot_ref

    currency = getattr(contract, "currency", None) or "CAD"
    amount = str(_amount_due_now(contract))

    return "ok", {
        "cemetery_name": cemetery_name,
        "cemetery_logo_url": logo_url,
        "family_name": contract.purchaser_name,
        "plot_reference": plot_ref,
        "contract_reference": contract.contract_number,
        "amount_due": amount,
        "currency": currency,
        "stripe_publishable_key": settings.STRIPE_PUBLISHABLE_KEY,
    }


async def create_intent_for_token(
    db: AsyncSession,
    token: str,
    tenant_id: UUID,
) -> tuple[str, Optional[str]]:
    """
    Returns (status, client_secret).
    status: 'ok' | 'not_found' | 'already_used' | 'expired'
    """
    from src.apps.payments.services.stripe_service import create_payment_intent

    status, contract = await lookup_contract_by_token_v2(db, token, tenant_id)
    if status != "ok" or contract is None:
        return status, None

    currency = getattr(contract, "currency", None) or "CAD"
    amount_cents = int(_amount_due_now(contract) * 100)

    intent = create_payment_intent(
        amount_cents=amount_cents,
        currency=currency,
        metadata={
            "contract_id": str(contract.id),
            "tenant_id": str(tenant_id),
            "payment_token": token,
        },
    )
    return "ok", intent.client_secret


async def confirm_payment_for_token(
    db: AsyncSession,
    token: str,
    tenant_id: UUID,
    payment_intent_id: str,
) -> tuple[str, bool]:
    """
    Synchronous fallback to the Stripe webhook, called by the payment page
    right after ``stripe.confirmCardPayment`` succeeds — marking the invoice
    paid must not depend solely on webhook delivery, which may be
    unconfigured (blank STRIPE_WEBHOOK_SECRET) in dev/UAT.

    Re-verifies the PaymentIntent directly with Stripe (never trusts the
    client-reported status) and cross-checks its metadata against this
    token/tenant before marking anything paid. Shares handle_payment_succeeded
    with the webhook path, so whichever arrives first wins and the other is a
    no-op (idempotent via the PaymentEvent.stripe_payment_intent_id check).

    Returns (status, processed):
      status: 'ok' | 'not_found' | 'mismatch' | 'not_succeeded'
    """
    from src.apps.payments.services.stripe_service import retrieve_payment_intent

    try:
        intent = retrieve_payment_intent(payment_intent_id)
    except Exception:
        logger.warning("Could not retrieve PaymentIntent %s for confirm", payment_intent_id)
        return "not_found", False

    metadata = intent.get("metadata", {}) or {}
    if metadata.get("payment_token") != token or metadata.get("tenant_id") != str(tenant_id):
        logger.warning(
            "PaymentIntent %s metadata mismatch for token confirm (tenant=%s)",
            payment_intent_id, tenant_id,
        )
        return "mismatch", False

    if intent.get("status") != "succeeded":
        return "not_succeeded", False

    charges = (intent.get("charges") or {}).get("data", [])
    charge_id = charges[0].get("id") if charges else None

    processed = await handle_payment_succeeded(
        db=db,
        stripe_payment_intent_id=intent.get("id", payment_intent_id),
        stripe_charge_id=charge_id,
        amount_received_cents=intent.get("amount_received", 0),
        currency=intent.get("currency", "cad"),
        contract_id=metadata.get("contract_id", ""),
        tenant_id=metadata.get("tenant_id", ""),
        receipt_email=intent.get("receipt_email") or metadata.get("receipt_email"),
    )
    return "ok", processed


async def handle_payment_succeeded(
    db: AsyncSession,
    stripe_payment_intent_id: str,
    stripe_charge_id: Optional[str],
    amount_received_cents: int,
    currency: str,
    contract_id: str,
    tenant_id: str,
    receipt_email: Optional[str],
) -> bool:
    """
    Process a verified payment_intent.succeeded event.
    Returns True if processed, False if already handled (idempotent).
    """
    # Idempotency: check for existing event
    existing = (await db.execute(
        select(PaymentEvent).where(
            PaymentEvent.stripe_payment_intent_id == stripe_payment_intent_id
        )
    )).scalar_one_or_none()
    if existing is not None:
        logger.info("Duplicate webhook for intent %s — skipping", stripe_payment_intent_id)
        return False

    contract_uuid = UUID(contract_id)
    tenant_uuid = UUID(tenant_id)

    contract = (await db.execute(
        select(Contract)
        .options(selectinload(Contract.invoices))
        .where(Contract.id == contract_uuid, Contract.tenant_id == tenant_uuid)
    )).scalar_one_or_none()
    if contract is None:
        logger.warning("Contract %s not found for webhook", contract_id)
        return False

    amount = Decimal(amount_received_cents) / 100

    # Apply the payment to whichever invoice(s) are actually due now — a
    # split payment plan (e.g. deposit_50) has a second invoice that isn't
    # due until 30 days after the deposit's due date; that one must stay
    # untouched until its own due date arrives, or a single deposit-sized
    # payment would incorrectly wipe out the whole contract's remaining
    # balance too. Sequential allocation (rather than "just the first
    # invoice") handles the ordinary case (one due-now invoice) and the
    # edge case (a partial payment leaves a remainder) the same way.
    today = date.today()
    due_now_invoices = [
        inv for inv in contract.invoices
        if inv.due_date is not None and inv.due_date <= today and inv.balance_due > 0
    ]
    remaining = amount
    paid_invoices = []
    for invoice in due_now_invoices:
        if remaining <= 0:
            break
        applied = min(remaining, invoice.balance_due)
        invoice.paid_amount = (invoice.paid_amount or Decimal("0")) + applied
        invoice.balance_due = invoice.balance_due - applied
        invoice.status = "paid" if invoice.balance_due <= 0 else "partial"
        remaining -= applied
        paid_invoices.append(invoice)

    primary_invoice = paid_invoices[0] if paid_invoices else None

    # Insert payment event
    event = PaymentEvent(
        tenant_id=tenant_uuid,
        contract_id=contract_uuid,
        invoice_id=primary_invoice.id if primary_invoice else None,
        stripe_payment_intent_id=stripe_payment_intent_id,
        stripe_charge_id=stripe_charge_id,
        amount=amount,
        currency=currency.upper(),
        status="succeeded",
        receipt_email=receipt_email or contract.purchaser_email,
    )
    db.add(event)
    await db.flush()

    # Invalidate payment token
    contract.payment_token = None
    contract.token_expires_at = None

    # Advance opportunity to contract_signed
    if contract.opportunity_id:
        opp = (await db.execute(
            select(Opportunity).where(
                Opportunity.id == contract.opportunity_id,
                Opportunity.tenant_id == tenant_uuid,
            )
        )).scalar_one_or_none()
        if opp and opp.stage not in ("contract_signed", "fully_paid"):
            opp.stage = "contract_signed"

    await db.flush()

    # Send receipt email (fire-and-forget; errors must not fail the webhook)
    _receipt_email = receipt_email or contract.purchaser_email
    if _receipt_email:
        # Report the specific invoice actually paid, not the whole contract —
        # the template reads "invoice {{invoice_number}} … Invoice total …
        # Remaining balance", which only makes sense per-invoice (previously
        # this passed the contract number and a sum across every invoice,
        # including the not-yet-due one on a split plan).
        invoice_number = primary_invoice.invoice_number if primary_invoice else contract.contract_number
        invoice_total = primary_invoice.total_amount if primary_invoice else Decimal("0")
        invoice_balance_due = primary_invoice.balance_due if primary_invoice else Decimal("0")
        asyncio.create_task(
            _send_receipt_email(
                db=db,
                event_id=event.id,
                to_email=_receipt_email,
                family_name=contract.purchaser_name or "",
                amount=amount,
                currency=currency.upper(),
                invoice_number=invoice_number,
                cemetery_name="",  # resolved inside helper
                tenant_id=tenant_uuid,
                invoice_total=invoice_total,
                balance_due=invoice_balance_due,
            )
        )

    return True


async def handle_payment_failed(
    db: AsyncSession,
    stripe_payment_intent_id: str,
    stripe_charge_id: Optional[str],
    amount_cents: int,
    currency: str,
    contract_id: str,
    tenant_id: str,
) -> bool:
    """
    Process a verified payment_intent.payment_failed event.
    Token is NOT invalidated on failure — allows retry.
    """
    existing = (await db.execute(
        select(PaymentEvent).where(
            PaymentEvent.stripe_payment_intent_id == stripe_payment_intent_id
        )
    )).scalar_one_or_none()
    if existing is not None:
        return False

    contract_uuid = UUID(contract_id)
    tenant_uuid = UUID(tenant_id)

    event = PaymentEvent(
        tenant_id=tenant_uuid,
        contract_id=contract_uuid,
        stripe_payment_intent_id=stripe_payment_intent_id,
        stripe_charge_id=stripe_charge_id,
        amount=Decimal(amount_cents) / 100,
        currency=currency.upper(),
        status="failed",
    )
    db.add(event)
    await db.flush()
    return True


async def _send_receipt_email(
    db: AsyncSession,
    event_id,
    to_email: str,
    family_name: str,
    amount: Decimal,
    currency: str,
    invoice_number: str,
    cemetery_name: str,
    tenant_id: UUID,
    invoice_total: Decimal,
    balance_due: Decimal,
) -> None:
    """Dispatch a payment receipt via the templated dispatch engine (INDL-54).

    Previously a raw boto3 SES call — now consolidated onto SMTP via the
    dispatch engine (invoice_payment_received template), which admins can edit.
    Stamps receipt_sent_at only when the email actually went out.
    """
    from src.core.constants import EmailTriggerKey
    from src.core.email_dispatch import cemetery_context, email_dispatch_service

    account = (await db.execute(
        select(Account).where(Account.id == tenant_id)
    )).scalar_one_or_none()

    context = {
        "purchaser_name": family_name or "valued family",
        "invoice_number": invoice_number or "",
        "amount_paid": f"${amount:,.2f} {currency}",
        "payment_date": datetime.now(timezone.utc).strftime("%B %d, %Y"),
        "invoice_total": f"${invoice_total:,.2f}",
        "balance_due": f"${balance_due:,.2f}",
        **cemetery_context(account),
    }
    result = await email_dispatch_service.send(
        db,
        trigger_key=EmailTriggerKey.INVOICE_PAYMENT_RECEIVED,
        tenant_id=tenant_id,
        to=to_email,
        context=context,
    )
    if result.sent:
        event = (await db.execute(
            select(PaymentEvent).where(PaymentEvent.id == event_id)
        )).scalar_one_or_none()
        if event:
            event.receipt_sent_at = datetime.now(timezone.utc)
            await db.flush()
