File size: 34,612 Bytes
8e722e0
 
 
 
92431ff
0ccc516
a4536e5
92431ff
8e722e0
 
 
a4536e5
8e722e0
9cdf25a
8e722e0
7c2e395
8e722e0
 
 
 
 
 
92431ff
 
 
 
 
 
 
 
 
 
 
 
 
 
a4536e5
bfb9136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e722e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92431ff
 
bfb9136
92431ff
 
 
 
 
 
 
 
 
 
 
ffe1980
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bfb9136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92431ff
bfb9136
92431ff
ef0ef1f
 
bfb9136
 
 
 
 
 
 
 
 
 
 
92431ff
bfb9136
92431ff
 
 
bfb9136
 
 
 
 
 
ef0ef1f
 
bfb9136
 
ef0ef1f
 
 
bfb9136
ef0ef1f
 
bfb9136
 
92431ff
ef0ef1f
bfb9136
 
 
 
 
 
 
 
 
 
 
 
 
ef0ef1f
bfb9136
ef0ef1f
5cd697b
 
 
 
 
 
 
 
 
 
 
 
 
bed96be
 
 
 
 
d62794c
 
 
 
 
 
 
 
 
bed96be
 
01d4963
bed96be
 
 
78bff30
 
 
97a2bcf
78bff30
97a2bcf
 
 
 
 
 
 
 
 
78bff30
 
97a2bcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78bff30
97a2bcf
 
 
 
 
 
78bff30
 
97a2bcf
 
 
 
 
 
 
 
78bff30
97a2bcf
78bff30
97a2bcf
 
 
 
 
 
 
 
 
 
 
 
 
ea934e3
97a2bcf
 
 
 
ea934e3
97a2bcf
ea934e3
97a2bcf
ea934e3
 
 
97a2bcf
 
 
 
 
ea934e3
97a2bcf
ea934e3
 
 
 
 
 
97a2bcf
ea934e3
 
 
97a2bcf
 
 
ea934e3
97a2bcf
 
 
 
 
ea934e3
 
 
 
97a2bcf
 
 
 
 
 
ea934e3
97a2bcf
ea934e3
 
 
97a2bcf
 
ea934e3
97a2bcf
 
 
 
 
78bff30
 
 
 
01d4963
 
dd581d8
01d4963
78bff30
97a2bcf
01d4963
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78bff30
97a2bcf
 
 
 
 
 
 
78bff30
 
01d4963
 
 
 
 
 
 
 
 
 
 
 
78bff30
01d4963
 
 
78bff30
01d4963
 
 
 
bed96be
 
 
 
 
 
 
 
 
 
 
 
 
d62794c
bed96be
 
 
d62794c
 
 
 
 
bed96be
 
 
 
 
 
 
 
d62794c
 
 
 
 
 
bed96be
 
 
 
 
d62794c
 
 
 
 
 
 
 
 
 
 
78bff30
 
 
 
 
 
d62794c
 
78bff30
 
 
 
 
 
 
97a2bcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d62794c
97a2bcf
 
 
d62794c
bed96be
ef0ef1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36da3d1
ef0ef1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffe1980
 
 
 
ef0ef1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b50409c
ef0ef1f
bfb9136
 
 
ef0ef1f
 
 
 
 
 
 
8e722e0
 
 
 
 
 
 
 
 
 
bfb9136
8e722e0
 
 
 
bfb9136
8e722e0
bfb9136
 
 
8e722e0
bfb9136
 
 
 
 
 
8e722e0
 
bfb9136
 
 
 
c067234
 
92431ff
bfb9136
 
 
c067234
bfb9136
 
8e722e0
92431ff
 
 
 
 
 
 
bfb9136
 
92431ff
 
 
a4536e5
8e722e0
 
 
 
 
 
f7fba1f
8e722e0
f7fba1f
 
 
 
 
 
8e722e0
 
 
 
 
 
 
 
 
 
7d4ded9
92431ff
 
 
 
 
b50409c
92431ff
b50409c
92431ff
 
 
45f0146
92431ff
45f0146
92431ff
 
b50409c
92431ff
 
 
 
b50409c
7d4ded9
b50409c
92431ff
 
b50409c
92431ff
 
7d4ded9
117c0a2
bed96be
36da3d1
bed96be
 
 
 
 
 
78bff30
 
 
 
 
 
 
 
 
 
97a2bcf
 
36da3d1
 
78bff30
bed96be
 
78bff30
 
 
 
 
 
 
 
 
 
97a2bcf
 
36da3d1
 
78bff30
36da3d1
8e722e0
 
bed96be
8e722e0
 
 
 
 
 
 
 
 
a4536e5
 
 
 
92431ff
a4536e5
 
 
c067234
92431ff
a4536e5
 
 
 
7d4ded9
 
 
 
 
8e722e0
 
 
92431ff
8e722e0
 
 
92431ff
8e722e0
 
 
97a2bcf
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
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
import io
import json
import logging
import os
import math
from typing import Optional, List
from collections import deque
from datetime import datetime, timedelta

import cv2
import numpy as np
import imagehash
import uvicorn
from fastapi import FastAPI, File, Form, UploadFile, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image, ImageOps
from ultralytics import YOLO

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# --- IN-MEMORY STATE FOR ADVANCED ANALYTICS ---
# 1. Recent Report History for Deduplication
# Stores: {'lat': float, 'lon': float, 'issue': str, 'time': datetime, 'user': str, 'hash': str}
REPORT_HISTORY = deque(maxlen=200)

# 2. User Activity Tracking for Spam (Velocity Check)
# Stores: {'user_email': str, 'timestamps': deque(maxlen=10)}
USER_ACTIVITY = {} 

# --- CONFIGURATION ---
DEDUP_DISTANCE_METERS = 20.0  # Radius to check for duplicates
DEDUP_TIME_WINDOW_HOURS = 24  # Time window for duplicates
SPAM_VELOCITY_LIMIT = 3       # Max reports
SPAM_VELOCITY_WINDOW_SECONDS = 60 # per time window

# --- FIREBASE INIT ---
import firebase_admin
from firebase_admin import credentials, db

try:
    if "FIREBASE_CREDENTIALS" in os.environ:
        cred_json = json.loads(os.environ["FIREBASE_CREDENTIALS"])
        cred = credentials.Certificate(cred_json)
        firebase_admin.initialize_app(cred, {
            'databaseURL': os.environ.get("FIREBASE_DB_URL", "https://arise-3aaac-default-rtdb.firebaseio.com/") # Fallback or Env
        })
        logger.info("Firebase Admin Initialized successfully.")
    else:
        logger.warning("FIREBASE_CREDENTIALS env var not found. Firebase features will be disabled.")
except Exception as e:
    logger.error(f"Failed to init Firebase: {e}")

# --- ML MODELS INIT ---
from sentence_transformers import SentenceTransformer, util
logger.info("Loading CLIP model (sentence-transformers/clip-ViT-B-32)...")
try:
    # This model handles both text and image embeddings in the same vector space!
    embedding_model = SentenceTransformer('sentence-transformers/clip-ViT-B-32')
    logger.info("CLIP model loaded.")
except Exception as e:
    logger.error(f"Failed to load CLIP model: {e}")
    embedding_model = None

app = FastAPI(title="Arise AI API", version="1.0.0")

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Load Model
try:
    model = YOLO("best.pt")
    logger.info("YOLO model loaded successfully.")
except Exception as e:
    logger.error(f"Failed to load model: {e}")
    model = None

# --- HELPER FUNCTIONS ---


def haversine_distance(lat1, lon1, lat2, lon2):
    """Calculate the great circle distance in meters between two points."""
    R = 6371000  # radius of Earth in meters
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)
    
    a = math.sin(dphi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2)**2
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
    return R * c

# --- FIREBASE FETCH HELPER ---
def fetch_firebase_history():
    """
    Fetches reports from Firebase Realtime Database to populate local history.
    This ensures we have the latest data for deduplication even if server restarted.
    """
    global REPORT_HISTORY
    try:
        if not firebase_admin._apps:
            return

        # Fetch last 100 reports (or time based)
        ref = db.reference('reports')
        # Order by timestamp to get recent ones
        snapshot = ref.order_by_child('timestamp').limit_to_last(100).get()
        
        if not snapshot:
            return

        new_history = deque(maxlen=200)
        
        for key, val in snapshot.items():
            try:
                # Extract fields
                r_lat = float(val.get('latitude', 0))
                r_lon = float(val.get('longitude', 0))
                r_issue = val.get('category', 'General')
                r_user = val.get('userId', 'unknown')
                
                # Time parsing (handling various formats)
                t_str = val.get('timestamp')
                dt = datetime.now()
                if isinstance(t_str, int): # millis
                    dt = datetime.fromtimestamp(t_str / 1000.0)
                elif isinstance(t_str, str):
                    try:
                        dt = datetime.fromisoformat(t_str.replace('Z', '+00:00'))
                    except: pass
                
                # Check if we have a hash stored
                r_hash = val.get('imageHash', "")
                
                # Add to local history list
                new_history.append({
                    'lat': r_lat,
                    'lon': r_lon,
                    'issue': r_issue,
                    'time': dt,
                    'user': r_user,
                    'hash': r_hash
                })
            except Exception as e:
                continue
        
        # Merge/Replace global history
        # We replace it to ensure we are in sync with DB
        REPORT_HISTORY = new_history
        logger.info(f"Refreshed history from Firebase: {len(REPORT_HISTORY)} items")
            
    except Exception as e:
        logger.error(f"Failed to fetch from Firebase: {e}")

# 1. Advanced SPAM Check using Firebase History + Velocity
def check_spam_status(user_email, current_time, blur_score):
    is_spam = False
    reasons = []
    
    # A. Blur Check (Legacy)
    if blur_score < 100.0:
        is_spam = True
        reasons.append(f"Image too blurry (Score: {int(blur_score)})")
        
    if not user_email:
        return is_spam, reasons

    # B. Velocity Check (In-Memory)
    if user_email not in USER_ACTIVITY:
        USER_ACTIVITY[user_email] = deque(maxlen=10)
    
    timestamps = USER_ACTIVITY[user_email]
    timestamps.append(current_time)
    
    # Filter timestamps within the window
    recent_activity = [t for t in timestamps if (current_time - t).total_seconds() <= SPAM_VELOCITY_WINDOW_SECONDS]
    
    if len(recent_activity) > SPAM_VELOCITY_LIMIT:
        is_spam = True
        reasons.append("Submission rate exceeded limit")
        
    # C. Firebase History Check (Persistent)
    # Check if user has been flagged as a specific spammer in the DB
    try:
        # If Firebase is live, we could check a 'users/{safe_email}/spam_count' node
        # For now, we simulate this by checking if they have ANY reports marked as "rejected" due to spam in recent history
        if firebase_admin._apps:
            # This is a placeholder for a real DB lookup. 
            # In production, you'd do: db.reference(f'users/{uid}').get()
            pass
    except Exception as e:
        logger.error(f"Firebase Check Failed: {e}")
        
    return is_spam, reasons

# 2. Hybrid Deduplication (Spatial -> Visual -> Semantic)
def check_hybrid_duplicate(lat, lon, issue_type, current_time, pil_image, description):
    """
    Checks for duplicates in layers:
    1. Spatial: Is it nearby? (< 20m) AND recent (< 24h)
    2. Category: Is it the same type of issue? (Loose match)
    3. Visual (CLIP): Do the images look the same? (Cosine Sim > 0.9)
    4. Semantic (CLIP): Is the description describing the same thing? (Cosine Sim > 0.85)
    """
    if lat == 0 or lon == 0:
        return False, None
    
    issue_lower = issue_type.lower()
    
    # Generate Embeddings (Once)
    img_emb = None
    txt_emb = None
    if embedding_model:
        try:
            img_emb = embedding_model.encode(pil_image, convert_to_tensor=True)
            if description:
                txt_emb = embedding_model.encode(description, convert_to_tensor=True)
        except Exception as e:
            logger.error(f"Embedding generation failed: {e}")
            
    for report in REPORT_HISTORY:
        # A. Time Window
        if (current_time - report['time']) > timedelta(hours=DEDUP_TIME_WINDOW_HOURS):
            continue
            
        # B. Spatial Check (The Filter)
        dist = haversine_distance(lat, lon, report['lat'], report['lon'])
        if dist > DEDUP_DISTANCE_METERS:
            continue
            
        # C. Category Check (Loose)
        report_issue_lower = report['issue'].lower()
        keywords = ["garbage", "pothole", "accident", "water", "streetlight"]
        cat_match = False
        if issue_lower == report_issue_lower: cat_match = True
        else:
            for kw in keywords:
                if kw in issue_lower and kw in report_issue_lower:
                    cat_match = True
                    break
        
        # If no category match, we might still check visual similarity if it's very close
        if not cat_match and dist > 5: # Only strict category if > 5m
            continue

        # Found a potential candidate nearby!
        
        # D. Advanced Check: Visual / Semantic Similarity
        # If we have embeddings for both, compare them
        if img_emb is not None and 'visual_emb' in report:
            hist_img_emb = report['visual_emb']
            if hist_img_emb is not None:
                sim = util.cos_sim(img_emb, hist_img_emb)
                if sim > 0.9:
                    return True, f"Visual Duplicate found {int(dist)}m away (Sim: {sim.item():.2f})"
        
        # Fallback to pure spatial/category if no embeddings
        return True, f"Similar report found {int(dist)}m away"

    return False, None

    return False, None

# --- MODELS ---
from pydantic import BaseModel

class HistoryItem(BaseModel):
    lat: float
    lon: float
    issue: str
    time: float
    user: str
    hash: Optional[str] = None

class EmailRequest(BaseModel):
    to_email: str
    subject: str
    body: str

class NotifyStatusRequest(BaseModel):
    status: str
    user_email: str
    issue_type: str
    severity: Optional[str] = "Medium"
    department: Optional[str] = "General"
    location: Optional[str] = ""
    imageUrl: Optional[str] = None

# --- EMAIL HELPER ---
import smtplib
import requests
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

# --- EMAIL TEMPLATE GENERATOR ---
def generate_html_email(title: str, status: str, details: dict, image_url: str = None):
    """
    Generates a world-class, responsive HTML email for Arise.
    """
    # Professional Color Systems
    colors = {
        "primary": "#0f172a",    # Dark Slate
        "accent": "#3b82f6",     # Royal Blue
        "bg": "#f8fafc",         # Cool Gray
        "card": "#ffffff",       # White
        "text": "#334155",       # Slate 700
        "sub": "#64748b",        # Slate 500
        "border": "#e2e8f0"      # Light Border
    }
    
    # Status Logic
    status_config = {
        "approved": {"color": "#16a34a", "bg": "#dcfce7", "text": "#15803d"}, # Success Green
        "resolved": {"color": "#16a34a", "bg": "#dcfce7", "text": "#15803d"},
        "rejected": {"color": "#dc2626", "bg": "#fee2e2", "text": "#b91c1c"}, # Error Red
        "inprogress": {"color": "#2563eb", "bg": "#dbeafe", "text": "#1e40af"}, # Blue
        "pending": {"color": "#d97706", "bg": "#fef3c7", "text": "#b45309"}, # Amber
        "auto-approved": {"color": "#7c3aed", "bg": "#ede9fe", "text": "#6d28d9"} # Violet
    }
    
    st_key = status.lower().replace(" ", "").replace("-", "")
    st = status_config.get(st_key, status_config["pending"])
    
    # Rows Builder
    rows_html = ""
    for k, v in details.items():
        rows_html += f"""
        <tr>
            <td style="padding: 16px 0; border-bottom: 1px solid {colors['border']}; color: {colors['sub']}; font-size: 14px; width: 40%; vertical-align: top;">
                {k}
            </td>
            <td style="padding: 16px 0 16px 16px; border-bottom: 1px solid {colors['border']}; color: {colors['primary']}; font-weight: 600; font-size: 14px; vertical-align: top; text-align: right;">
                {v}
            </td>
        </tr>
        """
        
    # Image logic
    image_section = ""
    if image_url:
        image_section = f"""
        <div style="margin: 32px 0; text-align: center;">
            <div style="border: 1px solid {colors['border']}; padding: 8px; border-radius: 12px; display: inline-block; background: #fff;">
                <img src="{image_url}" alt="Report Evidence" style="display: block; max-width: 100%; width: 500px; height: auto; border-radius: 8px;">
            </div>
            <p style="margin: 8px 0 0; color: {colors['sub']}; font-size: 12px;">Captured Evidence</p>
        </div>
        """

    return f"""
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{title}</title>
    </head>
    <body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background-color: {colors['bg']}; -webkit-font-smoothing: antialiased;">
        
        <!-- Main Container -->
        <table width="100%" cellpadding="0" cellspacing="0" role="presentation" style="background-color: {colors['bg']}; padding: 20px 0;">
            <tr>
                <td align="center">
                    
                    <!-- Content Card -->
                    <table width="600" cellpadding="0" cellspacing="0" role="presentation" style="background-color: {colors['card']}; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); overflow: hidden; max-width: 95%;">
                        
                        <!-- Header Image / Accent -->
                        <tr>
                            <td style="background-color: {colors['primary']}; padding: 30px; text-align: center;">
                                <h1 style="margin: 0; font-size: 24px; font-weight: 700; color: #ffffff; letter-spacing: 0.5px;">{title}</h1>
                                <p style="margin: 5px 0 0; color: #94a3b8; font-size: 14px;">Civic Issue Report Update</p>
                            </td>
                        </tr>
                        
                        <!-- Body -->
                        <tr>
                            <td style="padding: 30px;">
                                
                                <!-- Status Badge -->
                                <div style="text-align: center; margin-bottom: 30px;">
                                    <span style="background-color: {st['bg']}; color: {st['text']}; padding: 8px 16px; border-radius: 50px; font-weight: 700; font-size: 14px; text-transform: uppercase; letter-spacing: 1px;">
                                        {status}
                                    </span>
                                </div>
                                
                                <p style="font-size: 16px; line-height: 1.6; color: {colors['text']}; margin-bottom: 24px;">
                                    Hello, <br>
                                    This is an automated update regarding your civic report. The current status has been changed.
                                </p>
                                
                                <!-- Details Table -->
                                <table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom: 30px; border: 1px solid {colors['border']}; border-radius: 8px; overflow: hidden;">
                                    {rows_html}
                                </table>
                                
                                {image_section}
                                
                                <!-- Footer Message -->
                                <div style="border-top: 1px solid {colors['border']}; margin-top: 30px; padding-top: 20px; text-align: center;">
                                    <p style="margin: 0; color: {colors['sub']}; font-size: 14px;">
                                        Thank you for being a responsible citizen. <br>
                                        <strong style="color: {colors['primary']};">Team Arise</strong>
                                    </p>
                                </div>
                            </td>
                        </tr>
                        
                        <!-- Footer -->
                        <tr>
                            <td style="background-color: {colors['bg']}; padding: 15px; text-align: center; font-size: 12px; color: {colors['sub']};">
                                &copy; {datetime.now().year} Arise Civic AI. <br>
                                <a href="#" style="color: {colors['accent']}; text-decoration: none;">View on Dashboard</a>
                            </td>
                        </tr>
                        
                    </table>
                </td>
            </tr>
        </table>
        
    </body>
    </html>
    """

# --- BREVO (HTTP) CONFIG ---
# HARDCODE YOUR KEY HERE AS REQUESTED
BREVO_API_KEY = "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" # <--- PASTE YOUR KEY HERE INSIDE QUOTES

def send_email_via_brevo(to_email: str, subject: str, body: str, is_html: bool = True, attachment_base64: str = None):
    """Sends email via Brevo HTTP API (Bypasses SMTP ports). Includes optional Base64 attachment with CID support."""
    if not BREVO_API_KEY or "xkeysib" not in BREVO_API_KEY:
        logger.warning("BREVO_API_KEY not set or invalid. Skipping HTTP email.")
        return False
        
    url = "https://api.brevo.com/v3/smtp/email"
    headers = {
        "accept": "application/json",
        "api-key": BREVO_API_KEY,
        "content-type": "application/json"
    }
    
    payload = {
        "sender": {"name": "Arise Civic AI", "email": "shivarajmani2005@gmail.com"},
        "to": [{"email": to_email}],
        "subject": subject,
        "htmlContent": body if is_html else f"<p>{body}</p>" 
    }
    
    if attachment_base64:
        # Brevo expects: {"content": "BASE64...", "name": "filename.ext", "contentId": "myid"}
        # We ALWAYS use 'report_image' as contentId to match the 'cid:report_image' in the HTML
        payload["attachment"] = [{
            "content": attachment_base64, 
            "name": "report_image.jpg",
            "contentId": "report_image" 
        }]

    
    try:
        response = requests.post(url, json=payload, headers=headers, timeout=10)
        if response.status_code in [200, 201, 202]:
            logger.info(f"Brevo HTTP Email sent to {to_email}")
            return True
        else:
            logger.error(f"Brevo Error {response.status_code}: {response.text}")
            return False
    except Exception as e:
        logger.error(f"Brevo Request Failed: {e}")
        return False

def send_email_helper(to_email: str, subject: str, body: str, is_html: bool = False, attachment_base64: str = None):
    """Sends an email using Brevo (HTTP) first, falls back to Gmail SMTP."""
    
    # 1. Try Brevo (HTTP) - Preferred for HF Spaces
    if send_email_via_brevo(to_email, subject, body, is_html, attachment_base64):
        return True
        
    # 2. Fallback to SMTP (Gmail)
    logger.info("Falling back to SMTP...")
    sender_email = "shivarajmani2005@gmail.com"
    
    # USER OPTION: You can paste your 16-char App Password here directly if Secrets fail
    # Example: "abcd efgh ijkl mnop"
    GMAIL_APP_PASSWORD_DIRECT = "snja ljzu xbzd fdvl" 
    
    app_password = GMAIL_APP_PASSWORD_DIRECT if "abcd" not in GMAIL_APP_PASSWORD_DIRECT else os.environ.get("GMAIL_APP_PASSWORD")
    
    if not app_password or "abcd" in app_password: 
        logger.warning("GMAIL_APP_PASSWORD not set. Email not sent.")
        return False

    try:
        msg = MIMEMultipart('alternative') if is_html else MIMEMultipart()
        msg['From'] = sender_email
        msg['To'] = to_email
        msg['Subject'] = subject
        
        if is_html:
            msg.attach(MIMEText(body, 'html'))
        else:
            msg.attach(MIMEText(body, 'plain'))

        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.starttls()
        server.login(sender_email, app_password)
        server.send_message(msg)
        server.quit()
        logger.info(f"Email sent to {to_email}")
        return True
    except OSError as e:
        if e.errno == 101:
            logger.error(f"EMAIL FAILED: Network unreachable (Errno 101). This usually means the hosting platform (e.g. Hugging Face Free Tier) blocks outbound SMTP ports (587). Please use a paid tier or an HTTP-based email API.")
        else:
            logger.error(f"Failed to send email (OSError): {e}")
        return False
    except Exception as e:
        logger.error(f"Failed to send email: {e}")
        return False

@app.post("/send-email")
async def send_email_endpoint(req: EmailRequest, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email_helper, req.to_email, req.subject, req.body)
    return {"status": "success", "message": "Email queued"}

@app.post("/notify-status")
async def notify_status_endpoint(req: NotifyStatusRequest, background_tasks: BackgroundTasks):
    """
    Sends a rich HTML email notification with image support for status updates.
    """
    subject = f"Arise Update: {req.issue_type} - {req.status.capitalize()}"
    
    details = {
        "Issue Type": req.issue_type,
        "Department": req.department,
        "Severity": req.severity,
        "Location": req.location or "Provided",
        "Status Description": f"Your report has been marked as {req.status}. We will keep you updated."
    }
    
    html_body = generate_html_email(
        "Civic Report Update",
        req.status,
        details,
        req.imageUrl
    )
    
    cid_url = None
    processed_image = None
    
    # Image Handling: Check if imageUrl is a Base64 string OR a normal URL
    if req.imageUrl:
        if req.imageUrl.startswith("data:image"):
            # It's Base64. Decode it -> Attach it -> Use CID
            try:
                # Remove header "data:image/jpeg;base64,"
                header, encoded = req.imageUrl.split(",", 1)
                processed_image = encoded # This is what send_email_helper expects for attachment logic
                cid_url = "cid:report_image" # HTML will use this
            except Exception as e:
                logger.error(f"Failed to process base64 image in notify: {e}")
                cid_url = None
        else:
            # It's a normal URL (http...) or specific asset path
            cid_url = req.imageUrl
    
    html_body = generate_html_email(
        "Civic Report Update",
        req.status,
        details,
        cid_url
    )
    
    # We pass 'processed_image' (base64 string w/o header) to helper if we extracted it.
    # The helper (and Brevo func) already logic to attach it if provided.
    background_tasks.add_task(send_email_helper, req.user_email, subject, html_body, True, processed_image)
    return {"status": "success", "message": "Notification queued"}

@app.post("/sync-history")
async def sync_history(items: List[HistoryItem]):
    """
    Syncs recent history from the frontend (Firebase) to the backend.
    This allows the backend to perform deduplication and spam checks against
    data that persists across backend restarts.
    """
    count = 0
    for item in items:
        # Avoid re-adding if already known (simple check by time+user)
        # In a real overlap scenario we might need a better unique ID, but this is enough for simple seeding.
        # We only add if timestamp is within the last 24h window roughly.
        
        # Add to REPORT_HISTORY
        # Convert timestamp to datetime
        dt = datetime.fromtimestamp(item.time / 1000.0) # JS sends ms
        
        # Check if already exists (approximate)
        if any(r['user'] == item.user and abs((r['time'] - dt).total_seconds()) < 1.0 for r in REPORT_HISTORY):
            continue
            
        REPORT_HISTORY.append({
            'lat': item.lat,
            'lon': item.lon,
            'issue': item.issue,
            'time': dt,
            'user': item.user,
            'hash': item.hash or "" # Allow empty hash for legacy without re-analysis
        })
        
        # Add to USER_ACTIVITY for velocity checks
        if item.user:
            if item.user not in USER_ACTIVITY:
                USER_ACTIVITY[item.user] = deque(maxlen=10)
            USER_ACTIVITY[item.user].append(dt)
            
        count += 1
        
    logger.info(f"Synced {count} distinct history items from frontend.")
    return {"status": "success", "synced": count}

@app.post("/analyze")
async def analyze_endpoint(
    background_tasks: BackgroundTasks,
    image: UploadFile = File(...),
    description: str = Form(""),
    latitude: str = Form("0"),
    longitude: str = Form("0"),
    timestamp: str = Form(""),
    user_email: str = Form(None)
):
    try:
        # Parse inputs
        try:
            lat = float(latitude)
            lon = float(longitude)
        except ValueError:
            lat, lon = 0.0, 0.0
            
        current_time = datetime.now()
        
        # --- REFRESH DATA FROM FIREBASE ---
        # Ensure we have the latest context for deduplication
        fetch_firebase_history()

        # Load Image
        contents = await image.read()
        pil_image = Image.open(io.BytesIO(contents)).convert("RGB")
        
        # Handle EXIF Rotation
        try:
            pil_image = ImageOps.exif_transpose(pil_image)
        except Exception:
            pass # Keep original if EXIF fails

        img_np = np.array(pil_image)
        
        # --- ANALYSIS PHASE ---

        # 1. Spam Detection
        # A. Blur Check
        gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
        blur_score = cv2.Laplacian(gray, cv2.CV_64F).var()
        is_blur_spam = bool(blur_score < 100.0)
        
        # Call Advanced Spam Check
        is_spam, spam_reasons = check_spam_status(user_email, current_time, blur_score)
        spam_reason_str = ", ".join(spam_reasons) if spam_reasons else None

        # Run Inference
        logger.info("Running YOLO inference...")
        results = model(img_np, conf=0.1)
        
        detections = []
        primary_issue = "Unknown"
        max_conf = 0.0
        
        result = results[0]
        
        # Analyze Detections
        if len(result.boxes) > 0:
            for box in result.boxes:
                cls_id = int(box.cls)
                conf = float(box.conf)
                label = model.names[cls_id]
                detections.append({"class": label, "confidence": conf})
                if conf > max_conf:
                    max_conf = conf
                    primary_issue = label

        # Fallback: Check Description
        if primary_issue == "Unknown" and description:
            # ... (Existing keyword logic logic) ...
            pass # Simplified for diff clarity, keeping existing logic below is fine or re-insert it
            # Re-inserting keyword logic for safety:
            desc_lower = description.lower()
            keywords = {"pothole": "Pothole", "garbage": "Garbage", "accident": "Accident", "streetlight": "Streetlight", "water": "Water"}
            # Simple map first
            for k, v in keywords.items():
                if k in desc_lower:
                    primary_issue = v
                    max_conf = 0.5
                    break

        # 2. Deduplication detection (Hybrid)
        is_duplicate, dup_reason = check_hybrid_duplicate(lat, lon, primary_issue, current_time, pil_image, description)
        
        # Hash Check (Legacy Backup)
        current_hash = imagehash.phash(pil_image)
        phash_str = str(current_hash)
        
        # Generate embedding for storing
        current_img_emb = None
        if embedding_model:
            try:
                current_img_emb = embedding_model.encode(pil_image, convert_to_tensor=True)
            except: pass

        # Update History
        REPORT_HISTORY.append({
            'lat': lat,
            'lon': lon,
            'issue': primary_issue,
            'time': current_time,
            'user': user_email,
            'hash': phash_str, # Legacy
            'visual_emb': current_img_emb # New!
        })
        
        # Process Image for Overlay
        annotated_frame = result.plot(line_width=2, font_size=1.0)
        is_success, buffer = cv2.imencode(".jpg", cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR))
        processed_image_base64 = None
        if is_success:
            import base64
            processed_image_base64 = base64.b64encode(buffer).decode("utf-8")

        # Map to Civicsense categories (Bangalore Specific)
        category_map = {
            "pothole": "BBMP - Road Infrastructure",
            "garbage": "BBMP - Solid Waste Management",
            "streetlight": "BESCOM - Street Lights",
            "accident": "Traffic Police / Emergency",
            "drainagen": "BWSSB - Water & Sewerage",
            "water": "BWSSB - Water Supply"
        }
        
        department = category_map.get(primary_issue.lower(), "General")
        
        # Determine Severity
        severity = "Medium"
        if primary_issue.lower() in ["accident", "pothole"]:
            severity = "High" if max_conf > 0.7 else "Medium"
        elif primary_issue.lower() == "garbage":
            severity = "Low" if max_conf < 0.5 else "Medium"
        
        # Generate 4-Line Detailed Summary
        summary_lines = []
        
        # Line 1: Identification
        if primary_issue != "Unknown":
            summary_lines.append(f"Identification: AI detected {primary_issue} with {int(max_conf*100)}% confidence.")
        else:
            summary_lines.append("Identification: No specific civic issue could be confidently identified.")
            
        # Line 2: Quality Analysis
        if is_blur_spam:
            summary_lines.append(f"Image Quality: Poor/Blurry (Score: {int(blur_score)}). Please retake.")
        else:
            summary_lines.append(f"Image Quality: Good clarity (Score: {int(blur_score)}).")
            
        # Line 3: Assessment
        summary_lines.append(f"Assessment: Rated as {severity} severity, routed to {department}.")
        
        # Line 4: Status/Warnings
        status_parts = []
        if is_duplicate:
            status_parts.append(f"Duplicate: {dup_reason}.")
        if is_spam:
            status_parts.append(f"Spam Flag: {spam_reason_str}.")
        
        if not status_parts:
            status_parts.append("Status: Verified as a unique, valid report.")
            
        summary_lines.append(" ".join(status_parts))
        
        ai_summary = "\n".join(summary_lines)
        
        # --- AUTO-EMAIL LOGIC (ASYNC) ---
        # Only if valid report (not spam/dup)
        auto_status = "pending"
        if not is_spam and not is_duplicate and primary_issue != "Unknown":
            if max_conf > 0.8:
                # Auto-Approve -> Send to Dept
                auto_status = "inProgress"
                subject = f"Auto-Approved: High Confidence {primary_issue}"
                
                details = {
                    "Issue Type": primary_issue,
                    "Confidence": f"{int(max_conf*100)}%",
                    "Department": department,
                    "Location": f"{lat}, {lon}",
                    "Action": "Auto-Approved by AI"
                }
                
                # Use 'cid:report_image' which matches the contentId in send_email_via_brevo
                html_body = generate_html_email("New Priority Report", "Auto-Approved", details, "cid:report_image")
                
                logger.info("Queuing email to admin (Auto-Approve)...")
                background_tasks.add_task(send_email_helper, "rockybai8234@gmail.com", subject, html_body, True, processed_image_base64)
            else:
                # Pending -> Send to Admin
                subject = f"Pending Review: {primary_issue}"
                
                details = {
                    "Issue Type": primary_issue,
                    "Confidence": f"{int(max_conf*100)}%",
                    "Department": department,
                    "Location": f"{lat}, {lon}",
                    "Action": "Waiting for Admin Verification"
                }
                
                # Use 'cid:report_image'
                html_body = generate_html_email("New Report Received", "Pending Review", details, "cid:report_image")
                
                logger.info("Queuing email to admin (Pending Review)...")
                background_tasks.add_task(send_email_helper, "shivarajmani2005@gmail.com", subject, html_body, True, processed_image_base64)
        
        response_data = {
            "status": "Success",
            "suggested_status": auto_status, # Frontend can use this
            "issue_type": primary_issue,
            "civic_confidence": max_conf,
            "severity": severity,
            "department": department,
            "ai_summary": ai_summary,
            "detections": detections,
            "processed_image": processed_image_base64,
            "resolution_estimation": {
                "estimated_hours": 24 if severity == "High" else 48
            },
            "spam_analysis": {
                "is_spam": is_spam,
                "spam_score": round(blur_score, 2),
                "reason": spam_reason_str
            },
            "deduplication": {
                "is_duplicate": is_duplicate,
                "image_hash": phash_str,
                "method": "hybrid"
            },
            "steganography": {
                "has_hidden_data": False,
                "method": None
            },
            "transmission_analysis": {
                "status": "success",
                "binary_decoded": True,
                "integrity_check": "passed"
            }
        }
        
        logger.info(f"Analysis complete: {primary_issue}. Duplicate: {is_duplicate}. Spam: {is_spam}")
        return response_data

    except Exception as e:
        logger.error(f"Error analyzing image: {e}")
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)