"""
Tenant Model — the core of the multi-tenant platform.

Each tenant = one client website.
  - Has its own domain (example.co.uk)
  - Has its own Panel subdomain (pnl.yourdomain.com)
  - Has its own branding (logo, accent colour)
  - Has its own set of enabled modules
  - Has its own isolated data (FK on every content model)

YOUR COMPANY itself is also a tenant (the master tenant).
"""
from django.db import models
from django.utils.text import slugify
from apps.accounts.models import User
from apps.accounts.fields import WebPImageField


# ─── Available modules ────────────────────────────────────────────────────────

MODULE_HOMEPAGE     = 'homepage'
MODULE_PORTFOLIO    = 'portfolio'
MODULE_INSIGHTS     = 'insights'
MODULE_AUCTION      = 'auction'
MODULE_MARKETPLACE  = 'marketplace'
MODULE_FAQ          = 'faq'
MODULE_CONTACT      = 'contact'
MODULE_TICKETS      = 'tickets'
MODULE_CHAT         = 'chat'
MODULE_MENU         = 'menu'
MODULE_EVENTS       = 'events'
MODULE_GALLERY      = 'gallery'
MODULE_ECOMMERCE    = 'ecommerce'
MODULE_STARTUP      = 'startup'
MODULE_SERVICES     = 'services'
MODULE_MEDIA        = 'media'
MODULE_ANALYTICS    = 'analytics'
MODULE_CRM          = 'crm'
# Sub-brands of a group, each served at /<slug>/ on the parent domain. One
# install, one panel, many brands. Not tenants: see apps/brands/models.py.
MODULE_BRANDS       = 'brands'
MODULE_VEVENTA      = 'veventa'
# Sold as a subscription add-on rather than bundled with a plan, so it is
# deliberately absent from every entry in PLAN_MODULES below.
MODULE_CONSENT      = 'consent'

MODULE_CHOICES = [
    (MODULE_HOMEPAGE,    'Homepage Editor'),
    (MODULE_PORTFOLIO,   'Portfolio'),
    (MODULE_INSIGHTS,    'Blog / Insights'),
    (MODULE_SERVICES,    'Services'),
    (MODULE_FAQ,         'Support & FAQ'),
    (MODULE_CONTACT,     'Contact Form'),
    (MODULE_TICKETS,     'Tickets & Support'),
    (MODULE_CHAT,        'AI Chat Widget'),
    (MODULE_ECOMMERCE,   'E-commerce (Products & Orders)'),
    (MODULE_AUCTION,     'Domain Auction'),
    (MODULE_MARKETPLACE, 'Marketplace'),
    (MODULE_MEDIA,       'Media Hub'),
    (MODULE_CONSENT,     'Cookie Consent (add-on)'),
    (MODULE_GALLERY,     'Gallery'),
    (MODULE_MENU,        'Menu / Food & Drink'),
    (MODULE_EVENTS,      'Events & Bookings'),
    (MODULE_ANALYTICS,   'Analytics & Reporting'),
    (MODULE_CRM,         'CRM & Sales Pipeline'),
    (MODULE_STARTUP,     'Startup Solutions'),
    (MODULE_BRANDS,      'Brands / Sub-brands'),
    (MODULE_VEVENTA,     'Veventa — Events ERP'),
]

# These modules are ON for every tenant and cannot be disabled.
# Marketplace is included because it is how a client requests extra modules,
# upgrades and services from YOUR COMPANY. Without it they have no way to order.
ALWAYS_ON_MODULES = {MODULE_TICKETS, MODULE_CONTACT, MODULE_MARKETPLACE}

# Default module set for a new client site
DEFAULT_MODULES = {
    MODULE_HOMEPAGE, MODULE_PORTFOLIO, MODULE_INSIGHTS, MODULE_SERVICES,
    MODULE_FAQ, MODULE_CONTACT, MODULE_TICKETS, MODULE_MARKETPLACE,
}

# Which modules each plan unlocks (cumulative — each plan includes all below it)
PLAN_MODULES = {
    'starter': {
        MODULE_HOMEPAGE, MODULE_CONTACT, MODULE_TICKETS, MODULE_FAQ,
        MODULE_MARKETPLACE,
    },
    'standard': {
        MODULE_HOMEPAGE, MODULE_PORTFOLIO, MODULE_INSIGHTS, MODULE_SERVICES,
        MODULE_CONTACT, MODULE_TICKETS, MODULE_FAQ, MODULE_CHAT, MODULE_GALLERY,
        MODULE_MARKETPLACE,
    },
    'signature': {
        MODULE_HOMEPAGE, MODULE_PORTFOLIO, MODULE_INSIGHTS, MODULE_SERVICES,
        MODULE_CONTACT, MODULE_TICKETS, MODULE_FAQ, MODULE_CHAT, MODULE_GALLERY,
        MODULE_EVENTS, MODULE_MEDIA, MODULE_ANALYTICS, MODULE_BRANDS,
        MODULE_MARKETPLACE,
    },
    # CRM is a sales pipeline. A signature site is a brochure with a contact
    # form, so the pipeline had nothing to hold and the menu entry only invited
    # questions. It starts at store, where there are orders to track.
    'store': {
        MODULE_HOMEPAGE, MODULE_PORTFOLIO, MODULE_INSIGHTS, MODULE_SERVICES,
        MODULE_CONTACT, MODULE_TICKETS, MODULE_FAQ, MODULE_CHAT, MODULE_GALLERY,
        MODULE_EVENTS, MODULE_MEDIA, MODULE_ANALYTICS, MODULE_CRM,
        MODULE_ECOMMERCE, MODULE_AUCTION, MODULE_MARKETPLACE, MODULE_BRANDS,
    },
    'source': {
        k for k, _ in [
            (MODULE_HOMEPAGE, ''), (MODULE_PORTFOLIO, ''), (MODULE_INSIGHTS, ''),
            (MODULE_SERVICES, ''), (MODULE_FAQ, ''), (MODULE_CONTACT, ''),
            (MODULE_TICKETS, ''), (MODULE_CHAT, ''), (MODULE_MENU, ''),
            (MODULE_EVENTS, ''), (MODULE_GALLERY, ''), (MODULE_ECOMMERCE, ''),
            (MODULE_AUCTION, ''), (MODULE_MARKETPLACE, ''), (MODULE_MEDIA, ''),
            (MODULE_ANALYTICS, ''), (MODULE_CRM, ''), (MODULE_STARTUP, ''),
            (MODULE_BRANDS, ''),
            (MODULE_VEVENTA, ''),
        ]
    },
}


class Tenant(models.Model):
    """One row per client website."""

    PLAN_STARTER    = 'starter'
    PLAN_STANDARD   = 'standard'
    PLAN_SIGNATURE  = 'signature'
    PLAN_STORE      = 'store'
    PLAN_SOURCE     = 'source'      # YOUR COMPANY only

    PLAN_CHOICES = [
        (PLAN_STARTER,   'Starter'),
        (PLAN_STANDARD,  'Standard'),
        (PLAN_SIGNATURE, 'Signature'),
        (PLAN_STORE,     'Store'),
        (PLAN_SOURCE,    'Source'),
    ]

    # Identity
    name            = models.CharField(max_length=200, help_text='Client / site name')
    slug            = models.SlugField(unique=True, blank=True)
    # The letters in front of every reference this install issues: tickets,
    # orders, chats, project briefs. It was hardcoded in five places, so a
    # client site would have handed its customers another company's reference
    # numbers. Blank falls back to the slug, so an install that never sets it
    # still gets something of its own rather than someone else's.
    ref_prefix      = models.CharField(
        max_length=4, blank=True,
        help_text='Letters in front of ticket and order numbers. Blank uses the slug.',
    )
    plan            = models.CharField(max_length=20, choices=PLAN_CHOICES, default=PLAN_STARTER)

    # Domains
    primary_domain  = models.CharField(max_length=200, unique=True, help_text='e.g. example.co.uk')
    cms_subdomain   = models.CharField(max_length=200, unique=True, help_text='e.g. pnl.yourdomain.com')
    extra_domains   = models.TextField(blank=True, help_text='Additional domains, one per line')

    # Branding
    logo            = WebPImageField(upload_to='tenants/logos/', blank=True, null=True)
    favicon         = WebPImageField(upload_to='tenants/favicons/', blank=True, null=True)
    accent_colour    = models.CharField(max_length=7, default='#e8b400', help_text='Hex colour for CMS accent')
    cms_dark_colour  = models.CharField(max_length=7, default='#0d0d0d', help_text='CMS sidebar background colour')
    cms_text_colour  = models.CharField(max_length=7, default='#b8d4ea', help_text='CMS sidebar text colour')

    # Ownership
    owner           = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name='owned_tenants',
        help_text='Primary client account'
    )
    account_manager = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name='managed_tenants',
        limit_choices_to={'role__in': ['super_admin', 'admin', 'manager']},
        help_text='YOUR COMPANY staff member responsible for this account'
    )
    created_by      = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name='created_tenants',
        help_text='YOUR COMPANY user who added this tenant'
    )

    # Status
    is_active       = models.BooleanField(default=True)
    monitor_enabled = models.BooleanField(
        default=True,
        help_text='Probe this site for uptime. Turn off for domains that are not live yet, '
                  'so they stop raising incidents nobody needs to act on.'
    )
    is_archived     = models.BooleanField(
        default=False,
        help_text=('Client has left. The site stops serving and check-ins stop, '
                   'but every record is kept for accounting and history.')
    )
    archived_at     = models.DateTimeField(null=True, blank=True)
    archived_reason = models.CharField(max_length=200, blank=True)
    is_master       = models.BooleanField(default=False, help_text='True for the tenant this install serves. Exactly one row per install.')

    # AI Chat settings (per tenant)
    chat_enabled            = models.BooleanField(default=False)
    whatsapp_enabled        = models.BooleanField(default=False, help_text='Show WhatsApp chat widget on the site')
    chat_greeting       = models.TextField(
        blank=True,
        default='Hi! How can I help you today?'
    )
    chat_system_prompt  = models.TextField(
        blank=True,
        help_text='OpenAI system prompt describing this site and its services'
    )
    chat_whatsapp       = models.CharField(
        max_length=30, blank=True,
        help_text='WhatsApp number with country code e.g. +447700000000'
    )
    chat_whatsapp_trigger = models.TextField(
        blank=True,
        default='sales,quote,pricing,buy,purchase,order,booking,consult',
        help_text='Comma-separated keywords that trigger the WhatsApp handoff'
    )
    chat_inactivity_minutes = models.PositiveIntegerField(
        default=30,
        help_text='Minutes of inactivity before the session is automatically closed'
    )
    chat_farewell_message = models.TextField(
        blank=True,
        default="Thank you for contacting us, I'll be happy to help you at your return, have a good one!",
        help_text='Message sent by the bot before closing an inactive session'
    )

    chat_daily_token_budget = models.PositiveIntegerField(
        default=150000,
        help_text=('Maximum OpenAI tokens the chat may spend per day. '
                   'When reached, visitors are pointed at the contact form '
                   'until midnight. Set to 0 for no limit.')
    )

    # AI Chat widget colours
    chat_header_bg = models.CharField(
        max_length=7, blank=True, default='#033152',
        help_text='Chat header background colour'
    )
    chat_accent_color = models.CharField(
        max_length=7, blank=True, default='',
        help_text='Button / user bubble colour. Leave blank to use site accent colour.'
    )
    chat_bot_bubble_bg = models.CharField(
        max_length=7, blank=True, default='#f0f4ff',
        help_text='Bot message bubble background colour'
    )

    # Version tracking
    cms_version     = models.CharField(
        max_length=20, blank=True, default='',
        help_text='CMS version running on this tenant e.g. 26.7.1'
    )

    # Billing
    monthly_fee          = models.DecimalField(max_digits=8, decimal_places=2, default=0, help_text='Recurring monthly fee in GBP')
    billing_day          = models.PositiveSmallIntegerField(default=1, help_text='Day of month invoice is generated (1–28)')
    next_renewal_date    = models.DateField(null=True, blank=True, help_text='Date the next invoice is due')
    billing_email        = models.EmailField(blank=True, help_text='Where invoices are sent (defaults to owner email)')

    # Fleet control (reported by the tenant install via ft_checkin)
    api_key          = models.CharField(
        max_length=64, blank=True, default='',
        help_text='Shared secret this tenant uses to sign check-in requests'
    )
    reported_version = models.CharField(
        max_length=20, blank=True, default='',
        help_text='FT OS version the tenant last reported running'
    )
    target_version   = models.CharField(
        max_length=20, blank=True, default='',
        help_text='FT OS version this tenant should be upgraded to'
    )
    os_name          = models.CharField(max_length=60, blank=True, default='', help_text='e.g. AlmaLinux')
    os_version       = models.CharField(max_length=40, blank=True, default='', help_text='e.g. 8.9')
    python_version   = models.CharField(max_length=20, blank=True, default='')
    django_version   = models.CharField(max_length=20, blank=True, default='')
    db_engine        = models.CharField(max_length=30, blank=True, default='', help_text='e.g. postgresql, sqlite3')
    last_checkin_at  = models.DateTimeField(null=True, blank=True)

    # Notes
    notes           = models.TextField(blank=True)
    created_at      = models.DateTimeField(auto_now_add=True)
    updated_at      = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['name']
        verbose_name = 'Tenant'
        verbose_name_plural = 'Tenants'

    def __str__(self):
        return f'{self.name} ({self.primary_domain})'

    @property
    def prefix(self):
        """
        The reference prefix, always something, never blank.

        Order of preference: the field, then the slug, then the name. Upper
        case, letters only, four characters at most. Two tenants can end up
        with the same letters, which is why every lookup that uses a prefix
        also filters by tenant.
        """
        import re as _re
        for candidate in (self.ref_prefix, self.slug, self.name):
            letters = _re.sub(r'[^A-Za-z]', '', candidate or '')
            if letters:
                return letters[:4].upper()
        return 'REF'


    def save(self, *args, **kwargs):
        from django.conf import settings as _s
        current = getattr(_s, 'CMS_VERSION', '')
        if not self.cms_version:
            self.cms_version = current
        # The master install is where releases come from, so it cannot be
        # entitled to an older version than the code it is running. Without
        # this it keeps whatever it was stamped with on the day it was
        # created and drifts behind every release.
        if self.is_master and current and self.cms_version != current:
            self.cms_version = current
            fields = kwargs.get('update_fields')
            if fields is not None and 'cms_version' not in fields:
                kwargs['update_fields'] = list(fields) + ['cms_version']
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    @property
    def is_online(self):
        """True if the tenant checked in within the last 3 hours."""
        if not self.last_checkin_at:
            return False
        from django.utils import timezone
        from datetime import timedelta
        return timezone.now() - self.last_checkin_at < timedelta(hours=3)

    @property
    def needs_upgrade(self):
        """True if a target version is set and the tenant is not on it yet."""
        return bool(self.target_version) and self.reported_version != self.target_version

    @property
    def os_label(self):
        parts = [p for p in (self.os_name, self.os_version) if p]
        return ' '.join(parts) or 'Unknown'

    def ensure_api_key(self):
        """Generate the shared secret if this tenant does not have one yet."""
        if not self.api_key:
            import secrets
            self.api_key = secrets.token_hex(32)
            self.save(update_fields=['api_key'])
        return self.api_key

    @property
    def enabled_module_keys(self):
        return set(self.modules.filter(is_enabled=True).values_list('module', flat=True))

    def has_module(self, module_key):
        if module_key in ALWAYS_ON_MODULES:
            return True
        return module_key in self.enabled_module_keys

    def archive(self, reason=''):
        """Mark a departed client as archived. Nothing is deleted."""
        from django.utils import timezone
        self.is_archived = True
        self.is_active = False
        self.archived_at = timezone.now()
        self.archived_reason = (reason or '')[:200]
        self.save(update_fields=[
            'is_archived', 'is_active', 'archived_at', 'archived_reason',
        ])

    def restore(self):
        """Bring an archived client back."""
        self.is_archived = False
        self.is_active = True
        self.archived_at = None
        self.archived_reason = ''
        self.save(update_fields=[
            'is_archived', 'is_active', 'archived_at', 'archived_reason',
        ])

    @property
    def delete_blockers(self):
        """
        Reasons this tenant must not be deleted.

        Deleting cascades to invoices, orders and every piece of content, so
        it is only ever allowed for a row created by mistake, meaning one
        that never traded. Anything with financial history is archived
        instead, since UK business records must be kept for six years.
        """
        blockers = []
        if self.is_master:
            blockers.append('this is the master tenant')

        invoices = self.invoices.count()
        if invoices:
            blockers.append(f'{invoices} invoice(s) on record')

        try:
            orders = self.orders.count()
        except Exception:
            orders = 0
        if orders:
            blockers.append(f'{orders} order(s) on record')

        return blockers

    @property
    def can_delete(self):
        return not self.delete_blockers

    def apply_plan_modules(self):
        """
        Sync TenantModule rows to match the current plan's allowed set.
        Called after plan changes. Always-on modules are never disabled.
        """
        allowed = PLAN_MODULES.get(self.plan, set()) | ALWAYS_ON_MODULES
        all_keys = {k for k, _ in MODULE_CHOICES}
        for key in all_keys:
            TenantModule.objects.update_or_create(
                tenant=self, module=key,
                defaults={'is_enabled': key in allowed},
            )

    @property
    def all_domains(self):
        domains = [self.primary_domain, self.cms_subdomain]
        for d in self.extra_domains.splitlines():
            d = d.strip()
            if d:
                domains.append(d)
        return domains

    @property
    def whatsapp_link(self):
        if self.chat_whatsapp:
            number = self.chat_whatsapp.replace('+', '').replace(' ', '')
            return f'https://wa.me/{number}'
        return ''


class TenantUpgradeCharge(models.Model):
    """Records a paid CMS version upgrade for a tenant."""

    STATUS_PENDING = 'pending'
    STATUS_PAID    = 'paid'
    STATUS_WAIVED  = 'waived'
    STATUS_CHOICES = [
        (STATUS_PENDING, 'Pending'),
        (STATUS_PAID,    'Paid'),
        (STATUS_WAIVED,  'Waived'),
    ]

    tenant     = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='upgrade_charges')
    version    = models.CharField(max_length=20, help_text='Version upgraded to e.g. 26.8.1')
    fee        = models.DecimalField(max_digits=8, decimal_places=2)
    status     = models.CharField(max_length=10, choices=STATUS_CHOICES, default=STATUS_PENDING)
    notes      = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Tenant Upgrade Charge'
        verbose_name_plural = 'Tenant Upgrade Charges'

    def __str__(self):
        return f'{self.tenant.name} — v{self.version} — £{self.fee} ({self.status})'


# Invoice references run YYMM plus a four digit counter that never resets.
# The counter starts here rather than at 1, matching the numbering already in use.
INVOICE_SEQUENCE_START = 110


class TenantInvoice(models.Model):
    """Monthly (or one-off) invoice raised against a tenant by YOUR COMPANY."""

    TYPE_MONTHLY  = 'monthly'
    TYPE_SETUP    = 'setup'
    TYPE_ADDON    = 'addon'
    TYPE_CHOICES  = [
        (TYPE_MONTHLY, 'Monthly Subscription'),
        (TYPE_SETUP,   'Setup / Onboarding Fee'),
        (TYPE_ADDON,   'Add-on / Extra'),
    ]

    STATUS_DRAFT    = 'draft'
    STATUS_PENDING  = 'pending'
    STATUS_PAID     = 'paid'
    STATUS_OVERDUE  = 'overdue'
    STATUS_CANCELLED= 'cancelled'
    STATUS_CHOICES  = [
        (STATUS_DRAFT,     'Draft'),
        (STATUS_PENDING,   'Pending'),
        (STATUS_PAID,      'Paid'),
        (STATUS_OVERDUE,   'Overdue'),
        (STATUS_CANCELLED, 'Cancelled'),
    ]

    tenant          = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='invoices')
    invoice_number  = models.CharField(max_length=30, unique=True, blank=True)
    invoice_type    = models.CharField(max_length=20, choices=TYPE_CHOICES, default=TYPE_MONTHLY)
    description     = models.CharField(max_length=300, blank=True)
    amount          = models.DecimalField(max_digits=8, decimal_places=2)
    currency        = models.CharField(max_length=3, default='GBP')
    status          = models.CharField(max_length=15, choices=STATUS_CHOICES, default=STATUS_PENDING)
    due_date        = models.DateField()
    paid_at         = models.DateTimeField(null=True, blank=True)
    period_start    = models.DateField(null=True, blank=True)
    period_end      = models.DateField(null=True, blank=True)
    notes           = models.TextField(blank=True)
    created_at      = models.DateTimeField(auto_now_add=True)

    # Paying it. The token is what turns an emailed invoice into something a
    # client can settle without an account, and reminders_sent is what stops
    # the same notice going out twice.
    pay_token         = models.CharField(
        max_length=64, blank=True, db_index=True,
        help_text='Random token for the public payment link. Blank means the invoice has no link yet.',
    )
    stripe_session_id = models.CharField(
        max_length=120, blank=True,
        help_text='Checkout session, so a return from Stripe can be verified rather than believed',
    )
    reminders_sent    = models.CharField(
        max_length=40, blank=True,
        help_text='Comma separated day counts already emailed, e.g. "14,7,3". Stops a notice going twice.',
    )

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Tenant Invoice'
        verbose_name_plural = 'Tenant Invoices'

    def __str__(self):
        return f'{self.invoice_number}, {self.tenant.name}, £{self.amount} ({self.status})'

    def ensure_pay_token(self):
        """
        Give this invoice a payment link, once.

        Called before the first reminder goes out. The token is what the
        client's email links to, so it must not change between the fourteen
        day notice and the one sent on the day, or the earlier emails stop
        working.
        """
        if not self.pay_token:
            import secrets
            self.pay_token = secrets.token_urlsafe(32)
            self.save(update_fields=['pay_token'])
        return self.pay_token

    @property
    def reminder_days(self):
        """Which notices have already been sent, as a set of integers."""
        out = set()
        for part in (self.reminders_sent or '').split(','):
            part = part.strip()
            if part.lstrip('-').isdigit():
                out.add(int(part))
        return out

    def record_reminder(self, day):
        """Note that the notice for this threshold has gone out."""
        days = self.reminder_days | {int(day)}
        self.reminders_sent = ','.join(str(d) for d in sorted(days, reverse=True))[:40]
        self.save(update_fields=['reminders_sent'])

    @staticmethod
    def _digits(number):
        """
        The eight digit body of a reference, whatever is wrapped around it.

        Three shapes exist in the table and all three have to read:

            FT-26080123   current, prefix and hyphen
            26080123      before 22 August 2026, bare
            INV-2025-0043 the original form, which this ignores

        Returns an empty string for anything it does not recognise, so a
        hand-typed reference cannot break a page that only wanted the month.
        """
        body = (number or '').rsplit('-', 1)[-1]
        return body if len(body) == 8 and body.isdigit() else ''

    @property
    def issued_period(self):
        """The YYMM part, i.e. when the invoice was raised."""
        return self._digits(self.invoice_number)[:4]

    @property
    def sequence(self):
        """The running number, which is what actually identifies the invoice."""
        return self._digits(self.invoice_number)[4:] or self.invoice_number

    @classmethod
    def next_number(cls, when=None):
        """
        The next invoice reference: PREFIX-YYMM then a four digit counter that
        runs across every invoice and never resets.

        The hyphen is deliberate and invoices are the only reference that keeps
        one. Tickets, orders, chats and briefs run the letters together, so a
        hyphen on its own tells you the document you are holding is an invoice.

        The prefix is this install's, never the client's. These are invoices
        this business issues to its clients, so using the client's letters
        would claim they raised an invoice that was raised against them. The
        letters name whoever is asking to be paid.

        The counter is continuous rather than per month, because an invoice
        number is an accounting record and gaps or restarts in one invite
        questions. The YYMM in front says when it was raised, the four digits
        say which invoice it is.
        """
        from django.utils import timezone
        from apps.tenants.refs import prefix_for
        when = when or timezone.now()
        head = f'{prefix_for()}-{when:%y%m}'

        highest = INVOICE_SEQUENCE_START - 1
        for number in cls.objects.values_list('invoice_number', flat=True):
            body = cls._digits(number)
            if not body:
                continue
            try:
                highest = max(highest, int(body[4:]))
            except ValueError:
                continue

        return f'{head}{highest + 1:04d}'

    def save(self, *args, **kwargs):
        if not self.invoice_number:
            self.invoice_number = self.next_number()
        super().save(*args, **kwargs)


class TenantModuleLog(models.Model):
    """
    One row per module switched on or off, so there is a record of who changed
    a client's access and when. Written by the CMS, by Apply Plan, and by the
    fleet check-in, which is why the source is stored alongside the user.
    """
    SOURCE_MANUAL   = 'manual'
    SOURCE_FREE     = 'free'
    SOURCE_PURCHASE = 'purchase'
    SOURCE_PLAN     = 'plan'
    SOURCE_CHECKIN  = 'checkin'
    SOURCE_CHOICES  = [
        (SOURCE_MANUAL,   'Added manually'),
        (SOURCE_FREE,     'Granted free'),
        (SOURCE_PURCHASE, 'Purchased by client'),
        (SOURCE_PLAN,     'Applied from plan'),
        (SOURCE_CHECKIN,  'Applied from your provider'),
    ]

    # Sources that represent something the client acquired from us, as opposed
    # to routine housekeeping. These are what the Marketplace box lists.
    ACQUISITION_SOURCES = (SOURCE_FREE, SOURCE_PURCHASE)

    tenant      = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='module_logs')
    module      = models.CharField(max_length=30, choices=MODULE_CHOICES)
    is_enabled  = models.BooleanField()
    source      = models.CharField(max_length=20, choices=SOURCE_CHOICES, default=SOURCE_MANUAL)
    changed_by  = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True, blank=True,
        related_name='module_changes',
    )
    amount      = models.DecimalField(
        max_digits=8, decimal_places=2, null=True, blank=True,
        help_text='What the client paid, when this was a purchase'
    )
    note        = models.CharField(max_length=200, blank=True)
    created_at  = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Tenant Module Change'
        verbose_name_plural = 'Tenant Module Changes'

    def __str__(self):
        state = 'enabled' if self.is_enabled else 'disabled'
        return f'{self.tenant.name}: {self.module} {state}'

    @property
    def module_label(self):
        return dict(MODULE_CHOICES).get(self.module, self.module)

    @property
    def is_acquisition(self):
        """True when this entry represents something the client received."""
        return self.is_enabled and self.source in self.ACQUISITION_SOURCES


class TenantModule(models.Model):
    """Junction table — which modules are enabled per tenant."""
    tenant      = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='modules')
    module      = models.CharField(max_length=30, choices=MODULE_CHOICES)
    is_enabled  = models.BooleanField(default=True)
    enabled_at  = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['tenant', 'module']
        ordering = ['module']
        verbose_name = 'Tenant Module'
        verbose_name_plural = 'Tenant Modules'

    def __str__(self):
        status = 'ON' if self.is_enabled else 'OFF'
        return f'{self.tenant.name} — {self.get_module_display()} [{status}]'


class TenantUser(models.Model):
    """
    Links a User to a Tenant with a role.
    A Super Admin can see all tenants.
    A client user can only see their own tenant's CMS.
    """
    ROLE_OWNER   = 'owner'
    ROLE_ADMIN   = 'admin'
    ROLE_EDITOR  = 'editor'
    ROLE_VIEWER  = 'viewer'

    ROLE_CHOICES = [
        (ROLE_OWNER,  'Owner'),
        (ROLE_ADMIN,  'Admin'),
        (ROLE_EDITOR, 'Editor'),
        (ROLE_VIEWER, 'Viewer (read-only)'),
    ]

    tenant      = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='tenant_users')
    user        = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tenant_memberships')
    role        = models.CharField(max_length=20, choices=ROLE_CHOICES, default=ROLE_ADMIN)
    is_active   = models.BooleanField(default=True)
    invited_at  = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ['tenant', 'user']
        verbose_name = 'Tenant User'
        verbose_name_plural = 'Tenant Users'

    def __str__(self):
        return f'{self.user.username} @ {self.tenant.name} ({self.get_role_display()})'

    @property
    def can_edit(self):
        return self.role in [self.ROLE_OWNER, self.ROLE_ADMIN, self.ROLE_EDITOR]
