"""
Public auction section: domains and other listed assets.

One model, `Product` with `is_auction` ticked, sold two ways. Fixed price goes
through the normal checkout. Offers arrive as tickets for a human to answer.
Either way a reservation fee can hold the item for five working days, and that
fee comes off the final price.

Sales create ordinary Orders, so everything lands in the panel: revenue,
invoices, correspondence and the CRM.
"""
import logging
from datetime import timedelta

from django.conf import settings
from django.contrib import messages
from django.db.models import Q
from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone

from .models import Order, Product

log = logging.getLogger(__name__)

RESERVATION_WORKING_DAYS = 5


def _working_days_from(start, days=RESERVATION_WORKING_DAYS):
    """Add working days, skipping weekends. Bank holidays are not excluded."""
    d, added = start, 0
    while added < days:
        d += timedelta(days=1)
        if d.weekday() < 5:
            added += 1
    return d


def _client_ip(request):
    return (request.META.get('HTTP_X_FORWARDED_FOR') or '').split(',')[0].strip() \
        or request.META.get('REMOTE_ADDR') or None


def _release_if_lapsed(product):
    """A reservation nobody completed must not freeze the listing forever."""
    if (product.auction_status == Product.AUCTION_RESERVED
            and product.reserved_until
            and timezone.now() > product.reserved_until):
        product.auction_status = Product.AUCTION_AVAILABLE
        product.reserved_until = None
        product.save(update_fields=['auction_status', 'reserved_until'])
    return product


def _listing_qs():
    """Everything that belongs in the domains catalogue.

    Product type counts as well as the auction flag. A domain listed at a
    fixed price has is_auction unticked, and filtering on the flag alone gave
    it a 404 here while its own address redirected to this page.
    """
    return Product.objects.filter(is_active=True).filter(
        Q(is_auction=True) | Q(product_type=Product.TYPE_DOMAIN)
    )


def auction_list(request):
    """The domains catalogue, twelve at a time.

    Paginated on the server and topped up by fetch, the same way /services/
    works. The earlier version rendered every listing and hid the overflow
    with CSS, which stops being a page of results and starts being a page
    that lies about how many there are.
    """
    from django.core.paginator import Paginator

    items = _listing_qs().order_by('-is_featured', 'sort_order', '-created_at')
    for item in items:
        _release_if_lapsed(item)

    page_obj = Paginator(items, 12).get_page(request.GET.get('page'))

    if request.GET.get('partial') == '1':
        return render(request, 'public/partials/domain_cards.html',
                      {'listings': page_obj})

    return render(request, 'public/auction/list.html', {'listings': page_obj})


def auction_detail(request, slug):
    """The listing page for one domain.

    Renders the shop's product page, not the auction one. Both exist and they
    look different, and the shop page is the one that was designed for this:
    it carries the Premium Domains label, the buy and reserve panel and the
    Stripe reassurance. Moving domains to /domains/ silently swapped the
    design, which is not a rename, so the template follows the URL.
    """
    product = get_object_or_404(_listing_qs(), slug=slug)
    _release_if_lapsed(product)
    cart = request.session.get('cart', {}) or {}
    return render(request, 'public/shop/product_detail.html', {
        'product': product,
        'p': product,
        'can_act': product.auction_status == Product.AUCTION_AVAILABLE,
        'balance': product.balance_after_reservation,
        'interest_sent': request.session.pop(f'interest_sent_{product.pk}', False),
        'in_cart': str(product.pk) in cart,
        'cart_count': sum(v.get('qty', 0) for v in cart.values()),
    })


def auction_offer(request, slug):
    """An offer becomes a ticket. A person replies, not a machine."""
    product = get_object_or_404(_listing_qs(), slug=slug)
    if request.method != 'POST':
        return redirect('auction_detail', slug=slug)

    from apps.tickets.models import Ticket

    name   = request.POST.get('name', '').strip()
    email  = request.POST.get('email', '').strip()
    phone  = request.POST.get('phone', '').strip()
    amount = request.POST.get('offer_amount', '').strip()
    note   = request.POST.get('message', '').strip()

    if not name or not email:
        return render(request, 'public/auction/detail.html', {
            'p': product,
            'can_act': product.auction_status == Product.AUCTION_AVAILABLE,
            'balance': product.balance_after_reservation,
            'error': 'Please give us your name and email so we can reply.',
        })

    try:
        Ticket.objects.create(
            sender_name=name,
            sender_email=email,
            sender_phone=phone,
            subject=f'Offer for {product.name}',
            message=(
                f'Offer: {amount or "not stated"}\n'
                f'Listing: {product.name}\n'
                f'Asking price: {product.price}\n'
                f'Minimum offer: {product.auction_min_offer or "not set"}\n\n'
                f'{note}'
            ),
        )
    except Exception:
        log.exception('Could not create ticket for offer on %s', product.slug)

    return render(request, 'public/auction/thanks.html', {
        'p': product,
        'heading': 'Thank you, we have your offer',
        'body': 'One of our team will come back to you shortly, by email or by phone.',
    })


def auction_reserve(request, slug):
    """Take the reservation fee, hold the item, and record a real order."""
    product = get_object_or_404(_listing_qs(), slug=slug)
    _release_if_lapsed(product)

    if request.method != 'POST':
        return redirect('auction_detail', slug=slug)

    if product.auction_status != Product.AUCTION_AVAILABLE:
        messages.error(request, 'That listing is no longer available.')
        return redirect('auction_detail', slug=slug)

    fee = product.reservation_fee
    if not fee or fee <= 0:
        messages.error(request, 'This listing cannot be reserved online. Please make an offer instead.')
        return redirect('auction_detail', slug=slug)

    name  = request.POST.get('name', '').strip()
    email = request.POST.get('email', '').strip()
    phone = request.POST.get('phone', '').strip()
    if not name or not email:
        return render(request, 'public/auction/detail.html', {
            'p': product,
            'can_act': True,
            'balance': product.balance_after_reservation,
            'error': 'Please give us your name and email before reserving.',
        })

    tenant = None
    try:
        from apps.tenants.models import Tenant
        tenant = Tenant.objects.filter(is_master=True).first()
    except Exception:
        pass

    order = Order.objects.create(
        tenant=tenant,
        customer_name=name,
        customer_email=email,
        customer_phone=phone,
        subtotal=fee,
        total=fee,
        is_deposit_order=True,
        ip_address=_client_ip(request),
        customer_notes=f'Reservation fee for {product.name}.',
        internal_notes=(
            f'Reservation on {product.name}. Asking price {product.price}. '
            f'Fee {fee} comes off the price, balance {product.balance_after_reservation}. '
            f'Held for {RESERVATION_WORKING_DAYS} working days.'
        ),
    )

    try:
        import stripe
        from apps.ecommerce.stripe_utils import _get_stripe_secret_key
        stripe.api_key = _get_stripe_secret_key()
        session = stripe.checkout.Session.create(
            payment_method_types=['card'],
            line_items=[{
                'price_data': {
                    'currency': 'gbp',
                    'unit_amount': int(fee * 100),
                    'product_data': {
                        'name': f'Reservation: {product.name}',
                        'description': f'Holds {product.name} for {RESERVATION_WORKING_DAYS} working days. Deducted from the final price.',
                    },
                },
                'quantity': 1,
            }],
            mode='payment',
            success_url=request.build_absolute_uri(
                f'/auction/{product.slug}/reserved/?session_id={{CHECKOUT_SESSION_ID}}'),
            cancel_url=request.build_absolute_uri(f'/auction/{product.slug}/'),
            customer_email=email,
            metadata={
                'order_id':     str(order.id),
                'order_number': order.order_number,
                'product_id':   str(product.id),
                'purpose':      'auction_reservation',
            },
        )
    except Exception as exc:
        log.exception('Stripe session failed for reservation on %s', product.slug)
        return render(request, 'public/auction/detail.html', {
            'p': product,
            'can_act': True,
            'balance': product.balance_after_reservation,
            'error': f'We could not start the payment: {exc}. Please contact us and we will hold it for you.',
        })

    order.stripe_session_id = session.id
    order.save(update_fields=['stripe_session_id'])
    return redirect(session.url)


def auction_reserved(request, slug):
    """
    Back from Stripe. Confirm the payment, then hold the listing.

    The status is only changed once Stripe says the session is paid, so an
    abandoned checkout never reserves anything.
    """
    product = get_object_or_404(_listing_qs(), slug=slug)
    session_id = request.GET.get('session_id', '')
    paid = False
    order = None

    if session_id:
        try:
            import stripe
            from apps.ecommerce.stripe_utils import _get_stripe_secret_key
            stripe.api_key = _get_stripe_secret_key()
            session = stripe.checkout.Session.retrieve(session_id)
            paid = session.get('payment_status') == 'paid'
            order = Order.objects.filter(stripe_session_id=session_id).first()
        except Exception:
            log.exception('Could not verify Stripe session %s', session_id)

    if paid and product.auction_status == Product.AUCTION_AVAILABLE:
        product.auction_status = Product.AUCTION_RESERVED
        product.reserved_until = _working_days_from(timezone.now())
        product.reserved_order = order
        product.save(update_fields=['auction_status', 'reserved_until', 'reserved_order'])

        if order:
            try:
                order.amount_paid = order.total
                order.status = Order.STATUS_DEPOSIT_PAID
                order.save(update_fields=['amount_paid', 'status'])
            except Exception:
                log.exception('Could not mark reservation order %s paid', order.pk)

    return render(request, 'public/auction/thanks.html', {
        'p': product,
        'heading': 'Reserved, thank you' if paid else 'Payment not confirmed',
        'body': (
            f'{product.name} is held for you until '
            f'{product.reserved_until:%d %B %Y}. Your reservation fee comes off the price, '
            f'so the balance is £{product.balance_after_reservation}. '
            f'We will be in touch to complete the transfer.'
            if paid else
            'We could not confirm your payment. If you were charged, contact us and we will sort it out.'
        ),
    })
