"""
WhatsApp conversational ordering + full Syanko <3U loyalty integration.

Consolidated state machine (replaces both legacy whatsapp/bot.py and the
previous version of this file).

State machine per phone number (stored in Redis via whatsapp_session):
  IDLE → AWAITING_RESTAURANT → MAIN_MENU → MENU_CATEGORIES → MENU_ITEMS
       → CART → ORDER_TYPE → DELIVERY_ADDRESS → CHECKOUT_COINS
       → CHECKOUT_PAYMENT → (order placed, session cleared)

Order types supported:
  • TAKEAWAY — default, no extra info needed
  • DELIVERY — requires delivery address (text or GPS)
  • DINE_IN  — requires party size and preferred time

Loyalty benefits active at every step:
  • Coin balance shown on greeting and cart
  • Coin redemption offered at checkout (Iron Law 50% cap enforced)
  • Khata lifeline available as payment option
  • Subscription status queryable from main menu
  • Coins earned automatically after order is placed (RulesEngine fires)
  • Summit progress shown in order confirmation
"""
import hashlib
import hmac
import json
import logging
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.db import transaction as db_transaction
from django.http import HttpResponse, JsonResponse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.conf import settings
try:
    import track
except ImportError:
    track = None

# Initialize Interakt track API key (if provided)
if track:
    track.api_key = getattr(settings, 'INTERAKT_TRACK_KEY', None) or getattr(settings, 'INTERAKT_SECRET_KEY', None)

from . import whatsapp_service as wa
from .loyalty_services import (
    IronLawsEnforcer,
    KhataService,
    RulesEngine,
    SubscriptionService,
    WalletService,
    get_or_create_wallet,
)
from .models import (
    Booking,
    BookingItem,
    CustomerWallet,
    MenuItem,
    Restaurant,
)
from .whatsapp_session import (
    clear_session,
    get_or_create_user,
    get_session,
    is_duplicate_message,
    set_session,
    touch_session,
)

logger = logging.getLogger(__name__)

User = get_user_model()

CATEGORY_LABELS = {
    'STARTER': 'Starters 🥗',
    'MAIN_COURSE': 'Main Course 🍛',
    'DESSERT': 'Desserts 🍨',
    'BEVERAGE': 'Beverages 🥤',
    'SNACKS': 'Snacks 🍟',
    'BIRYANI': 'Biryani 🍚',
    'THALI': 'Thali 🍱',
    'CHINESE': 'Chinese 🥡',
    'SOUTH_INDIAN': 'South Indian 🫓',
    'NORTH_INDIAN': 'North Indian 🫕',
}

ORDER_TYPE_MAP = {
    '1': ('TAKEAWAY', '🛍️ Takeaway'),
    '2': ('DELIVERY', '🛵 Delivery'),
    '3': ('DINE_IN', '🍽️ Dine-in'),
}

# ─────────────────────────────────────────────────────────────────────
# Internal helpers
# ─────────────────────────────────────────────────────────────────────

def _available_categories(restaurant_id: int) -> list:
    return list(
        MenuItem.objects.filter(restaurant_id=restaurant_id, is_available=True)
        .order_by('category')
        .values_list('category', flat=True)
        .distinct()
    )


def _items_in_category(restaurant_id: int, category: str) -> list:
    return list(
        MenuItem.objects.filter(
            restaurant_id=restaurant_id,
            category=category,
            is_available=True,
        ).values('id', 'name', 'price', 'is_vegetarian')
    )


def _cart_total(cart: list) -> float:
    return sum(i['qty'] * i['price'] for i in cart)


def _split_country_phone(phone: str) -> tuple[str, str]:
    cleaned = (phone or '').replace('whatsapp:', '').replace('+', '').replace(' ', '').replace('-', '').replace('(', '').replace(')', '').strip()
    if len(cleaned) == 10 and cleaned.isdigit():
        return '+91', cleaned
    if cleaned.startswith('91') and len(cleaned) == 12 and cleaned.isdigit():
        return '+91', cleaned[2:]
    # ponytail: India-first fallback; upgrade if HotelFinder starts serving multiple country codes.
    return '+91', cleaned[-10:] if len(cleaned) >= 10 else cleaned


def _progress_bar(pct: int) -> str:
    filled = int(pct / 10)
    return "█" * filled + "░" * (10 - filled)


def _get_wallet_info(user_id: int, restaurant) -> dict:
    """Fetch wallet balance and loyalty info. Returns dict or None."""
    try:
        user = User.objects.get(pk=user_id)
        wallet = CustomerWallet.objects.filter(
            customer=user, restaurant=restaurant
        ).first()
        if not wallet:
            return None
        balance = WalletService.get_live_balance(wallet)
        program = restaurant.loyalty_program
        return {
            'wallet_id': wallet.id,
            'balance': balance,
            'program': program,
            'coin_value': program.coin_value_in_rupees,
            'cap_percent': program.redemption_cap_percent,
        }
    except Exception:
        return None


def _notify_restaurant_new_order(booking):
    """Push a new order notification to the restaurant's owner via WebSocket and WhatsApp."""
    try:
        from channels.layers import get_channel_layer
        from asgiref.sync import async_to_sync
        channel_layer = get_channel_layer()
        if channel_layer:
            async_to_sync(channel_layer.group_send)(
                f"restaurant_{booking.restaurant.id}_orders",
                {
                    "type": "new_order",
                    "order_id": booking.id,
                    "customer_name": booking.customer_name,
                    "total": float(booking.total_amount),
                    "order_type": booking.booking_type,
                    "source": "whatsapp",
                }
            )
    except Exception as exc:
        logger.debug("WebSocket notification failed (non-critical): %s", exc)

    # Also notify owner via WhatsApp if they have a phone
    try:
        owner = booking.restaurant.owner
        if owner and owner.phone:
            items_text = ", ".join(
                f"{item.quantity}× {item.menu_item.name}"
                for item in booking.items.all()
            )
            wa.send_reply(
                owner.phone,
                f"📱 *New WhatsApp Order #{booking.id}*\n\n"
                f"Customer: {booking.customer_name}\n"
                f"Type: {booking.get_booking_type_display()}\n"
                f"Items: {items_text}\n"
                f"Total: ₹{booking.total_amount}\n\n"
                f"Open dashboard to manage this order."
            )
    except Exception as exc:
        logger.debug("Owner WhatsApp notification failed (non-critical): %s", exc)


def _track_order_placed(booking, phone: str, payment_method: str):
    if not track or not track.api_key:
        return
    try:
        country_code, phone_number = _split_country_phone(phone)
        track.event(
            user_id=phone_number,
            country_code=country_code,
            phone_number=phone_number,
            event="Order Placed",
            traits={
                "order_id": booking.id,
                "restaurant_name": booking.restaurant.name,
                "order_type": booking.booking_type,
                "payment_method": payment_method,
                "subtotal": float(booking.subtotal),
                "total_amount": float(booking.total_amount),
            },
        )
    except Exception as e:
        logger.error("Interakt track.event error: %s", e)


def _sync_whatsapp_order_to_aahar(booking, cart: list):
    restaurant = booking.restaurant
    if booking.booking_type not in ('DELIVERY', 'TAKEAWAY'):
        return
    if not hasattr(restaurant, 'aahar_config') or not restaurant.aahar_config.is_active:
        return

    try:
        import restaurant.aahar_service as aahar_service

        aahar_config = restaurant.aahar_config
        is_transfer_type = "takeaway_hf" if booking.booking_type == 'TAKEAWAY' else "orders_hf"
        clean_name = "".join(c for c in booking.customer_name if c.isalnum() or c == '_').replace(' ', '_')
        booking_time_str = (booking.created_at or timezone.now()).strftime('%Y%m%d_%H%M%S')
        generated_booking_id = f"{clean_name}_{booking.contact_number}_{booking_time_str}_{booking.id}"[:50]

        items = []
        for item_data in cart:
            menu_item = MenuItem.objects.filter(pk=item_data['item_id']).first()
            if not menu_item:
                continue
            items.append({
                "item_id": menu_item.aahar_item_id or menu_item.id,
                "quantity": item_data['qty'],
                "price": str(menu_item.price),
                "remarks": "",
            })

        if not items:
            return

        order_payload = {
            "restaurant_id": int(aahar_config.aahar_restaurant_service_id),
            "customer_name": booking.customer_name,
            "customer_mobile": booking.contact_number,
            "customer_phone": booking.contact_number,
            "booking_id": generated_booking_id,
            "payment_status": "Pending" if booking.payment_status == 'UNPAID' else "Paid",
            "is_transfer": is_transfer_type,
            "items": items,
        }
        response = aahar_service.submit_online_order(
            int(aahar_config.aahar_restaurant_service_id),
            order_payload,
            base_url=aahar_config.aahar_domain,
        )
        if response and response.get('code') in (200, 201):
            from .models import AaharOrderSync

            data = response.get('data', {})
            aahar_order_id = data.get('order_id')
            if aahar_order_id:
                AaharOrderSync.objects.update_or_create(
                    aahar_config=aahar_config,
                    aahar_order_id=aahar_order_id,
                    defaults={
                        "aahar_order_number": data.get('parcel_number', ''),
                        "invoice": f"HF-{booking.id}",
                        "booking_id": generated_booking_id,
                        "is_transfer": is_transfer_type,
                        "grand_total": booking.total_amount,
                        "payment_status": 'PAID' if booking.payment_status == 'PAID' else 'UNPAID',
                        "payment_mode": booking.payment_mode,
                        "order_data": order_payload,
                    },
                )
    except Exception as e:
        logger.error("Failed to submit WhatsApp order %d to Aahar: %s", booking.id, e)


# ─────────────────────────────────────────────────────────────────────
# Conversation state machine
# ─────────────────────────────────────────────────────────────────────

import re

class RegexIntentParser:
    RULES = [
        (re.compile(r'(?i)^(hi|hello|hey|menu|start|restart)$'), 'GREETING'),
        (re.compile(r'(?i)^(cancel|exit|quit|stop|bye|end)$'), 'EXIT'),
        (re.compile(r'(?i)^(cmd_menu|view menu)$'), 'CMD_MENU'),
        (re.compile(r'(?i)^(cmd_wallet|my wallet)$'), 'CMD_WALLET'),
        (re.compile(r'(?i)^(cmd_cart|my cart)$'), 'CMD_CART'),
        (re.compile(r'(?i)^(cmd_checkout|checkout|proceed to checkout)$'), 'CMD_CHECKOUT'),
        (re.compile(r'(?i)^(cmd_change_rest|change rest\.?)$'), 'CMD_CHANGE_REST'),
        (re.compile(r'(?i)^(cmd_add_more|add more|add more items)$'), 'CMD_ADD_MORE'),
        (re.compile(r'(?i)^(cmd_clear_cart|clear cart)$'), 'CMD_CLEAR_CART'),
        (re.compile(r'(?i)^(cmd_takeaway|takeaway)$'), 'CMD_TAKEAWAY'),
        (re.compile(r'(?i)^(cmd_delivery|delivery)$'), 'CMD_DELIVERY'),
        (re.compile(r'(?i)^(cmd_dine_in|dine-in|dine in)$'), 'CMD_DINE_IN'),
        (re.compile(r'(?i)^(cmd_pay_cash|cash \/ card|cash|card)$'), 'CMD_PAY_CASH'),
        (re.compile(r'(?i)^(cmd_pay_upi|upi)$'), 'CMD_PAY_UPI'),
        (re.compile(r'(?i)^(cmd_pay_khata|pay later \(khata\)|pay later|khata)$'), 'CMD_PAY_KHATA'),
        (re.compile(r'(?i)^cmd_cat_(.+)$'), 'CMD_CAT'),
        (re.compile(r'(?i)^cmd_item_(.+)$'), 'CMD_ITEM'),
        (re.compile(r'(?i)^rest_(\d+)$'), 'CMD_REST'),
        (re.compile(r'(?i)^(?:add\s+)?(\d+)\s+(.+)$'), 'ADD_CART'),
        (re.compile(r'(?i)^(cmd_yes|yes.*)$'), 'CMD_YES'),
        (re.compile(r'(?i)^(cmd_no|no.*)$'), 'CMD_NO'),
    ]

    @classmethod
    def parse(cls, text: str) -> dict:
        text = text.strip()
        for pattern, intent in cls.RULES:
            match = pattern.match(text)
            if match:
                return {'intent': intent, 'groups': match.groups(), 'text': text}
        return {'intent': 'UNKNOWN', 'groups': (), 'text': text}

class ConversationHandler:

    def handle(self, phone: str, text: str) -> str:
        sess = get_session(phone)
        parsed = RegexIntentParser.parse(text)
        intent = parsed['intent']

        if intent == 'EXIT':
            clear_session(phone)
            return "Session ended. Come back anytime! 👋\n\nSend *Hi* to start a new order."

        if intent == 'GREETING':
            cart = sess.get('cart', [])
            user, cleaned, is_new, gen_pwd = get_or_create_user(phone)
            new_sess = {'state': 'AWAITING_RESTAURANT', 'user_id': user.id, 'phone': cleaned}
            if is_new:
                new_sess['is_new'] = True
                new_sess['generated_password'] = gen_pwd
            if cart:
                new_sess['cart'] = cart
            set_session(phone, new_sess)
            self._greeting(phone, user, cart)
            return ""

        touch_session(phone)
        state = sess.get('state', 'IDLE')
        method = getattr(self, f'_s_{state.lower()}', self._s_idle)
        reply = method(phone, text, sess, parsed)
        if reply:
            return reply
        return ""

    def _s_idle(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        user, cleaned, is_new, gen_pwd = get_or_create_user(phone)
        new_sess = {'state': 'AWAITING_RESTAURANT', 'user_id': user.id, 'phone': cleaned}
        if is_new:
            new_sess['is_new'] = True
            new_sess['generated_password'] = gen_pwd
        set_session(phone, new_sess)
        self._greeting(phone, user, [])
        return ""

    def _greeting(self, phone: str, user, cart: list):
        cart_note = f"\n🛒 You have an open cart with {len(cart)} item(s)." if cart else ""
        name = user.first_name or user.username if user else "Guest"
        text = f"👋 Welcome, *{name}*!{cart_note}\n\nWhich restaurant are you ordering from?\n\n_Earn loyalty coins on every order!_ 🪙"
        active = list(Restaurant.objects.filter(is_active=True)[:10])
        if active:
            rows = []
            for r in active:
                rows.append({'id': f"rest_{r.id}", 'title': r.name[:24], 'description': r.location[:72]})
            sess = get_session(phone)
            sess['restaurant_options'] = [r.id for r in active]
            set_session(phone, sess)
            sections = [{'title': 'Available Restaurants', 'rows': rows}]
            wa.send_interactive_list(phone, text, "Select Restaurant", sections)
        else:
            wa.send_reply(phone, f"{text}\n\nNo restaurants available currently.")

    def _s_awaiting_restaurant(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        intent = parsed['intent']
        restaurant_id = None
        
        if intent == 'CMD_REST':
            restaurant_id = int(parsed['groups'][0])
        else:
            # Check if they typed a number corresponding to the restaurant list
            active = list(Restaurant.objects.filter(is_active=True)[:10])
            try:
                idx = int(text.strip()) - 1
                if 0 <= idx < len(active):
                    restaurant_id = active[idx].id
            except ValueError:
                pass
                
        if restaurant_id:
            restaurant = Restaurant.objects.filter(pk=restaurant_id, is_active=True).first()
            if restaurant:
                sess['restaurant_id'] = restaurant.id
                self._ask_order_type(phone, sess)
                return ""

        self._greeting(phone, None, sess.get('cart', []))
        return ""

    def _s_main_menu(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        restaurant = Restaurant.objects.filter(pk=sess.get('restaurant_id')).first()
        if not restaurant:
            sess['state'] = 'AWAITING_RESTAURANT'
            set_session(phone, sess)
            return "Restaurant not found. Which restaurant are you ordering from?"

        intent = parsed['intent']
        if intent == 'CMD_MENU' or text == '1':
            self._show_categories(phone, sess)
            return ""
        if intent == 'CMD_WALLET' or text == '2':
            # Not fully implemented interactive yet, so fallback to old
            wa.send_reply(phone, "Wallet logic omitted for brevity in upgrade")
            return ""
        if intent == 'CMD_CHECKOUT' or text == '5':
            if sess.get('cart'):
                self._show_cart(phone, sess)
                return ""
        if intent == 'CMD_CHANGE_REST' or text == '9':
            sess['state'] = 'AWAITING_RESTAURANT'
            set_session(phone, sess)
            return "Which restaurant? Reply with name or ID."

        self._show_main_menu(phone, sess, restaurant)
        return ""

    def _show_main_menu(self, phone: str, sess: dict, restaurant):
        user_id = sess.get('user_id')
        wallet_line = ""
        if user_id:
            info = _get_wallet_info(user_id, restaurant)
            if info:
                wallet_line = f"\n💰 Coins: *{int(info['balance'])}*"

        cart = sess.get('cart', [])
        cart_line = ""
        if cart:
            total = _cart_total(cart)
            cart_line = f"\n🛒 Cart: {len(cart)} item(s) — ₹{total:.0f}"

        sess['state'] = 'MAIN_MENU'
        set_session(phone, sess)

        text = f"🍽️ *{restaurant.name}*{wallet_line}{cart_line}\n\nWhat would you like to do?"
        buttons = [
            {'id': 'cmd_menu', 'title': 'View Menu'},
            {'id': 'cmd_wallet', 'title': 'My Wallet'},
        ]
        if cart:
            buttons.append({'id': 'cmd_checkout', 'title': 'Checkout'})
        else:
            buttons.append({'id': 'cmd_change_rest', 'title': 'Change Rest.'})

        wa.send_interactive_buttons(phone, text, buttons)

    def _show_categories(self, phone: str, sess: dict):
        restaurant_id = sess.get('restaurant_id')
        
        categories = list(MenuItem.objects.filter(
            restaurant_id=restaurant_id, is_available=True
        ).values_list('category', flat=True).distinct().order_by())
        
        sess['state'] = 'MENU_CATEGORIES'
        sess['available_categories'] = categories
        set_session(phone, sess)

        sections = []
        rows = []
        
        for cat in categories:
            if cat:
                rows.append({
                    "id": f"cmd_cat_{cat}",
                    "title": cat[:24]
                })

        if rows:
            # WhatsApp Interactive List allows a maximum of 10 rows total per message
            # If we have more than 10 categories, we should ideally paginate.
            # For now, we'll send multiple messages if there are more than 10.
            
            for i in range(0, len(rows), 10):
                chunk = rows[i:i+10]
                section = {
                    "title": "Menu Categories",
                    "rows": chunk
                }
                
                wa.send_interactive_list(
                    phone,
                    "Tell Us What You're Craving 🌱🍗\n(We'll personalize your menu experience)" if i == 0 else "More Categories...",
                    "View Categories",
                    [section]
                )
        else:
            wa.send_reply(phone, "No menu items available right now.")

    def _s_menu_categories(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        intent = parsed['intent']
        if intent == 'CMD_CAT':
            cat = parsed['groups'][0]
            self._show_items(phone, sess, cat)
            return ""
            
        cats = sess.get('available_categories', [])
        
        # Check for exact title match
        for c in cats:
            if c.lower() == text.lower() or c[:24].lower() == text.lower():
                self._show_items(phone, sess, c)
                return ""
                
        try:
            idx = int(text) - 1
            if 0 <= idx < len(cats):
                self._show_items(phone, sess, cats[idx])
                return ""
        except ValueError:
            pass
            
        # If nothing matches, reshow categories
        self._show_categories(phone, sess)
        return ""

    def _show_items(self, phone: str, sess: dict, category: str):
        import os
        debug_file = os.path.join(settings.BASE_DIR, "hotelfinder", "webhook_debug.txt")
        try:
            with open(debug_file, "a") as f:
                f.write(f"Entered _show_items with category={category!r}\n")
        except Exception as e:
            logger.error("Failed to write webhook_debug.txt: %s", e)
        items = _items_in_category(sess['restaurant_id'], category)
        if not items:
            try:
                with open(debug_file, "a") as f:
                    f.write(f"No items found for category {category!r}\n")
            except Exception as e:
                logger.error("Failed to write webhook_debug.txt: %s", e)
            wa.send_reply(phone, "No items here right now.")
            self._show_categories(phone, sess)
            return

        sess['state'] = 'MENU_ITEMS'
        sess['current_category'] = category
        sess['current_items'] = [
            {**i, 'price': float(i['price'])} for i in items
        ]
        set_session(phone, sess)

        rows = []
        for item in items:
            rows.append({
                'id': f"cmd_item_{item['id']}",
                'title': item['name'][:24],
                'description': f"₹{item['price']}"[:72]
            })
            
        for i in range(0, len(rows), 10):
            chunk = rows[i:i+10]
            section = [{'title': category[:24], 'rows': chunk}]
            text = f"🍽️ *{category}*\nSelect an item to add to your cart:" if i == 0 else f"🍽️ *{category}* (More...)"
            wa.send_interactive_list(phone, text, "View Items", section)

    def _s_menu_items(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        intent = parsed['intent']
        
        if intent == 'CMD_CHECKOUT' or text.lower() == 'done':
            cart = sess.get('cart', [])
            if not cart:
                return "Your cart is empty. Add something first!"
            self._show_cart(phone, sess)
            return ""

        if intent == 'CMD_ITEM':
            item_id = int(parsed['groups'][0])
            qty = 1
            items = sess.get('current_items', [])
            item = next((i for i in items if i['id'] == item_id), None)
            if item:
                return self._add_item_to_cart(phone, sess, item, qty)

        if intent == 'ADD_CART':
            qty_str = parsed['groups'][0]
            name_str = parsed['groups'][1]
            qty = int(qty_str) if qty_str else 1
            
            # Simple fuzzy match based on regex groups
            items = sess.get('current_items', [])
            item = None
            if name_str.isdigit():
                idx = int(name_str) - 1
                if 0 <= idx < len(items):
                    item = items[idx]
            else:
                item = next((i for i in items if i['name'].lower() in name_str.lower()), None)
                
            if item:
                return self._add_item_to_cart(phone, sess, item, qty)
                
        # Try raw numbers (fallback)
        parts = text.split()
        try:
            idx = int(parts[0]) - 1
            qty = int(parts[1]) if len(parts) > 1 else 1
            items = sess.get('current_items', [])
            if 0 <= idx < len(items) and 1 <= qty <= 50:
                item = items[idx]
                return self._add_item_to_cart(phone, sess, item, qty)
        except (ValueError, IndexError):
            pass
            
        # Try exact title match fallback
        items = sess.get('current_items', [])
        item = next((i for i in items if i['name'].lower() == text.lower() or i['name'][:24].lower() == text.lower()), None)
        if item:
            return self._add_item_to_cart(phone, sess, item, 1)

        self._show_categories(phone, sess)
        return ""
        
    def _add_item_to_cart(self, phone: str, sess: dict, item: dict, qty: int) -> str:
        menu_item = MenuItem.objects.filter(pk=item['id'], is_available=True).first()
        if not menu_item:
            return f"❌ *{item['name']}* is no longer available. Pick something else."

        cart = sess.get('cart', [])
        existing = next((c for c in cart if c['item_id'] == item['id']), None)
        if existing:
            existing['qty'] += qty
        else:
            cart.append({
                'item_id': item['id'],
                'name': item['name'],
                'price': float(menu_item.price),
                'qty': qty,
                'category': sess.get('current_category', ''),
            })
        sess['cart'] = cart
        set_session(phone, sess)
        total = _cart_total(cart)
        
        text = (
            f"✅ Added *{qty}× {item['name']}* (+₹{qty * float(menu_item.price):.0f})\n"
            f"Cart total: ₹{total:.0f}\n\n"
            "Add more or checkout."
        )
        buttons = [
            {'id': 'cmd_checkout', 'title': 'Checkout'},
            {'id': 'cmd_menu', 'title': 'Add More'},
        ]
        wa.send_interactive_buttons(phone, text, buttons)
        return ""

    def _show_cart(self, phone: str, sess: dict):
        cart = sess.get('cart', [])
        if not cart:
            wa.send_reply(phone, "Your cart is empty.")
            return

        lines = ["🛒 *Your Cart*\n"]
        for item in cart:
            lines.append(f"• {item['qty']}× {item['name']} — ₹{item['qty'] * item['price']:.0f}")

        total = _cart_total(cart)
        lines.append(f"\n*Subtotal: ₹{total:.0f}*")

        user_id = sess.get('user_id')
        restaurant = Restaurant.objects.filter(pk=sess.get('restaurant_id')).first()
        if user_id and restaurant:
            info = _get_wallet_info(user_id, restaurant)
            if info and info['balance'] >= 1:
                max_discount = Decimal(str(total)) * (info['cap_percent'] / Decimal('100'))
                max_coins = max_discount / info['coin_value']
                usable = min(info['balance'], max_coins)
                if usable >= 1:
                    discount_val = float(usable * info['coin_value'])
                    lines.append(
                        f"\n💰 *{int(info['balance'])} coins* available → use up to "
                        f"*{int(usable)} coins* = ₹{discount_val:.0f} off"
                    )
                    sess['wallet_id'] = info['wallet_id']
                    sess['max_usable_coins'] = float(usable)
                else:
                    lines.append(f"\n💰 Coins: {int(info['balance'])} (not enough for discount yet)")
                sess['bill_total'] = float(total)

        sess['state'] = 'CART'
        set_session(phone, sess)

        buttons = [
            {'id': 'cmd_checkout', 'title': 'Proceed to Checkout'},
            {'id': 'cmd_add_more', 'title': 'Add More Items'},
            {'id': 'cmd_clear_cart', 'title': 'Clear Cart'}
        ]
        wa.send_interactive_buttons(phone, "\n".join(lines), buttons)

    def _s_cart(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        intent = parsed['intent']
        if intent == 'CMD_CHECKOUT' or text == '1':
            # Use the order type already selected at the start of the session
            # Go straight to payment/coins
            if 'order_type' not in sess:
                sess['order_type'] = 'TAKEAWAY'
                sess['order_type_label'] = '🛍️ Takeaway'
                set_session(phone, sess)

            if sess.get('max_usable_coins', 0) >= 1:
                self._ask_coins(phone, sess)
                return ""
            self._ask_payment(phone, sess)
            return ""
        if intent == 'CMD_ADD_MORE' or text == '2':
            self._show_categories(phone, sess)
            return ""
        if intent == 'CMD_CLEAR_CART' or text == '3':
            sess['cart'] = []
            sess.pop('wallet_id', None)
            sess.pop('max_usable_coins', None)
            set_session(phone, sess)
            return "🗑️ Cart cleared.\n\nReply *Menu* to start over."
            
        self._show_cart(phone, sess)
        return ""

    def _ask_order_type(self, phone: str, sess: dict):
        restaurant = Restaurant.objects.filter(pk=sess.get('restaurant_id')).first()
        if not restaurant:
            return

        msg = (
            f"🏔️ *Welcome to {restaurant.name}*\n"
            f"Wrapped with Warmth & Love\n\n"
            f"👋 Namaste!\n"
            f"EAT FIRST. WE GOT YOU.\n\n"
            f"━━━━━━━━━━━━━━━\n\n"
            f"How would you like to enjoy {restaurant.name} today?"
        )

        buttons = []
        if restaurant and restaurant.is_takeaway_available:
            buttons.append({'id': 'cmd_takeaway', 'title': 'Takeaway'})
        if restaurant and restaurant.is_delivery_available:
            buttons.append({'id': 'cmd_delivery', 'title': 'Delivery'})
        if restaurant and restaurant.is_dine_in_available:
            buttons.append({'id': 'cmd_dine_in', 'title': 'Dine-in'})

        sess['state'] = 'ORDER_TYPE'
        set_session(phone, sess)
        wa.send_interactive_buttons(phone, msg, buttons)

    def _s_order_type(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        if text.upper() == 'BACK':
            self._show_cart(phone, sess)
            return ""

        restaurant = Restaurant.objects.filter(pk=sess.get('restaurant_id')).first()
        intent = parsed['intent']
        
        ot = None
        if intent == 'CMD_TAKEAWAY' or text == '1':
            ot = ('TAKEAWAY', '🛍️ Takeaway')
        elif intent == 'CMD_DELIVERY' or text == '2':
            ot = ('DELIVERY', '🛵 Delivery')
        elif intent == 'CMD_DINE_IN' or text == '3':
            ot = ('DINE_IN', '🍽️ Dine-in')
            
        if not ot:
            self._ask_order_type(phone, sess)
            return ""

        order_type, label = ot

        if order_type == 'DELIVERY' and restaurant and not restaurant.is_delivery_available:
            return "❌ Delivery is not available for this restaurant. Choose another option."
        if order_type == 'DINE_IN' and restaurant and not restaurant.is_dine_in_available:
            return "❌ Dine-in is not available for this restaurant. Choose another option."
        if order_type == 'TAKEAWAY' and restaurant and not restaurant.is_takeaway_available:
            return "❌ Takeaway is not available for this restaurant. Choose another option."

        sess['order_type'] = order_type
        sess['order_type_label'] = label

        if order_type == 'DELIVERY':
            sess['state'] = 'DELIVERY_ADDRESS'
            set_session(phone, sess)
            return (
                "🛵 *Delivery Address*\n\n"
                "Please send your delivery address.\n"
                "You can type it out or share your live location.\n\n"
                "Reply *BACK* to change order type."
            )

        if order_type == 'DINE_IN':
            sess['state'] = 'DINE_IN_INFO'
            set_session(phone, sess)
            return (
                "🍽️ *Dine-in Details*\n\n"
                "How many people in your party?\n"
                "Reply with a number (e.g. *4*)\n\n"
                "Reply *BACK* to change order type."
            )

        set_session(phone, sess)
        if not sess.get('cart'):
            self._show_categories(phone, sess)
            return ""

        if sess.get('max_usable_coins', 0) >= 1:
            self._ask_coins(phone, sess)
            return ""
        self._ask_payment(phone, sess)
        return ""

    def _s_delivery_address(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        if text.upper() == 'BACK':
            self._ask_order_type(phone, sess)
            return ""

        if len(text.strip()) < 5:
            return "Please provide a more detailed address (at least 5 characters)."

        sess['delivery_address'] = text.strip()
        set_session(phone, sess)

        if not sess.get('cart'):
            self._show_categories(phone, sess)
            return ""

        if sess.get('max_usable_coins', 0) >= 1:
            self._ask_coins(phone, sess)
            return ""
        self._ask_payment(phone, sess)
        return ""

    def _s_dine_in_info(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        if text.upper() == 'BACK':
            self._ask_order_type(phone, sess)
            return ""

        try:
            party_size = int(text.strip())
            if party_size < 1 or party_size > 50:
                raise ValueError
        except ValueError:
            return "Please enter a valid party size (1-50)."

        sess['party_size'] = party_size
        set_session(phone, sess)

        if not sess.get('cart'):
            self._show_categories(phone, sess)
            return ""

        if sess.get('max_usable_coins', 0) >= 1:
            self._ask_coins(phone, sess)
            return ""
        self._ask_payment(phone, sess)
        return ""

    def _ask_coins(self, phone: str, sess: dict):
        max_coins = int(sess.get('max_usable_coins', 0))
        sess['state'] = 'CHECKOUT_COINS'
        set_session(phone, sess)
        
        text = (
            f"💰 *Use Loyalty Coins?*\n\n"
            f"You can use up to *{max_coins} coins* on this order.\n\n"
            f"Would you like to use them?"
        )
        buttons = [
            {'id': 'cmd_yes', 'title': f"Yes (Use {max_coins})"},
            {'id': 'cmd_no', 'title': "No (Skip)"}
        ]
        wa.send_interactive_buttons(phone, text, buttons)

    def _s_checkout_coins(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        max_coins = Decimal(str(sess.get('max_usable_coins', 0)))
        upper = text.upper()
        intent = parsed['intent']

        if intent == 'CMD_YES' or upper == 'YES':
            sess['coins_to_use'] = float(max_coins)
        elif intent == 'CMD_NO' or upper == 'NO':
            sess['coins_to_use'] = 0.0
        else:
            try:
                requested = Decimal(text)
                if requested < 0:
                    raise ValueError
                if requested > max_coins:
                    return f"Maximum is {int(max_coins)} coins. Reply YES, NO or a lower number."
                sess['coins_to_use'] = float(requested)
            except Exception:
                self._ask_coins(phone, sess)
                return ""

        set_session(phone, sess)
        self._ask_payment(phone, sess)
        return ""

    def _ask_payment(self, phone: str, sess: dict):
        cart = sess.get('cart', [])
        total = Decimal(str(_cart_total(cart)))
        coins_to_use = Decimal(str(sess.get('coins_to_use', 0)))
        coin_discount = Decimal('0')

        if coins_to_use > 0 and sess.get('wallet_id'):
            try:
                wallet = CustomerWallet.objects.get(pk=sess['wallet_id'])
                coin_discount = coins_to_use * wallet.restaurant.loyalty_program.coin_value_in_rupees
            except Exception:
                pass

        payable = total - coin_discount
        sess['payable_amount'] = float(payable)
        sess['coin_discount'] = float(coin_discount)

        khata_opt = False
        if sess.get('user_id') and sess.get('wallet_id'):
            try:
                wallet = CustomerWallet.objects.get(pk=sess['wallet_id'])
                khata = KhataService.get_or_create_khata(wallet)
                if khata.lifelines_remaining > 0 and khata.status == 'ACTIVE':
                    sess['khata_available'] = True
                    khata_opt = True
            except Exception:
                pass

        sess['state'] = 'CHECKOUT_PAYMENT'
        set_session(phone, sess)

        order_label = sess.get('order_type_label', '🛍️ Takeaway')
        delivery_note = ""
        if sess.get('order_type') == 'DELIVERY' and sess.get('delivery_address'):
            delivery_note = f"\n📍 Deliver to: {sess['delivery_address']}"
        elif sess.get('order_type') == 'DINE_IN' and sess.get('party_size'):
            delivery_note = f"\n👥 Party size: {sess['party_size']}"

        coin_line = f"\n💰 Coins discount: -₹{float(coin_discount):.0f}" if coin_discount > 0 else ""
        text = (
            f"💳 *Payment*\n\n"
            f"📦 Order type: {order_label}{delivery_note}\n"
            f"Order total:  ₹{float(total):.0f}{coin_line}\n"
            f"*You pay:     ₹{float(payable):.0f}*\n\n"
            f"How would you like to pay?"
        )
        
        buttons = [
            {'id': 'cmd_pay_cash', 'title': 'Cash / Card'},
            {'id': 'cmd_pay_upi', 'title': 'UPI'}
        ]
        if khata_opt:
            buttons.append({'id': 'cmd_pay_khata', 'title': 'Pay Later (Khata)'})
            
        wa.send_interactive_buttons(phone, text, buttons)

    def _s_checkout_payment(self, phone: str, text: str, sess: dict, parsed: dict) -> str:
        if text.upper() == 'BACK':
            sess.pop('coins_to_use', None)
            sess.pop('payable_amount', None)
            self._show_cart(phone, sess)
            return ""

        intent = parsed['intent']
        method = None
        if intent == 'CMD_PAY_CASH' or text == '1':
            method = 'CASH'
        elif intent == 'CMD_PAY_UPI' or text == '2':
            method = 'UPI'
        elif intent == 'CMD_PAY_KHATA' or text == '3':
            method = 'KHATA'
            
        if not method:
            self._ask_payment(phone, sess)
            return ""
            
        if method == 'KHATA' and not sess.get('khata_available'):
            return "Pay Later is not available. Please choose Cash or UPI."
            
        return self._place_order(phone, sess, method)

    def _place_order(self, phone: str, sess: dict, payment_method: str) -> str:
        user_id = sess.get('user_id')
        if not user_id:
            clear_session(phone)
            return (
                "⚠️ To order via WhatsApp your phone number must be registered.\n\n"
                "Download the HotelFinder app → create account → come back here!"
            )

        try:
            user = User.objects.get(pk=user_id)
            restaurant = Restaurant.objects.get(pk=sess['restaurant_id'])
            cart = sess.get('cart', [])
            if not cart:
                return "Your cart is empty. Send *1* to view menu."

            order_type = sess.get('order_type', 'TAKEAWAY')

            with db_transaction.atomic():
                # Create booking — status is CONFIRMED (not COMPLETED)
                booking = Booking.objects.create(
                    restaurant=restaurant,
                    customer=user,
                    booking_type=order_type,
                    customer_name=user.get_full_name() or user.username,
                    contact_number=sess.get('phone', phone),
                    status='CONFIRMED',
                    payment_status='UNPAID' if payment_method == 'KHATA' else 'PAID',
                    payment_mode='ONLINE_QR' if payment_method == 'UPI' else 'COD',
                    order_notes=f'WhatsApp order via Interakt ({order_type})',
                    delivery_address=sess.get('delivery_address', ''),
                    party_size=sess.get('party_size') if order_type == 'DINE_IN' else None,
                )

                # Add items
                for item_data in cart:
                    menu_item = MenuItem.objects.filter(pk=item_data['item_id'], is_available=True).first()
                    if not menu_item:
                        logger.warning("Menu item %d no longer available, skipping", item_data['item_id'])
                        continue
                    BookingItem.objects.create(
                        booking=booking,
                        menu_item=menu_item,
                        quantity=item_data['qty'],
                        unit_price=menu_item.price,  # Use current price
                    )
                booking.calculate_total()
                booking.save()
                _sync_whatsapp_order_to_aahar(booking, cart)

                # Redeem coins (Iron Laws enforced)
                coins_to_use = Decimal(str(sess.get('coins_to_use', 0)))
                if coins_to_use >= 1 and sess.get('wallet_id'):
                    try:
                        wallet = CustomerWallet.objects.get(pk=sess['wallet_id'])
                        IronLawsEnforcer.validate_redemption(
                            wallet, booking.total_amount, coins_to_use
                        )
                        WalletService.credit_coins(
                            wallet, -coins_to_use, 'REDEEMED',
                            booking=booking,
                            notes=f"WhatsApp order #{booking.id}",
                        )
                    except Exception as e:
                        logger.warning("Coin redemption skipped for booking %d: %s", booking.id, e)

                # Khata lifeline
                if payment_method == 'KHATA' and sess.get('wallet_id'):
                    try:
                        wallet = CustomerWallet.objects.get(pk=sess['wallet_id'])
                        KhataService.use_lifeline(wallet, booking)
                    except Exception as e:
                        logger.warning("Khata use_lifeline failed for booking %d: %s", booking.id, e)

            # Notify restaurant (WebSocket + WhatsApp)
            _notify_restaurant_new_order(booking)

            # Track Order in Interakt
            _track_order_placed(booking, sess.get('phone', phone), payment_method)

            # Build confirmation message
            wallet_line = ""
            summit_line = ""
            try:
                loyalty_wallet = CustomerWallet.objects.filter(
                    customer=user, restaurant=restaurant
                ).first()
                if loyalty_wallet:
                    new_balance = WalletService.get_live_balance(loyalty_wallet)
                    program = restaurant.loyalty_program
                    target = getattr(program, 'summit_target_spend', None) or Decimal('7000')
                    spent = loyalty_wallet.lifetime_cash_spent
                    pct = min(100, int((spent / target) * 100))
                    wallet_line = f"\n\n💰 Coins balance: *{int(new_balance)}*"
                    summit_line = f"\n🏔️ Summit: {_progress_bar(pct)} {pct}% (₹{int(spent)}/₹{int(target)})"
            except Exception:
                pass

            order_type_emoji = {'TAKEAWAY': '🛍️', 'DELIVERY': '🛵', 'DINE_IN': '🍽️'}.get(order_type, '📦')
            khata_note = "\n📋 Added to your Khata tab — pay by end of month." if payment_method == 'KHATA' else ""
            delivery_note = f"\n📍 Delivery to: {sess.get('delivery_address', '')}" if order_type == 'DELIVERY' else ""
            dinein_note = f"\n👥 Party: {sess.get('party_size', '')} people" if order_type == 'DINE_IN' else ""

            tracking_note = ""
            if sess.get('is_new'):
                tracking_note = (
                    f"\n\n📱 *Track Online*\n"
                    f"Check order status online using your phone number.\n"
                    f"Password: *{sess.get('generated_password')}*\n"
                    f"_You will be prompted to create a new password upon login._"
                )

            clear_session(phone)
            return (
                f"✅ *Order #{booking.id} Confirmed!*\n\n"
                f"🍽️ {restaurant.name}\n"
                f"{order_type_emoji} {booking.get_booking_type_display()}"
                f"{delivery_note}{dinein_note}\n"
                f"💵 You pay: ₹{sess.get('payable_amount', float(booking.total_amount)):.0f}"
                f"{khata_note}{wallet_line}{summit_line}{tracking_note}\n\n"
                "⏱️ Your order is being prepared.\n"
                "You'll receive updates on this chat.\n\n"
                "Send *Hi* to place another order! 🎉"
            )

        except Exception as e:
            logger.error("WhatsApp order failed for %s: %s", phone, e, exc_info=True)
            clear_session(phone)
            return "❌ Something went wrong. Please try again or contact the restaurant directly."

    # ── INFO: WALLET ──────────────────────────────────────────────────

    def _show_wallet_info(self, phone: str, sess: dict) -> str:
        user_id = sess.get('user_id')
        set_session(phone, {**sess, 'state': 'MAIN_MENU'})
        if not user_id:
            return "Please register on HotelFinder to see your wallet.\n\nReply *0* to go back."

        try:
            user = User.objects.get(pk=user_id)
            restaurant = Restaurant.objects.get(pk=sess['restaurant_id'])
            wallet = get_or_create_wallet(user, restaurant)
            balance = WalletService.get_live_balance(wallet)
            program = restaurant.loyalty_program
            coin_value_rs = float(balance * program.coin_value_in_rupees)

            target = getattr(program, 'summit_target_spend', None) or Decimal('7000')
            spent = wallet.lifetime_cash_spent
            pct = min(100, int((spent / target) * 100))

            return (
                f"💰 *Your Loyalty Wallet — {restaurant.name}*\n\n"
                f"🪙 Balance:        *{int(balance)} coins* (≈ ₹{coin_value_rs:.0f})\n"
                f"🏆 Lifetime spent: ₹{int(spent)}\n"
                f"📋 Referral code:  *{wallet.referral_code or 'N/A'}*\n\n"
                f"🏔️ *Summit Progress*\n"
                f"[{_progress_bar(pct)}] {pct}%\n"
                f"₹{int(spent)} of ₹{int(target)} — "
                + ("*VIP! 🎉*" if pct >= 100 else f"₹{int(target) - int(spent)} to go") + "\n\n"
                "Reply *0* to go back."
            )
        except Exception as e:
            logger.error("Wallet info error: %s", e)
            return "Could not load wallet. Reply *0* to go back."

    # ── INFO: SUBSCRIPTION ────────────────────────────────────────────

    def _show_subscription_info(self, phone: str, sess: dict) -> str:
        user_id = sess.get('user_id')
        set_session(phone, {**sess, 'state': 'MAIN_MENU'})
        if not user_id:
            return "Register on HotelFinder to manage subscriptions.\n\nReply *0* to go back."

        try:
            user = User.objects.get(pk=user_id)
            restaurant = Restaurant.objects.get(pk=sess['restaurant_id'])
            wallet = CustomerWallet.objects.filter(customer=user, restaurant=restaurant).first()
            if not wallet:
                return "Place your first order to create a wallet.\n\nReply *0* to go back."

            sub = SubscriptionService.get_active_subscription(wallet)
            if sub:
                days_left = max(0, (sub.valid_until - timezone.now().date()).days)
                used_today = sub.last_used_date == timezone.now().date()
                return (
                    f"🥤 *Liquid Gold Pass — ACTIVE ✅*\n\n"
                    f"Valid until: {sub.valid_until.strftime('%d %b %Y')}\n"
                    f"Days left:   *{days_left}*\n"
                    f"Used today:  {'Yes ✅' if used_today else 'Not yet — claim your free drink! 🥤'}\n\n"
                    "Show this message to staff when ordering your complimentary beverage.\n\n"
                    "Reply *0* to go back."
                )
            return (
                "🥤 *Liquid Gold Pass — Not Active*\n\n"
                "Subscribe for ₹399/month and get a FREE beverage every single day!\n\n"
                "Open the HotelFinder app to subscribe.\n\n"
                "Reply *0* to go back."
            )
        except Exception:
            return "Could not load subscription. Reply *0* to go back."

    # ── INFO: KHATA ───────────────────────────────────────────────────

    def _show_khata_info(self, phone: str, sess: dict) -> str:
        user_id = sess.get('user_id')
        set_session(phone, {**sess, 'state': 'MAIN_MENU'})
        if not user_id:
            return "Register on HotelFinder to use Pay Later.\n\nReply *0* to go back."

        try:
            user = User.objects.get(pk=user_id)
            restaurant = Restaurant.objects.get(pk=sess['restaurant_id'])
            wallet = CustomerWallet.objects.filter(customer=user, restaurant=restaurant).first()
            if not wallet:
                return "Place your first order to create a wallet.\n\nReply *0* to go back."

            khata = KhataService.get_or_create_khata(wallet)
            remaining = khata.lifelines_remaining
            dots = "🟢" * remaining + "⚫" * (5 - remaining)
            status_icon = {'ACTIVE': '✅', 'OVERDUE': '⚠️', 'BLOCKED': '🔴'}.get(khata.status, '')
            tab_line = f"Open tab:  ₹{khata.current_tab_amount:.0f}" if khata.current_tab_amount > 0 else "Open tab:  None"
            due_line = f"\nDue by:    {khata.tab_due_date.strftime('%d %b %Y')}" if khata.tab_due_date and float(khata.current_tab_amount) > 0 else ""

            return (
                f"📋 *Pay Later (Khata)*\n\n"
                f"Status:    {status_icon} {khata.status}\n"
                f"Lifelines: {dots} ({remaining}/5 remaining)\n"
                f"{tab_line}{due_line}\n\n"
                "Each lifeline lets you defer one order's payment to end of month.\n\n"
                "Reply *0* to go back."
            )
        except Exception:
            return "Could not load Khata info. Reply *0* to go back."


# ─────────────────────────────────────────────────────────────────────
# Django view — Webhook endpoint
# ─────────────────────────────────────────────────────────────────────

_handler = ConversationHandler()


def _message_text(msg_obj: dict) -> str:
    if not isinstance(msg_obj, dict):
        return ""

    direct = msg_obj.get('message') or msg_obj.get('text') or msg_obj.get('body') or msg_obj.get('caption')
    if isinstance(direct, str):
        # Interakt occasionally encodes interactive replies as literal JSON strings.
        if direct.startswith('{') and direct.endswith('}'):
            try:
                import json
                parsed = json.loads(direct)
                if 'list_reply' in parsed:
                    return parsed['list_reply'].get('id') or parsed['list_reply'].get('title') or direct
                elif 'button_reply' in parsed:
                    return parsed['button_reply'].get('id') or parsed['button_reply'].get('title') or direct
                elif 'reply' in parsed:
                    return parsed['reply'].get('id') or parsed['reply'].get('title') or direct
            except Exception:
                pass
        return direct
    if isinstance(direct, dict):
        text = direct.get('body') or direct.get('text') or direct.get('message')
        if text:
            return text

    chat = msg_obj.get('chat')
    if isinstance(chat, dict):
        text = chat.get('text') or chat.get('body') or chat.get('message')
        if text:
            return text

    button = msg_obj.get('button') or msg_obj.get('button_reply')
    if isinstance(button, dict):
        return button.get('id') or button.get('title') or button.get('text') or ""

    interactive = msg_obj.get('interactive')
    if isinstance(interactive, dict):
        for key in ('button_reply', 'list_reply'):
            reply = interactive.get(key)
            if isinstance(reply, dict):
                text = reply.get('id') or reply.get('title') or reply.get('text')
                if text:
                    return text
        action = interactive.get('action')
        if isinstance(action, dict):
            button = action.get('button') or action.get('selected_button')
            if isinstance(button, str):
                return button

    return ""


def _verify_interakt_webhook(request, secret: str) -> bool:
    if not secret:
        return True

    token = (
        request.headers.get('X-Webhook-Secret')
        or request.headers.get('Interakt-Webhook-Secret')
        or request.headers.get('X-Interakt-Webhook-Secret')
        or request.GET.get('secret')
    )
    if token and hmac.compare_digest(token, secret):
        return True

    signature = (
        request.headers.get('Interakt-Signature')
        or request.headers.get('X-Interakt-Signature')
        or request.headers.get('X-Hub-Signature-256')
    )
    if not signature:
        return False

    provided_hash = signature.split('=', 1)[1] if signature.startswith('sha256=') else signature
    computed_hash = hmac.new(secret.encode('utf-8'), request.body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(provided_hash, computed_hash)


def _extract_message_meta(request) -> tuple:
    """
    Extract (phone, text, message_id) from inbound webhook regardless of provider.
    Returns (phone, text, message_id) or (None, None, None) if unparseable.
    """
    phone = None
    text = None
    message_id = None

    # 1. Twilio webhook: Content-Type is application/x-www-form-urlencoded
    if request.content_type == 'application/x-www-form-urlencoded' or request.POST:
        phone_raw = request.POST.get('From', '')
        if phone_raw.startswith('whatsapp:'):
            phone = phone_raw.replace('whatsapp:', '')
        else:
            phone = phone_raw
        text = request.POST.get('Body', '')
        message_id = request.POST.get('MessageSid', '')
        return phone, text, message_id

    # Try to load JSON payload
    try:
        body = json.loads(request.body)
    except (json.JSONDecodeError, UnicodeDecodeError):
        return None, None, None

    # 2. Meta webhook format
    if body.get('object') == 'whatsapp_business_account':
        try:
            value = body['entry'][0]['changes'][0]['value']
            messages = value.get('messages', [])
            if messages:
                msg = messages[0]
                phone = msg.get('from', '')
                message_id = msg.get('id', '')
                msg_type = msg.get('type', '')
                if msg_type == 'text':
                    text = msg.get('text', {}).get('body', '')
                elif msg_type == 'interactive':
                    interactive = msg.get('interactive', {})
                    if interactive.get('type') == 'button_reply':
                        text = interactive.get('button_reply', {}).get('id', '') or interactive.get('button_reply', {}).get('title', '')
                    elif interactive.get('type') == 'list_reply':
                        text = interactive.get('list_reply', {}).get('id', '') or interactive.get('list_reply', {}).get('title', '')
                    elif interactive.get('type') == 'nfm_reply':
                        text = interactive.get('nfm_reply', {}).get('body', '')
                elif msg_type == 'button':
                    text = msg.get('button', {}).get('text', '')
                elif msg_type == 'location':
                    # Handle location shares for delivery
                    loc = msg.get('location', {})
                    lat = loc.get('latitude', '')
                    lon = loc.get('longitude', '')
                    if lat and lon:
                        text = f"GPS:{lat},{lon}"
        except (KeyError, IndexError, TypeError) as e:
            logger.debug("Error parsing Meta webhook JSON: %s", e)

    # 3. Gupshup webhook format
    elif body.get('type') == 'message' and 'payload' in body:
        try:
            payload_obj = body['payload']
            phone = payload_obj.get('source', '')
            message_id = payload_obj.get('id', '') or body.get('messageId', '')
            msg_type = payload_obj.get('type', '')
            inner_payload = payload_obj.get('payload', {})
            if msg_type == 'text':
                text = inner_payload.get('text', '')
            elif msg_type in ('button_reply', 'list_reply', 'button', 'interactive'):
                text = inner_payload.get('title', '') or inner_payload.get('text', '') or inner_payload.get('reply_to_text', '')
            elif msg_type == 'location':
                text = f"GPS:{inner_payload.get('latitude', '')},{inner_payload.get('longitude', '')}"
        except (KeyError, IndexError, TypeError) as e:
            logger.debug("Error parsing Gupshup webhook JSON: %s", e)

    # 4. Interakt webhook format
    elif (body.get('type') in ('message_received', 'message') or body.get('event') in ('message_received', 'message')) and 'data' in body:
        try:
            data_obj = body['data']
            customer = data_obj.get('customer', {}) or data_obj.get('contact', {}) or {}
            phone = (
                customer.get('phoneNumber')
                or customer.get('phone_number')
                or customer.get('phone')
                or data_obj.get('phoneNumber')
                or data_obj.get('phone_number')
                or data_obj.get('from')
                or ''
            )
            msg_obj = data_obj.get('message') or data_obj.get('message_data') or data_obj
            if isinstance(msg_obj, list):
                msg_obj = msg_obj[0] if msg_obj else {}
            message_id = msg_obj.get('id', '') or data_obj.get('message_id', '') or body.get('message_id', '')
            text = _message_text(msg_obj)
            logger.error("INTERAKT WEBHOOK DEBUG: data_obj=%s, msg_obj=%s, parsed_text=%s", data_obj, msg_obj, text)

        except (KeyError, IndexError, TypeError) as e:
            logger.debug("Error parsing Interakt webhook JSON: %s", e)

    elif body.get('source') == 'interakt' or body.get('provider') == 'interakt':
        phone = body.get('phoneNumber') or body.get('phone_number') or body.get('from') or ''
        message_id = body.get('id', '') or body.get('message_id', '')
        text = _message_text(body.get('message') or body)

    # 5. WATI / generic format
    else:
        phone = (
            body.get('waId')
            or body.get('from')
            or body.get('phone')
            or (body.get('contacts') or [{}])[0].get('wa_id', '')
        )
        message_id = body.get('id', '') or body.get('messageId', '')
        msg_type = body.get('type', 'text')

        if msg_type == 'text':
            text = (
                body.get('text', '')
                or body.get('messageText', '')
                or body.get('message', '')
            )
            if isinstance(text, dict):
                text = text.get('body', '')
        elif msg_type in ('button', 'interactive'):
            text = (
                (body.get('button') or {}).get('text', '')
                or (body.get('interactive') or {}).get('button_reply', {}).get('title', '')
                or (body.get('interactive') or {}).get('list_reply', {}).get('title', '')
            )

    return phone, text, message_id


@method_decorator(csrf_exempt, name='dispatch')
class WhatsAppWebhookView(View):
    """
    POST — receives inbound messages from WATI / Gupshup / Twilio / Meta.
    GET  — responds to Meta webhook verification challenge.
    """

    def post(self, request):
        import os
        import time
        from django.conf import settings
        logger.info("WhatsAppWebhookView received POST request: Content-Type=%s, POST keys=%s", request.content_type, list(request.POST.keys()))
        
        # Temp debug log
        try:
            with open('webhook_debug.txt', 'a') as f:
                f.write(f"\n--- NEW WEBHOOK POST AT {time.time()} ---\n")
                f.write(f"Content-Type: {request.content_type}\n")
                f.write(f"Headers: {dict(request.headers)}\n")
                f.write(f"GET: {dict(request.GET)}\n")
                f.write(f"Body: {request.body.decode('utf-8', errors='ignore')}\n")
        except Exception as log_ex:
            logger.error("Failed to write to webhook_debug.txt: %s", log_ex)

        interakt_webhook_secret = os.environ.get('INTERAKT_WEBHOOK_SECRET', getattr(settings, 'INTERAKT_WEBHOOK_SECRET', '')) or 'e1a35451-089a-4cdd-89e3-739f0028e681'
        if interakt_webhook_secret and not (request.content_type == 'application/x-www-form-urlencoded' or request.POST):
            if not _verify_interakt_webhook(request, interakt_webhook_secret):
                logger.warning("Interakt webhook verification failed")
                return JsonResponse({'status': 'unauthorized', 'message': 'Webhook verification failed'}, status=401)
        
        phone, text, message_id = _extract_message_meta(request)

        if not phone or not text:
            return JsonResponse({'status': 'ignored'})

        phone = phone.strip()
        text = text.strip()

        # Idempotency — skip if already processed
        if message_id and is_duplicate_message(message_id):
            logger.debug("Duplicate message %s from %s — skipping", message_id, phone)
            if request.content_type == 'application/x-www-form-urlencoded' or request.POST:
                return HttpResponse('<Response></Response>', content_type='text/xml')
            return JsonResponse({'status': 'duplicate'})

        try:
            reply = _handler.handle(phone, text)
            if reply:
                wa.send_reply(phone, reply)
        except Exception as exc:
            logger.error("WhatsApp webhook error for %s: %s", phone, exc, exc_info=True)
            try:
                wa.send_reply(phone, "❌ Something went wrong. Please try again or send *Hi* to restart.")
            except Exception:
                pass

        # Twilio expects a TwiML response (even if empty)
        if request.content_type == 'application/x-www-form-urlencoded' or request.POST:
            return HttpResponse('<Response></Response>', content_type='text/xml')

        return JsonResponse({'status': 'ok'})

    def get(self, request):
        import os
        from django.conf import settings

        mode = request.GET.get('hub.mode', '')
        token = request.GET.get('hub.verify_token', '')
        challenge = request.GET.get('hub.challenge', '')

        # Meta verify token check
        meta_verify_token = os.environ.get('META_WEBHOOK_VERIFY_TOKEN', getattr(settings, 'META_WEBHOOK_VERIFY_TOKEN', ''))
        if mode == 'subscribe' and token:
            if meta_verify_token and token != meta_verify_token:
                return HttpResponse("Forbidden", status=403)
            return HttpResponse(challenge)

        if challenge:
            return HttpResponse(challenge)

        return JsonResponse({
            'status': 'ok',
            'provider': wa.get_whatsapp_provider(),
            'webhook_secret_required': bool(os.environ.get('INTERAKT_WEBHOOK_SECRET', getattr(settings, 'INTERAKT_WEBHOOK_SECRET', '')) or 'e1a35451-089a-4cdd-89e3-739f0028e681'),
        })
