"""
Warn clients before their subscription renews, and give them a way to pay.

Runs once a day. For every active tenant with a renewal date it works out how
many days are left, and if that lands on one of the notice thresholds it raises
an invoice (the first time only) and emails the client a link to pay it.

    14 days   heads up, invoice raised here and only here
     7 days   reminder, same invoice, same link
     3 days   reminder
     0 days   due today

Two rules matter more than the rest:

  Never send the same notice twice. Each invoice records which thresholds have
  been emailed, so a cron that runs twice, or a day that gets retried, does not
  land in the client's inbox again.

  Never raise a second invoice. The fourteen day notice creates one and every
  later notice reuses it, so a client is billed once per period no matter how
  many reminders go out.

Nothing here suspends anything. A client who ignores every notice keeps their
site, and the panel simply shows the invoice as overdue.

Cron, daily at 08:00:
    0 8 * * * cd /home/<user>/public_html/backend && \\
      venv/bin/python manage.py send_renewal_warnings --settings=config.settings.production
"""
from datetime import date

from django.conf import settings
from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.utils import timezone

# The notices, furthest out first. Changing this changes the schedule and
# nothing else: the invoice is raised on whichever is largest.
NOTICE_DAYS = [14, 7, 3, 0]


class Command(BaseCommand):
    help = 'Email clients before their subscription renews, raising a payable invoice on the first notice.'

    def add_arguments(self, parser):
        parser.add_argument(
            '--dry-run', action='store_true',
            help='Print what would be sent and change nothing. Use this first.',
        )
        parser.add_argument(
            '--slug', default='',
            help='Only this tenant, by slug. For testing against one account.',
        )

    def handle(self, *args, **options):
        from apps.ftlink.models import JobRun
        from apps.tenants.models import Tenant, TenantInvoice

        dry = options['dry_run']
        today = timezone.now().date()

        qs = Tenant.objects.filter(
            is_active=True, is_archived=False, next_renewal_date__isnull=False,
        ).exclude(is_master=True)
        if options['slug']:
            qs = Tenant.objects.filter(slug=options['slug'])

        sent = raised = 0
        for tenant in qs:
            days_left = (tenant.next_renewal_date - today).days
            if days_left not in NOTICE_DAYS:
                continue

            recipient = (tenant.billing_email
                         or getattr(tenant.owner, 'email', '')
                         or '').strip()
            if not recipient:
                self.stdout.write(self.style.WARNING(
                    f'{tenant.name}: {days_left} days to renewal but no billing email, skipped'
                ))
                continue

            invoice = self._invoice_for(tenant, today, dry)
            if invoice is None:
                self.stdout.write(self.style.WARNING(
                    f'{tenant.name}: no monthly fee set, nothing to invoice, skipped'
                ))
                continue
            if getattr(invoice, '_was_created', False):
                raised += 1

            if days_left in invoice.reminder_days:
                self.stdout.write(f'{tenant.name}: {days_left} day notice already sent, skipping')
                continue

            if dry:
                self.stdout.write(
                    f'[dry run] {tenant.name}: would email {recipient} the {days_left} day notice '
                    f'for {invoice.invoice_number} at £{invoice.amount}'
                )
                sent += 1
                continue

            token = invoice.ensure_pay_token()
            if self._send(tenant, invoice, recipient, days_left, token):
                invoice.record_reminder(days_left)
                sent += 1
                self.stdout.write(self.style.SUCCESS(
                    f'{tenant.name}: {days_left} day notice sent to {recipient}'
                ))

        summary = f'{sent} notice(s) sent, {raised} invoice(s) raised'
        self.stdout.write(self.style.SUCCESS(f'Done. {summary}'))
        if not dry:
            JobRun.record('send_renewal_warnings', note=summary)

    # ── helpers ──────────────────────────────────────────────────────────────

    def _invoice_for(self, tenant, today, dry):
        """
        The invoice for this renewal period, creating it only once.

        Matched on the renewal date rather than on "most recent unpaid", so a
        client with an old unpaid invoice still gets a correct new one, and a
        second run on the same day reuses rather than duplicates.
        """
        from apps.tenants.models import TenantInvoice

        renewal = tenant.next_renewal_date
        existing = TenantInvoice.objects.filter(
            tenant=tenant,
            invoice_type=TenantInvoice.TYPE_MONTHLY,
            due_date=renewal,
        ).exclude(status=TenantInvoice.STATUS_CANCELLED).first()
        if existing:
            return existing

        amount = tenant.monthly_fee or 0
        if not amount:
            return None

        if dry:
            # Return an unsaved stand-in so the dry run can report properly
            # without writing anything.
            stub = TenantInvoice(
                tenant=tenant, amount=amount, due_date=renewal,
                invoice_number='(would be created)',
            )
            stub._was_created = True
            return stub

        invoice = TenantInvoice.objects.create(
            tenant=tenant,
            invoice_type=TenantInvoice.TYPE_MONTHLY,
            description=f'{tenant.get_plan_display()} subscription',
            amount=amount,
            due_date=renewal,
            period_start=renewal,
            status=TenantInvoice.STATUS_PENDING,
        )
        invoice._was_created = True
        return invoice

    def _send(self, tenant, invoice, recipient, days_left, token):
        """Send one notice. A failure is reported, never raised, so one bad
        address cannot stop every other client being told."""
        from django.core.mail import EmailMultiAlternatives

        base = (getattr(settings, 'SITE_URL', '') or '').rstrip('/')
        pay_url = f'{base}/renew/{token}/'

        subject = (f'{tenant.name}: subscription renews today'
                   if days_left <= 0 else
                   f'{tenant.name}: subscription renews in {days_left} days')

        from apps.pages.branding import brand
        html = render_to_string('public/emails/renewal_warning.html', {
            **brand(),
            'recipient_name': (getattr(tenant.owner, 'first_name', '') or tenant.name),
            'site_name':      tenant.name,
            'plan_label':     tenant.get_plan_display(),
            'invoice_number': invoice.invoice_number,
            'amount':         invoice.amount,
            'days_left':      days_left,
            'renewal_date':   tenant.next_renewal_date,
            'pay_url':        pay_url,
            # Whoever runs this install, not a name baked into the template.
            'operator_domain': base.split('//')[-1].strip('/'),
        })
        text = (
            f'Your subscription for {tenant.name} renews on '
            f'{tenant.next_renewal_date:%d %b %Y}.\n\n'
            f'Invoice {invoice.invoice_number}, £{invoice.amount}.\n'
            f'Pay here: {pay_url}\n\n'
            f'Nothing is switched off automatically.\n'
        )

        from apps.pages.email_backend import outgoing_address
        try:
            msg = EmailMultiAlternatives(subject, text, from_email=outgoing_address() or None, to=[recipient])
            msg.attach_alternative(html, 'text/html')
            msg.send(fail_silently=False)
            return True
        except Exception as exc:
            self.stderr.write(f'{tenant.name}: could not email {recipient}: {exc}')
            return False
