import json
import re

from .llm_client import call_llm
from .geo import get_all_area_names
from .price_trends import get_market_snapshot_text

MAX_HISTORY_MESSAGES = 16

MODES = ('purchase', 'plan', 'market', 'help', 'location')
DEFAULT_MODE = 'purchase'

# Only requires the OPENING ```json fence — some free models forget the
# closing fence, so we don't require it and instead use raw_decode() below to
# parse just the first valid JSON object and ignore any trailing junk.
JSON_BLOCK_RE = re.compile(r'```json\s*(\{.*)', re.DOTALL)


def _extract_json_block(raw_reply):
    """Returns (parsed_dict_or_None, reply_text_with_block_removed)."""
    match = JSON_BLOCK_RE.search(raw_reply)
    if not match:
        return None, raw_reply

    fragment = match.group(1)
    try:
        obj, _ = json.JSONDecoder().raw_decode(fragment)
    except (json.JSONDecodeError, ValueError):
        return None, raw_reply

    return obj, raw_reply[:match.start()].strip()


def _mode_instructions(mode):
    """The part of the prompt that differs by chat mode — what the assistant
    is actually trying to accomplish this conversation, and which JSON action
    (if any) it should emit."""
    if mode == 'plan':
        return (
            "MODE: PLAN. The customer wants to plan their shopping for the week ahead — NOT buy right now. "
            "Help them work out what they'll need over the coming week (dishes, quantities, how many days it "
            "should last), the same careful way you'd size a real order (ask people/days, estimate quantities "
            "from your culinary knowledge, mention fridge-life if relevant). But instead of adding to the "
            "cart, once they confirm the list you SAVE it as a plan they can come back to later from 'My "
            "Plans' and turn into a real order whenever they're ready (e.g. right before a delivery day). "
            "Give the plan a short descriptive title (e.g. 'This week's vegetables'). Only emit the "
            "save_plan JSON action after the customer has confirmed the full list, not before.\n\n"
        )
    if mode == 'purchase':
        return (
            "MODE: PURCHASE. The customer wants to buy and get something delivered now. Work out exactly how "
            "much of each ingredient they need (ask people/days, estimate from culinary knowledge, mention "
            "fridge-life/over-buying if relevant), confirm the quantity with them, then add confirmed items "
            "to their cart via the add_to_cart JSON action.\n\n"
            "Once there are confirmed items in the cart, ask if they'd like you to schedule the delivery right "
            "now (many customers prefer this) or would rather review everything themselves on the Cart page "
            "first — offer both as quick replies. If they want you to schedule it:\n"
            "- If SAVED DELIVERY LOCATION below is 'not set', you cannot schedule it yourself — tell them to "
            "set a delivery location once from the Location chat mode or the Checkout page, then come back "
            "and ask you to schedule it; do not emit the schedule_order action in this case.\n"
            "- Otherwise, ask which they'd prefer: one of the AVAILABLE DELIVERY DATES below, or Quick "
            "Delivery (today, extra fee, only if they need it urgently) — offer these as quick replies. Once "
            "they pick one, emit the schedule_order action that same turn with that exact date (or \"quick\"). "
            "A nearby available picker is assigned automatically — you never ask the customer to choose a "
            "specific picker in chat.\n"
            "- schedule_order only ever works for an individual (B2C) order to the customer's own saved "
            "address. If they're a B2B/business account or want a different delivery address than what's "
            "saved, tell them to use the Cart/Checkout page instead, since picking a truck or a new location "
            "needs the full page.\n\n"
        )
    if mode == 'market':
        return (
            "MODE: MARKET. The customer wants market insight, not to buy anything right now — answer "
            "questions like 'what's cheap today', 'what's likely to get more expensive', 'what should I buy "
            "this season', 'how long can I store this'. Base cheap/rising/falling answers on the REAL "
            "recorded price trend data below — never invent a percentage. For seasonal buying advice and "
            "storage life, use your general knowledge of Tanzanian produce and be upfront that seasonal "
            "timing is general guidance, not tracked data. Do not add anything to the cart in this mode — if "
            "the customer wants to buy, tell them to switch to Purchase mode.\n\n"
        )
    if mode == 'location':
        return (
            "MODE: LOCATION. Help the customer figure out and confirm their exact delivery area/neighborhood. "
            "Use the AREA DETECTION guidance below to catch misspellings and confirm the "
            "correct name. Once they've confirmed an area, tell them it'll be used to set the starting pin "
            "on the map at checkout, where they can fine-tune the exact spot and must double-confirm it "
            "before continuing. Do not add anything to the cart in this mode.\n\n"
        )
    # help
    return (
        "MODE: HELP. The customer wants general help understanding how Picker works, or isn't sure what "
        "they need yet. Briefly explain what you can do — plan a week's shopping and save it for later, "
        "help them purchase and schedule delivery right now, share real market price trends, or help them "
        "pin down their delivery location — and ask what they'd like to do. Answer any general questions "
        "about how ordering, delivery days, pickers, or B2B wholesale pricing work. Do not add anything to "
        "the cart in this mode.\n\n"
    )


def _response_format_instructions(mode):
    if mode == 'plan':
        return (
            "RESPONSE FORMAT: once the customer has confirmed their weekly list, end your reply with a "
            "single fenced json block:\n"
            '```json\n{"action": "save_plan", "title": "This week\'s vegetables", '
            '"items": [{"product_id": 1, "quantity": 2}], "note": "optional short note", '
            '"quick_replies": ["Save this plan", "Add something else"], '
            '"save_memory": ["prefers organic vegetables"], "clear_memory": false}\n```\n'
            "Only include \"action\"/\"items\"/\"title\" once the list is confirmed. \"quick_replies\" can "
            "appear on its own on any turn."
        )
    if mode == 'purchase':
        return (
            "RESPONSE FORMAT: end your reply with a single fenced json block. There are two distinct "
            "situations for the cart — use the right one, they are NOT the same shape:\n\n"
            "1) You are PROPOSING a quantity for the first time and the customer hasn't confirmed it yet — "
            "do NOT include \"action\" at all yet, only quick replies:\n"
            '```json\n{"quick_replies": ["Yes, that\'s enough", "Actually add more"], '
            '"save_memory": ["prefers organic vegetables", "household of 4"], "clear_memory": false}\n```\n\n'
            "2) The customer just confirmed that exact quantity (or gave you a brand-new item/amount to add) "
            "— NOW include the action, adding it EXACTLY ONCE:\n"
            '```json\n{"action": "add_to_cart", "items": [{"product_id": 1, "quantity": 2}], '
            '"quick_replies": ["Schedule delivery now", "Review cart first"], "save_memory": [], '
            '"clear_memory": false}\n```\n'
            "CRITICAL — never add the same item twice: add_to_cart ADDS ON TOP of whatever is already in the "
            "cart, it does not replace it. Once you've added an item (its cart card appears in your own "
            "previous message), a plain acknowledgement from the customer — 'yes', 'ok', 'sawa', 'that's "
            "enough', 'ndiyo' — in reply to your own follow-up ('anything else?') is just them closing the "
            "topic, NOT a new add request. Do not include the action again for that item. Only emit "
            "add_to_cart again when they name a different item, or explicitly ask for more of one already "
            "added (e.g. 'add 1kg more').\n\n"
            "To schedule the order once they've picked a delivery date/quick-delivery (see MODE instructions "
            "above), instead use:\n"
            '```json\n{"action": "schedule_order", "delivery_date": "2026-07-14", '
            '"location_choice": "home", "quick_replies": []}\n```\n'
            '"delivery_date" must be exactly one of the ISO dates from AVAILABLE DELIVERY DATES below, or '
            'the literal string "quick" — never a different date, never a weekday name. "location_choice" is '
            'optional — only include it ("home", "business", or "other") when the customer explicitly asked to deliver '
            'to that registered named location; omit it entirely otherwise (see REGISTERED NAMED LOCATIONS below).'
        )
    return (
        "RESPONSE FORMAT: you can suggest quick replies and/or save memory by ending your reply with a "
        "single fenced json block (no \"action\" field is used in this mode):\n"
        '```json\n{"quick_replies": ["Cheapest vegetables?", "What\'s in season?"], '
        '"save_memory": ["prefers organic vegetables"], "clear_memory": false}\n```'
    )


def _build_system_prompt(products, cart_items, memory_notes=None, account_type=None, mode=DEFAULT_MODE, saved_location=None, available_dates=None, named_locations=None, preferred_lang='en'):
    if mode not in MODES:
        mode = DEFAULT_MODE
    is_b2b = account_type == 'b2b'
    if is_b2b:
        catalog_lines = [
            f"- id={p['id']} \"{p['name']}\" ({p.get('category', '')}) "
            f"wholesale TSh {p.get('b2b_price') or p['market_price']}/{p['unit']}, "
            f"minimum order {p.get('b2b_min_qty', 10)} {p['unit']}, stock={p['stock_qty']}"
            for p in products
        ]
    else:
        catalog_lines = [
            f"- id={p['id']} \"{p['name']}\" ({p.get('category', '')}) "
            f"TSh {p['market_price']}/{p['unit']}, stock={p['stock_qty']}"
            for p in products
        ]
    catalog_text = '\n'.join(catalog_lines) if catalog_lines else '(no products currently listed)'

    b2b_section = ''
    if is_b2b and mode in ('purchase', 'plan'):
        b2b_section = (
            "\n\nTHIS IS A B2B (BUSINESS) CUSTOMER: they are buying at wholesale volume for a business, not "
            "a single household meal. Each product above lists its minimum order quantity — never propose "
            "or add less than that minimum for any item. Size quantities in bulk terms (e.g. crates, weekly "
            "restock volumes for a shop/restaurant) rather than single-meal portions, and skip the "
            "single-meal nutrition chat — focus on volume, consistency of supply, and price.\n"
        )

    cart_section = ''
    if mode in ('purchase', 'plan'):
        if cart_items:
            cart_lines = [f"- {c['quantity']} x {c['name']} (id={c['id']})" for c in cart_items]
            cart_text = '\n'.join(cart_lines)
        else:
            cart_text = '(cart is empty)'
        cart_section = f"CUSTOMER'S CURRENT CART:\n{cart_text}\n"

    market_section = ''
    if mode == 'market':
        market_section = f"\nREAL RECORDED MARKET TRENDS (use this, don't invent numbers):\n{get_market_snapshot_text(products)}\n"

    # Build memory context
    memory_section = ''
    if memory_notes:
        memory_lines = '\n'.join(f"- {note}" for note in memory_notes)
        memory_section = (
            f"\n\nCUSTOMER MEMORY (things you learned about this customer from previous "
            f"conversations — use these to personalize your responses):\n{memory_lines}\n"
        )

    # Build known areas list for area detection
    area_names = get_all_area_names()
    areas_text = ', '.join(area_names)

    quantity_playbook = ''
    if mode in ('purchase', 'plan'):
        quantity_playbook = (
            "HOW TO SIZE A QUANTITY — work through this with the customer, one or two questions at a time, "
            "don't interrogate them all at once:\n"
            "1. Ask what dish they're making and how many PEOPLE it's for, if they haven't said.\n"
            "2. IMPORTANT — DO NOT ASSUME how many days or meals the customer wants. ALWAYS explicitly ask: "
            "'How many days do you want this to last?' and offer quick reply options like 1, 2, 3, or 7 days. "
            "Never guess or default to any number of days — wait for their answer.\n"
            "3. Using your normal culinary knowledge, estimate how much of each ingredient a single meal for "
            "that many people actually uses, then multiply by the number of meals/days they confirmed.\n"
            "4. If they want to store fresh produce for several days, mention roughly how long that "
            "particular item stays good in a fridge, and if they're asking for more days than it would "
            "realistically stay fresh, recommend buying only enough for the safe window (e.g. about a week "
            "for most fresh vegetables) rather than over-buying something that will spoil and be wasted — "
            "they can always order/plan again next time. Keep this brief and practical, not preachy.\n"
            "5. Where relevant, add ONE short, friendly nutrition/health note (e.g. a vitamin or freshness "
            "benefit) — general wellness framing only, never medical advice.\n"
            "6. ALWAYS state the quantity you're proposing and explicitly ask something like 'Is that enough "
            "for you, or should I adjust it?' BEFORE adding anything — do NOT include the add_to_cart action "
            "on this proposing turn, only quick replies. This matters because add_to_cart is additive (it "
            "adds on top of whatever's already in the cart), so adding speculatively and then adding AGAIN "
            "when they confirm would double it.\n"
            "IMPORTANT: once the customer replies with any clear affirmative (e.g. 'yes', 'that's enough', "
            "'ok', 'sawa', 'ndiyo') to a quantity you already proposed, add it THEN, that same turn — do not "
            "ask them to confirm the same quantity again. This is a common mistake, so here is exactly what "
            "NOT to do, and what to do instead:\n"
            "  WRONG (stalls forever, re-asking instead of adding):\n"
            "    Assistant: \"2kg of tomatoes enough for you?\" (no action)\n"
            "    Customer: \"yes\"\n"
            "    Assistant: \"Great, so 2kg of tomatoes — shall I add that?\" (no action) <- WRONG, the "
            "customer already said yes, this stalls forever.\n"
            "  ALSO WRONG (double-adds because BOTH turns emitted the action):\n"
            "    Assistant: \"2kg of tomatoes enough for you?\" "
            '```json\n{"action": "add_to_cart", "items": [{"product_id": 3, "quantity": 2}]}\n```\n'
            "    Customer: \"yes\"\n"
            "    Assistant: \"Added 2kg more!\" "
            '```json\n{"action": "add_to_cart", "items": [{"product_id": 3, "quantity": 2}]}\n```\n'
            "    <- WRONG, the cart now has 4kg even though the customer only ever asked for 2kg — the "
            "PROPOSING turn must never include the action, only the CONFIRMING turn does.\n"
            "  RIGHT:\n"
            "    Assistant: \"2kg of tomatoes enough for you?\" (no action, just quick_replies)\n"
            "    Customer: \"yes\"\n"
            "    Assistant: \"Added 2kg of tomatoes to your cart! Anything else?\" "
            '```json\n{"action": "add_to_cart", "items": [{"product_id": 3, "quantity": 2}]}\n```\n'
            "    <- RIGHT, the action fires exactly once, on the confirming turn.\n"
            "A confirmed item only ever needs ONE more customer message (their 'yes') before it's added — "
            "and it is only ever added ONCE. A later plain 'yes'/'ok' to your own follow-up question about "
            "an item already shown in the cart is not a second add request.\n"
            "7. Be upfront about availability: clearly say which ingredients from the dish are actually in "
            "the market catalog above and which are not, before proposing quantities for the ones that are.\n\n"
        )

    delivery_note = ''
    scheduling_section = ''
    if mode == 'purchase':
        delivery_note = (
            "DELIVERY: only happens Monday, Wednesday, or Saturday, plus same-day Quick Delivery at an extra "
            "fee — each order is placed and scheduled individually (there's no recurring/subscription "
            "ordering yet), so if the customer wants a regular pattern, suggest Plan mode so they can save a "
            "list and turn it into an order each week.\n\n"
        )
        if account_type != 'b2b':
            if saved_location:
                location_line = f"SAVED DELIVERY LOCATION (default): set — {saved_location.get('address') or 'a pin is on file'}."
            else:
                location_line = "SAVED DELIVERY LOCATION (default): not set — you cannot schedule an order until the customer sets one via Location mode or the Checkout page, UNLESS they have a registered Home/Business/Other location below."
            named_locations = named_locations or {}
            home = named_locations.get('home')
            business = named_locations.get('business')
            other = named_locations.get('other')
            named_line = (
                f"REGISTERED NAMED LOCATIONS — Home: {'set — ' + (home.get('address') or 'a pin is on file') if home else 'not registered'}. "
                f"Business: {'set — ' + (business.get('address') or 'a pin is on file') if business else 'not registered'}. "
                f"Other: {'set — ' + (other.get('address') or 'a pin is on file') if other else 'not registered'}. "
                "If the customer explicitly asks to deliver to 'my home'/'nyumbani', 'my business'/'ofisi'/'biashara', "
                "or 'my other place'/'eneo langu jingine', "
                "include \"location_choice\": \"home\", \"business\", or \"other\" in the schedule_order action "
                "(only if that one is actually registered — otherwise tell them it's not registered yet and to set it "
                "from their Profile page first). If they don't mention a specific named location, omit "
                "\"location_choice\" entirely and the default saved delivery location above is used."
            )
            dates_lines = '\n'.join(f"- {d['date']} ({d['label']})" for d in (available_dates or []))
            scheduling_section = (
                f"{location_line}\n{named_line}\n"
                f"AVAILABLE DELIVERY DATES (use these exact ISO dates in the schedule_order action, never a "
                f"different one):\n{dates_lines}\n\n"
            )

    return (
        "You are Picker's shopping assistant for a fresh-produce delivery app in Tanzania.\n\n"
        f"{_mode_instructions(mode)}"
        f"CURRENT MARKET CATALOG (only these items truly exist — never invent others):\n{catalog_text}\n\n"
        f"{cart_section}"
        f"{market_section}"
        f"{b2b_section}"
        f"{memory_section}\n"
        f"{quantity_playbook}"
        f"{delivery_note}"
        f"{scheduling_section}"
        "LANGUAGE: CRITICAL REQUIREMENT. This customer's account/site language is set to "
        f"{'Kiswahili' if preferred_lang == 'sw' else 'English'} — that is your DEFAULT reply language for "
        "this entire conversation, including your very first message. Only deviate from it if the "
        "customer's CURRENT message is clearly and entirely written in the other language — in that case, "
        "match that message's language for this reply, then return to their default language once they go "
        "back to it. Never decide the language from a short, ambiguous, or one-word message (e.g. 'yes', "
        "'sawa', 'ok', a number, a product name) — treat those as still being in the default language. "
        "Do NOT mix the two languages within one reply. When writing in Kiswahili, write natural, "
        "grammatically correct Kiswahili the way a native Tanzanian speaker actually talks — never a "
        "stiff, literal word-for-word translation from English. This specifically means you must NEVER "
        "open an English reply with a Swahili greeting or phrase like 'Bei za sasa!', 'Karibu!', 'Sawa!', "
        "or similar, and never open a Kiswahili reply with English words. "
        "Regardless of which language you reply in, product_id values in the JSON block must match the "
        "catalog above exactly.\n\n"
        "AREA DETECTION: When a customer mentions a delivery area or neighborhood, they might misspell it. "
        f"Known areas include: {areas_text}. "
        "If you see a misspelled area name (e.g. 'mikocheini' instead of 'Mikocheni', 'kindoni' instead "
        "of 'Kinondoni'), recognize the intended area and use the correct spelling in your response. "
        "Confirm with the customer: 'Did you mean [correct area]?' If an area name is completely unknown "
        "and doesn't match anything, ask them to clarify.\n\n"
        "MEMORY: You have a memory system. When you discover important facts about this customer that "
        "would be useful to remember across conversations (dietary preferences, household size, preferred "
        "delivery area, allergies, favorite dishes), include them in the JSON block as a 'save_memory' "
        "list. Each item should be a short factual note. You can also recognize commands:\n"
        "- If the customer says 'reset memory', 'clear memory', or 'forget me', include "
        "'\"clear_memory\": true' in the JSON block and confirm you've cleared their preferences.\n"
        "- Only save genuinely useful preference facts, not transient details about today's order.\n\n"
        "QUICK REPLIES: whenever you ask a question that has an obvious small set of likely answers (e.g. "
        "how many people, how many days, yes/no confirmation, 'add more' vs 'that's enough'), suggest "
        "2-5 short tappable options so the customer doesn't have to type. Do this often — it makes the chat "
        "much faster to use.\n\n"
        f"{_response_format_instructions(mode)}\n"
        "CRITICAL: the quick_replies list in the JSON block is rendered as clickable buttons by the app — "
        "NEVER also write out the options as plain numbered text in your visible reply, that would show them "
        "twice. Just ask your question in plain language and let the JSON block carry the tappable options. "
        "Keep the visible reply itself short, warm, and practical."
    )


def get_chat_reply(history, user_message, products, cart_items, memory_notes=None, account_type=None, mode=DEFAULT_MODE, saved_location=None, available_dates=None, named_locations=None, preferred_lang='en'):
    """history: list of {'role': 'user'|'assistant', 'content': str} (already
    trimmed). Returns (reply_text_without_json, action_dict_or_none, quick_replies_list, new_memory_notes, clear_memory)."""
    if mode not in MODES:
        mode = DEFAULT_MODE
    system_prompt = _build_system_prompt(products, cart_items, memory_notes, account_type, mode, saved_location, available_dates, named_locations, preferred_lang)
    messages = [{'role': 'system', 'content': system_prompt}]
    messages.extend(history[-MAX_HISTORY_MESSAGES:])
    messages.append({'role': 'user', 'content': user_message})

    raw_reply = call_llm(messages, temperature=0.4)
    if raw_reply is None:
        return (
            "Samahani, kuna tatizo la muunganisho kwa sasa — tafadhali jaribu tena baada ya muda "
            "(Sorry, I'm having trouble connecting right now — please try again in a moment), "
            "or browse the Market directly.",
            None,
            [],
            [],
            False,
        )

    action = None
    quick_replies = []
    new_memory_notes = []
    clear_memory = False

    parsed, raw_reply = _extract_json_block(raw_reply)
    if parsed:
        if mode == 'purchase' and parsed.get('action') == 'add_to_cart' and isinstance(parsed.get('items'), list):
            action = parsed
        elif mode == 'purchase' and parsed.get('action') == 'schedule_order' and parsed.get('delivery_date'):
            action = parsed
        elif mode == 'plan' and parsed.get('action') == 'save_plan' and isinstance(parsed.get('items'), list):
            action = parsed
        if isinstance(parsed.get('quick_replies'), list):
            quick_replies = [str(q) for q in parsed['quick_replies'] if str(q).strip()][:5]
        if isinstance(parsed.get('save_memory'), list):
            new_memory_notes = [str(n).strip() for n in parsed['save_memory'] if str(n).strip()][:5]
        if parsed.get('clear_memory'):
            clear_memory = True

    return raw_reply, action, quick_replies, new_memory_notes, clear_memory


def apply_cart_action(action, products_by_id, customer_id, account_type=None):
    """Validates the AI's requested add_to_cart action against the real
    catalog and applies it. Returns a list of {'name', 'quantity'} actually added."""
    from . import cart as cart_utils
    from .pricing import get_min_qty

    added = []
    if not action:
        return added

    for item in action.get('items', []):
        try:
            product_id = int(item.get('product_id'))
            quantity = int(item.get('quantity', 1))
        except (TypeError, ValueError):
            continue
        product = products_by_id.get(product_id)
        if not product:
            continue
        min_qty = get_min_qty(product, account_type)
        quantity = max(min_qty, min(quantity, max(min_qty, 200) if account_type == 'b2b' else 20))
        cart_utils.add_to_cart(customer_id, product_id, quantity)
        added.append({'name': product['name'], 'quantity': quantity})

    return added


def apply_save_plan_action(action, products_by_id, customer_id, account_type=None):
    """Validates the AI's requested save_plan action against the real catalog
    and saves it as a weekly plan note. Returns the saved items list, or []."""
    from .pricing import get_min_qty
    from .saved_plans import save_plan

    if not action:
        return []

    items = []
    for item in action.get('items', []):
        try:
            product_id = int(item.get('product_id'))
            quantity = int(item.get('quantity', 1))
        except (TypeError, ValueError):
            continue
        product = products_by_id.get(product_id)
        if not product:
            continue
        min_qty = get_min_qty(product, account_type)
        quantity = max(min_qty, min(quantity, max(min_qty, 200) if account_type == 'b2b' else 20))
        items.append({'product_id': product_id, 'quantity': quantity, 'name': product['name']})

    if items:
        title = str(action.get('title') or 'Weekly plan').strip()[:150]
        note = str(action.get('note') or '').strip()[:500]
        save_plan(customer_id, title, items, note)

    return items
