from __future__ import annotations

import asyncio
import logging
from datetime import 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__)


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(contract.total_amount or "0.00")

    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((contract.total_amount or Decimal("0")) * 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 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

    # Insert payment event
    event = PaymentEvent(
        tenant_id=tenant_uuid,
        contract_id=contract_uuid,
        invoice_id=contract.invoices[0].id if contract.invoices 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

    # Mark linked invoices as paid
    for invoice in contract.invoices:
        invoice.status = "paid"
        invoice.paid_amount = invoice.total_amount
        invoice.balance_due = Decimal("0")

    # 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:
        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(),
                contract_reference=contract.contract_number,
                cemetery_name="",  # resolved inside helper
                tenant_id=tenant_uuid,
            )
        )

    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,
    contract_reference: str,
    cemetery_name: str,
    tenant_id: UUID,
) -> 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": contract_reference or "",
        "amount_paid": f"{amount:,.2f} {currency}",
        "payment_date": datetime.now(timezone.utc).strftime("%B %d, %Y"),
        "invoice_total": "",
        "balance_due": "",
        **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()
