#!/usr/bin/env python3
from http.server import SimpleHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
import urllib.request
import urllib.error
import urllib.parse
import json
import gzip
import struct
import socketserver
import os
import re
import signal
import subprocess
import base64
import threading
import time
from concurrent.futures import ThreadPoolExecutor

# Free port 8080/8081 if an old http.server is stuck
try:
    out = subprocess.check_output(["pgrep", "-f", "http.server"], text=True)
    for pid in out.strip().splitlines():
        if int(pid) != os.getpid():
            os.kill(int(pid), signal.SIGKILL)
except Exception:
    pass

def read_response_bytes(resp):
    raw = resp.read()
    if resp.headers.get('Content-Encoding', '').lower() == 'gzip' or (len(raw) > 2 and raw[:2] == b'\x1f\x8b'):
        try:
            return gzip.decompress(raw)
        except Exception:
            return raw
    return raw

# =========================================================================
# THREAD-SAFE PER-USER ISOLATED SESSION CACHE (100+ CONCURRENT USERS READY!)
# =========================================================================
SESSION_LOCK = threading.Lock()
USER_SESSIONS = {}

def get_user_session(cid, dev):
    key = f"{str(cid).strip()}_{str(dev).strip()}"
    with SESSION_LOCK:
        if key not in USER_SESSIONS:
            USER_SESSIONS[key] = {
                "csrf": "dNBVdnjWvmNQ-uBHZhcY-E6JWMV3g6eLZ-EXmIco",
                "cookies": {},
                "last_aid": "",
                "last_share_link": "",
                "updated_at": time.time()
            }
        USER_SESSIONS[key]["updated_at"] = time.time()
        if len(USER_SESSIONS) > 600:
            cutoff = time.time() - 3600
            for k in list(USER_SESSIONS.keys()):
                if USER_SESSIONS[k]["updated_at"] < cutoff:
                    del USER_SESSIONS[k]
        return USER_SESSIONS[key]

SWEEP_QUERIES = [
    "Biryani", "Dosa", "Pizza", "Burger", "Noodles", "Fried Rice",
    "Chicken", "Paneer", "Rolls", "Shawarma", "Thali", "Paratha",
    "Cake", "Ice Cream", "Momos", "Sandwich", "Idli", "Pasta",
    "Soup", "Manchurian", "Kebab", "Tandoori", "Coffee", "Sweets"
]

DISCOUNT_COLLECTIONS = ["419140", "417660", "429606", "412007", "209102", "400157"]

IGNORE_CONDIMENT_WORDS = (
    "ketchup", "sauce", "mayo", "mayonnaise", "dip", "chutney", "raita", "salan",
    "papad", "pickle", "water bottle", "mineral water", "carry bag", "cutlery",
    "extra ", "addon", "add-on", "green salad", "onion salad", "lemon", "tissue"
)

FOOD_SYNONYMS = {
    "biryani": ["biryani", "biriyani", "briyani", "burrito rice", "kushka", "pulav", "pulao"],
    "briyani": ["biryani", "biriyani", "briyani", "burrito rice", "kushka", "pulav", "pulao"],
    "biriyani": ["biryani", "biriyani", "briyani", "burrito rice", "kushka", "pulav", "pulao"],
    "chicken": ["chicken", "chiken", "chk", "murg", "murgh", "kozhi"],
    "chiken": ["chicken", "chiken", "chk", "murg", "murgh", "kozhi"],
    "dosa": ["dosa", "dosai", "doshai", "masala dosa", "kal dosa", "roast", "uthappam"],
    "pizza": ["pizza", "pizzas", "margherita", "pepperoni"],
    "burger": ["burger", "burgers", "zinger", "whopper", "mcchicken", "maharaja"],
    "noodles": ["noodles", "noodle", "hakka", "chowmein", "chow mein", "schezwan"],
    "paneer": ["paneer", "panner", "cottage cheese"],
    "shawarma": ["shawarma", "shawrma", "shwarma", "roll", "wrap", "frankie"]
}

def expand_food_query_tokens(food_query):
    q = (food_query or "").strip().lower()
    if not q:
        return []
    words = [w for w in re.split(r'\s+', q) if len(w) >= 2]
    token_groups = []
    for w in words:
        if w in FOOD_SYNONYMS:
            token_groups.append(FOOD_SYNONYMS[w])
        else:
            token_groups.append([w])
    return token_groups

def dish_name_matches_query(dish_name, token_groups, cat_name=""):
    if not token_groups:
        return True
    hay = f"{dish_name or ''} {cat_name or ''}".lower()
    for grp in token_groups:
        if not any(variant in hay for variant in grp):
            return False
    return True

def update_cookies_from_headers(headers, user_sess):
    for k, v in headers.items():
        if k.lower() == 'set-cookie':
            parts = v.split(';')[0].split('=', 1)
            if len(parts) == 2:
                ck_name = parts[0].strip()
                ck_val = parts[1].strip()
                if ck_name != "userLocation":
                    user_sess["cookies"][ck_name] = ck_val

def build_cookie_header(dev, sid, cid, tid, token, lat, lng, user_sess):
    base = {
        "__SW": "OhHG5TmyLPaxwCFzMxnpuS5jbN-U7dSp",
        "_device_id": dev,
        "_swuid": dev,
        "_sid": sid,
        "_is_logged_in": "1",
        "_cid": str(cid),
        "tid": tid,
        "token": token,
        "a_platform": "jiohotstar",
        "x-channel": "jiohotstar",
        "dadl": "true"
    }
    base.update(user_sess.get("cookies", {}))
    # Strictly bind this request's own lat/lng so no user ever gets another user's location
    base["userLocation"] = f"%7B%22lat%22%3A{lat}%2C%22lng%22%3A{lng}%2C%22address%22%3A%22India%22%7D"
    return "; ".join([f"{k}={v}" for k, v in base.items() if v])

def is_restaurant_open_and_orderable(inf):
    if not isinstance(inf, dict):
        return False
    if inf.get("unorderableMessage") or inf.get("availabilityServiceabilityMessage"):
        return False
    avail = inf.get("availability")
    if isinstance(avail, dict):
        if avail.get("opened") is False:
            return False
        closed_meta = avail.get("restaurantClosedMeta")
        if isinstance(closed_meta, dict) and (closed_meta.get("title") or closed_meta.get("subtitle")):
            return False
    ord_comm = inf.get("orderabilityCommunication")
    if isinstance(ord_comm, dict):
        sub_t = ((ord_comm.get("subTitle") or {}).get("text") or "").upper()
        if "UNAVAILABLE" in sub_t or "CLOSED" in sub_t:
            return False
    sla = inf.get("sla")
    if isinstance(sla, dict):
        serv = (sla.get("serviceability") or "").upper()
        if serv in ("NON_SERVICEABLE", "UNSERVICEABLE", "CLOSED"):
            return False
    return True

def get_card_offer_string(inf):
    if not isinstance(inf, dict):
        return ""
    d3 = inf.get("aggregatedDiscountInfoV3")
    if isinstance(d3, dict):
        h = (d3.get("header") or "").strip()
        s = (d3.get("subHeader") or "").strip()
        if h or s:
            return f"{h} {s}".strip()
    d2 = inf.get("aggregatedDiscountInfoV2")
    if isinstance(d2, dict) and d2.get("header"):
        return str(d2.get("header")).strip()
    if inf.get("offer") and inf.get("offer") != "REGULAR MENU":
        return str(inf.get("offer")).strip()
    return ""

def get_free_delivery_text(inf):
    if not isinstance(inf, dict):
        return ""
    loy = inf.get("loyaltyDiscoverPresentationInfo")
    if isinstance(loy, dict) and loy.get("freedelMessage"):
        return str(loy.get("freedelMessage")).strip()
    return ""

def fetch_search_v3_cards(lat, lng, query_str, web_headers, meta_str="", submit_action="ENTER"):
    q_enc = urllib.parse.quote(query_str)
    meta_param = f"&metaData={urllib.parse.quote(meta_str)}" if meta_str else ""
    urls = [
        f"https://www.swiggy.com/mapi/restaurants/search/v3?lat={lat}&lng={lng}&str={q_enc}&trackingId=cb9fdfe2-9356-6afa-9c41-4e04f9dcfaab&submitAction={submit_action}&queryUniqueId=6c3a0b25-949d-42d3-8db5-b92b125c8f81{meta_param}",
        f"https://www.swiggy.com/dapi/restaurants/search/v3?lat={lat}&lng={lng}&str={q_enc}&trackingId=cb9fdfe2-9356-6afa-9c41-4e04f9dcfaab&submitAction={submit_action}&queryUniqueId=6c3a0b25-949d-42d3-8db5-b92b125c8f81{meta_param}"
    ]
    found_cards = []
    for u in urls:
        try:
            req = urllib.request.Request(u, headers=web_headers, method='GET')
            with urllib.request.urlopen(req, timeout=8) as resp:
                raw = read_response_bytes(resp)
                j = json.loads(raw.decode('utf-8', errors='ignore'))
                if j.get("data") and isinstance(j["data"], dict):
                    cards = j["data"].get("cards", [])
                    if cards:
                        found_cards.extend(cards)
                        break
        except Exception:
            pass
    return found_cards

def fetch_collection_v5_cards(lat, lng, coll_id, offset, sort_by, filters, web_headers, user_sess):
    u = (
        f"https://www.swiggy.com/mapi/restaurants/list/v5?"
        f"lat={lat}&lng={lng}&collection={coll_id}&tags=layout_hotstar"
        f"&sortBy={urllib.parse.quote(sort_by or '')}&filters={urllib.parse.quote(filters or '')}"
        f"&type=rcv2&offset={offset}&carousel=true&third_party_vendor=1"
    )
    h = dict(web_headers)
    h['Referer'] = f"https://www.swiggy.com/collections?collection_id={coll_id}&tags=layout_hotstar&type=rcv2"
    try:
        req = urllib.request.Request(u, headers=h, method='GET')
        with urllib.request.urlopen(req, timeout=9) as resp:
            update_cookies_from_headers(resp.headers, user_sess)
            raw = read_response_bytes(resp)
            return json.loads(raw.decode('utf-8', errors='ignore'))
    except Exception:
        return {}

def safe_int_price(val):
    try:
        v = int(float(str(val).strip()))
        return v if v > 0 else 0
    except Exception:
        return 0

def extract_restaurants_from_cards(cards, include_closed=False):
    """
    Extracts unique restaurants from any Swiggy card structure (including DishGroup from search/v3!).
    Properly handles `swiggy_final_price: "0"` vs `finalPrice: 9900` so discounted items in Dish Search show exact `₹159 -> ⚡ ₹99`!
    """
    restos = {}
    def walk(obj):
        if isinstance(obj, dict):
            if obj.get("@type") == "type.googleapis.com/swiggy.presentation.food.v2.DishGroup" or (obj.get("restaurant") and isinstance(obj.get("dishes"), list)):
                r_wrap = obj.get("restaurant") or {}
                inf = r_wrap.get("info") if isinstance(r_wrap, dict) else None
                if isinstance(inf, dict) and inf.get("id") and inf.get("name"):
                    if not include_closed and not is_restaurant_open_and_orderable(inf):
                        return
                    rid = str(inf["id"])
                    raw_dishes = obj.get("dishes") or []
                    valid_dishes = []
                    for d_item in raw_dishes:
                        d_inf = d_item.get("info") if isinstance(d_item, dict) else (d_item if isinstance(d_item, dict) else None)
                        if isinstance(d_inf, dict) and d_inf.get("name"):
                            if d_inf.get("nextAvailableAtMessage"):
                                continue
                            if "inStock" in d_inf and d_inf.get("inStock") in (0, False, "0"):
                                continue
                            if d_inf.get("isEnabled") in (0, False, "0"):
                                continue

                            meta_map = d_inf.get("instrumentationMetaMap") or {}
                            l_paise = (
                                safe_int_price(meta_map.get("swiggy_listing_price"))
                                or safe_int_price(d_inf.get("price"))
                                or safe_int_price(d_inf.get("defaultPrice"))
                            )
                            f_paise = (
                                safe_int_price(d_inf.get("finalPrice"))
                                or safe_int_price(meta_map.get("swiggy_final_price"))
                            )
                            eff_paise = f_paise if (0 < f_paise < l_paise) else l_paise
                            if eff_paise <= 0:
                                continue

                            l_rs = round(l_paise / 100) if l_paise > 0 else round(eff_paise / 100)
                            f_rs = round(f_paise / 100) if (0 < f_paise < l_paise) else 0
                            eff_rs = f_rs if f_rs > 0 else l_rs

                            rat_obj = ((d_inf.get("ratings") or {}).get("aggregatedRating") or {})
                            d_rating = str(rat_obj.get("rating") or "").strip()
                            d_rcount = str(rat_obj.get("ratingCountV2") or rat_obj.get("ratingCount") or "").strip()
                            rib_text = str((d_inf.get("ribbon") or {}).get("text") or "").strip()
                            is_veg = bool(d_inf.get("isVeg") in (1, True, "1") or ((d_inf.get("itemAttribute") or {}).get("vegClassifier") == "VEG"))
                            cat_str = str(d_inf.get("category") or "").strip()

                            valid_dishes.append({
                                "id": str(d_inf.get("id", "")),
                                "name": d_inf.get("name"),
                                "category": cat_str,
                                "description": str(d_inf.get("description") or "").strip(),
                                "priceRupees": eff_rs,
                                "listingRupees": l_rs,
                                "finalRupees": f_rs,
                                "isAlreadyDiscounted": (0 < f_rs < l_rs) or ("99 store" in cat_str.lower()) or ("items starting" in cat_str.lower()),
                                "is99Store": (eff_rs <= 149 and (0 < f_rs < l_rs or "99" in cat_str.lower())),
                                "isVeg": is_veg,
                                "isBestseller": ("BESTSELLER" in rib_text.upper()) or bool(d_inf.get("isBestseller")),
                                "rating": d_rating,
                                "ratingCount": d_rcount,
                                "imageId": str(d_inf.get("imageId") or "").lstrip("/")
                            })
                    if raw_dishes and not valid_dishes:
                        return
                    if rid not in restos:
                        inf_copy = dict(inf)
                        inf_copy["offer"] = get_card_offer_string(inf)
                        inf_copy["freeDeliveryText"] = get_free_delivery_text(inf)
                        if valid_dishes:
                            inf_copy["_matchedDishes"] = valid_dishes
                        restos[rid] = inf_copy
                    elif valid_dishes:
                        existing_d = restos[rid].get("_matchedDishes", [])
                        seen_dids = {x["id"] for x in existing_d}
                        for vd in valid_dishes:
                            if vd["id"] not in seen_dids:
                                existing_d.append(vd)
                        restos[rid]["_matchedDishes"] = existing_d
                return

            if obj.get("restaurant") and isinstance(obj["restaurant"], dict) and obj["restaurant"].get("info"):
                inf = obj["restaurant"]["info"]
                if isinstance(inf, dict) and inf.get("id") and inf.get("name"):
                    if include_closed or is_restaurant_open_and_orderable(inf):
                        rid = str(inf["id"])
                        if rid not in restos:
                            inf_copy = dict(inf)
                            inf_copy["offer"] = get_card_offer_string(inf)
                            inf_copy["freeDeliveryText"] = get_free_delivery_text(inf)
                            restos[rid] = inf_copy

            elif obj.get("info") and isinstance(obj["info"], dict):
                inf = obj["info"]
                if inf.get("id") and inf.get("name") and (inf.get("sla") or inf.get("costForTwo") or inf.get("cloudinaryImageId") or inf.get("aggregatedDiscountInfoV3")):
                    if include_closed or is_restaurant_open_and_orderable(inf):
                        rid = str(inf["id"])
                        if rid not in restos or (not get_card_offer_string(restos[rid]) and get_card_offer_string(inf)):
                            inf_copy = dict(inf)
                            inf_copy["offer"] = get_card_offer_string(inf)
                            inf_copy["freeDeliveryText"] = get_free_delivery_text(inf)
                            restos[rid] = inf_copy

            for v in obj.values():
                walk(v)
        elif isinstance(obj, list):
            for it in obj:
                walk(it)
    walk(cards)
    return list(restos.values())

def fetch_raw_menu_for_resto(lat, lng, r_id, web_headers):
    menu_url = f"https://www.swiggy.com/mapi/menu/pl?page-type=REGULAR_MENU&complete-menu=true&lat={lat}&lng={lng}&restaurantId={r_id}&submitAction=ENTER"
    mh = dict(web_headers)
    mh['Accept'] = 'application/x-protobuf'
    mh['Referer'] = f"https://www.swiggy.com/jhs/menu?restaurant_id={r_id}"
    try:
        req = urllib.request.Request(menu_url, headers=mh, method='GET')
        with urllib.request.urlopen(req, timeout=7) as resp:
            return read_response_bytes(resp)
    except Exception:
        return b""

def extract_dish_main_floats(ch_bytes):
    """
    100% Exact Dish Header Float Extractor (Verified on MK Food 1411483 & New Hyderabad 901062 dumps):
    - Splits BEFORE `R\\x00Z\\x00` so we NEVER touch `r` (Field 14 Add-ons like Extra Cheese ₹20, Naan ₹49, Raita ₹15)!
    - Reads `E` (0x45 = Field 8 float32 = Listing Price in paise) and `M` (0x4d = Field 9 float32 = Final Discount Price in paise).
    """
    header_part = ch_bytes.split(b"R\x00Z\x00")[0] if b"R\x00Z\x00" in ch_bytes else ch_bytes[:300]
    lp_paise = 0
    fp_paise = 0

    # Case A: Both `E<4bytes>` (Field 8 Listing Price) and `M<4bytes>` (Field 9 Discounted Price) exist consecutively!
    m_both = re.search(rb'E(.{4})M(.{4})', header_part, flags=re.DOTALL)
    if m_both:
        try:
            f1 = struct.unpack('<f', m_both.group(1))[0]
            f2 = struct.unpack('<f', m_both.group(2))[0]
            if 500 <= f1 <= 1500000:
                lp_paise = int(round(f1))
            if 500 <= f2 < lp_paise:
                fp_paise = int(round(f2))
            return lp_paise, fp_paise
        except Exception:
            pass

    # Case B: Only `E<4bytes>` (Field 8 Listing Price, NO Discount!)
    for m_e in re.finditer(rb'E(.{4})', header_part, flags=re.DOTALL):
        try:
            f1 = struct.unpack('<f', m_e.group(1))[0]
            if 500 <= f1 <= 1500000 and abs(f1 - round(f1)) < 0.05 and (round(f1) % 100 == 0):
                lp_paise = int(round(f1))
                break
        except Exception:
            pass

    # Case C: Multi-variant dish where base price is inside variant group `\x15<4bytes>` (Field 2 float32)
    if lp_paise == 0:
        for m_v in re.finditer(rb'\x15(.{4})', ch_bytes[:420], flags=re.DOTALL):
            try:
                fv = struct.unpack('<f', m_v.group(1))[0]
                if 1500 <= fv <= 500000 and abs(fv - round(fv)) < 0.05 and (round(fv) % 100 == 0):
                    lp_paise = int(round(fv))
                    break
            except Exception:
                pass

    return lp_paise, fp_paise

def quick_parse_menu_bytes(raw_bytes):
    dishes = []
    coupons = []
    header_offer = ""
    is_closed = False

    if not raw_bytes:
        return dishes, coupons, header_offer, True

    text = raw_bytes.decode('utf-8', errors='ignore')

    closed_markers = (
        "Currently closed for delivery",
        "Currently not accepting orders",
        "Outlet is not accepting orders",
        "Currently not taking orders",
        "UNAVAILABLE\x12\x0dAT THE MOMENT"
    )
    if any(cm.lower() in text.lower() for cm in closed_markers) or b"\n\x06CLOSED" in raw_bytes or b"\n\x06Closed\x12" in raw_bytes:
        is_closed = True

    m_top_offer = re.search(r'(\d+%\s*Off\s*\|\s*Use\s+[A-Z0-9]+|Flat\s*₹\d+\s*off[^"\x00-\x1f]{0,30}|\d+%\s*OFF\s*UPTO\s*₹\d+)', text, flags=re.IGNORECASE)
    if m_top_offer:
        header_offer = m_top_offer.group(1).strip()

    coupon_matches = re.findall(
        r'((?:\d+%\s*OFF(?:\s*UPTO\s*₹\d+)?|FLAT\s*₹?\d+%?\s*OFF|ITEMS\s*AT\s*₹\d+|FREE\s*DELIVERY|Free\s+[A-Za-z0-9\s\(\)]+))[\s\S]{1,220}?((?:USE\s+[A-Z0-9]+|NO\s+CODE\s+REQUIRED|DEAL\s+OF\s+DAY))[\s\S]{0,70}?((?:ABOVE\s*₹\d+|ON\s*SELECT\s*ITEMS)?)',
        text, flags=re.IGNORECASE
    )
    seen_codes = set()
    for hdr, code, above in coupon_matches:
        h_clean = hdr.strip()
        c_clean = code.strip().upper()
        key = f"{h_clean}_{c_clean}"
        if key not in seen_codes:
            seen_codes.add(key)
            coupons.append({
                "header": h_clean,
                "code": c_clean,
                "min": above.strip()
            })

    if not header_offer and coupons:
        header_offer = f"{coupons[0]['header']} ({coupons[0]['code']})"

    if raw_bytes.lstrip().startswith(b'{'):
        try:
            j = json.loads(text)
            seen_ids = set()
            def walk_j(o, cat_name="Recommended"):
                nonlocal is_closed
                if isinstance(o, dict):
                    if o.get("unorderableMessage") or o.get("availabilityServiceabilityMessage"):
                        is_closed = True
                    if isinstance(o.get("title"), str) and len(o["title"]) < 60 and (o.get("itemCards") or o.get("categories")):
                        cat_name = o["title"]
                    if o.get("itemCards") and isinstance(o["itemCards"], list):
                        for ic in o["itemCards"]:
                            inf = (ic.get("card") or {}).get("info") or ic.get("info") or {}
                            did = str(inf.get("id", ""))
                            dname = (inf.get("name") or "").strip()
                            if not dname or did in seen_ids:
                                continue
                            if inf.get("nextAvailableAtMessage"):
                                continue
                            if "inStock" in inf and not inf.get("inStock"):
                                continue
                            lp = safe_int_price(inf.get("price")) or safe_int_price(inf.get("defaultPrice"))
                            fp = safe_int_price(inf.get("finalPrice"))
                            if lp > 0:
                                seen_ids.add(did)
                                lp_rs = round(lp / 100)
                                fp_rs = round(fp / 100) if (0 < fp < lp) else 0
                                dishes.append({
                                    "id": did,
                                    "name": dname,
                                    "category": cat_name,
                                    "listingRupees": lp_rs,
                                    "finalRupees": fp_rs,
                                    "isAlreadyDiscounted": (0 < fp_rs < lp_rs) or ("99 store" in cat_name.lower())
                                })
                    for v in o.values():
                        walk_j(v, cat_name)
                elif isinstance(o, list):
                    for it in o:
                        walk_j(it, cat_name)
            walk_j(j)
            return dishes, coupons, header_offer, is_closed
        except Exception:
            pass

    chunks = raw_bytes.split(b"type.googleapis.com/swiggy.presentation.food.v2.Dish")
    seen_ids = set()

    for ch in chunks[1:]:
        ch_str = ch.decode('utf-8', errors='ignore')
        if "Next available at" in ch_str:
            continue

        m_id_name = re.search(r'(\d{6,12})\x12[\x02-\x60]([A-Za-z0-9][^\x00-\x1f]{1,90})', ch_str)
        if not m_id_name:
            continue
        d_id = m_id_name.group(1)
        d_name = m_id_name.group(2).strip()
        if d_id in seen_ids:
            continue

        m_list = re.search(r'swiggy_listing_price\x12[\x01-\x08](\d{1,7})', ch_str)
        m_final = re.search(r'swiggy_final_price\x12[\x01-\x08](\d{1,7})', ch_str)

        lp = int(m_list.group(1)) if m_list else 0
        fp = int(m_final.group(1)) if m_final else 0

        hdr_lp, hdr_fp = extract_dish_main_floats(ch)
        if lp < 500 and hdr_lp >= 500:
            lp = hdr_lp
        if fp == 0 and 500 <= hdr_fp < lp:
            fp = hdr_fp

        if lp < 500 or lp > 1500000:
            continue

        lp_rs = max(1, round(lp / 100))
        fp_rs = round(fp / 100) if (0 < fp < lp) else 0

        seen_ids.add(d_id)
        dishes.append({
            "id": d_id,
            "name": d_name,
            "category": "",
            "listingRupees": lp_rs,
            "finalRupees": fp_rs,
            "isAlreadyDiscounted": (0 < fp_rs < lp_rs)
        })

    return dishes, coupons, header_offer, is_closed

def does_offer_match_filter(discount_filter, combined_offer_text):
    if not discount_filter or discount_filter == "ALL_RESTOS":
        return True
    up_text = (combined_offer_text or "").upper()
    if not up_text:
        return False
    if discount_filter == "ALL_DISCOUNT":
        return bool(re.search(r'(\d+%\s*OFF|FLAT\s*₹?\d+|ITEMS\s*AT\s*₹\d+|₹\d+\s*OFF)', up_text))

    clean_num = re.sub(r'[^0-9]', '', discount_filter)
    if clean_num and len(clean_num) <= 2:
        target_pct = int(clean_num)
        pct_matches = [int(x) for x in re.findall(r'(\d+)%\s*OFF', up_text)]
        if any(p >= target_pct for p in pct_matches):
            return True
        return False
    return discount_filter in up_text

def estimate_net_price_after_coupons(base_price, coupons, combined_offer_text):
    best_net = base_price
    text_pool = " | ".join([f"{c.get('header','')} {c.get('min','')}" for c in (coupons or [])]) + " | " + (combined_offer_text or "")
    for m in re.finditer(r'(\d+)%\s*OFF(?:\s*UPTO\s*₹(\d+))?', text_pool, flags=re.IGNORECASE):
        pct = int(m.group(1))
        cap = int(m.group(2)) if m.group(2) else 150
        disc = min(round(base_price * pct / 100), cap)
        best_net = min(best_net, max(1, base_price - disc))
    for m_flat in re.finditer(r'FLAT\s*₹(\d+)\s*OFF', text_pool, flags=re.IGNORECASE):
        f_disc = int(m_flat.group(1))
        if base_price >= f_disc + 50:
            best_net = min(best_net, base_price - f_disc)
    return best_net

def analyze_single_restaurant_for_compare(r_info, lat, lng, discount_filter, food_query, token_groups, web_headers):
    if not is_restaurant_open_and_orderable(r_info):
        return None
    rid = str(r_info.get("id"))
    card_offer = get_card_offer_string(r_info)

    raw_m = fetch_raw_menu_for_resto(lat, lng, rid, web_headers)
    dishes, coupons, menu_hdr_offer, is_closed_menu = quick_parse_menu_bytes(raw_m)

    if is_closed_menu:
        return None

    pre_matched = r_info.get("_matchedDishes") or []
    if pre_matched and not dishes:
        dishes = pre_matched

    if not dishes:
        return None

    all_coupon_text = " | ".join([f"{c['header']} {c['code']}" for c in coupons])
    combined_offer_haystack = f"{card_offer} | {menu_hdr_offer} | {all_coupon_text}".strip()

    if not does_offer_match_filter(discount_filter, combined_offer_haystack):
        return None

    display_offer = card_offer or menu_hdr_offer or (f"{coupons[0]['header']} ({coupons[0]['code']})" if coupons else "")

    pct_list = [int(x) for x in re.findall(r'(\d+)%\s*OFF', combined_offer_haystack.upper())]
    max_pct = max(pct_list) if pct_list else 0

    non_discounted_dishes = [d for d in dishes if not d.get("isAlreadyDiscounted")]

    if food_query:
        matched_dishes = []
        for d in non_discounted_dishes:
            dname_low = d["name"].lower()
            if d["listingRupees"] < 30:
                continue
            if any(cw in dname_low for cw in IGNORE_CONDIMENT_WORDS):
                continue
            if dish_name_matches_query(d["name"], token_groups, d.get("category", "")):
                matched_dishes.append(d)

        if not matched_dishes and pre_matched:
            for pm in pre_matched:
                if not pm.get("isAlreadyDiscounted") and pm.get("listingRupees", 0) >= 30:
                    matched_dishes.append(pm)

        if not matched_dishes:
            for d in dishes:
                dname_low = d["name"].lower()
                if d["listingRupees"] < 40 or d.get("finalRupees") in (9, 19, 29, 49, 79, 89, 99):
                    continue
                if any(cw in dname_low for cw in IGNORE_CONDIMENT_WORDS):
                    continue
                if dish_name_matches_query(d["name"], token_groups, d.get("category", "")):
                    matched_dishes.append(d)

        if not matched_dishes:
            return None

        matched_dishes.sort(key=lambda x: x["listingRupees"])
        best_dish = matched_dishes[0]
        sample_slice = matched_dishes[:10]
        avg_base_price = round(sum(x["listingRupees"] for x in sample_slice) / len(sample_slice))
        est_net = estimate_net_price_after_coupons(best_dish["listingRupees"], coupons, combined_offer_haystack)

        r_copy = dict(r_info)
        r_copy["offer"] = display_offer
        r_copy["_compare"] = {
            "mode": "FOOD_SPECIFIC",
            "foodQuery": food_query,
            "matchedDishCount": len(matched_dishes),
            "primaryDishName": best_dish["name"],
            "primaryBasePrice": best_dish["listingRupees"],
            "estimatedNetPrice": est_net,
            "avg10BasePrice": avg_base_price,
            "sortScorePrice": best_dish["listingRupees"],
            "discountPercent": max_pct,
            "offerBanner": display_offer,
            "topDishes": matched_dishes[:6],
            "coupons": coupons,
            "totalNonDiscountedCount": len(non_discounted_dishes)
        }
        return r_copy
    else:
        pool_for_avg = non_discounted_dishes if len(non_discounted_dishes) >= 4 else dishes
        real_food_dishes = []
        for d in pool_for_avg:
            dname_low = d["name"].lower()
            if d["listingRupees"] < 45:
                continue
            if any(cw in dname_low for cw in IGNORE_CONDIMENT_WORDS):
                continue
            real_food_dishes.append(d)

        real_food_dishes.sort(key=lambda x: x["listingRupees"])
        if len(real_food_dishes) < 3:
            return None

        top_10_foods = real_food_dishes[:10]
        avg_10_price = round(sum(x["listingRupees"] for x in top_10_foods) / len(top_10_foods))
        min_food = top_10_foods[0]
        est_net = estimate_net_price_after_coupons(avg_10_price, coupons, combined_offer_haystack)

        r_copy = dict(r_info)
        r_copy["offer"] = display_offer
        r_copy["_compare"] = {
            "mode": "MIN_10_FOOD_AVG",
            "foodQuery": "",
            "matchedDishCount": len(real_food_dishes),
            "primaryDishName": f"Avg of {len(top_10_foods)} Main Foods (Starts ₹{min_food['listingRupees']})",
            "primaryBasePrice": avg_10_price,
            "estimatedNetPrice": est_net,
            "avg10BasePrice": avg_10_price,
            "cheapestMainFoodName": min_food["name"],
            "cheapestMainFoodPrice": min_food["listingRupees"],
            "sortScorePrice": avg_10_price,
            "discountPercent": max_pct,
            "offerBanner": display_offer,
            "topDishes": top_10_foods[:10],
            "coupons": coupons,
            "totalNonDiscountedCount": len(non_discounted_dishes)
        }
        return r_copy

def fetch_swiggy_address_share_link(aid, tid, sid, token, dev, cid, lat, lng, web_headers):
    """
    Calls `GET https://app.swiggy.com/getlink/v1?aid={aid}` using exact Swiggy-Android headers
    from your dump to generate the one-time Address Share Link (`https://swiggy.onelink.me/...`)!
    """
    loc_ctx = base64.b64encode(json.dumps({
        "cityId": "43",
        "cityName": "India",
        "latLng": {"latitude": float(lat), "longitude": float(lng)},
        "zoneId": "540",
        "geoZoneIds": ["540"]
    }).encode()).decode()

    android_headers = {
        'pl-version': '141',
        'User-Agent': 'Swiggy-Android',
        'Content-Type': 'application/json; charset=utf-8',
        'tid': tid,
        'sid': sid,
        'version-code': '1817',
        'app-version': '4.115.1',
        'latitude': str(lat),
        'longitude': str(lng),
        'os-version': '13',
        'accessibility_enabled': 'false',
        'x-device-frame': 'LOW',
        'x-device-memory': 'HIGH',
        'x-device-network': 'HIGH',
        'encoded-location-context': loc_ctx,
        'swuid': dev,
        'token': token,
        'deviceid': dev,
        'x-network-quality': 'MODERATE',
        'faw-flags': '1354',
        'Accept-Encoding': 'gzip',
        'Cache-Control': 'no-store',
        'Accept': 'application/json; charset=utf-8'
    }

    if not aid:
        discover_urls = [
            "https://www.swiggy.com/dapi/address/all",
            "https://www.swiggy.com/mapi/address/all",
            "https://maps.swiggy.com/addresses/v1"
        ]
        for du in discover_urls:
            try:
                hdrs = android_headers if "maps.swiggy.com" in du else web_headers
                req_d = urllib.request.Request(du, headers=hdrs, method='GET')
                with urllib.request.urlopen(req_d, timeout=6) as resp_d:
                    raw_d = read_response_bytes(resp_d)
                    txt_d = raw_d.decode('utf-8', errors='ignore')
                    m_aid = re.search(r'"id"\s*:\s*"([a-z0-9]{15,26})"', txt_d)
                    if not m_aid:
                        m_aid = re.search(r'\b(d[a-z0-9]{17,22})\b', txt_d)
                    if m_aid:
                        aid = m_aid.group(1)
                        break
            except Exception:
                pass

    if not aid:
        return "", ""

    getlink_url = f"https://app.swiggy.com/getlink/v1?aid={urllib.parse.quote(aid)}"
    try:
        req_g = urllib.request.Request(getlink_url, headers=android_headers, method='GET')
        with urllib.request.urlopen(req_g, timeout=8) as resp_g:
            raw_g = read_response_bytes(resp_g)
            jg = json.loads(raw_g.decode('utf-8', errors='ignore'))
            data_g = jg.get("data") or {}
            return str(data_g.get("link") or "").strip(), str(data_g.get("message") or "").strip()
    except Exception as e:
        print("[SwiggyGetLink] Error:", e)
        return "", ""

class SwiggyHandler(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', '*')
        super().end_headers()

    def do_OPTIONS(self):
        self.send_response(200)
        self.end_headers()

    def do_POST(self):
        if self.path == '/swiggy_api':
            length = int(self.headers.get('Content-Length', 0))
            body = json.loads(self.rfile.read(length).decode('utf-8'))
            
            action = body.get('action', 'list')
            lat = body.get('lat', 11.946127)
            lng = body.get('lng', 79.7780537)
            tid = body.get('tid', '')
            token = body.get('token', '')
            sid = body.get('sid', '')
            dev = body.get('deviceId', 'aa026a55-2112-5e11-6cef-27165ab6ab0d')
            cid = body.get('customerId', '151244572')

            user_sess = get_user_session(cid, dev)
            cookie_str = build_cookie_header(dev, sid, cid, tid, token, lat, lng, user_sess)
            web_headers = {
                'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Mobile Safari/537.36',
                '__fetch_req__': 'true',
                'usecache': 'true',
                'platform': 'mweb',
                'x-channel': 'jiohotstar',
                'user-id': str(cid),
                'Accept': '*/*',
                'Content-Type': 'application/json',
                'Origin': 'https://www.swiggy.com',
                'Referer': 'https://www.swiggy.com/search',
                'Accept-Encoding': 'gzip',
                'Cookie': cookie_str
            }

            # =========================================================================
            # ACTION 5: SAVE ADDRESS VIA RELAY & EXTRACT `aid` + SHARE LINK (`getlink/v1`)
            # =========================================================================
            if action == 'save_address_and_link':
                pb_b64 = body.get('protoBase64', '')
                aid_found = body.get('aid', '') or user_sess.get("last_aid", "")
                if pb_b64:
                    pb_bytes = base64.b64decode(pb_b64)
                    save_url = "https://maps.swiggy.com/addresses/v1"
                    s_headers = {
                        'tid': tid, 'sid': sid,
                        'version-code': '1817', 'app-version': '4.115.1',
                        'swuid': dev, 'token': token, 'deviceid': dev,
                        'User-Agent': 'Swiggy-Android',
                        'Content-Type': 'application/x-protobuf',
                        'Accept': 'application/x-protobuf'
                    }
                    try:
                        req_sv = urllib.request.Request(save_url, data=pb_bytes, headers=s_headers, method='POST')
                        with urllib.request.urlopen(req_sv, timeout=9) as resp_sv:
                            raw_sv = read_response_bytes(resp_sv)
                            txt_sv = raw_sv.decode('utf-8', errors='ignore')
                            m_id = re.search(r'\b(d[a-z0-9]{16,24})\b', txt_sv)
                            if not m_id:
                                m_id = re.search(r'(\d{8,14})', txt_sv)
                            if m_id:
                                aid_found = m_id.group(1)
                                user_sess["last_aid"] = aid_found
                    except Exception as e:
                        print("[SwiggyAddressSave] Error:", e)

                share_link, share_msg = fetch_swiggy_address_share_link(aid_found, tid, sid, token, dev, cid, lat, lng, web_headers)
                if share_link:
                    user_sess["last_share_link"] = share_link
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.end_headers()
                self.wfile.write(json.dumps({
                    "statusCode": 0,
                    "aid": aid_found,
                    "shareLink": share_link,
                    "shareMessage": share_msg
                }).encode('utf-8'))
                return

            # =========================================================================
            # ACTION 6: GET CHECKOUT SHARE LINK (`https://app.swiggy.com/getlink/v1?aid=...`)
            # =========================================================================
            if action == 'get_checkout_link':
                aid_req = str(body.get('aid', '')).strip() or user_sess.get("last_aid", "")
                share_link, share_msg = fetch_swiggy_address_share_link(aid_req, tid, sid, token, dev, cid, lat, lng, web_headers)
                if aid_req:
                    user_sess["last_aid"] = aid_req
                if share_link:
                    user_sess["last_share_link"] = share_link
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.end_headers()
                self.wfile.write(json.dumps({
                    "statusCode": 0,
                    "aid": aid_req,
                    "shareLink": share_link or user_sess.get("last_share_link", ""),
                    "shareMessage": share_msg
                }).encode('utf-8'))
                return

            # =========================================================================
            # ACTION 4A: GATHER ALL CANDIDATE RESTAURANTS FOR PROGRESSIVE STREAMING COMPARE
            # =========================================================================
            if action == 'compare_gather':
                discount_filter = str(body.get('discountFilter', 'ALL_DISCOUNT')).strip().upper()
                food_query = str(body.get('foodQuery', '')).strip().lower()
                sort_by = str(body.get('sortBy', '')).strip()
                existing_restos = body.get('loadedRestaurants', [])

                print(f"\n[SwiggyCompare-Gather] User={cid} | Discount='{discount_filter}' | Food='{food_query}'")

                raw_cards = []
                search_terms = []
                if food_query:
                    search_terms.append(food_query)
                    if "briyani" in food_query or "biriyani" in food_query:
                        search_terms.append(re.sub(r'briyani|biriyani', 'Biryani', food_query, flags=re.IGNORECASE))
                    elif "biryani" in food_query:
                        search_terms.append("Chicken Biryani")
                    search_terms.extend(["Restaurant", "South Indian", "North Indian"])
                else:
                    search_terms = ["Biryani", "Restaurant", "North Indian", "South Indian", "Chinese", "Fast Food", "Dosa", "Burger", "Pizza"]

                with ThreadPoolExecutor(max_workers=14) as ex:
                    futures = []
                    for cid_col in ["419140", "417660", "429606", "412007"]:
                        for off_val in [0, 14, 28, 42]:
                            futures.append(ex.submit(fetch_collection_v5_cards, lat, lng, cid_col, off_val, sort_by, "", web_headers, user_sess))
                    s_futures = [ex.submit(fetch_search_v3_cards, lat, lng, st, web_headers) for st in search_terms]

                    for fut in futures:
                        try:
                            jc = fut.result()
                            if jc.get("data") and isinstance(jc["data"], dict):
                                raw_cards.extend(jc["data"].get("cards", []))
                        except Exception:
                            pass
                    for sf in s_futures:
                        try:
                            raw_cards.extend(sf.result())
                        except Exception:
                            pass

                harvested = extract_restaurants_from_cards(raw_cards, include_closed=False)
                merged_map = {}
                for r in harvested:
                    merged_map[str(r["id"])] = r
                if isinstance(existing_restos, list):
                    for er in existing_restos:
                        if isinstance(er, dict) and er.get("id") and is_restaurant_open_and_orderable(er):
                            rid = str(er["id"])
                            if rid not in merged_map:
                                merged_map[rid] = er
                            elif not get_card_offer_string(merged_map[rid]) and get_card_offer_string(er):
                                merged_map[rid]["offer"] = get_card_offer_string(er)

                all_open = list(merged_map.values())
                tier1, tier2, tier3 = [], [], []
                for r in all_open:
                    co = get_card_offer_string(r)
                    has_pre = bool(r.get("_matchedDishes"))
                    if does_offer_match_filter(discount_filter, co) and has_pre:
                        tier1.append(r)
                    elif does_offer_match_filter(discount_filter, co) or has_pre:
                        tier2.append(r)
                    else:
                        tier3.append(r)

                final_candidates = tier1 + tier2 + tier3
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.end_headers()
                self.wfile.write(json.dumps({
                    "statusCode": 0,
                    "candidates": final_candidates
                }).encode('utf-8'))
                return

            # =========================================================================
            # ACTION 4B: FAST BATCH MENU SCANNER (10 AT A TIME FOR LIVE UI UPDATE!)
            # =========================================================================
            if action == 'compare_batch':
                discount_filter = str(body.get('discountFilter', 'ALL_DISCOUNT')).strip().upper()
                food_query = str(body.get('foodQuery', '')).strip().lower()
                batch_restos = body.get('batchRestaurants', [])
                token_groups = expand_food_query_tokens(food_query)

                batch_results = []
                with ThreadPoolExecutor(max_workers=10) as ex:
                    futures = [
                        ex.submit(analyze_single_restaurant_for_compare, r, lat, lng, discount_filter, food_query, token_groups, web_headers)
                        for r in batch_restos
                    ]
                    for f in futures:
                        try:
                            res_r = f.result()
                            if res_r is not None:
                                batch_results.append(res_r)
                        except Exception:
                            pass

                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.end_headers()
                self.wfile.write(json.dumps({
                    "statusCode": 0,
                    "batchResults": batch_results
                }).encode('utf-8'))
                return

            # =========================================================================
            # ACTION 3: FETCH RESTAURANT COMPLETE MENU
            # =========================================================================
            if action == 'menu':
                r_id = str(body.get('restaurantId', '')).strip()
                search_q = str(body.get('query', '')).strip()
                q_param = f"&query={urllib.parse.quote(search_q)}" if search_q else ""
                menu_pb_url = f"https://www.swiggy.com/mapi/menu/pl?page-type=REGULAR_MENU&complete-menu=true&lat={lat}&lng={lng}&restaurantId={r_id}{q_param}&submitAction=ENTER"
                pb_headers = dict(web_headers)
                pb_headers['Accept'] = 'application/x-protobuf'
                pb_headers['Referer'] = f"https://www.swiggy.com/jhs/menu?restaurant_id={r_id}"

                try:
                    req_m = urllib.request.Request(menu_pb_url, headers=pb_headers, method='GET')
                    with urllib.request.urlopen(req_m, timeout=10) as resp_m:
                        update_cookies_from_headers(resp_m.headers, user_sess)
                        raw_m = read_response_bytes(resp_m)
                        if len(raw_m) > 200:
                            ctype = 'application/json; charset=utf-8' if raw_m.lstrip().startswith(b'{') else 'application/x-protobuf'
                            self.send_response(200)
                            self.send_header('Content-Type', ctype)
                            self.end_headers()
                            self.wfile.write(raw_m)
                            return
                except Exception as e:
                    print("[SwiggyMenu] mAPI Protobuf error:", e)

                json_urls = [
                    f"https://www.swiggy.com/mapi/menu/pl?page-type=REGULAR_MENU&complete-menu=true&lat={lat}&lng={lng}&restaurantId={r_id}&submitAction=ENTER",
                    f"https://www.swiggy.com/dapi/menu/pl?page-type=REGULAR_MENU&complete-menu=true&lat={lat}&lng={lng}&restaurantId={r_id}&catalog_qa=undefined&submitAction=ENTER"
                ]
                json_headers = dict(web_headers)
                json_headers['Accept'] = 'application/json, text/plain, */*'
                json_headers['Referer'] = f"https://www.swiggy.com/jhs/menu?restaurant_id={r_id}"

                for ju in json_urls:
                    try:
                        req_j = urllib.request.Request(ju, headers=json_headers, method='GET')
                        with urllib.request.urlopen(req_j, timeout=10) as resp_j:
                            raw_j = read_response_bytes(resp_j)
                            if len(raw_j) > 200:
                                ctype = 'application/json; charset=utf-8' if raw_j.lstrip().startswith(b'{') else 'application/x-protobuf'
                                self.send_response(200)
                                self.send_header('Content-Type', ctype)
                                self.end_headers()
                                self.wfile.write(raw_j)
                                return
                    except Exception:
                        pass

                self.send_response(502)
                self.send_header('Content-Type', 'application/json')
                self.end_headers()
                self.wfile.write(b'{"error": "Failed to fetch restaurant menu"}')
                return

            # =========================================================================
            # ACTION 1: LIVE RESTAURANT & DISH SEARCH (SUGGEST + DISHGROUP SEARCH V3)
            # =========================================================================
            if action == 'search':
                query_str = body.get('query', '').strip()
                meta_str = body.get('metadata', '')
                submit_act = body.get('submitAction', 'SUGGESTION' if meta_str else 'ENTER')
                q_enc = urllib.parse.quote(query_str)

                suggest_url = f"https://www.swiggy.com/mapi/restaurants/search/suggest?lat={lat}&lng={lng}&str={q_enc}&trackingId=6a848d62-d77e-a2dc-a463-d2750dbc764a&includeIMItem=true"
                suggestions_list = []
                search_cards = []

                with ThreadPoolExecutor(max_workers=3) as ex:
                    def get_suggest():
                        req_s = urllib.request.Request(suggest_url, headers=web_headers, method='GET')
                        with urllib.request.urlopen(req_s, timeout=7) as resp_s:
                            update_cookies_from_headers(resp_s.headers, user_sess)
                            raw_s = read_response_bytes(resp_s)
                            return json.loads(raw_s.decode('utf-8', errors='ignore'))

                    f_sug = ex.submit(get_suggest)
                    f_v3 = ex.submit(fetch_search_v3_cards, lat, lng, query_str, web_headers, meta_str, submit_act)

                    try:
                        js = f_sug.result()
                        if js.get("csrfToken"):
                            user_sess["csrf"] = js["csrfToken"]
                        if js.get("data") and isinstance(js["data"], dict):
                            suggestions_list = js["data"].get("suggestions", [])
                    except Exception:
                        pass

                    try:
                        search_cards.extend(f_v3.result())
                    except Exception:
                        pass

                matched_restos = extract_restaurants_from_cards(search_cards, include_closed=False)
                out_resp = {
                    "statusCode": 0,
                    "suggestions": suggestions_list,
                    "matchedRestaurants": matched_restos,
                    "data": {
                        "cards": search_cards
                    }
                }
                self.send_response(200)
                self.send_header('Content-Type', 'application/json; charset=utf-8')
                self.end_headers()
                self.wfile.write(json.dumps(out_resp).encode('utf-8'))
                return

            # =========================================================================
            # ACTION 2: INFINITE SCROLL RESTAURANT LIST (+ HOTSTAR DISCOUNT COLLECTIONS)
            # =========================================================================
            page = int(body.get('page', 0))
            next_offset = body.get('nextOffset', '')
            widget_offset = body.get('widgetOffset', None)
            csrf_token = body.get('csrfToken', '') or user_sess["csrf"]
            sort_by = str(body.get('sortBy', '')).strip()
            coll_id = str(body.get('collectionId', '')).strip()

            web_headers['Referer'] = 'https://www.swiggy.com/ipl-landing?x-channel=jiohotstar'

            combined_cards = []
            out_next_offset = next_offset or "CPKEAhCp0gEogMic1cHMzOcqMJY8"
            out_widget_offset = widget_offset if isinstance(widget_offset, dict) else {}
            out_csrf = csrf_token

            if coll_id:
                jc = fetch_collection_v5_cards(lat, lng, coll_id, page * 14, sort_by, "", web_headers, user_sess)
                if jc.get("data") and isinstance(jc["data"], dict):
                    combined_cards.extend(jc["data"].get("cards", []))

            if page == 0:
                def get_mapi_initial():
                    u = f"https://www.swiggy.com/mapi/landing/IPL_LANDING_PAGE_JHS?lat={lat}&lng={lng}"
                    req = urllib.request.Request(u, headers=web_headers, method='GET')
                    with urllib.request.urlopen(req, timeout=8) as resp:
                        update_cookies_from_headers(resp.headers, user_sess)
                        return json.loads(read_response_bytes(resp).decode('utf-8', errors='ignore'))

                def get_dapi_initial():
                    u = f"https://www.swiggy.com/dapi/restaurants/list/v5?lat={lat}&lng={lng}&is-seo-homepage-enabled=true&page_type=DESKTOP_WEB_LISTING"
                    req = urllib.request.Request(u, headers=web_headers, method='GET')
                    with urllib.request.urlopen(req, timeout=8) as resp:
                        update_cookies_from_headers(resp.headers, user_sess)
                        return json.loads(read_response_bytes(resp).decode('utf-8', errors='ignore'))

                with ThreadPoolExecutor(max_workers=4) as ex:
                    f_mapi = ex.submit(get_mapi_initial)
                    f_dapi = ex.submit(get_dapi_initial)
                    f_c419 = ex.submit(fetch_collection_v5_cards, lat, lng, "419140", 0, sort_by, "", web_headers, user_sess)
                    f_c417 = ex.submit(fetch_collection_v5_cards, lat, lng, "417660", 0, sort_by, "", web_headers, user_sess)

                    try:
                        jm = f_mapi.result()
                        if jm.get("csrfToken"):
                            user_sess["csrf"] = jm["csrfToken"]
                            out_csrf = jm["csrfToken"]
                        if jm.get("data") and isinstance(jm["data"], dict):
                            combined_cards.extend(jm["data"].get("cards", []))
                            po = jm["data"].get("pageOffset", {})
                            out_next_offset = po.get("nextOffset", out_next_offset)
                            out_widget_offset = po.get("widgetOffset", out_widget_offset)
                    except Exception:
                        pass

                    for extra_f in [f_dapi, f_c419, f_c417]:
                        try:
                            jd = extra_f.result()
                            if jd.get("data") and isinstance(jd["data"], dict):
                                combined_cards.extend(jd["data"].get("cards", []))
                        except Exception:
                            pass

            base_off = 11 + (page * 15)
            offsets_to_fetch = [base_off, base_off + 5, base_off + 10]

            def fetch_jhs_offset(off_val):
                b64_jhs = base64.b64encode(json.dumps({"collectionId": 369295, "offset": off_val}).encode()).decode()
                wo = {
                    "JioHotstar_SW_Locked_Compoundwidget": "",
                    "JioHotstar_LandingPage_Ad_Compoundwidget1": "",
                    "JioHotstar_AllRestaurants_Header_Fullbleed": "",
                    "JioHotstar_PopularCuisines_Compoundwidget": "",
                    "collectionV5RestaurantListWidget_SimRestoRelevance_JHS_byName": b64_jhs
                }
                payload = json.dumps({
                    "lat": float(lat),
                    "lng": float(lng),
                    "data": {
                        "nextOffset": out_next_offset,
                        "widgetOffset": wo
                    },
                    "_csrf": out_csrf
                }).encode('utf-8')
                req = urllib.request.Request("https://www.swiggy.com/mapi/landing/IPL_LANDING_PAGE_JHS", data=payload, headers=web_headers, method='POST')
                with urllib.request.urlopen(req, timeout=8) as resp:
                    return json.loads(read_response_bytes(resp).decode('utf-8', errors='ignore'))

            sweep_kw1 = SWEEP_QUERIES[(page * 3) % len(SWEEP_QUERIES)]
            sweep_kw2 = SWEEP_QUERIES[(page * 3 + 1) % len(SWEEP_QUERIES)]
            sweep_kw3 = SWEEP_QUERIES[(page * 3 + 2) % len(SWEEP_QUERIES)]
            coll_pick = DISCOUNT_COLLECTIONS[page % len(DISCOUNT_COLLECTIONS)]

            with ThreadPoolExecutor(max_workers=7) as ex:
                jhs_futures = [ex.submit(fetch_jhs_offset, o) for o in offsets_to_fetch]
                sweep_f1 = ex.submit(fetch_search_v3_cards, lat, lng, sweep_kw1, web_headers)
                sweep_f2 = ex.submit(fetch_search_v3_cards, lat, lng, sweep_kw2, web_headers)
                sweep_f3 = ex.submit(fetch_search_v3_cards, lat, lng, sweep_kw3, web_headers)
                coll_f = ex.submit(fetch_collection_v5_cards, lat, lng, coll_pick, (page % 4) * 14, sort_by, "", web_headers, user_sess)

                for jf in jhs_futures:
                    try:
                        res_j = jf.result()
                        if res_j.get("csrfToken"):
                            user_sess["csrf"] = res_j["csrfToken"]
                            out_csrf = res_j["csrfToken"]
                        if res_j.get("data") and isinstance(res_j["data"], dict):
                            combined_cards.extend(res_j["data"].get("cards", []))
                            po = res_j["data"].get("pageOffset", {})
                            if po.get("nextOffset"):
                                out_next_offset = po["nextOffset"]
                    except Exception:
                        pass

                for sf in [sweep_f1, sweep_f2, sweep_f3]:
                    try:
                        combined_cards.extend(sf.result())
                    except Exception:
                        pass

                try:
                    jc_res = coll_f.result()
                    if jc_res.get("data") and isinstance(jc_res["data"], dict):
                        combined_cards.extend(jc_res["data"].get("cards", []))
                except Exception:
                    pass

            open_restos = extract_restaurants_from_cards(combined_cards, include_closed=False)

            merged_response = {
                "statusCode": 0,
                "csrfToken": out_csrf,
                "openRestaurants": open_restos,
                "data": {
                    "statusMessage": "done successfully",
                    "pageOffset": {
                        "nextOffset": out_next_offset,
                        "widgetOffset": out_widget_offset
                    },
                    "cards": combined_cards
                }
            }
            self.send_response(200)
            self.send_header('Content-Type', 'application/json; charset=utf-8')
            self.end_headers()
            self.wfile.write(json.dumps(merged_response).encode('utf-8'))
        else:
            super().do_POST()

class ThreadedTCPServer(ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True
    daemon_threads = True
    request_queue_size = 256

if __name__ == '__main__':
    for port in range(8080, 8090):
        try:
            server = ThreadedTCPServer(('0.0.0.0', port), SwiggyHandler)
            print(f"\n==========================================")
            print(f"🚀 SWIGGY 100-USER MULTI-THREADED SERVER LIVE ON PORT {port}!")
            print(f"👉 Open in Chrome: http://0.0.0.0:{port}/Swiggy.html")
            print(f"==========================================\n")
            server.serve_forever()
            break
        except OSError as e:
            if e.errno == 98:
                print(f"⚠️ Port {port} busy hai, switching to Port {port + 1}...")
            else:
                raise
