"""
The public side of a renewal.

A client gets an email with one link. That link has to work without an account,
without a login, and months after it was sent, because the fourteen day notice
and the one sent on the day carry the same URL.

Three views, and nothing else lives here:

    renew_invoice   the page the link opens, showing what is owed
    renew_checkout  creates the Stripe session and sends them to it
    renew_return    where Stripe sends them back, and the only place a payment
                    is ever recorded

The return view verifies the session with Stripe rather than believing the
query string. Anyone can visit a success URL with a made up session id, so a
payment is recorded only when Stripe itself says it was paid.
"""
from django.contrib import messages
from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_POST


def _invoice_or_404(token):
    """
    Find the invoice behind a payment link.

    An empty token must never match. A blank pay_token is the normal state for
    every invoice raised by hand, so matching on it would hand a stranger the
    oldest unpaid invoice in the table.
    """
    from apps.tenants.models import TenantInvoice
    token = (token or '').strip()
    if not token:
        raise Http404
    return get_object_or_404(TenantInvoice.objects.select_related('tenant'), pay_token=token)


def renew_invoice(request, token):
    """The page the emailed link opens. No login, no account."""
    invoice = _invoice_or_404(token)
    return render(request, 'public/renew.html', {
        'invoice': invoice,
        'tenant': invoice.tenant,
        'is_paid': invoice.status == invoice.STATUS_PAID,
        'days_left': (invoice.due_date - timezone.now().date()).days if invoice.due_date else None,
    })


@require_POST
def renew_checkout(request, token):
    """Hand the client to Stripe. POST only, so a crawler cannot start one."""
    import stripe
    from apps.ecommerce.stripe_utils import _get_stripe_secret_key

    invoice = _invoice_or_404(token)
    if invoice.status == invoice.STATUS_PAID:
        return redirect('renew_invoice', token=token)

    key = _get_stripe_secret_key(invoice.tenant)
    if not key:
        # No gateway configured is an operator problem, not a client one, so
        # say something a client can act on rather than showing a stack trace.
        messages.error(request, 'Card payment is not available right now. Please reply to your renewal email.')
        return redirect('renew_invoice', token=token)

    stripe.api_key = key
    session = stripe.checkout.Session.create(
        payment_method_types=['card'],
        mode='payment',
        line_items=[{
            'price_data': {
                'currency': (invoice.currency or 'GBP').lower(),
                'unit_amount': int(invoice.amount * 100),
                'product_data': {
                    'name': f'{invoice.tenant.name} subscription renewal',
                    'description': invoice.description or f'Invoice {invoice.invoice_number}',
                },
            },
            'quantity': 1,
        }],
        success_url=request.build_absolute_uri(
            reverse('renew_return', kwargs={'token': token}) + '?session_id={CHECKOUT_SESSION_ID}'
        ),
        cancel_url=request.build_absolute_uri(reverse('renew_invoice', kwargs={'token': token})),
        customer_email=invoice.tenant.billing_email or None,
        metadata={
            'tenant_invoice_id': str(invoice.pk),
            'invoice_number': invoice.invoice_number,
            'tenant_slug': invoice.tenant.slug,
        },
    )
    invoice.stripe_session_id = session.id
    invoice.save(update_fields=['stripe_session_id'])
    return redirect(session.url, permanent=False)


def renew_return(request, token):
    """
    Where Stripe sends the client back.

    Nothing here trusts the browser. The session id is looked up against
    Stripe, and the invoice is only marked paid if Stripe reports it paid and
    the session is the one this invoice started.
    """
    import stripe
    from apps.ecommerce.stripe_utils import _get_stripe_secret_key

    invoice = _invoice_or_404(token)
    session_id = (request.GET.get('session_id') or '').strip()

    if invoice.status != invoice.STATUS_PAID and session_id:
        try:
            stripe.api_key = _get_stripe_secret_key(invoice.tenant)
            session = stripe.checkout.Session.retrieve(session_id)
            paid = session.get('payment_status') == 'paid'
            ours = session_id == invoice.stripe_session_id
            if paid and ours:
                mark_invoice_paid(invoice)
        except Exception:
            # A failure here means we could not confirm, not that it failed.
            # The webhook and the panel both remain sources of truth.
            pass

    return render(request, 'public/renew.html', {
        'invoice': invoice,
        'tenant': invoice.tenant,
        'is_paid': invoice.status == invoice.STATUS_PAID,
        'just_paid': invoice.status == invoice.STATUS_PAID,
        'days_left': None,
    })


def mark_invoice_paid(invoice):
    """
    Record the payment and move the subscription forward.

    Called from the return view and safe to call twice: an invoice already
    marked paid does nothing, so a client refreshing the success page cannot
    push the renewal date out by another month.
    """
    if invoice.status == invoice.STATUS_PAID:
        return invoice

    invoice.status = invoice.STATUS_PAID
    invoice.paid_at = timezone.now()
    invoice.save(update_fields=['status', 'paid_at'])

    tenant = invoice.tenant
    base = tenant.next_renewal_date or invoice.due_date or timezone.now().date()
    tenant.next_renewal_date = add_one_month(base)
    # Nothing is ever suspended, so there is no flag to switch back on here.
    # The date moving forward is the whole of what payment changes.
    tenant.save(update_fields=['next_renewal_date'])
    return invoice


def add_one_month(d):
    """
    The same date next month, clamped to the end of a shorter one.

    python-dateutil is not in requirements.txt, so importing relativedelta
    would work in development and fail on the server. Twelve lines is cheaper
    than a dependency, and the clamp is the only part that needs thought: a
    renewal on the 31st has to land on the 30th in a thirty day month, not
    roll into the next one.
    """
    import calendar
    year = d.year + (1 if d.month == 12 else 0)
    month = 1 if d.month == 12 else d.month + 1
    day = min(d.day, calendar.monthrange(year, month)[1])
    return d.replace(year=year, month=month, day=day)
