from decimal import Decimal
import secrets
"""Digital magazine: free preview, payment requests, approval and reading access."""
import io
import re
from urllib.parse import quote

from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.utils import timezone

from apps.core.telegram import send_telegram_message

from .models import DigitalAccess, Product, StorePageContent

COOKIE_PREFIX = 'rt_mag_'
COOKIE_AGE = 60 * 60 * 24 * 365 * 3  # 3 years
BD_PHONE_RE = re.compile(r'^01[3-9]\d{8}$')


def normalize_phone(value):
    digits = re.sub(r'\D', '', value or '')
    if digits.startswith('880'):
        digits = digits[2:]
    return digits


def normalize_trx(value):
    return re.sub(r'[^A-Za-z0-9]', '', value or '').upper()


def whatsapp_number(page=None):
    page = page or StorePageContent.get_solo()
    raw = normalize_phone(page.whatsapp_number or page.payment_number)
    return '88' + raw if raw.startswith('01') else raw


# ─── Free preview ─────────────────────────────────────────────────────────────

def build_preview(product: Product):
    """
    Read the uploaded PDF once: record its page count, and for Paid issues cut the
    pages chosen in “preview pages” into a small public preview PDF
    (for Free issues this is what guests see before they sign in).
    Non-buyers only ever download that preview — never the full file.
    """
    if not product.is_magazine or not product.pdf_file:
        return
    from pypdf import PdfReader, PdfWriter

    from .models import parse_page_list
    with product.pdf_file.open('rb') as fh:
        reader = PdfReader(fh)
        total = len(reader.pages)
        # Paid issues: the free sample. Free issues: what guests see before signing in.
        chosen = parse_page_list(product.preview_pages, total) if product.digital_mode in ('paid', 'free') else []
        out = None
        if chosen:
            writer = PdfWriter()
            for n in chosen:
                writer.add_page(reader.pages[n - 1])
            out = io.BytesIO()
            writer.write(out)
    if product.preview_pdf:
        product.preview_pdf.delete(save=False)
    if out is not None:
        product.preview_pdf.save(f'{product.slug}-preview.pdf', ContentFile(out.getvalue()), save=False)
    else:
        product.preview_pdf = None
    product.preview_page_list = chosen
    product.page_count = total
    Product.objects.filter(pk=product.pk).update(
        preview_pdf=product.preview_pdf.name if product.preview_pdf else '',
        preview_page_list=chosen, page_count=total)


def reader_pages(product, full):
    """Which real page numbers the reader shows, in order (the rest are locked)."""
    if full:
        return list(range(1, (product.page_count or 0) + 1))
    return list(product.preview_page_list or [])


# ─── Access ───────────────────────────────────────────────────────────────────

def access_for(request, product):
    """The reader's access for this issue: from their account if signed in, else their browser cookie."""
    user = getattr(request, 'user', None)
    if user is not None and user.is_authenticated:
        mine = DigitalAccess.objects.filter(product=product, user=user)
        best = mine.filter(status='approved').first() or mine.first()
        if best:
            return best
    token = request.COOKIES.get(f'{COOKIE_PREFIX}{product.pk}')
    if not token:
        return None
    access = DigitalAccess.objects.filter(product=product, access_token=token).first()
    if access and access.user_id is None and user is not None and user.is_authenticated:
        access.user = user  # signed-in reader holding the secret cookie owns this purchase
        access.save(update_fields=['user'])
    return access


def access_states(request):
    """{product_id: 'approved' | 'pending'} for every issue this reader has bought or asked for
    (their account, plus unlock cookies in this browser). Worked out once per request."""
    cached = getattr(request, '_rt_mag_states', None)
    if cached is not None:
        return cached
    states = {}
    q = None
    user = getattr(request, 'user', None)
    from django.db.models import Q
    if user is not None and user.is_authenticated:
        q = Q(user=user)
    tokens = [v for k, v in request.COOKIES.items() if k.startswith(COOKIE_PREFIX)]
    if tokens:
        q = (q | Q(access_token__in=tokens[:50])) if q is not None else Q(access_token__in=tokens[:50])
    if q is not None:
        for pid, status in DigitalAccess.objects.filter(q, status__in=('approved', 'pending')).values_list('product_id', 'status'):
            if states.get(pid) != 'approved':
                states[pid] = status
    request._rt_mag_states = states
    return states


def has_full_access(request, product):
    if product.is_free_digital:
        # Free issues are free for everyone with an account (sign-up is free); guests see the preview pages.
        return bool(getattr(request, 'user', None) and request.user.is_authenticated)
    if request.user.is_authenticated and request.user.is_staff:
        return True  # editors can always proofread
    access = access_for(request, product)
    return bool(access and access.status == 'approved')


def remember(response, access):
    response.set_cookie(f'{COOKIE_PREFIX}{access.product_id}', access.access_token,
                        max_age=COOKIE_AGE, httponly=True, samesite='Lax')
    return response


def digital_coupon(product, code, user=None, phone=''):
    """Check a coupon for this issue's digital edition. Returns a coupons.Result or raises CouponError."""
    from . import coupons
    return coupons.check(code, [(product, product.digital_price, 1, 'digital')], user=user, phone=phone)


def create_request(product: Product, data, user=None) -> DigitalAccess:
    from django.db import transaction
    from . import coupons
    errors = {}
    name = (data.get('name') or '').strip()[:150]
    phone = normalize_phone(data.get('phone'))
    sender = normalize_phone(data.get('sender_number')) or phone
    trx = normalize_trx(data.get('trx_id'))
    code = (data.get('coupon_code') or '').strip()
    result = None
    if not product.can_buy_digital:
        errors['general'] = 'The digital edition of this issue is not available yet.'
    if not name:
        errors['name'] = 'Please enter your name.'
    if not BD_PHONE_RE.match(phone):
        errors['phone'] = 'Enter a valid mobile number, e.g. 01712345678.'
    if code and not errors.get('general'):
        try:
            result = digital_coupon(product, code, user=user, phone=phone)
        except coupons.CouponError as exc:
            errors['coupon'] = str(exc)
    price = product.digital_price or Decimal('0')
    discount = result.discount if result else Decimal('0')
    amount = max(Decimal('0'), price - discount)
    free = result is not None and amount == 0
    if free:
        trx, sender = 'FREE' + secrets.token_hex(5).upper(), sender if BD_PHONE_RE.match(sender) else phone
    else:
        if not BD_PHONE_RE.match(sender):
            errors['sender_number'] = 'Enter the number you sent money from, e.g. 01712345678.'
        if not 6 <= len(trx) <= 20:
            errors['trx_id'] = 'Enter the Transaction ID (TrxID) from your payment SMS.'
        elif DigitalAccess.objects.filter(trx_id=trx).exists():
            errors['trx_id'] = 'This Transaction ID was already submitted. Use “Already paid?” below to unlock.'
    if errors:
        raise ValidationError(errors)
    try:
        with transaction.atomic():
            access = DigitalAccess.objects.create(
                product=product, name=name, phone=phone, sender_number=sender, trx_id=trx, amount=amount,
                coupon=result.coupon if result else None, discount_amount=discount,
                status='approved' if free else 'pending', reviewed_at=timezone.now() if free else None,
                user=user if (user is not None and user.is_authenticated) else None)
            if result:
                coupons.redeem(result, digital_access=access)
    except coupons.CouponError as exc:
        raise ValidationError({'coupon': str(exc)})
    coupon_line = f"<b>Coupon:</b> {result.coupon.code} (−৳{discount:.0f})\n" if result else ''
    send_telegram_message(
        ("🎁 <b>Digital magazine unlocked with a 100% coupon</b>\n\n" if free else
         "📖 <b>Digital magazine payment — needs approval</b>\n\n") +
        f"<b>Ref:</b> {access.reference}\n<b>Issue:</b> {product.title}\n<b>Name:</b> {name}\n"
        f"<b>Phone:</b> {phone}\n<b>Account:</b> {user.email if user is not None and user.is_authenticated else 'guest'}\n<b>Sent from:</b> {sender}\n<b>TrxID:</b> {trx}\n{coupon_line}<b>Amount:</b> ৳{access.amount}\n\n"
        + ("" if free else "Approve in Admin → Store → Digital magazine access"))
    email_order(access, user)
    return access


def email_order(access, user=None):
    """Email the team about a new digital order (address set in Store page content → Order alert email)."""
    from django.conf import settings
    from django.core.mail import send_mail
    from django.template.loader import render_to_string
    from django.urls import reverse
    from apps.core.seo import absolute
    page = StorePageContent.get_solo()
    to = [e.strip() for e in (page.order_email or '').split(',') if '@' in e]
    if not to:
        return
    free = access.status == 'approved'
    ctx = {
        'a': access, 'free': free, 'page': page,
        'account': user.email if user is not None and user.is_authenticated else '',
        'admin_url': absolute(reverse('admin:store_digitalaccess_change', args=[access.pk])),
        'issue_url': absolute(access.product.get_absolute_url()),
    }
    subject = (f"{'🎁 Free unlock' if free else '📖 New digital order'} {access.reference} — "
               f"{access.product.title} — ৳{access.amount:.0f}")
    text = render_to_string('store/email/digital_order.txt', ctx)
    html = render_to_string('store/email/digital_order.html', ctx)

    def send():
        send_mail(subject, text, settings.DEFAULT_FROM_EMAIL, to, html_message=html, fail_silently=True)

    if 'locmem' in settings.EMAIL_BACKEND:
        send()  # tests: send straight away so they can check the outbox
    else:
        import threading
        threading.Thread(target=send, daemon=True).start()  # don't make the reader wait for Gmail


def find_for_unlock(product, phone, code):
    phone, code = normalize_phone(phone), normalize_trx(code)
    if not phone or not code:
        return None
    for a in DigitalAccess.objects.filter(product=product, phone=phone):
        if code in (a.trx_id, normalize_trx(a.reference)):
            return a
    return None


def approve(access):
    access.status = 'approved'
    access.reviewed_at = timezone.now()
    access.save(update_fields=['status', 'reviewed_at'])


def whatsapp_link(access, page=None):
    page = page or StorePageContent.get_solo()
    text = (f"আসসালামু আলাইকুম! আমি দ্য রিভার্স টাইমস ম্যাগাজিনের ডিজিটাল কপি কিনেছি।\n\n"
            f"সংখ্যা: {access.product.title}\n"
            f"টাকা: ৳{access.amount:.0f} পাঠিয়েছি {page.payment_number} নম্বরে\n"
            f"TrxID: {access.trx_id}\n"
            f"যে নম্বর থেকে: {access.sender_number}\n"
            f"নাম: {access.name}\n"
            f"মোবাইল: {access.phone}\n"
            f"রেফারেন্স: {access.reference}\n\n"
            f"অনুগ্রহ করে আমার ডিজিটাল কপি চালু করে দিন।")
    return f'https://wa.me/{whatsapp_number(page)}?text={quote(text)}'
