File size: 44,550 Bytes
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af31ea
85354fe
72eef4f
 
3a8bd11
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aecb7e8
 
 
 
 
 
 
 
 
 
 
 
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af31ea
72eef4f
 
 
6af31ea
72eef4f
 
 
6af31ea
 
72eef4f
 
 
 
3a8bd11
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a8bd11
6af31ea
72eef4f
 
 
6af31ea
 
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aecb7e8
72eef4f
 
 
 
 
 
aecb7e8
 
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aecb7e8
 
 
72eef4f
 
 
 
 
 
aecb7e8
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aecb7e8
 
 
 
 
 
 
 
 
 
 
 
72eef4f
 
 
 
 
 
 
85354fe
72eef4f
 
 
 
 
 
 
6af31ea
 
 
72eef4f
6af31ea
72eef4f
 
6af31ea
72eef4f
 
 
 
 
c0636b1
72eef4f
 
85354fe
 
 
 
 
 
 
 
 
6af31ea
72eef4f
 
 
 
 
 
6af31ea
 
 
 
85354fe
 
 
6af31ea
 
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
c0636b1
72eef4f
 
6af31ea
85354fe
 
 
 
 
 
 
 
 
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af31ea
 
 
 
 
 
 
 
 
72eef4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aecb7e8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6af31ea
aecb7e8
 
6af31ea
 
 
72eef4f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
#your_app/ai_features.py


import re
from flask import Blueprint, request, jsonify, current_app
from bson.objectid import ObjectId
from datetime import datetime, date, timedelta
from flask_jwt_extended import jwt_required, get_jwt_identity
from .extensions import bcrypt, mongo
from .Company_Info import company_info
from .general_utils import backfill_orders,get_next_order_serial
from google import genai
from google.genai import types
import os
import json
import traceback
from .xero_utils import trigger_invoice_creation,trigger_contact_creation
from .email_utils import send_order_confirmation_email
# +++ START: WHATSAPP FEATURE IMPORTS +++
import requests
import random
from twilio.twiml.messaging_response import MessagingResponse
# +++ END: WHATSAPP FEATURE IMPORTS +++

ai_bp = Blueprint('ai', __name__)

today = date.today()
weekday_number = today.weekday()
days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
weekday_name = days_of_week[weekday_number]
current_date = datetime.now().strftime('%Y-%m-%d')



# MODIFIED
add_items_to_cart_function = {
    "name": "add_items_to_cart",
    "description": "Extracts order details from a message to add products to a specific customer's shopping cart. Use this to handle orders like 'Mr Vaibhav Arora Tomato 1 Case Apples 2 Bags'.",
    "parameters": {
        "type": "object", "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer for whom the order is being placed. This MUST be extracted from the message."
            },
            "items": { "type": "array", "description": "A list of items to add to the cart.", "items": { "type": "object", "properties": { "product_name": { "type": "string", "description": "The name of the product. This must match one of the available product names.", }, "quantity": { "type": "number", "description": "The quantity of the product.", }, "unit": { "type": "string", "description": "The unit of measure for the quantity (e.g., 'case', 'bag', 'piece').This must match one of the available units for the product.Must be lowercase, Eg: The correct unit is:`bag` , and invalid unit is  `bags`", }, }, "required": ["product_name", "quantity", "unit"], }, },
            "delivery_date": {
                "type": "string",
                "description": "The desired delivery date for the order, in YYYY-MM-DD format. Extract if the user explicitly states a date.",
            }
        },
        "required": ["user_identifier", "items"],
    },
}

# MODIFIED
remove_items_from_cart_function = {
    "name": "remove_items_from_cart",
    "description": "Removes one or more specific items from a customer's shopping cart based on their name.",
    "parameters": {
        "type": "object",
        "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer. Must be extracted from the user's message."
            },
            "items": {
                "type": "array",
                "description": "A list of items to remove from the cart. Only product_name is required.",
                "items": {
                    "type": "object",
                    "properties": {
                        "product_name": {
                            "type": "string",
                            "description": "The name of the product to remove. This must match an available product name.",
                        },
                    },
                    "required": ["product_name"],
                },
            }
        },
        "required": ["user_identifier", "items"],
    },
}

# MODIFIED
clear_cart_function = {
    "name": "clear_cart",
    "description": "Removes all items from a specific customer's shopping cart.",
    "parameters": {
        "type": "object",
        "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer whose cart should be cleared. Must be extracted from the user's message."
            }
        },
        "required": ["user_identifier"]
    }
}

# +++ START: NEW FUNCTION DEFINITION FOR DIRECT ORDER +++
create_direct_order_function = {
    "name": "create_direct_order",
    "description": "Parses a complete order from a single message and places it directly, bypassing the cart. Use this when the user provides the customer name, items, and a delivery date all at once.",
    "parameters": {
        "type": "object",
        "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer for whom the order is being placed. This MUST be extracted from the message."
            },
            "items": {
                "type": "array",
                "description": "A list of items for the order.",
                "items": {
                    "type": "object",
                    "properties": {
                        "product_name": {"type": "string", "description": "The name of the product."},
                        "quantity": {"type": "number", "description": "The quantity of the product."},
                        "unit": {"type": "string", "description": "The unit of measure (e.g., 'case', 'bag', 'piece').Must be lowercase"}
                    },
                    "required": ["product_name", "quantity", "unit"]
                }
            },
            "delivery_date": {
                "type": "string",
                "description": "The desired delivery date for the order, in YYYY-MM-DD format. This is required to use this function."
            },
            "additional_info": {
                "type": "string",
                "description": "Any special instructions or notes from the user for the delivery."
            }
        },
        "required": ["user_identifier", "items", "delivery_date"],
    },
}

navigate_to_page_function = {
    "name": "navigate_to_page",
    "description": "Provides a button in the chat to navigate the user to a specific page.",
    "parameters": {
        "type": "object",
        "properties": {
            "page": {
                "type": "string",
                "description": "The destination page. Must be one of the allowed pages.",
                "enum": ["orders", "cart", "catalog", "home", "about", "care", "login", "register"]
            }
        },
        "required": ["page"]
    }
}

# MODIFIED
get_my_orders_function = {
    "name": "get_my_orders",
    "description": "Retrieves a summary of a specific customer's most recent orders to display in the chat.",
    "parameters": {
        "type": "object",
        "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer whose orders are being requested."
            }
        },
        "required": ["user_identifier"]
    }
}

# MODIFIED
get_cart_items_function = {
    "name": "get_cart_items",
    "description": "Retrieves the items currently in a specific customer's shopping cart and lists them.",
    "parameters": {
        "type": "object",
        "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer whose cart is being viewed."
            }
        },
        "required": ["user_identifier"]
    }
}

get_order_details_function = {
    "name": "get_order_details",
    "description": "Retrieves the specific items and details for a single order based on its ID.",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The ID of the order to fetch. Can be the full ID or the last 6 characters."
            }
        },
        "required": ["order_id"]
    }
}

cancel_order_function = {
    "name": "cancel_order",
    "description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The ID of the order to be cancelled."
            }
        },
        "required": ["order_id"]
    }
}

# MODIFIED
place_order_function = {
    "name": "place_order",
    "description": "Places a final order for a customer using the items in their cart. This action is final and will clear the cart.",
    "parameters": {
        "type": "object",
        "properties": {
            "user_identifier": {
                "type": "string",
                "description": "The name, email, or business name of the customer for whom the order is being placed. Must be extracted from the user's message."
            },
            "delivery_date": {
                "type": "string",
                "description": "The desired delivery date in YYYY-MM-DD format. Must be confirmed with the user."
            },
            "additional_info": {
                "type": "string",
                "description": "Any special instructions or notes from the user for the delivery."
            }
        },
        "required": ["user_identifier", "delivery_date"]
    }
}

register_new_customer_function = {
    "name": "register_new_customer",
    "description": "Registers a new customer by collecting their essential details. Use this when a user expresses intent to sign up or is not recognized.",
    "parameters": {
        "type": "object",
        "properties": {
            "businessName": {
                "type": "string",
                "description": "The customer's business or company name."
            },
            "email": {
                "type": "string",
                "description": "The customer's primary email address. This will be their login username."
            },
            "password": {
                "type": "string",
                "description": "A password for the user's account. The user must provide this."
            },
            "phoneNumber": {
                "type": "string",
                "description": "The user's contact phone number."
            },
            "businessAddress": {
                "type": "string",
                "description": "The full delivery address for the business."
            },
            "contactPerson": {
                "type": "string",
                "description": "The customer's full name or primary contact person's name."
            }
        },
        "required": ["businessName", "email", "password", "phoneNumber", "businessAddress"]
    }
}

# +++ START: NEW FUNCTION DEFINITION FOR LISTING ACCOUNTS +++
list_all_accounts_function = {
    "name": "list_all_accounts",
    "description": "Lists all registered customer accounts with their name, business name, and email. Use this when the admin asks for a list of all users or accounts.",
    "parameters": {
        "type": "object",
        "properties": {}
    }
}
# +++ END: NEW FUNCTION DEFINITION FOR LISTING ACCOUNTS +++


get_my_orders_function_website = {
    "name": "get_my_orders",
    "description": "Retrieves a summary of the user's most recent orders to display in the chat.",
    "parameters": {"type": "object", "properties": {}}
}

add_items_to_cart_function_website = {
    "name": "add_items_to_cart",
    "description": "Extracts order details from a user's message to add products to their shopping cart. Can also set a delivery date.",
    "parameters": {
        "type": "object", "properties": {
            "items": { "type": "array", "description": "A list of items to add to the cart.", "items": { "type": "object", "properties": { "product_name": { "type": "string", "description": "The name of the product. This must match one of the available product names.", }, "quantity": { "type": "number", "description": "The quantity of the product.", }, "unit": { "type": "string", "description": "The unit of measure for the quantity (e.g., 'lb', 'pieces', 'box', 'bunch'). This must match one of the available units for the product.", }, }, "required": ["product_name", "quantity", "unit"], }, },
            "delivery_date": {
                "type": "string",
                "description": "The desired delivery date for the order, in YYYY-MM-DD format. The user must explicitly state a date.",
            }
        },
        "required": ["items"],
    },
}
cancel_order_function_website = {
    "name": "cancel_order",
    "description": "Proposes to cancel an order based on its ID. This requires user confirmation.",
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {
                "type": "string",
                "description": "The ID of the order to be cancelled. Can be the full ID or the last 6 characters."
            }
        },
        "required": ["order_id"]
    }
}

@ai_bp.route('/chat', methods=['POST'])
@jwt_required()
def handle_ai_chat():
        current_app.logger.info("Handling AI chat request.")
        
    # ... (Web chat endpoint remains unchanged)
        user_email = get_jwt_identity()
        all_products = list(mongo.db.products.find({}, {'name': 1, 'modes': 1, '_id': 0}))
        product_context_list = []
        for p in all_products:
            if 'name' not in p: continue
            modes_obj = p.get('modes', {})
            available_modes = list(modes_obj.keys()) if modes_obj else []
            if available_modes:
                product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
            else:
                product_context_list.append(p['name'])
        client = genai.Client(api_key=random.choice(json.loads(os.getenv("GEMINI_KEY_LIST"))))

        min_delivery_date = (datetime.now() + timedelta(days=2)).strftime('%Y-%m-%d')

        tools = types.Tool(function_declarations=[
            add_items_to_cart_function_website,
            navigate_to_page_function,
            get_my_orders_function_website,
            cancel_order_function_website,
        ])
        config = types.GenerateContentConfig(tools=[tools])
        
        instruction_prompt = f"""

        You are an expert assistant for a food wholesale company. Your capabilities include:

        1.  Ordering & Delivery Date: Use `add_items_to_cart` to add items to the user's cart. You can also set a delivery date if the user specifies one. The current date is {current_date,weekday_name}. The earliest possible delivery date is {min_delivery_date}. If the user asks for a date earlier than this, inform them of the constraint and ask them to provide a new valid date.

        2.  Product List: {'; '.join(product_context_list)}.

        3.  Page Navigation: Use `navigate_to_page` if the user wants to GO to a page like 'my orders', 'about us', 'customer care', 'login', etc.

        4.  Order Summary: Use `get_my_orders` to LIST a summary of recent orders in the chat.

        5.  Order Cancellation: Use `cancel_order` to start the cancellation process for a SPECIFIC order. The user must provide an order ID.

        6.  New Customer Registration: Use `register_new_customer` if a user wants to sign up.

        7.  The Capability You must use AT ALL TIMES: Luring the customer to place an order and persuading them to buy more products.

        *Always Brag about the quality of our products and our customer service and try to persuade the customer to place an order.*

        Company Info for general questions: {company_info}

        *** IMPORTANT: When providing a text-based answer, YOU MUST USE MARKDOWN. Use headings, bold text, bullet points (`* item`), and other formatting to make your answers clear, structured, and easy to read. ***

        Always refer to the chat history for context. **If information is missing, ask for it.**

        """

        user_input_part, history_data = None, []
        if 'multipart/form-data' in request.content_type:
            audio_file = request.files.get('audio')
            history_json = request.form.get('history', '[]')
            history_data = json.loads(history_json)
            user_input_part = types.Part.from_bytes(data=audio_file.read(), mime_type=audio_file.mimetype.split(';')[0])
        elif 'application/json' in request.content_type:
            json_data = request.get_json()
            history_data = json_data.get('history', [])
            user_input_part = json_data.get('message')
        else: return jsonify({"msg": "Unsupported format"}), 415

        history_contents = [msg.get('data') for msg in history_data if msg.get('type') == 'text']
        final_contents = [instruction_prompt] + history_contents + [user_input_part]
        response = client.models.generate_content(model="gemini-2.5-flash", contents=final_contents, config=config)
        response_part = response.candidates[0].content.parts[0]

        if response_part.function_call:
            backfill_orders()
            function_call = response_part.function_call

            if function_call.name == "add_items_to_cart":
                proposal_data = {
                    "items": function_call.args.get('items', []),
                    "delivery_date": function_call.args.get('delivery_date')
                }
                return jsonify({"type": "order_proposal", "data": proposal_data}), 200

            elif function_call.name == "register_new_customer":
                return jsonify({
                    "type": "navigation_proposal",
                    "data": {
                        "path": "/register",
                        "button_text": "Go to Sign Up Page",
                        "prompt_text": "Excellent! I can help with that. Please click the button below to go to our registration page."
                    }
                }), 200

            elif function_call.name == "navigate_to_page":
                page = function_call.args.get('page')
                path_map = {'orders': '/order', 'cart': '/cart', 'catalog': '/catalog', 'home': '/', 'about': '/about', 'care': '/care', 'login': '/login', 'register': '/register'}
                text_map = {'orders': 'Go to My Orders', 'cart': 'Go to Cart', 'catalog': 'Go to Catalog', 'home': 'Go to Homepage', 'about': 'Go to About Page', 'care': 'Go to Customer Care', 'login': 'Go to Login', 'register': 'Go to Sign Up'}
                if page in path_map:
                    return jsonify({"type": "navigation_proposal", "data": {"path": path_map[page], "button_text": text_map[page], "prompt_text": f"Sure, let's go to the {page} page."}}), 200

            elif function_call.name == "get_my_orders":
                recent_orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(5))
                if not recent_orders: return jsonify({"type": "text", "data": "You have no recent orders."}), 200
                details_text=""
                for order in recent_orders:
                    product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
                    products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
                    item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order.get('items', [])]
                    details_text += f"\n\n **Details for Order #{str(order['serial_no'])}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
                return jsonify({"type": "text", "data": "**Here are your recent orders**:\n" + details_text}), 200

            elif function_call.name == "cancel_order":
                order_id_frag = function_call.args.get("order_id", "").strip()
                all_user_orders = mongo.db.orders.find({'user_email': user_email})
                order = None
                for o in all_user_orders:
                    if str(o['serial_no']) == order_id_frag:
                        order = o
                        break
                if not order: return jsonify({"type": "text", "data": f"Sorry, I couldn't find an order with ID matching '{order_id_frag}'."}), 200
                if order.get('status') not in ['pending', 'confirmed']: return jsonify({"type": "text", "data": f"Order #{order['serial_no']} cannot be cancelled as its status is '{order.get('status')}'."}), 200
                return jsonify({
                    "type": "cancellation_proposal",
                    "data": {"order_id": str(order['_id']), "prompt_text": f"Are you sure you want to cancel order #{order['serial_no']}?"}
                }), 200

        return jsonify({"type": "text", "data": response.text}), 200

def _find_user_by_identifier(identifier):
    """

    Finds a user by contact person, business name, or email with improved matching.

    Handles prefixes and prompts for clarification on multiple matches.

    """
    if not identifier or not isinstance(identifier, str):
        return None, "No user identifier was provided. Please specify a customer."
    
    # Clean the identifier to remove common titles for better name matching
    titles = ['mr', 'mrs', 'ms', 'dr', 'mr.', 'mrs.']
    original_identifier = identifier.strip()
    # Clean the identifier for name searches by removing titles
    cleaned_identifier = ' '.join([word for word in original_identifier.split() if word.lower().strip('.') not in titles])
    
    # If cleaning results in an empty string (e.g., input was just "Mr."), use the original.
    if not cleaned_identifier:
        cleaned_identifier = original_identifier

    query = {
        "$or": [
            # Search for the cleaned name within the contactPerson and businessName fields
            {"contactPerson": {"$regex": re.escape(cleaned_identifier), "$options": "i"}},
            {"businessName": {"$regex": re.escape(cleaned_identifier), "$options": "i"}},
            # Use a more exact match for the email with the original input
            {"email": {"$regex": f"^{re.escape(original_identifier)}$", "$options": "i"}}
        ]
    }
    users_found = list(mongo.db.users.find(query))
    
    if len(users_found) == 0:
        return None, f"I could not find a customer matching '{identifier}'. Please check the name , or try finding by using their email id or register them as a new customer."
    
    if len(users_found) > 1:
        options_text = "*Did you mean:*\n"
        for u in users_found:
            contact_person = u.get('contactPerson', 'N/A')
            business_name = u.get('businessName', 'N/A')
            options_text += f"- {contact_person} (from {business_name})?\n"
        options_text += "\nPlease clarify by providing the full name, business name, or email."
        return None, options_text

    return users_found[0], None # Return user object and no error message

@ai_bp.route('/whatsapp', methods=['POST'])
def whatsapp_reply():
    """ Responds to incoming WhatsApp messages (text or audio) via Twilio. """
    final_response_text = "I'm sorry, I encountered an error. Please try again."
    twilio_resp = MessagingResponse()
    
    try:
        # --- 1. Get Message from Twilio Request ---
        whatsapp_number = request.values.get('From')
        user_message_text = request.values.get('Body', '').strip()
        num_media = int(request.values.get('NumMedia', 0))

        if not whatsapp_number:
            current_app.logger.error("Request received without a 'From' number.")
            twilio_resp.message("Could not identify your number. Please try again.")
            return str(twilio_resp)

        # --- 2. Setup AI Client and Product Context ---
        client = genai.Client(api_key=random.choice(json.loads(os.getenv("GEMINI_KEY_LIST"))))
        all_products = list(mongo.db.products.find({}, {'name': 1, 'modes': 1, '_id': 0}))
        product_context_list = []
        for p in all_products:
            if 'name' not in p: continue
            modes_obj = p.get('modes', {})
            available_modes = list(modes_obj.keys()) if modes_obj else []
            if available_modes:
                product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
            else:
                product_context_list.append(p['name'])        
        # --- 3. Process User Input (Text or Audio) ---
        user_input_part = None
        if num_media > 0:
            media_url = request.values.get('MediaUrl0')
            mime_type = request.values.get('MediaContentType0')
            if 'audio' in mime_type:
                audio_response = requests.get(media_url)
                if audio_response.status_code == 200:
                    user_input_part = types.Part.from_bytes(data=audio_response.content, mime_type=mime_type.split(';')[0])
        
        if not user_input_part and user_message_text:
            user_input_part = user_message_text

        if not user_input_part:
            twilio_resp.message("Please send a message or a voice note.")
            return str(twilio_resp)

        # --- 4. Configure AI for Multi-Customer Admin ---
        history_doc = mongo.db.whatsapp_history.find_one({'whatsapp_number': whatsapp_number})
        chat_history = history_doc.get('history', []) if history_doc else []

        tools = types.Tool(function_declarations=[
            create_direct_order_function, 
            add_items_to_cart_function,
            remove_items_from_cart_function,
            clear_cart_function,
            get_cart_items_function,
            place_order_function,
            get_my_orders_function,
            register_new_customer_function,
            list_all_accounts_function
        ])
        
        instruction_prompt = f"""

        The current date is {current_date,weekday_name}.

        You are an expert wholesale ordering assistant for 'Matax Express', communicating via WhatsApp. You are talking to an admin user who will place orders and manage accounts for MULTIPLE different customers.



        Your primary capabilities are:

        1.  **Placing and Managing Orders for Customers:**

            - The admin's message will specify the customer by their name (e.g., "Mr. Vaibhav Arora"), business name, or email.

            - Your FIRST step is to extract this customer identifier. You MUST provide this `user_identifier` in all function calls related to carts or orders.

            - If you cannot identify a customer, or if the name is ambiguous, you must ask the admin for clarification.



        2.  **Direct Orders (One-Shot):**

            - If the user provides a customer name, a list of items, AND a delivery date in a single message, use the `create_direct_order` function to place the order immediately, bypassing the cart.



        3.  **Registering New Customers:**

            - If the admin asks to register a new customer, use the `register_new_customer` function.

        

        4.  **Listing All Customers:**

            - If the admin asks for a list of all accounts or users, use the `list_all_accounts` function.



        **Function Guide & Rules:**

        - `create_direct_order`: Use for complete, one-shot orders with a delivery date.

        - `add_items_to_cart`: To add items for a specific customer when NO delivery date is given.

        - `place_order`: To finalize and place a customer's order *from their cart*.

        - `register_new_customer`: To sign up a new customer.

        - `list_all_accounts`: To get a list of all registered customers.

        - **Product List:** {', '.join(product_context_list)}.

        - **Company Info:** For general questions: {company_info}.

        - **Communication:** Always be professional. Confirm actions clearly. Respond using WhatsApp-compatible markdown (*bold*, _italic_).

        - **Website Info:** When a customer is registered or an order is placed, inform the admin that the customer can log in at https://matax-express.vercel.app/.

        """
        
        config = types.GenerateContentConfig(tools=[tools])
        
        final_contents = [instruction_prompt] + chat_history + [user_input_part]
        response = client.models.generate_content(
            model="gemini-2.5-flash", contents=final_contents, config=config
        )
        
        response_part = response.candidates[0].content.parts[0]

        # --- 5. Process AI Response (Function Call or Text) ---
        if response_part.function_call:
            backfill_orders()
            function_call = response_part.function_call
            args = function_call.args
            
            # Handle functions that don't need a user lookup first
            if function_call.name == 'register_new_customer':
                email = args.get('email').lower()
                if mongo.db.users.find_one({'email': email}):
                    final_response_text = f"An account with the email '{email}' already exists."
                else:
                    hashed_password = bcrypt.generate_password_hash(args.get('password')).decode('utf-8')
                    user_doc = { "businessName": args.get('businessName'), "companyName": args.get('businessName'), "email": email, "password": hashed_password, "phoneNumber": args.get('phoneNumber'), "businessAddress": args.get('businessAddress'), "contactPerson": args.get('contactPerson'), "is_approved": False, "is_admin": False, "created_at": datetime.utcnow() }
                    mongo.db.users.insert_one(user_doc)
                    final_response_text = f"The User ,  {args.get('contactPerson')} is now registered and their application is pending for approval. After Apprvoal , {args.get('contactPerson')}  can log in at https://matax-express.vercel.app/."

            elif function_call.name == 'get_order_details':
                order_doc = mongo.db.orders.find_one({'_id': ObjectId(args.get("order_id"))}) if ObjectId.is_valid(args.get("order_id")) else mongo.db.orders.find_one({'_id': {'$regex': f'.*{args.get("order_id")}$'}} )
                if not order_doc:
                    final_response_text = f"Sorry, I couldn't find an order with ID matching '{args.get('order_id')}'."
                else:
                    p_ids = [ObjectId(item['productId']) for item in order_doc.get('items', [])]
                    p_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': p_ids}})}
                    item_lines = [f"- {i['quantity']} {i.get('mode', 'pcs')} of {p_map.get(i['productId'], {}).get('name', 'Unknown')}" for i in order_doc.get('items', [])]
                    details = f"*Details for Order #{order['serial_no']}*\n*Customer:* {order_doc.get('user_email')}\n*Status:* {order_doc.get('status', 'N/A')}\n*Delivery:* {order_doc.get('delivery_date', 'N/A')}\n\n*Items:*\n" + "\n".join(item_lines)
                    final_response_text = details
            
            elif function_call.name == 'list_all_accounts':
                all_users = list(mongo.db.users.find({}, {'email': 1, 'contactPerson': 1, 'businessName': 1, '_id': 0}))
                if not all_users:
                    final_response_text = "There are no customer accounts registered in the system yet."
                else:
                    account_list_text = "*Here is the list of all customer accounts:*\n\n"
                    for user in all_users:
                        account_list_text += f"- *Name:* {user.get('contactPerson', 'N/A')}\n"
                        account_list_text += f"  *Business:* {user.get('businessName', 'N/A')}\n"
                        account_list_text += f"  *Email:* {user.get('email', 'N/A')}\n\n"
                    final_response_text = account_list_text.strip()

            # For all other functions, perform user lookup
            else:
                user, error_msg = _find_user_by_identifier(args.get("user_identifier"))
                if error_msg:
                    final_response_text = error_msg
                else: # User was found successfully
                    user_email = user['email']
                    user_id = mongo.db.users.find_one({'email': user_email})
                    user_name = user.get('contactPerson', 'the customer')

                    if function_call.name == 'create_direct_order':
                        items_from_args = args.get('items', [])
                        validated_items, error_items = [], []
                        for item in items_from_args:
                            p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
                            unit = item.get("unit")
                            if p_doc and unit in p_doc.get("modes", {}):
                                validated_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": unit})
                            else:
                                error_items.append(f"{item.get('product_name')} (unit: {unit})")
                        
                        if error_items:
                            final_response_text = f"I couldn't place the order for *{user_name}* because these products/units were not found or are invalid: {', '.join(error_items)}. Please try again."
                        else:
                            next_serial = get_next_order_serial()
                            order_doc = {'user_email': user_email, 'items': validated_items, 'delivery_date': args.get('delivery_date'), 'delivery_address': user.get('businessAddress'), 'mobile_number': user.get('phoneNumber'), 'additional_info': args.get('additional_info', ''), 'status': 'pending', 'created_at': datetime.utcnow(),'serial_no': next_serial}
                            order_id = mongo.db.orders.insert_one(order_doc).inserted_id
                            order_details_for_xero = {
                            "order_id": str(next_serial), "user_email": user_email, "items": validated_items,
                            "delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
                            }
                            order_doc['_id'] = order_id
                            product_ids = [ObjectId(item['productId']) for item in validated_items]
                            products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
                            order_doc['populated_items'] = [{
                                "name": products_map.get(item['productId'], {}).get('name', 'N/A'),
                                "quantity": item['quantity'],
                                "mode": item.get('mode', 'pieces')
                            } for item in validated_items]
                            send_order_confirmation_email(order_doc, user_id)
                            trigger_invoice_creation(order_details_for_xero)
                            final_response_text = f"Thank you! I have directly placed order #{next_serial} for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."

                    elif function_call.name == 'add_items_to_cart':
                        items_to_add, added_messages, db_items = args.get('items', []), [], []
                        for item in items_to_add:
                            p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
                            unit = item.get("unit")
                            if p_doc and unit in p_doc.get("modes", {}):
                                db_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": unit})
                                modes=unit
                                if modes == 'weight':
                                    modes='lb'
                                added_messages.append(f"{item.get('quantity')} {modes} of {p_doc['name']}")
                            else: 
                                added_messages.append(f"could not find '{item.get('product_name')}' or unit '{unit}' is invalid")
                        if db_items: mongo.db.carts.update_one({'user_email': user_email}, {'$push': {'items': {'$each': db_items}}, '$set': {'updated_at': datetime.utcnow()}}, upsert=True)
                        final_response_text = f"OK, I've updated the cart for *{user_name}*: I added {', '.join(added_messages)}."

                    elif function_call.name == 'place_order':

                        next_serial = get_next_order_serial()
                        cart = mongo.db.carts.find_one({'user_email': user_email})
                        if not cart or not cart.get('items'):
                            final_response_text = f"The cart for *{user_name}* is empty. Please add items first."
                        else:
                            order_doc = {'user_email': user_email, 'items': cart['items'], 'delivery_date': args.get('delivery_date'), 'delivery_address': user.get('businessAddress'), 'mobile_number': user.get('phoneNumber'), 'additional_info': args.get('additional_info', ''), 'status': 'pending', 'created_at': datetime.utcnow(),'serial_no': next_serial}
                            order_id = mongo.db.orders.insert_one(order_doc).inserted_id
                            order_details_for_xero = {
                            "order_id": str(next_serial), "user_email": user_email, "items": cart['items'],
                            "delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
                            }
                            trigger_invoice_creation(order_details_for_xero)
                            order_doc['_id'] = order_id
                            product_ids = [ObjectId(item['productId']) for item in cart['items']]
                            products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
                            order_doc['populated_items'] = [{
                                "name": products_map.get(item['productId'], {}).get('name', 'N/A'),
                                "quantity": item['quantity'],
                                "mode": item.get('mode', 'pieces')
                            } for item in cart['items']]
                            send_order_confirmation_email(order_doc, user_id)
                            mongo.db.carts.delete_one({'user_email': user_email}) # Clear cart
                            final_response_text = f"Thank you! Order `#{next_serial}` has been placed for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
                    
                    elif function_call.name == 'get_cart_items':
                        cart = mongo.db.carts.find_one({'user_email': user_email})
                        if not cart or not cart.get('items'):
                            final_response_text = f"The shopping cart for *{user_name}* is currently empty."
                        else:
                            p_ids = [ObjectId(item['productId']) for item in cart.get('items', [])]
                            p_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': p_ids}})}
                            item_lines = [f"- {i.get('quantity')} {i.get('mode', 'pcs')} of {p_map.get(i.get('productId'), {}).get('name', 'Unknown')}" for i in cart.get('items', [])]
                            final_response_text = f"*Here are the items in {user_name}'s cart:*\n" + "\n".join(item_lines) if item_lines else f"The cart for *{user_name}* is empty."
                    
                    elif function_call.name == 'get_my_orders':
                        orders = list(mongo.db.orders.find({'user_email': user_email}).sort('created_at', -1).limit(3))
                        details_text = ""
                        if not orders:
                            final_response_text = f"*{user_name}* doesn't have any recent orders."
                        else:
                            for order in orders:
                                order_display_id = order.get('serial_no')
                                product_ids = [ObjectId(item['productId']) for item in order.get('items', [])]
                                products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
                                item_lines = [f"- {item['quantity']} {item.get('mode', 'pcs')} of {products_map.get(item['productId'], {}).get('name', 'Unknown Product')}" for item in order.get('items', [])]
                                details_text += f"\n\n **Details for Order #{order_display_id}** _(Delivery Status: {order.get('status')})_:\n" + "\n".join(item_lines)
                            final_response_text ="Here are your recent orders:\n" + details_text
        else:
            final_response_text = response.text
            current_app.logger.info(f"AI response: {final_response_text}")
            try:
                final_response_text = final_response_text.replace("weight","pound")
            except Exception as e:
                pass
            try:
                final_response_text = final_response_text.replace("Weight","pound")
            except Exception as e:
                pass

        # --- 6. Save Conversation to Database ---
        user_message_to_save = user_message_text if user_message_text else "Audio message"
        chat_history.append({"role": "user", "parts": [{"text": user_message_to_save}]})
        chat_history.append({"role": "model", "parts": [{"text": final_response_text}]})
        if len(chat_history) > 10: chat_history = chat_history[-10:]
        mongo.db.whatsapp_history.update_one(
            {'whatsapp_number': whatsapp_number},
            {'$set': {'history': chat_history, 'updated_at': datetime.utcnow()}},
            upsert=True
        )

    except Exception as e:
        print(traceback.format_exc())
        current_app.logger.error(f"WhatsApp endpoint error: {e}")
        final_response_text = "I'm having a little trouble right now. Please try again in a moment."

    # --- 7. Send Response via Twilio ---
    MAX_LENGTH = 1600
    if len(final_response_text) > MAX_LENGTH:
        # Split the message into parts and send them sequentially
        parts = []
        temp_string = final_response_text
        while len(temp_string) > 0:
            if len(temp_string) > MAX_LENGTH:
                # Find the last newline or space to avoid splitting mid-word
                split_point = temp_string[:MAX_LENGTH].rfind('\n')
                if split_point == -1:
                    split_point = temp_string[:MAX_LENGTH].rfind('. ')
                if split_point == -1:
                    split_point = temp_string[:MAX_LENGTH].rfind(' ')
                # If no natural break point, just split at the max length
                if split_point == -1:
                    split_point = MAX_LENGTH
                
                parts.append(temp_string[:split_point])
                temp_string = temp_string[split_point:].strip()
            else:
                parts.append(temp_string)
                break
        
        for part in parts:
            twilio_resp.message(part)
    else:
        twilio_resp.message(final_response_text)
        
    return str(twilio_resp)