"""
Ecommerce models — products, orders, Stripe payments, domain registrations.
"""
import uuid
from decimal import Decimal

from django.db import models
from django.utils.text import slugify
from django.utils import timezone
from apps.accounts.fields import WebPImageField


class ProductCategory(models.Model):
    """Grouping for products (e.g. Web Design, Hosting, Domains)."""
    TYPE_PRODUCT      = 'product'
    TYPE_SUBSCRIPTION = 'subscription'
    CATEGORY_TYPE_CHOICES = [
        (TYPE_PRODUCT,      'Product'),
        (TYPE_SUBSCRIPTION, 'Subscription'),
    ]

    name          = models.CharField(max_length=100)
    slug          = models.SlugField(max_length=120, blank=True)
    description   = models.CharField(max_length=200, blank=True, default='')
    category_type = models.CharField(max_length=20, choices=CATEGORY_TYPE_CHOICES, default=TYPE_PRODUCT)
    icon_class    = models.CharField(max_length=100, blank=True, default='', help_text='FontAwesome class, e.g. fa-solid fa-globe')
    sort_order    = models.PositiveIntegerField(default=0)

    class Meta:
        ordering = ['sort_order', 'name']
        verbose_name = 'Product Category'
        verbose_name_plural = 'Product Categories'

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)


class Product(models.Model):
    """A sellable item: service package, domain TLD, physical product, hosting plan, or subscription."""

    TYPE_WEB          = 'web'
    TYPE_SERVICE      = 'service'
    TYPE_DOMAIN       = 'domain'
    TYPE_PRODUCT      = 'product'
    TYPE_HOSTING      = 'hosting'
    TYPE_SUBSCRIPTION = 'subscription'

    TYPE_CHOICES = [
        (TYPE_WEB,          'Web Design / Development'),
        (TYPE_SERVICE,      'Service Package'),
        (TYPE_DOMAIN,       'Domain Name'),
        (TYPE_PRODUCT,      'Physical / Digital Product'),
        (TYPE_HOSTING,      'Application / Hosting Plan'),
        (TYPE_SUBSCRIPTION, 'Subscription / Retainer'),
    ]

    BILLING_ONE_TIME = 'one_time'
    BILLING_MONTHLY  = 'monthly'
    BILLING_ANNUAL   = 'annual'

    BILLING_CHOICES = [
        (BILLING_ONE_TIME, 'One-time payment'),
        (BILLING_MONTHLY,  'Monthly subscription'),
        (BILLING_ANNUAL,   'Annual subscription'),
    ]

    tenant            = models.ForeignKey(
        'tenants.Tenant', on_delete=models.CASCADE,
        null=True, blank=True, related_name='products'
    )
    category          = models.ForeignKey(
        ProductCategory, on_delete=models.SET_NULL,
        null=True, blank=True, related_name='products'
    )
    name              = models.CharField(max_length=200)
    slug              = models.SlugField(max_length=220, blank=True)
    product_type      = models.CharField(max_length=20, choices=TYPE_CHOICES, default=TYPE_SERVICE)
    billing_period    = models.CharField(max_length=20, choices=BILLING_CHOICES, default=BILLING_ONE_TIME)

    short_description = models.CharField(max_length=300, blank=True)
    description       = models.TextField(blank=True, help_text='Full description shown on product page')
    features          = models.TextField(
        blank=True,
        help_text='One feature per line — displayed as bullet list'
    )

    # Pricing
    price             = models.DecimalField(max_digits=10, decimal_places=2, help_text='Full price / monthly price (GBP)')
    annual_price      = models.DecimalField(
        max_digits=10, decimal_places=2, null=True, blank=True,
        help_text='Annual price (GBP) — if set, customers can choose monthly or annual billing'
    )
    deposit_amount    = models.DecimalField(
        max_digits=10, decimal_places=2, null=True, blank=True,
        help_text='Deposit amount — if set, customers can pay this now and the balance later'
    )
    compare_at_price  = models.DecimalField(
        max_digits=10, decimal_places=2, null=True, blank=True,
        help_text='Original / crossed-out price for showing a discount'
    )
    tax_included      = models.BooleanField(default=True, help_text='Is VAT included in the price?')

    # Stripe
    stripe_product_id = models.CharField(max_length=100, blank=True)
    stripe_price_id   = models.CharField(max_length=100, blank=True)

    # Display
    image             = WebPImageField(upload_to='products/', blank=True, null=True)
    icon_class        = models.CharField(max_length=100, blank=True, help_text='FontAwesome class e.g. fa-solid fa-globe')
    badge             = models.CharField(max_length=50, blank=True, help_text='Badge text e.g. "Most Popular"')
    sort_order        = models.PositiveIntegerField(default=0)
    is_active         = models.BooleanField(default=True)
    is_featured       = models.BooleanField(default=False)
    show_in_shop      = models.BooleanField(default=True, help_text='Show this product on the public shop page')

    # SEO
    meta_title       = models.CharField(max_length=70, blank=True, help_text='SEO title (30-60 chars)')
    meta_description = models.TextField(blank=True, help_text='Meta description (50-160 chars)')
    focus_keyword    = models.CharField(max_length=100, blank=True)
    # Some subscriptions and apps have a site of their own. When this is set
    # the hero shows a Visit button instead of the promo badge, because a
    # working product does not need a Top Pick sticker next to its name.
    website_url      = models.URLField(blank=True, help_text='Its own website, if it has one. Shows a Visit button on the page.')
    # Shown, but not sold. The page keeps its description, features and Visit
    # button, and every route to payment disappears: no price, no basket, no
    # Buy or Subscribe. Work that is quoted rather than bought needs this,
    # and setting the price to zero does not do it, it just sells for nothing.
    enquiry_only     = models.BooleanField(default=False, help_text='Show it, do not sell it. Visitors enquire instead of paying.')
    canonical_url    = models.URLField(blank=True, help_text='Leave empty to auto-generate from page URL')
    og_image_url     = models.URLField(blank=True, help_text='Social share image (WhatsApp, LinkedIn, X). Leave empty to use product image.')
    target_region    = models.CharField(max_length=10, blank=True, default='global',
                           choices=[
                               ('global', 'Global'), ('europe', 'Europe'),
                               ('uk', 'United Kingdom'), ('ir', 'Iran'), ('gcc', 'GCC'),
                           ])
    seo_score        = models.PositiveSmallIntegerField(default=0, editable=False,
                           help_text='Auto-computed 0-100 SEO score.')


    def get_absolute_url(self):
        """
        Public address for this product.

        Domains live under /domains/<slug>/ so they read as their own
        catalogue rather than as shop items. Everything else stays at
        /products/<slug>/.

        Product type decides this as well as the auction flag. A domain sold
        at a fixed price has is_auction unticked, and routing on the flag
        alone left those on the shop address while their neighbours moved.
        """
        from django.urls import reverse
        if self.is_auction or self.product_type == self.TYPE_DOMAIN:
            return reverse('auction_detail', args=[self.slug])
        return reverse('shop_product_detail', args=[self.slug])

    # Domain-specific (when product_type = 'domain')
    domain_tld        = models.CharField(max_length=20, blank=True, help_text='e.g. .co.uk — one TLD per product')
    reg_years_default = models.PositiveSmallIntegerField(default=1, help_text='Default registration years')

    # Hosting-specific
    disk_space        = models.CharField(max_length=30, blank=True, help_text='e.g. 10 GB SSD')
    bandwidth         = models.CharField(max_length=30, blank=True, help_text='e.g. Unlimited')
    domains_included  = models.PositiveSmallIntegerField(null=True, blank=True, help_text='Number of domains included')

    # Auction
    AUCTION_AVAILABLE = 'available'
    AUCTION_RESERVED  = 'reserved'
    AUCTION_SOLD      = 'sold'
    AUCTION_STATUS_CHOICES = [
        (AUCTION_AVAILABLE, 'Available'),
        (AUCTION_RESERVED,  'Reserved'),
        (AUCTION_SOLD,      'Sold'),
    ]

    is_auction        = models.BooleanField(default=False, help_text='List this product as an auction item')
    auction_reserve   = models.DecimalField(
        max_digits=10, decimal_places=2, null=True, blank=True,
        help_text='Reserve price — auction will not sell below this (internal)'
    )
    auction_min_offer = models.DecimalField(
        max_digits=10, decimal_places=2, null=True, blank=True,
        help_text='Minimum offer shown publicly on the product page'
    )
    auction_ends_at   = models.DateTimeField(null=True, blank=True, help_text='Auction end date/time')
    auction_status    = models.CharField(
        max_length=10, choices=AUCTION_STATUS_CHOICES, default=AUCTION_AVAILABLE,
        help_text='Current marketplace status for auction items'
    )

    # How this item is sold. Fixed takes the asking price through checkout.
    # Offers invites a bid at or above auction_min_offer, which arrives as a
    # ticket for you to accept, counter or decline.
    SALE_FIXED = 'fixed'
    SALE_OFFER = 'offer'
    SALE_METHOD_CHOICES = [
        (SALE_FIXED, 'Fixed price'),
        (SALE_OFFER, 'Offers over minimum'),
    ]
    sale_method       = models.CharField(
        max_length=10, choices=SALE_METHOD_CHOICES, default=SALE_FIXED,
        help_text='Fixed price, or open to offers above a minimum'
    )
    reservation_fee   = models.DecimalField(
        max_digits=10, decimal_places=2, null=True, blank=True,
        help_text='Paid to hold this item. Comes off the final price.'
    )
    reserved_until    = models.DateTimeField(
        null=True, blank=True,
        help_text='Reservation lapses after this, five working days from payment'
    )
    reserved_order    = models.ForeignKey(
        'Order', on_delete=models.SET_NULL, null=True, blank=True,
        related_name='reserved_products',
        help_text='The order that paid the reservation fee'
    )

    @property
    def is_reserved(self):
        from django.utils import timezone as _tz
        return (
            self.auction_status == self.AUCTION_RESERVED
            and (self.reserved_until is None or _tz.now() <= self.reserved_until)
        )

    @property
    def balance_after_reservation(self):
        """What is still owed once the reservation fee is deducted."""
        if not self.price:
            return None
        fee = self.reservation_fee or 0
        return max(self.price - fee, 0)

    created_at        = models.DateTimeField(auto_now_add=True)
    updated_at        = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['sort_order', 'name']
        verbose_name = 'Product'
        verbose_name_plural = 'Products'

    def __str__(self):
        return self.name

    def compute_seo_score(self):
        checks = [
            bool(self.name),
            30 <= len(self.meta_title) <= 60,
            50 <= len(self.meta_description) <= 160,
            bool(self.focus_keyword),
            bool(self.image),
            True,   # TL;DR — auto-passes
            True,   # Tags  — auto-passes
            bool(self.short_description),
            bool(self.og_image_url) or bool(self.image),
            bool(self.category_id),
        ]
        passed = sum(1 for c in checks if c)
        return round(passed / len(checks) * 100)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        update_fields = kwargs.get('update_fields')
        if update_fields is None or 'seo_score' in update_fields:
            self.seo_score = self.compute_seo_score()
        super().save(*args, **kwargs)

    @property
    def features_list(self):
        return [f.strip() for f in self.features.splitlines() if f.strip()]

    @property
    def has_deposit(self):
        return self.deposit_amount is not None and self.deposit_amount > 0

    @property
    def balance_due(self):
        if self.has_deposit:
            return self.price - self.deposit_amount
        return None

    @property
    def discount_percent(self):
        if self.compare_at_price and self.compare_at_price > self.price:
            return int((1 - self.price / self.compare_at_price) * 100)
        return None

    BADGE_STYLES = {
        'Most Popular': ('#0891b2', '#ecfeff'),
        'Best Value':   ('#15803d', '#f0fdf4'),
        'New':          ('#7c3aed', '#faf5ff'),
        'Limited':      ('#dc2626', '#fef2f2'),
        'Featured':     ('#b45309', '#fffbeb'),
        'Free Trial':   ('#0d9488', '#f0fdfa'),
        'Top Pick':     ('#033152', '#eff6ff'),
    }

    @property
    def badge_color(self):
        return self.BADGE_STYLES.get(self.badge, ('#ff404b', '#fff0f0'))[0]

    @property
    def badge_bg(self):
        return self.BADGE_STYLES.get(self.badge, ('#ff404b', '#fff0f0'))[1]


class ProductInterest(models.Model):
    """Lead capture for auction / enquiry products."""
    product    = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='interests')
    name       = models.CharField(max_length=150)
    email      = models.EmailField()
    phone      = models.CharField(max_length=30, blank=True)
    offer      = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text='Indicative offer amount')
    message    = models.TextField(blank=True)
    is_read    = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']

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


class Order(models.Model):
    """A completed or in-progress customer order."""

    STATUS_PENDING      = 'pending'
    STATUS_DEPOSIT_PAID = 'deposit_paid'
    STATUS_PAID         = 'paid'
    STATUS_PROCESSING   = 'processing'
    STATUS_COMPLETED    = 'completed'
    STATUS_CANCELLED    = 'cancelled'
    STATUS_REFUNDED     = 'refunded'

    STATUS_CHOICES = [
        (STATUS_PENDING,      'Pending'),
        (STATUS_DEPOSIT_PAID, 'Deposit Paid'),
        (STATUS_PAID,         'Paid'),
        (STATUS_PROCESSING,   'Processing'),
        (STATUS_COMPLETED,    'Completed'),
        (STATUS_CANCELLED,    'Cancelled'),
        (STATUS_REFUNDED,     'Refunded'),
    ]

    tenant              = models.ForeignKey(
        'tenants.Tenant', on_delete=models.CASCADE,
        null=True, blank=True, related_name='orders'
    )
    order_number        = models.CharField(max_length=20, unique=True, editable=False)
    status              = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)

    # Customer
    customer_name       = models.CharField(max_length=150)
    customer_email      = models.EmailField()
    customer_phone      = models.CharField(max_length=30, blank=True)
    customer_company    = models.CharField(max_length=150, blank=True)
    customer_address    = models.CharField(max_length=255, blank=True)
    customer_postcode   = models.CharField(max_length=20, blank=True)
    customer_country    = models.CharField(max_length=100, blank=True)

    # Financials
    subtotal            = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    tax_amount          = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    total               = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    amount_paid         = models.DecimalField(max_digits=10, decimal_places=2, default=0)
    stripe_fee          = models.DecimalField(max_digits=10, decimal_places=2, default=0, help_text='Actual Stripe fee charged on this order')

    # Stripe
    card_brand          = models.CharField(max_length=30, blank=True, help_text='Card brand reported by Stripe, e.g. visa')
    card_last4          = models.CharField(max_length=4, blank=True, help_text='Last four digits only, never the full number')
    cardholder_name     = models.CharField(max_length=150, blank=True, help_text='Name on the card as entered at checkout')
    card_country        = models.CharField(max_length=2, blank=True, help_text='Billing country code from Stripe')
    card_postcode       = models.CharField(max_length=20, blank=True, help_text='Billing postcode from Stripe')

    stripe_session_id        = models.CharField(max_length=200, blank=True)
    stripe_payment_intent_id = models.CharField(max_length=200, blank=True)
    is_deposit_order         = models.BooleanField(default=False, help_text='True if this payment was a deposit')
    parent_order             = models.ForeignKey(
        'self', on_delete=models.SET_NULL,
        null=True, blank=True, related_name='balance_payments',
        help_text='For balance payments, points to the original deposit order'
    )

    # Notes
    customer_notes      = models.TextField(blank=True)
    internal_notes      = models.TextField(blank=True)

    ip_address          = models.GenericIPAddressField(null=True, blank=True)
    created_at          = models.DateTimeField(auto_now_add=True)
    updated_at          = models.DateTimeField(auto_now=True)
    paid_at             = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Order'
        verbose_name_plural = 'Orders'

    def __str__(self):
        return f'#{self.order_number} — {self.customer_name}'

    def save(self, *args, **kwargs):
        if not self.order_number:
            self.order_number = self._generate_order_number(getattr(self, 'tenant', None))
        super().save(*args, **kwargs)

    @staticmethod
    def _generate_order_number(tenant=None):
        from apps.tenants.refs import next_for, ORDER
        return next_for(Order, 'order_number', ORDER, tenant)

    @property
    def display_number(self):
        return self.order_number

    @property
    def balance_due(self):
        """
        What the customer still owes.

        A refunded or cancelled order owes nothing. Without this the refund
        zeroes amount_paid, the subtraction returns the full total, and the
        orders list shows "£1.00 due" in amber against an order that was
        refunded a second earlier, which reads as though the customer is in
        arrears.
        """
        if self.status in (self.STATUS_REFUNDED, self.STATUS_CANCELLED):
            return 0
        return max(self.total - self.amount_paid, 0)

    @property
    def is_fully_paid(self):
        return self.amount_paid >= self.total

    # Same fallback the CRM uses for orders paid before the real fee was
    # captured, so both pages report the same deduction.
    STRIPE_EST_RATE  = Decimal('0.015')
    STRIPE_EST_FIXED = Decimal('0.20')

    @property
    def stripe_fee_is_estimated(self):
        """True when no real fee came back from Stripe for this order."""
        return not self.stripe_fee and self.amount_paid > 0

    @property
    def stripe_fee_effective(self):
        """
        The fee to show and deduct. Uses the real figure from Stripe when we
        have it, otherwise the same estimate the CRM applies.
        """
        if self.stripe_fee:
            return self.stripe_fee
        if self.amount_paid > 0:
            return (self.amount_paid * self.STRIPE_EST_RATE
                    + self.STRIPE_EST_FIXED).quantize(Decimal('0.01'))
        return Decimal('0')

    @property
    def net_profit(self):
        """What is left after Stripe takes its cut."""
        return self.amount_paid - self.stripe_fee_effective

    @property
    def order_kind(self):
        """
        A plain label for what was bought, taken from the line items.
        Mixed baskets report the type that carries the most value.
        """
        labels = {
            'web':          'Web Design / Development',
            'service':      'Service',
            'domain':       'Domain Name',
            'product':      'Product',
            'hosting':      'Hosting Plan',
            'subscription': 'Subscription',
        }
        totals = {}
        for item in self.items.all():
            key = item.product_type or 'service'
            totals[key] = totals.get(key, 0) + (item.total_price or 0)
        if not totals:
            return ''
        if len(totals) > 1:
            best = max(totals, key=totals.get)
            return f'Mixed, mostly {labels.get(best, best.title())}'
        only = next(iter(totals))
        return labels.get(only, only.title())

    def mark_paid(self, amount, payment_intent_id=''):
        """
        Record a payment. Safe to call more than once for the same payment.

        This adds to amount_paid, because a deposit followed by a balance is
        two real payments on one order. That made it dangerous: three separate
        paths record a payment now, the return page, the Stripe webhook and
        reconcile_payments, and Stripe retries a webhook for up to three days.
        Any of those firing twice used to add the money again.

        It is not theoretical. On 31 July 2026 order FT619539 ended up showing
        £2.00 paid against a £1.00 order, and on 22 August 2026 the same thing
        happened again the moment a webhook was resent. It also quietly
        inflated every revenue figure that summed amount_paid.

        A payment is identified by its Stripe payment intent. The same intent
        applied twice is the same money twice, so it is ignored. A genuine
        second payment, a balance after a deposit, carries a different intent
        and still applies.

        Returns True if this call recorded a payment, False if it was a
        duplicate. Callers use that to decide whether to email. The money
        being right is not enough: a customer who receives five identical
        "Payment Confirmed" emails for one order has been told five times
        that something happened once, which is what every webhook retry did.
        """
        if payment_intent_id and payment_intent_id == self.stripe_payment_intent_id:
            # Already recorded. Not an error, just a duplicate delivery.
            return False

        self.amount_paid += amount
        if payment_intent_id:
            self.stripe_payment_intent_id = payment_intent_id
            # Fetch actual Stripe fee from balance transaction
            try:
                import stripe as _stripe
                from apps.ecommerce.stripe_utils import _get_stripe_secret_key
                from django.db import connection as _conn
                tenant = getattr(self, 'tenant', None)
                _stripe.api_key = _get_stripe_secret_key(tenant)
                pi = _stripe.PaymentIntent.retrieve(
                    payment_intent_id,
                    expand=['latest_charge.balance_transaction']
                )
                bt = pi.latest_charge.balance_transaction
                if bt and bt.fee:
                    self.stripe_fee += Decimal(str(bt.fee)) / 100
                self.apply_card_details(pi.latest_charge)
            except Exception:
                pass
        if self.amount_paid >= self.total:
            self.status = self.STATUS_PAID
            self.paid_at = timezone.now()
        else:
            self.status = self.STATUS_DEPOSIT_PAID
        self.save()
        self._sync_auction_product_status()
        return True

    def apply_card_details(self, charge):
        """
        Copy the non-sensitive card details off a Stripe charge.

        Only brand, last four digits, cardholder name and billing
        country/postcode are stored. The full card number and CVC are
        never returned by Stripe and are never held here.
        """
        if not charge:
            return
        details = (charge.get('payment_method_details') or {}).get('card') or {}
        billing = charge.get('billing_details') or {}
        address = billing.get('address') or {}

        self.card_brand = (details.get('brand') or '')[:30]
        self.card_last4 = (details.get('last4') or '')[:4]
        self.cardholder_name = (billing.get('name') or '')[:150]
        self.card_country = (address.get('country') or details.get('country') or '')[:2]
        self.card_postcode = (address.get('postal_code') or '')[:20]

    def _sync_auction_product_status(self):
        """After payment, update auction_status on any auction products in this order."""
        for item in self.items.select_related('product').all():
            p = item.product
            if p and p.is_auction:
                if self.status in (self.STATUS_PAID, self.STATUS_PROCESSING, self.STATUS_COMPLETED):
                    p.auction_status = Product.AUCTION_SOLD
                    p.is_active = False
                    p.save(update_fields=['auction_status', 'is_active'])
                elif self.status == self.STATUS_DEPOSIT_PAID:
                    p.auction_status = Product.AUCTION_RESERVED
                    p.save(update_fields=['auction_status'])


class OrderItem(models.Model):
    """A line item within an order."""

    order             = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
    product           = models.ForeignKey(
        Product, on_delete=models.SET_NULL, null=True, blank=True
    )

    # Snapshot at time of purchase (so historical orders stay correct)
    product_name      = models.CharField(max_length=200)
    product_type      = models.CharField(max_length=20, blank=True)
    quantity          = models.PositiveSmallIntegerField(default=1)
    unit_price        = models.DecimalField(max_digits=10, decimal_places=2)
    total_price       = models.DecimalField(max_digits=10, decimal_places=2)

    # Domain-specific
    domain_name       = models.CharField(max_length=255, blank=True, help_text='e.g. mybusiness.co.uk')
    domain_tld        = models.CharField(max_length=20, blank=True)
    registration_years = models.PositiveSmallIntegerField(default=1)

    custom_description = models.TextField(blank=True, help_text='For custom / manual line items')

    class Meta:
        verbose_name = 'Order Item'
        verbose_name_plural = 'Order Items'

    def __str__(self):
        return f'{self.product_name} x{self.quantity}'

    def save(self, *args, **kwargs):
        self.total_price = self.unit_price * self.quantity
        super().save(*args, **kwargs)


class DealAgreement(models.Model):
    """Digital agreement sent to a client before payment. They sign → pay → done."""

    TERMS_SERVICE      = 'service'
    TERMS_SUBSCRIPTION = 'subscription'
    TERMS_INSTALMENT   = 'instalment'
    TERMS_DOMAIN       = 'domain'
    TERMS_GENERAL      = 'general'

    TERMS_CHOICES = [
        (TERMS_SERVICE,      'Service / Project'),
        (TERMS_SUBSCRIPTION, 'Subscription / Retainer'),
        (TERMS_INSTALMENT,   'Instalment Plan'),
        (TERMS_DOMAIN,       'Domain Registration'),
        (TERMS_GENERAL,      'General'),
    ]

    order        = models.ForeignKey(
        'Order', on_delete=models.CASCADE, related_name='agreements'
    )
    token        = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
    terms_type   = models.CharField(max_length=20, choices=TERMS_CHOICES, default=TERMS_GENERAL)

    # Delivery
    sent_to_email = models.EmailField()
    sent_to_name  = models.CharField(max_length=150)
    sent_at       = models.DateTimeField(null=True, blank=True)
    created_by    = models.ForeignKey(
        'accounts.User', on_delete=models.SET_NULL,
        null=True, blank=True, related_name='deals_sent'
    )

    # Signature (filled when client accepts)
    signed_name  = models.CharField(max_length=150, blank=True)
    accepted_at  = models.DateTimeField(null=True, blank=True)
    accepted_ip  = models.GenericIPAddressField(null=True, blank=True)
    user_agent   = models.TextField(blank=True)

    accepted_terms = models.TextField(
        blank=True,
        help_text='Which policies the client ticked, recorded at the moment of signing'
    )

    # Stripe session created after acceptance
    stripe_session_id = models.CharField(max_length=200, blank=True)

    created_at   = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Deal Agreement'
        verbose_name_plural = 'Deal Agreements'


class EmailLog(models.Model):
    """Record of every outgoing email sent by the system."""
    TYPE_ORDER_CONFIRM  = 'order_confirmation'
    TYPE_ADMIN_NOTIFY   = 'admin_notification'
    TYPE_QUOTE          = 'quote'
    TYPE_DEAL           = 'deal'
    TYPE_OTP            = 'otp'
    TYPE_CANCELLATION   = 'order_cancellation'
    TYPE_ALERT          = 'alert'
    TYPE_OTHER          = 'other'
    TYPE_CHOICES = [
        (TYPE_ORDER_CONFIRM, 'Order Confirmation'),
        (TYPE_ADMIN_NOTIFY,  'Admin Notification'),
        (TYPE_QUOTE,         'Quote'),
        (TYPE_DEAL,          'Deal Agreement'),
        (TYPE_OTP,           'OTP / 2FA'),
        (TYPE_CANCELLATION,  'Cancellation'),
        (TYPE_ALERT,         'Urgent Alert'),
        (TYPE_OTHER,         'Other'),
    ]

    email_type  = models.CharField(max_length=40, choices=TYPE_CHOICES, default=TYPE_OTHER)
    subject     = models.CharField(max_length=500)
    to_email    = models.EmailField()
    from_email  = models.CharField(max_length=200, blank=True)
    html_body   = models.TextField(blank=True)
    sent_at     = models.DateTimeField(auto_now_add=True)
    ok          = models.BooleanField(default=True)
    is_archived = models.BooleanField(
        default=False,
        help_text='Hidden from the main list but kept as a record'
    )
    error       = models.TextField(blank=True)

    class Meta:
        ordering = ['-sent_at']
        verbose_name = 'Email Log'
        verbose_name_plural = 'Email Logs'

    def __str__(self):
        return f'{self.subject} → {self.to_email}'

    def __str__(self):
        return f'Deal #{self.order.order_number} — {self.sent_to_name}'

    @property
    def is_accepted(self):
        return bool(self.accepted_at)

    @classmethod
    def detect_terms_type(cls, order):
        """Auto-detect the appropriate terms type from order items."""
        types = set(order.items.values_list('product_type', flat=True))
        if 'subscription' in types:
            return cls.TERMS_SUBSCRIPTION
        if 'domain' in types:
            return cls.TERMS_DOMAIN
        if order.is_deposit_order or order.status == Order.STATUS_DEPOSIT_PAID:
            return cls.TERMS_INSTALMENT
        return cls.TERMS_SERVICE


class Expense(models.Model):
    """A cost or supplier payment that reduces net profit."""

    CAT_HOSTING     = 'hosting'
    CAT_DOMAIN      = 'domain'
    CAT_CONTRACTOR  = 'contractor'
    CAT_SOFTWARE    = 'software'
    CAT_MARKETING   = 'marketing'
    CAT_OTHER       = 'other'

    CATEGORY_CHOICES = [
        (CAT_HOSTING,    'Hosting / Server'),
        (CAT_DOMAIN,     'Domain Registration'),
        (CAT_CONTRACTOR, 'Contractor / Freelancer'),
        (CAT_SOFTWARE,   'Software / Licence'),
        (CAT_MARKETING,  'Marketing / Ads'),
        (CAT_OTHER,      'Other'),
    ]

    BILLING_HOURLY  = 'hourly'
    BILLING_MONTHLY = 'monthly'
    BILLING_ANNUAL  = 'annual'
    BILLING_ONCE    = 'once'
    BILLING_AGREED  = 'agreed'

    BILLING_CHOICES = [
        (BILLING_HOURLY,  'Hourly'),
        (BILLING_MONTHLY, 'Monthly'),
        (BILLING_ANNUAL,  'Annual'),
        (BILLING_ONCE,    'One-off'),
        (BILLING_AGREED,  'Agreed Deal'),
    ]

    category        = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default=CAT_OTHER)
    billing_period  = models.CharField(max_length=10, choices=BILLING_CHOICES, default=BILLING_ONCE)
    supplier        = models.CharField(max_length=150, blank=True, help_text='e.g. Namecheap, AWS, Upwork')
    description     = models.CharField(max_length=300)
    amount          = models.DecimalField(max_digits=10, decimal_places=2, help_text='Cost in GBP')
    date            = models.DateField(help_text='Date the expense was incurred / start date')
    end_date        = models.DateField(null=True, blank=True, help_text='End date (for recurring contracts)')
    created_at      = models.DateTimeField(auto_now_add=True)
    updated_at      = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-date', '-created_at']
        verbose_name = 'Expense'
        verbose_name_plural = 'Expenses'

    def __str__(self):
        return f'{self.get_category_display()} — {self.description} (£{self.amount})'


class DomainRegistration(models.Model):
    """Tracks automated domain registrations triggered by orders."""

    STATUS_PENDING    = 'pending'
    STATUS_REGISTERED = 'registered'
    STATUS_FAILED     = 'failed'
    STATUS_EXPIRED    = 'expired'
    STATUS_CANCELLED  = 'cancelled'

    STATUS_CHOICES = [
        (STATUS_PENDING,    'Pending'),
        (STATUS_REGISTERED, 'Registered'),
        (STATUS_FAILED,     'Failed'),
        (STATUS_EXPIRED,    'Expired'),
        (STATUS_CANCELLED,  'Cancelled'),
    ]

    order_item        = models.OneToOneField(
        OrderItem, on_delete=models.CASCADE, related_name='domain_registration'
    )
    domain_name       = models.CharField(max_length=255)
    tld               = models.CharField(max_length=20)
    registration_years = models.PositiveSmallIntegerField(default=1)
    registrar         = models.CharField(max_length=50, default='namecheap')
    registrar_order_id = models.CharField(max_length=100, blank=True)
    status            = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
    nameservers       = models.TextField(blank=True, help_text='JSON list of nameservers')
    error_message     = models.TextField(blank=True)
    registered_at     = models.DateTimeField(null=True, blank=True)
    expires_at        = models.DateTimeField(null=True, blank=True)
    created_at        = models.DateTimeField(auto_now_add=True)
    updated_at        = models.DateTimeField(auto_now=True)

    class Meta:
        verbose_name = 'Domain Registration'
        verbose_name_plural = 'Domain Registrations'

    def __str__(self):
        return f'{self.domain_name} ({self.get_status_display()})'
