File size: 92,817 Bytes
5ee2bc6
 
bcfae1d
5ee2bc6
 
 
 
 
 
 
 
 
2b2cffb
5ee2bc6
 
 
 
bcfae1d
5ee2bc6
bcfae1d
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bc244ad
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
 
 
 
2b2cffb
5ee2bc6
 
 
2b2cffb
5ee2bc6
 
 
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
 
 
 
 
 
 
555ec41
5ee2bc6
 
 
 
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555ec41
5ee2bc6
 
 
 
 
 
 
555ec41
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555ec41
5ee2bc6
 
a71d24c
5ee2bc6
 
 
 
a71d24c
5ee2bc6
 
a71d24c
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bcfae1d
5ee2bc6
 
 
 
 
cde4684
5ee2bc6
 
 
 
 
 
 
 
2b2cffb
5ee2bc6
 
 
2b2cffb
 
5ee2bc6
 
 
 
 
c7588c7
 
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c7588c7
5ee2bc6
 
c7588c7
5ee2bc6
 
 
c7588c7
5ee2bc6
 
 
 
 
 
 
 
c7588c7
5ee2bc6
 
c7588c7
5ee2bc6
 
 
 
 
 
c7588c7
5ee2bc6
 
d808a4a
5ee2bc6
 
2b2cffb
5ee2bc6
 
e5980ed
5ee2bc6
 
 
7de721e
5ee2bc6
 
 
 
 
d808a4a
5ee2bc6
 
21ac85d
2b2cffb
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b2cffb
5ee2bc6
 
ef44260
5ee2bc6
 
 
 
 
 
fb24c84
5ee2bc6
 
788d071
5ee2bc6
 
fb24c84
5ee2bc6
 
fb24c84
5ee2bc6
 
 
 
 
 
 
 
 
 
fb24c84
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fb24c84
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b2cffb
5ee2bc6
 
 
788d071
5ee2bc6
788d071
5ee2bc6
 
 
788d071
5ee2bc6
 
 
 
 
 
 
b82d56d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
 
 
 
b82d56d
 
 
 
5ee2bc6
b82d56d
5ee2bc6
 
 
 
b82d56d
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
b82d56d
 
788d071
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82d56d
 
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
788d071
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
 
5ee2bc6
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
 
3a12601
 
 
 
5ee2bc6
 
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b2cffb
3a12601
 
788d071
2b2cffb
3a12601
5ee2bc6
3a12601
 
 
 
5ee2bc6
2b2cffb
3a12601
 
 
 
5ee2bc6
3a12601
 
 
 
 
 
 
 
5ee2bc6
3a12601
5ee2bc6
 
3a12601
 
 
 
 
 
5ee2bc6
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b7a78d
3a12601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
3a12601
 
 
5ee2bc6
 
 
 
3a12601
 
 
5ee2bc6
3a12601
 
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
 
 
 
 
 
 
 
 
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b7a78d
5ee2bc6
 
 
 
 
 
 
 
9b7a78d
5ee2bc6
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44c69bf
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44c69bf
5ee2bc6
 
 
 
 
 
 
44c69bf
 
5ee2bc6
 
3a12601
5ee2bc6
 
 
 
44c69bf
5ee2bc6
 
44c69bf
5ee2bc6
 
 
 
 
44c69bf
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44c69bf
 
5ee2bc6
 
3a12601
5ee2bc6
 
 
 
44c69bf
5ee2bc6
 
 
 
 
 
 
 
9b7a78d
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
2b2cffb
5ee2bc6
 
38bb831
5ee2bc6
544f6ed
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
3a12601
5ee2bc6
 
 
 
3a12601
 
5ee2bc6
 
 
 
3a12601
5ee2bc6
 
3a12601
544f6ed
3a12601
 
 
 
 
 
 
 
5ee2bc6
 
3a12601
5ee2bc6
 
3a12601
 
5ee2bc6
3a12601
5ee2bc6
3a12601
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
3a12601
5ee2bc6
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
8c0e1ac
5ee2bc6
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82d56d
5ee2bc6
b82d56d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ee2bc6
 
 
 
b82d56d
5ee2bc6
b82d56d
 
 
5ee2bc6
 
 
 
b82d56d
5ee2bc6
 
 
b82d56d
5ee2bc6
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c0e1ac
5ee2bc6
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
8c0e1ac
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12601
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c0e1ac
5ee2bc6
 
 
 
8c0e1ac
5ee2bc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c0e1ac
5ee2bc6
 
3a12601
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
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
# -*- coding: utf-8 -*-
from flask import Flask, request, jsonify, g as flask_g # flask_g for storing user context
from flask_cors import CORS
import json
import os
import graphviz
from datetime import datetime
import re
import uuid # For generating unique IDs
import random # For quiz options shuffling
import google.generativeai as genai # Gemini API
from collections import defaultdict # For timeline and sibling grouping
import firebase_admin
from firebase_admin import credentials, db, auth as firebase_auth # Added firebase_auth
from copy import deepcopy # For comparing data states
from dictdiffer import diff # Using dictdiffer for easier comparison
from functools import wraps # For authentication decorator

# --- Flask Setup ---
app = Flask(__name__)
CORS(app) # Enable CORS for all routes

# --- Configuration & Constants ---
DEFAULT_PROFILE = {
    "profile": {"name": "", "dob": "", "gender": "Unknown", "phone": ""},
    "family_members": [],
    "relationships": [],
    "settings": {"theme": "Default", "privacy": "Private"},
    "metadata": {"owner_phone": "", "tree_name": "My Family Tree", "created_at": ""}
}
DEFAULT_MEMBER_STRUCTURE = {
    "id": "", "name": "", "dob": "", "dod": "", "gender": "Unknown",
    "phone": "", "stories": [], "totem": "",
    "created_at": "", "created_by": "",
    "last_edited_at": "", "last_edited_by": ""
}
DEFAULT_STORY_STRUCTURE = {
    "timestamp": "", "text": "", "added_by": ""
}
GEMINI_MODEL_NAME = "gemini-2.0-flash"

# --- Environment Variable Loading ---
Firebase_DB_URL = os.environ.get("Firebase_DB")
Firebase_Credentials_JSON_Str = os.environ.get("FIREBASE")
GOOGLE_API_KEY = os.environ.get("Gemini")

# --- Firebase Initialization ---
firebase_app = None
firebase_db_ref = None
firebase_error = None

try:
    if Firebase_Credentials_JSON_Str and Firebase_DB_URL:
        credentials_json_parsed = json.loads(Firebase_Credentials_JSON_Str)
        if not firebase_admin._apps:
             cred = credentials.Certificate(credentials_json_parsed)
             firebase_app = firebase_admin.initialize_app(cred, {'databaseURL': Firebase_DB_URL})
             firebase_db_ref = db.reference('/')
             app.logger.info("Firebase Admin SDK initialized successfully.")
        else:
             firebase_app = firebase_admin.get_app()
             firebase_db_ref = db.reference('/')
             app.logger.info("Firebase Admin SDK already initialized.")
    else:
        firebase_error = "Firebase secrets (Firebase_DB, FIREBASE) missing in environment variables."
        app.logger.error(firebase_error)
except Exception as e:
    firebase_error = f"Error initializing Firebase: {e}"
    app.logger.error(firebase_error)

# --- Gemini API Client Initialization ---
genai_client = None
api_key_error = False
try:
    if not GOOGLE_API_KEY:
        api_key_error = True
        app.logger.error("GOOGLE_API_KEY missing in environment variables.")
    else:
        genai.configure(api_key=GOOGLE_API_KEY)
        genai_client = genai
        app.logger.info("Gemini API Client initialized successfully.")
except Exception as e:
    api_key_error = True
    app.logger.error(f"Error initializing Gemini API Client: {e}")


# --- UID to Phone Mapping Functions (NEW for email auth) ---
def get_uid_phone_mapping_path(uid):
    """Get the Firebase path for storing user's phone number by UID"""
    return f'users_by_uid/{uid}'

def get_user_phone_by_uid(uid):
    """Look up a user's phone number from their UID"""
    if firebase_error or not firebase_db_ref:
        return None, firebase_error or "Firebase not initialized"
    try:
        path = get_uid_phone_mapping_path(uid)
        data = firebase_db_ref.child(path).get()
        if data and isinstance(data, dict):
            return data.get('phone_number'), None
        return None, "Phone number not linked for this user"
    except Exception as e:
        return None, f"Error looking up phone: {e}"

def set_user_phone_by_uid(uid, phone_number, display_name=None, email=None):
    """Store a user's phone number mapping by UID"""
    if firebase_error or not firebase_db_ref:
        return False, firebase_error or "Firebase not initialized"
    norm_phone = normalize_phone(phone_number)
    if not norm_phone:
        return False, "Invalid phone number format"
    try:
        path = get_uid_phone_mapping_path(uid)
        user_data = {
            'phone_number': norm_phone,
            'linked_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        }
        if display_name:
            user_data['display_name'] = display_name
        if email:
            user_data['email'] = email
        firebase_db_ref.child(path).set(user_data)
        return True, None
    except Exception as e:
        return False, f"Error saving phone mapping: {e}"


# --- Authentication Decorator (UPDATED for email auth) ---
def token_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if firebase_error: # Check if Firebase Admin SDK initialized properly
            return jsonify({"error": "Authentication service not available", "detail": "Firebase Admin SDK not initialized."}), 503

        auth_header = request.headers.get('Authorization')
        if not auth_header or not auth_header.startswith('Bearer '):
            return jsonify({"error": "Authorization token is missing or invalid"}), 401
        
        id_token = auth_header.split('Bearer ')[1]
        try:
            decoded_token = firebase_auth.verify_id_token(id_token)
            uid = decoded_token.get('uid')
            email = decoded_token.get('email')
            
            # First check if phone is in the token (legacy phone auth)
            phone_from_token = decoded_token.get('phone_number')
            
            if phone_from_token:
                # Legacy phone authentication
                phone_number = normalize_phone(phone_from_token)
                if not phone_number:
                    app.logger.error(f"Failed to normalize phone number from token for UID {uid}.")
                    return jsonify({"error": "Invalid phone number format in token."}), 403
                flask_g.user = {
                    "uid": uid,
                    "email": email,
                    "phone_number": phone_number,
                    "phone_linked": True
                }
            else:
                # Email authentication - look up phone from database
                phone_number, lookup_error = get_user_phone_by_uid(uid)
                if not phone_number:
                    # Phone not yet linked - allow access but mark as unlinked
                    flask_g.user = {
                        "uid": uid,
                        "email": email,
                        "phone_number": None,
                        "phone_linked": False
                    }
                else:
                    flask_g.user = {
                        "uid": uid,
                        "email": email,
                        "phone_number": phone_number,
                        "phone_linked": True
                    }
            
        except firebase_auth.ExpiredIdTokenError:
            return jsonify({"error": "Token has expired"}), 401
        except firebase_auth.InvalidIdTokenError:
            return jsonify({"error": "Token is invalid"}), 401
        except Exception as e:
            app.logger.error(f"Error during token verification: {e}")
            return jsonify({"error": "Could not verify token", "detail": str(e)}), 500
        
        return f(*args, **kwargs)
    return decorated_function


def phone_required(f):
    """Decorator for endpoints that require a linked phone number"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not flask_g.user.get("phone_linked") or not flask_g.user.get("phone_number"):
            return jsonify({
                "error": "Phone number not linked",
                "message": "Please link your phone number to access this feature.",
                "action_required": "link_phone"
            }), 403
        return f(*args, **kwargs)
    return decorated_function


# --- Helper Functions (Adapted for API context) ---
def normalize_phone(phone):
    if not phone: return None
    phone_str = str(phone).strip()
    if not re.match(r"^\+\d{5,}$", phone_str): # Basic E.164-like check
        # Allow numbers that might come from Firebase Auth without '+' if they are otherwise valid
        if re.match(r"^\d{10,}$", phone_str.replace("+", "")): # If it's digits only and long enough
            digits_only = "".join(filter(str.isdigit, phone_str))
            return f"+{digits_only}" # Attempt to normalize by adding '+'
        return None
    digits = "".join(filter(str.isdigit, phone_str))
    return f"+{digits}"

def get_user_db_path(owner_phone):
    normalized = normalize_phone(owner_phone)
    if not normalized: return None
    return f'users/{normalized}'

def get_phone_index_path(member_phone):
    normalized = normalize_phone(member_phone)
    if not normalized: return None
    return f'phone_index/{normalized}'

def get_pending_changes_path(owner_phone, proposer_phone=None):
    norm_owner = normalize_phone(owner_phone)
    if not norm_owner: return None
    base_path = f'pending_changes/{norm_owner}'
    if proposer_phone:
        norm_proposer = normalize_phone(proposer_phone)
        if not norm_proposer: return None
        return f'{base_path}/{norm_proposer}'
    return base_path

# --- Data Loading (Adapted) ---
def load_tree_data(owner_phone_to_load): # Renamed param to avoid conflict with flask_g.user.phone_number
    if firebase_error or not firebase_db_ref:
        return None, firebase_error or "Firebase not initialized"
    
    norm_owner_phone_to_load = normalize_phone(owner_phone_to_load)
    if not norm_owner_phone_to_load:
        return None, "Invalid owner phone format for loading data."

    user_path = get_user_db_path(norm_owner_phone_to_load)
    if not user_path:
        return None, "Invalid owner phone format for loading data (path generation failed)."
    try:
        data = firebase_db_ref.child(user_path).get()
        if data and isinstance(data, dict):
            merged_data = deepcopy(DEFAULT_PROFILE)
            if "metadata" not in data or not isinstance(data["metadata"], dict): data["metadata"] = {}
            merged_data.update(data)
            merged_data["settings"] = {**DEFAULT_PROFILE["settings"], **data.get("settings", {})}
            merged_data["metadata"] = {**DEFAULT_PROFILE["metadata"], **data.get("metadata", {})}
            merged_data["metadata"]["owner_phone"] = norm_owner_phone_to_load

            updated_members = []
            for member_data in merged_data.get("family_members", []):
                if isinstance(member_data, dict):
                    complete_member = deepcopy(DEFAULT_MEMBER_STRUCTURE)
                    member_data.pop('photo_path', None)
                    complete_member.update(member_data)
                    updated_members.append(complete_member)
            merged_data["family_members"] = updated_members

            if not isinstance(merged_data.get("relationships"), list): merged_data["relationships"] = []

            for member in merged_data["family_members"]:
                 if "stories" not in member or not isinstance(member["stories"], list): member["stories"] = []
                 cleaned_stories = []
                 for story in member.get("stories", []):
                     if isinstance(story, dict):
                         complete_story = deepcopy(DEFAULT_STORY_STRUCTURE)
                         complete_story.update(story)
                         cleaned_stories.append(complete_story)
                 member["stories"] = cleaned_stories
            return merged_data, None
        elif data is None: # New tree
            app.logger.info(f"No existing tree data for {norm_owner_phone_to_load}. Initializing new tree.")
            new_tree = deepcopy(DEFAULT_PROFILE)
            new_tree["metadata"]["owner_phone"] = norm_owner_phone_to_load
            new_tree["metadata"]["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            
            # The 'Me' node represents the owner of this tree.
            # Its phone number is the owner's phone number.
            owner_me_node = deepcopy(DEFAULT_MEMBER_STRUCTURE)
            owner_me_node.update({
                "id": "Me", "name": "Me", # User can update name later via profile
                "phone": norm_owner_phone_to_load, # This is key
                "created_at": new_tree["metadata"]["created_at"],
                "created_by": norm_owner_phone_to_load # Owner created their own 'Me' node
            })
            new_tree["family_members"].append(owner_me_node)
            new_tree["profile"]["phone"] = norm_owner_phone_to_load # Profile phone also set

            # Save this new tree structure immediately
            initial_save_success, initial_save_error = save_tree_data(norm_owner_phone_to_load, new_tree, DEFAULT_PROFILE) # Pass owner's phone
            if not initial_save_success:
                app.logger.error(f"Failed to save initial tree for {norm_owner_phone_to_load}: {initial_save_error}")
                return None, f"Failed to initialize and save new tree: {initial_save_error}"
            app.logger.info(f"Successfully initialized and saved new tree for {norm_owner_phone_to_load}.")
            return new_tree, None
        else:
             return None, f"Invalid data format found for tree owner {norm_owner_phone_to_load}."
    except Exception as e:
        app.logger.error(f"Error loading tree data for {norm_owner_phone_to_load}: {e}")
        return None, f"Error loading tree data: {e}"

def find_linked_trees(member_phone):
    if firebase_error or not firebase_db_ref: return {}, firebase_error or "Firebase not initialized"
    index_path = get_phone_index_path(member_phone)
    if not index_path: return {}, "Invalid member phone for index query."
    try:
        owner_phones_dict = firebase_db_ref.child(index_path).child("trees").get()
        return owner_phones_dict if owner_phones_dict and isinstance(owner_phones_dict, dict) else {}, None
    except Exception as e:
        app.logger.error(f"Error querying phone index for {member_phone}: {e}")
        return {}, f"Error querying phone index: {e}"

# --- Index Update Logic (Adapted) ---
def update_phone_index(owner_phone_of_tree, previous_members, current_members): # Renamed param
    if firebase_error or not firebase_db_ref:
        return False, firebase_error or "Firebase not initialized"

    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return False, "Invalid owner phone for index update."

    prev_phones = {normalize_phone(p.get('phone')) for p in previous_members if isinstance(p, dict) and normalize_phone(p.get('phone'))}
    curr_phones = {normalize_phone(p.get('phone')) for p in current_members if isinstance(p, dict) and normalize_phone(p.get('phone'))}

    phones_added = curr_phones - prev_phones
    phones_removed = prev_phones - curr_phones

    updates = {}
    for phone in phones_added:
        if phone:
            index_entry_path = f"{get_phone_index_path(phone)}/trees/{norm_owner_phone_of_tree}"
            updates[index_entry_path] = True
    for phone in phones_removed:
        if phone:
            index_entry_path = f"{get_phone_index_path(phone)}/trees/{norm_owner_phone_of_tree}"
            updates[index_entry_path] = None

    if not updates: return True, None
    try:
        firebase_db_ref.update(updates)
        return True, None
    except Exception as e:
        app.logger.error(f"Error updating phone index: {e}. Updates: {json.dumps(updates)}")
        return False, f"Error updating phone index: {e}"

# --- Data Saving (Owner - Adapted) ---
def save_tree_data(owner_phone_of_tree, current_data, previous_data): # Renamed param
    if firebase_error or not firebase_db_ref:
        return False, firebase_error or "Firebase not initialized. Cannot save."
    
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree:
        return False, "Invalid owner phone format for saving."

    user_path = get_user_db_path(norm_owner_phone_of_tree)
    if not user_path: # Should not happen if norm_owner_phone_of_tree is valid
        return False, "Failed to generate user DB path for saving."


    update_totems(current_data)
    current_data["metadata"]["owner_phone"] = norm_owner_phone_of_tree # Ensure this is set correctly
    if "family_members" in current_data:
        for member in current_data["family_members"]:
            if isinstance(member, dict): member.pop('photo_path', None)

    try:
        firebase_db_ref.child(user_path).set(current_data)
    except Exception as e:
        app.logger.error(f"Error saving tree data to {user_path}: {e}")
        return False, f"Error saving tree data: {e}"

    prev_members = previous_data.get("family_members", []) if previous_data and isinstance(previous_data, dict) else []
    curr_members = current_data.get("family_members", [])
    index_success, index_error = update_phone_index(norm_owner_phone_of_tree, prev_members, curr_members)
    if not index_success:
        app.logger.warning(f"Tree data saved for {norm_owner_phone_of_tree}, but failed to update phone index: {index_error}")
        return True, f"Tree data saved, but phone index update failed: {index_error}" # Data saved, but index failed
    return True, None

# --- Collaboration Functions (Adapted) ---
# Proposer phone is the authenticated user (flask_g.user.phone_number)
def propose_changes(owner_phone_of_tree, proposer_phone_from_token, proposed_data):
    if firebase_error or not firebase_db_ref:
        return False, firebase_error or "Firebase not initialized. Cannot propose."
    
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    norm_proposer_phone = normalize_phone(proposer_phone_from_token)

    if not norm_owner_phone_of_tree or not norm_proposer_phone:
        return False, "Invalid owner or proposer phone format for proposing changes."

    pending_path = get_pending_changes_path(norm_owner_phone_of_tree, norm_proposer_phone)
    if not pending_path: # Should not happen if phones are valid
        return False, "Failed to generate pending changes path."

    proposal_payload = {
        "proposed_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "proposer_phone": norm_proposer_phone, # Storing the proposer's phone
        "tree_data": proposed_data
    }
    try:
        firebase_db_ref.child(pending_path).set(proposal_payload)
        return True, None
    except Exception as e:
        app.logger.error(f"Error proposing changes to {pending_path}: {e}")
        return False, f"Error proposing changes: {e}"

# Owner phone is the authenticated user (flask_g.user.phone_number)
def load_pending_changes(owner_phone_from_token):
    if firebase_error or not firebase_db_ref: return {}, firebase_error or "Firebase not initialized"
    
    norm_owner_phone = normalize_phone(owner_phone_from_token)
    if not norm_owner_phone: return {}, "Invalid owner phone for loading pending changes."

    pending_base_path = get_pending_changes_path(norm_owner_phone)
    if not pending_base_path: return {}, "Failed to generate pending base path."
    try:
        data = firebase_db_ref.child(pending_base_path).get()
        return data if data and isinstance(data, dict) else {}, None
    except Exception as e:
        app.logger.error(f"Error loading pending changes for {norm_owner_phone}: {e}")
        return {}, f"Error loading pending changes: {e}"

# Owner phone is the authenticated user (flask_g.user.phone_number)
def accept_changes(owner_phone_from_token, proposer_phone_to_accept):
    if firebase_error or not firebase_db_ref: return False, firebase_error or "Firebase not initialized"

    norm_owner_phone = normalize_phone(owner_phone_from_token)
    norm_proposer_phone = normalize_phone(proposer_phone_to_accept)

    if not norm_owner_phone or not norm_proposer_phone:
        return False, "Invalid owner or proposer phone for accepting changes."

    pending_path = get_pending_changes_path(norm_owner_phone, norm_proposer_phone)
    if not pending_path: return False, "Failed to generate pending path for accept."

    try:
        proposal_payload = firebase_db_ref.child(pending_path).get()
        if not proposal_payload or "tree_data" not in proposal_payload:
            return False, f"Proposal from {norm_proposer_phone} not found or invalid."
        accepted_data = proposal_payload["tree_data"]

        previous_data, load_err = load_tree_data(norm_owner_phone) # Load owner's current tree
        if load_err:
            return False, f"Failed to load current owner data before accepting: {load_err}"

        save_success, save_error = save_tree_data(norm_owner_phone, accepted_data, previous_data)
        if save_success:
            firebase_db_ref.child(pending_path).delete()
            return True, None
        else: # save_error contains the reason
            return False, f"Failed to save accepted changes or update index: {save_error}. Proposal not deleted."
    except Exception as e:
        app.logger.error(f"Error accepting changes from {norm_proposer_phone}: {e}")
        return False, f"Error accepting changes: {e}"

# Owner phone is the authenticated user (flask_g.user.phone_number)
def reject_changes(owner_phone_from_token, proposer_phone_to_reject):
    if firebase_error or not firebase_db_ref: return False, firebase_error or "Firebase not initialized"
    
    norm_owner_phone = normalize_phone(owner_phone_from_token)
    norm_proposer_phone = normalize_phone(proposer_phone_to_reject)

    if not norm_owner_phone or not norm_proposer_phone:
        return False, "Invalid owner or proposer phone for rejecting."

    pending_path = get_pending_changes_path(norm_owner_phone, norm_proposer_phone)
    if not pending_path: return False, "Failed to generate pending path for reject."
    try:
        firebase_db_ref.child(pending_path).delete()
        return True, None
    except Exception as e:
        app.logger.error(f"Error rejecting changes from {norm_proposer_phone}: {e}")
        return False, f"Error rejecting changes: {e}"

# (generate_diff_summary, find_person_by_id, find_person_by_name, generate_unique_id remain the same)
# (Graphviz and AI functions like format_node_label, get_father_id, update_totems, generate_graphviz_object, safe_json_loads, call_gemini, etc. remain largely the same logic)
# --- find_person_by_id, find_person_by_name, generate_unique_id ---
def find_person_by_id(data, person_id):
    for person in data.get("family_members", []):
        if isinstance(person, dict) and person.get("id") == person_id: return person
    return None

def find_person_by_name(data, name):
    if not name: return None
    normalized_name = name.strip().lower()
    return [
        person for person in data.get("family_members", [])
        if isinstance(person, dict) and person.get("name", "").strip().lower() == normalized_name
    ]

def generate_unique_id(data=None): return uuid.uuid4().hex


# --- Graphviz, AI Functions (Adapted for API context) ---
def format_node_label(person):
    label = f"{person.get('name', 'Unknown')}"
    details = []
    if person.get('dob'): details.append(f"b. {person.get('dob')}")
    if person.get('dod'): details.append(f"d. {person.get('dod')}")
    if details: label += f"\n({' / '.join(details)})"
    if person.get('totem'): label += f"\nTotem: {person.get('totem')}"
    return label

def get_father_id(person_id, relationships, members):
    id_to_person = {p['id']: p for p in members if isinstance(p, dict)}
    for rel in relationships:
        if isinstance(rel, dict) and rel.get('type') == 'parent' and rel.get('to_id') == person_id:
            parent_id = rel.get('from_id')
            parent = id_to_person.get(parent_id)
            if parent and parent.get('gender', '').lower() == 'male':
                return parent_id
    return None

def update_totems(user_data): # Modifies user_data in-place
    members = user_data.get("family_members", [])
    relationships = user_data.get("relationships", [])
    if not members or not relationships: return
    id_to_person = {p['id']: p for p in members if isinstance(p, dict)}
    visited = set(); processed_in_queue = set()
    all_child_ids = {r['to_id'] for r in relationships if isinstance(r, dict) and r.get('type') == 'parent'}
    queue = [p['id'] for p in members if isinstance(p, dict) and p['id'] not in all_child_ids]
    queue.extend([p['id'] for p in members if isinstance(p, dict) and p['id'] not in queue])
    processed_count = 0
    max_process = len(members) * 2 + 5
    while queue and processed_count < max_process:
        person_id = queue.pop(0)
        processed_count += 1
        if person_id in visited: continue
        person = id_to_person.get(person_id)
        if not person: continue
        totem_updated = False
        current_totem = person.get('totem')
        if current_totem is None or current_totem == "":
            father_id = get_father_id(person_id, relationships, members)
            if father_id:
                father = id_to_person.get(father_id)
                if father and father.get('totem') and (father_id in visited or father_id not in all_child_ids):
                    person['totem'] = father.get('totem')
                    totem_updated = True
        visited.add(person_id)
        for rel in relationships:
             if isinstance(rel, dict) and rel.get('type') == 'parent' and rel.get('from_id') == person_id:
                child_id = rel.get('to_id')
                if child_id and child_id not in visited and child_id not in queue:
                    queue.append(child_id)
        if totem_updated:
            for rel in relationships:
                if isinstance(rel, dict) and rel.get('type') == 'parent' and rel.get('from_id') == person_id:
                    child_id = rel.get('to_id')
                    if child_id and child_id in visited:
                        visited.remove(child_id)
                        if child_id not in queue: queue.append(child_id)
                    elif child_id and child_id not in queue:
                        queue.append(child_id)
    if processed_count >= max_process:
        app.logger.warning("WARN: Totem update BFS reached max iterations, potential loop?")


def generate_graphviz_object(data): # Renamed to avoid conflict if original is kept
    dot = graphviz.Digraph(comment='Family Tree', format='svg')
    dot.attr(rankdir='TB', splines='ortho', nodesep='0.6', ranksep='0.8')
    theme = data.get("settings", {}).get("theme", "Default")
    node_style = {'shape': 'box', 'style': 'filled', 'margin': '0.1'}
    edge_color, marriage_node_color = 'gray50', 'gray50'
    if theme == "Dark":
        dot.attr(bgcolor='black', fontcolor='white'); node_style.update(fillcolor='grey30', fontcolor='white', color='white'); edge_color, marriage_node_color = 'white', 'white'
    else: dot.attr(bgcolor='transparent'); node_style.update(fillcolor='lightblue', fontcolor='black', color='black')
    members = data.get("family_members", []); relationships = data.get("relationships", [])
    if not members: return dot, "No members to graph"
    member_ids = {m['id'] for m in members if isinstance(m, dict)}; id_to_person = {p['id']: p for p in members if isinstance(p, dict)}
    marriage_nodes = {}; parents_of_child = defaultdict(list); children_by_parent_pair = defaultdict(list)
    for person in members:
        if isinstance(person, dict): dot.node(person['id'], label=format_node_label(person), **node_style)
    processed_spouses = set()
    for rel in relationships:
        if not isinstance(rel, dict): continue
        from_id, to_id, rel_type = rel.get("from_id"), rel.get("to_id"), rel.get("type")
        if from_id not in member_ids or to_id not in member_ids: continue
        if rel_type == 'parent': parents_of_child[to_id].append(from_id)
        elif rel_type == 'spouse':
            pair = frozenset([from_id, to_id])
            if pair not in processed_spouses:
                m_id = f"m_{uuid.uuid4().hex[:8]}"; marriage_nodes[pair] = m_id
                dot.node(m_id, shape='point', width='0.1', height='0.1', label='', color=marriage_node_color)
                dot.edge(from_id, m_id, style='invis', dir='none', weight='10')
                dot.edge(to_id, m_id, style='invis', dir='none', weight='10')
                with dot.subgraph(name=f"cluster_m_{m_id}") as sub:
                    sub.attr(rank='same', style='invis')
                    sub.node(from_id); sub.node(to_id)
                processed_spouses.add(pair)
    for child_id, parent_ids_list in parents_of_child.items():
        valid_p_ids = sorted([p_id for p_id in parent_ids_list if p_id in member_ids]);
        if not valid_p_ids: continue
        parent_key = frozenset(valid_p_ids); children_by_parent_pair[parent_key].append(child_id)
    processed_child_links = set()
    for parent_key, children_list in children_by_parent_pair.items():
        p_ids = list(parent_key); source_id = None
        if len(p_ids) == 1: source_id = p_ids[0]
        elif len(p_ids) > 1: source_id = marriage_nodes.get(parent_key)
        if source_id:
            for child_id in children_list:
                 if child_id in member_ids and (source_id, child_id) not in processed_child_links:
                     dot.edge(source_id, child_id, arrowhead='none', color=edge_color); processed_child_links.add((source_id, child_id))
        elif len(p_ids) > 1 and not source_id:
             app.logger.warning(f"WARN: Missing marriage node for parents {p_ids}, linking child {children_list} from first parent.")
             source_id = p_ids[0]
             for child_id in children_list:
                 if child_id in member_ids and (source_id, child_id) not in processed_child_links:
                     dot.edge(source_id, child_id, arrowhead='none', color=edge_color); processed_child_links.add((source_id, child_id))
    for parent_key, siblings in children_by_parent_pair.items():
        valid_sibs = [sid for sid in siblings if sid in member_ids]
        if len(valid_sibs) > 1:
            with dot.subgraph(name=f"cluster_s_{uuid.uuid4().hex[:8]}") as sub:
                sub.attr(rank='same', style='invis')
                for sid in sorted(valid_sibs): sub.node(sid)
    return dot, None


def safe_json_loads(text):
    match = re.search(r"```(?:json)?\s*(\{.*\}|\[.*\])\s*```", text, re.DOTALL | re.IGNORECASE)
    if match: json_text = match.group(1)
    else:
        start_brace, start_bracket = text.find('{'), text.find('[')
        end_brace, end_bracket = text.rfind('}'), text.rfind(']')
        start = min(start_brace, start_bracket) if start_brace != -1 and start_bracket != -1 else (start_brace if start_brace != -1 else start_bracket)
        end = -1
        if start == start_brace and end_brace != -1: end = end_brace
        elif start == start_bracket and end_bracket != -1: end = end_bracket
        elif end_brace != -1 and end_bracket != -1: end = max(end_brace, end_bracket)
        else: end = end_brace if end_brace != -1 else end_bracket
        if start != -1 and end != -1 and end > start: json_text = text[start:end+1]
        else:
            json_text = text.strip()
            if not ((json_text.startswith('{') and json_text.endswith('}')) or \
                    (json_text.startswith('[') and json_text.endswith(']'))):
                return None, f"Could not find JSON structure. Response: {text[:500]}..."
    json_text = re.sub(r",\s*(\]|\})", r"\1", json_text)
    try:
        return json.loads(json_text), None
    except json.JSONDecodeError as e:
        return None, f"Error parsing JSON: {e}. Attempted JSON: {json_text[:500]}..."
    except Exception as e:
        return None, f"Unexpected error during JSON parsing: {e}. Response: {text[:500]}..."

def call_gemini(prompt_text, is_json_output_expected=True):
    if not genai_client or api_key_error:
        return None, "Gemini API not ready or API key error."
    try:
        model = genai.GenerativeModel(GEMINI_MODEL_NAME)
        safety_settings = [
            {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
            {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
            {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
            {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
        ]
        
        # Fixed: Use proper GenerationConfig parameters
        gen_config = None
        if is_json_output_expected:
            gen_config = genai.types.GenerationConfig(
                temperature=0.2,  # Lower temperature for more consistent JSON
                top_p=0.8,
                top_k=40,
                max_output_tokens=2048,
            )
        
        response = model.generate_content(
            prompt_text, 
            generation_config=gen_config, 
            safety_settings=safety_settings
        )

        if not response.parts:
            reason = "Unknown"
            try:
                reason = response.prompt_feedback.block_reason.name if hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason else None
                if not reason and response.candidates: 
                    reason = response.candidates[0].finish_reason.name
            except Exception: 
                pass
            return None, f"AI response empty or blocked. Reason: {reason}"
        
        if not hasattr(response, 'text') or not response.text:
            return None, "AI response text empty."

        if is_json_output_expected:
            # Clean the response text to extract JSON
            response_text = response.text.strip()
            # Remove markdown code blocks if present
            if response_text.startswith('```json'):
                response_text = response_text[7:]
            if response_text.startswith('```'):
                response_text = response_text[3:]
            if response_text.endswith('```'):
                response_text = response_text[:-3]
            response_text = response_text.strip()
            
            parsed_json, err = safe_json_loads(response_text)
            if err: 
                return None, f"AI response received, but failed to parse as JSON: {err}. Raw: {response_text[:500]}..."
            return parsed_json, None
        else:
            return response_text, None
            
    except Exception as e:
        app.logger.error(f"Gemini API Call Error: {e} (Type: {type(e).__name__})")
        return None, f"Gemini API Call Error: {e}"


def generate_tree_from_description_gemini(description):
    prompt = f"""Please analyze the following family description. Extract individuals mentioned, including their name, and if available, their date of birth (dob), date of death (dod), gender (Male/Female/Other/Unknown), and totem. Also, identify direct relationships like parent_of, spouse_of, or sibling_of between these individuals.

Format the output as a single JSON object containing two keys:
1.  "people": A list of objects, where each object represents an individual and has keys for "name", "dob", "dod", "gender", and "totem" (use null or omit if information is not present). Use the name "Me" if the description uses first-person references without providing a name.
2.  "relationships": A list of objects, where each object represents a relationship. Each relationship object should have "person1_name", "person2_name", and "type" (e.g., "parent_of", "spouse_of", "sibling_of"). Ensure the names match exactly those extracted in the "people" list.

Strictly output only the JSON object, without any introductory text, explanations, or markdown formatting like ```json.

Description:
"{description}"

JSON Output:
"""
    return call_gemini(prompt, is_json_output_expected=True)



def generate_quiz_questions_gemini(members, relationships, num_questions=3):
    if not members or len(members) < 2: return None, "Need at least 2 members for a quiz."
    id_to_name = {p["id"]: p.get("name", "Unknown") for p in members if isinstance(p, dict)}
    rel_strings, processed_pairs = [], set()
    for rel in relationships:
        if not isinstance(rel, dict): continue
        p1_id, p2_id, rt = rel.get("from_id"), rel.get("to_id"), rel.get("type")
        p1n, p2n = id_to_name.get(p1_id), id_to_name.get(p2_id)
        if p1n and p2n and p1n != p2n:
            pair = frozenset([p1_id, p2_id])
            if rt == "parent": rel_strings.append(f"'{p1n}' is a Parent of '{p2n}'")
            elif rt == "spouse" and pair not in processed_pairs: rel_strings.append(f"'{p1n}' is a Spouse of '{p2n}'"); processed_pairs.add(pair)
            elif rt == "sibling" and pair not in processed_pairs: rel_strings.append(f"'{p1n}' is a Sibling of '{p2n}'"); processed_pairs.add(pair)
    if not rel_strings: return None, "No relationships found to base quiz questions on."
    unique_names = set(id_to_name.values())
    min_required_names, actual_num_questions = 4, num_questions
    if len(unique_names) < min_required_names:
        if len(unique_names) >= 2: actual_num_questions = max(1, len(unique_names) - 1)
        else: return None, "Need at least 2 unique names in the tree to generate a quiz."
    prompt = f"""Based *only* on the provided list of family relationships and names, generate {actual_num_questions} unique multiple-choice quiz questions.

Each question should test knowledge about one specific relationship.
The output format must be a single JSON list, where each element is an object representing a question:
`{{"text": "Question text?", "options": ["Correct Answer", "Wrong Option 1", "Wrong Option 2", "Wrong Option 3"], "correct": "Correct Answer"}}`

- The "options" list must contain the correct answer and 3 incorrect answers chosen from the provided 'Names' list.
- All options within a single question must be unique names.
- If fewer than 4 unique names are available in the 'Names' list, use all available names as options, ensuring the correct answer is included.
- Do not invent relationships or information not present in the 'Relationships' list.
- Ensure the 'correct' value exactly matches one of the strings in the 'options' list.
- Strictly output only the JSON list, without any other text or markdown formatting.

Names:
{chr(10).join(f"- {name}" for name in sorted(list(unique_names)))}

Relationships:
{chr(10).join(f"- {rel}" for rel in rel_strings)}

JSON Output:
"""
    return call_gemini(prompt, is_json_output_expected=True)

def generate_diff_summary(current_data, proposed_data):
    summary = []
    try:
        # Use dictdiffer to find changes
        result = list(diff(current_data, proposed_data, ignore={'last_edited_at', 'last_edited_by', 'created_at', 'created_by'})) # Ignore timestamps/editors for diff summary

        # Process members
        current_members = {m['id']: m for m in current_data.get('family_members', []) if isinstance(m, dict)}
        proposed_members = {m['id']: m for m in proposed_data.get('family_members', []) if isinstance(m, dict)}
        added_members = set(proposed_members.keys()) - set(current_members.keys())
        deleted_members = set(current_members.keys()) - set(proposed_members.keys())
        common_members = set(current_members.keys()) & set(proposed_members.keys())

        if added_members:
            summary.append("**Members Added:**")
            for mid in added_members:
                summary.append(f"- {proposed_members[mid].get('name', 'Unnamed')} (ID: ...{mid[-6:]})")
        if deleted_members:
            summary.append("**Members Deleted:**")
            for mid in deleted_members:
                summary.append(f"- {current_members[mid].get('name', 'Unnamed')} (ID: ...{mid[-6:]})")

        # Check for edited members
        edited_summary = []
        for mid in common_members:
            curr_m, prop_m = current_members[mid], proposed_members[mid]
            changes = []
            for key in ['name', 'dob', 'dod', 'gender', 'totem', 'phone']:
                if curr_m.get(key) != prop_m.get(key):
                    changes.append(f"{key}: '{curr_m.get(key)}' -> '{prop_m.get(key)}'")
            # Story changes
            curr_stories = set(s.get('timestamp') for s in curr_m.get('stories', []) if isinstance(s, dict))
            prop_stories = set(s.get('timestamp') for s in prop_m.get('stories', []) if isinstance(s, dict))
            if curr_stories != prop_stories:
                added_s = len(prop_stories - curr_stories)
                removed_s = len(curr_stories - prop_stories)
                if added_s > 0: changes.append(f"{added_s} storie(s) added")
                if removed_s > 0: changes.append(f"{removed_s} storie(s) removed")
            if changes:
                edited_summary.append(f"- {curr_m.get('name', 'Unnamed')} (ID: ...{mid[-6:]}): {', '.join(changes)}")
        if edited_summary:
            summary.append("**Members Edited:**")
            summary.extend(edited_summary)

        # Process relationships
        curr_rels = set(tuple(sorted([r['from_id'],r['to_id']])) + (r['type'],) for r in current_data.get('relationships', []) if isinstance(r, dict) and all(k in r for k in ['from_id', 'to_id', 'type']))
        prop_rels = set(tuple(sorted([r['from_id'],r['to_id']])) + (r['type'],) for r in proposed_data.get('relationships', []) if isinstance(r, dict) and all(k in r for k in ['from_id', 'to_id', 'type']))
        added_rels = prop_rels - curr_rels
        deleted_rels = curr_rels - prop_rels

        if added_rels:
            summary.append("**Relationships Added:**")
            for rel_tuple in added_rels:
                name1 = proposed_members.get(rel_tuple[0], {}).get('name', f'...{rel_tuple[0][-6:]}')
                name2 = proposed_members.get(rel_tuple[1], {}).get('name', f'...{rel_tuple[1][-6:]}')
                summary.append(f"- {name1} <--> {name2} ({rel_tuple[2]})")
        if deleted_rels:
            summary.append("**Relationships Deleted:**")
            for rel_tuple in deleted_rels:
                name1 = current_members.get(rel_tuple[0], {}).get('name', f'...{rel_tuple[0][-6:]}')
                name2 = current_members.get(rel_tuple[1], {}).get('name', f'...{rel_tuple[1][-6:]}')
                summary.append(f"- {name1} <--> {name2} ({rel_tuple[2]})")
        
        # Settings/Metadata changes
        settings_changes = []
        if current_data.get('settings', {}).get('theme') != proposed_data.get('settings', {}).get('theme'):
            settings_changes.append(f"Theme: '{current_data.get('settings', {}).get('theme')}' -> '{proposed_data.get('settings', {}).get('theme')}'")
        if current_data.get('metadata', {}).get('tree_name') != proposed_data.get('metadata', {}).get('tree_name'):
            settings_changes.append(f"Tree Name: '{current_data.get('metadata', {}).get('tree_name')}' -> '{proposed_data.get('metadata', {}).get('tree_name')}'")
        if settings_changes:
            summary.append("**Settings/Metadata Changed:**")
            summary.extend([f"- {c}" for c in settings_changes])

        if not summary: return "No significant changes detected."
        return "\n".join(summary)
    except Exception as e:
        app.logger.error(f"Error generating diff summary: {e}")
        return f"Error generating diff summary: {e}"


# --- System Health Check Helper ---
def check_system_health():
    if firebase_error:
        return jsonify({"error": "Firebase not available", "detail": firebase_error}), 503
    if not firebase_db_ref:
        return jsonify({"error": "Firebase database reference not set."}), 503
    return None


# ======================= API Endpoints =======================

@app.route('/health', methods=['GET'])
def health_check():
    status = {
        "status": "ok",
        "firebase": "ok" if not firebase_error and firebase_db_ref else f"error: {firebase_error or 'DB ref not set'}",
        "gemini_api": "ok" if not api_key_error and genai_client else "error or not configured"
    }
    overall_ok = status["firebase"] == "ok"
    return jsonify(status), 200 if overall_ok else 503


# --- NEW: User Management Endpoints for Email Auth ---
@app.route('/user/me', methods=['GET'])
@token_required
def get_current_user_api():
    """Get the authenticated user's information"""
    uid = flask_g.user["uid"]
    email = flask_g.user.get("email")
    phone_number = flask_g.user.get("phone_number")
    phone_linked = flask_g.user.get("phone_linked", False)
    
    response = {
        "uid": uid,
        "email": email,
        "phone_number": phone_number,
        "phone_linked": phone_linked
    }
    
    # If phone is linked, get additional user info and primary tree
    if phone_linked and phone_number:
        tree_data, _ = load_tree_data(phone_number)
        if tree_data:
            response["display_name"] = tree_data.get("profile", {}).get("name")
            response["primary_tree"] = {
                "owner_phone": phone_number,
                "tree_name": tree_data.get("metadata", {}).get("tree_name", "My Family Tree"),
                "created_at": tree_data.get("metadata", {}).get("created_at")
            }
    else:
        # Get display name from uid mapping if available
        try:
            path = get_uid_phone_mapping_path(uid)
            user_data = firebase_db_ref.child(path).get()
            if user_data and isinstance(user_data, dict):
                response["display_name"] = user_data.get("display_name")
        except Exception:
            pass
    
    return jsonify(response), 200


@app.route('/user/link_phone', methods=['POST'])
@token_required
def link_phone_api():
    """Link a phone number to the authenticated user's account"""
    uid = flask_g.user["uid"]
    email = flask_g.user.get("email")
    
    # Check if phone is already linked
    if flask_g.user.get("phone_linked"):
        return jsonify({
            "message": "Phone number already linked",
            "phone_number": flask_g.user.get("phone_number")
        }), 200
    
    req_data = request.json
    if not req_data or "phone_number" not in req_data:
        return jsonify({"error": "Missing phone_number in request body"}), 400
    
    phone_number = req_data["phone_number"]
    display_name = req_data.get("display_name")
    
    # Validate phone format
    norm_phone = normalize_phone(phone_number)
    if not norm_phone:
        return jsonify({"error": "Invalid phone number format. Use +263... format."}), 400
    
    # Check if this phone is already linked to another user
    try:
        all_users = firebase_db_ref.child('users_by_uid').get()
        if all_users:
            for existing_uid_key, user_data in all_users.items():
                if isinstance(user_data, dict) and user_data.get('phone_number') == norm_phone:
                    if existing_uid_key != uid:
                        return jsonify({
                            "error": "Phone number already linked",
                            "message": "This phone number is already linked to another account."
                        }), 409
    except Exception as e:
        app.logger.warning(f"Could not check for existing phone links: {e}")
    
    # Store the phone mapping
    success, error = set_user_phone_by_uid(uid, norm_phone, display_name, email)
    if not success:
        return jsonify({"error": "Failed to link phone number", "detail": error}), 500
    
    # Initialize user's tree if it doesn't exist
    tree_data, load_error = load_tree_data(norm_phone)
    if load_error and "Failed to initialize" in load_error:
        return jsonify({"error": "Phone linked but tree initialization failed", "detail": load_error}), 500
    
    # Update profile name if display_name provided
    if display_name and tree_data:
        tree_data["profile"]["name"] = display_name
        me_node = find_person_by_id(tree_data, "Me")
        if me_node:
            me_node["name"] = display_name
            me_node["last_edited_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            me_node["last_edited_by"] = norm_phone
        save_tree_data(norm_phone, tree_data, tree_data)
    
    return jsonify({
        "message": "Phone number linked successfully",
        "phone_number": norm_phone,
        "tree_initialized": tree_data is not None
    }), 200


# --- Tree Data Endpoints ---
@app.route('/tree/<string:owner_phone>', methods=['GET'])
def get_tree_data_api(owner_phone):
    system_error_response = check_system_health()
    if system_error_response: return system_error_response
    norm_phone = normalize_phone(owner_phone)
    if not norm_phone: return jsonify({"error": "Invalid owner_phone format"}), 400

    tree_data, error = load_tree_data(norm_phone)
    if error:
        if tree_data is None and "Failed to initialize" not in error : # If it's not an init error but still no data
             return jsonify({"error": "Tree data not found or could not be loaded.", "detail": error}), 404
        return jsonify({"error": "Could not load or initialize tree data", "detail": error}), 500
    return jsonify(tree_data), 200


@app.route('/user/linked_trees', methods=['GET'])
@token_required
def get_linked_trees_api():
    """Gets trees the authenticated user is a member of (excluding their own primary tree)."""
    current_user_phone = flask_g.user.get("phone_number")
    
    # Handle unlinked phone users gracefully
    if not current_user_phone:
        return jsonify({
            "message": "Link your phone number to see linked trees",
            "trees": [],
            "action_required": "link_phone"
        }), 200
    
    linked_owner_phones_map, error = find_linked_trees(current_user_phone)
    if error:
        return jsonify({"error": "Could not fetch linked trees", "detail": error}), 500

    result = []
    for owner_ph, status_val_in_index in linked_owner_phones_map.items():
        norm_owner_ph = normalize_phone(owner_ph)
        if norm_owner_ph == current_user_phone: # Skip user's own tree
            continue
        if norm_owner_ph:
            tree_d, err = load_tree_data(norm_owner_ph)
            tree_name = tree_d.get("metadata", {}).get("tree_name", f"Tree by ...{norm_owner_ph[-4:]}") if tree_d and not err else f"Unknown Tree ({norm_owner_ph})"
            result.append({"owner_phone": norm_owner_ph, "tree_name": tree_name})

    return jsonify(result), 200


@app.route('/tree/<string:owner_phone_of_tree>/graph', methods=['GET'])
# This can remain public for viewing, or add @token_required if graphs should be private.
# For now, keeping it similar to get_tree_data_api (public GET).
def get_tree_graph_api(owner_phone_of_tree):
    system_error_response = check_system_health()
    if system_error_response: return system_error_response
    norm_phone = normalize_phone(owner_phone_of_tree)
    if not norm_phone: return jsonify({"error": "Invalid owner_phone format"}), 400

    tree_data, error = load_tree_data(norm_phone)
    if error or not tree_data: return jsonify({"error": "Could not load tree data for graph", "detail": error or "Tree data is empty"}), 404
    
    try:
        graph_dot, graph_err = generate_graphviz_object(tree_data)
        if graph_err: return jsonify({"error": "Could not generate graph", "detail": graph_err}), 500
        
        svg_data = graph_dot.pipe(format='svg').decode('utf-8')
        return jsonify({"svg": svg_data, "dot_source": graph_dot.source}), 200
    except graphviz.backend.execute.ExecutableNotFound:
        app.logger.error("Graphviz executable not found.")
        return jsonify({"error": "Graphviz executable not found on server."}), 500
    except Exception as e:
        app.logger.error(f"Error generating graph SVG: {e}")
        return jsonify({"error": "Error generating graph SVG", "detail": str(e)}), 500


# --- Member Management Endpoints (Protected) ---
@app.route('/tree/<string:owner_phone_of_tree>/members', methods=['POST'])
@token_required
@phone_required
def add_member_api(owner_phone_of_tree):
    current_user_phone = flask_g.user["phone_number"] # Authenticated user
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format in URL"}), 400

    # Authorization: Only tree owner can add members directly to their own tree.
    # Collaborators use the proposal mechanism.
    if current_user_phone != norm_owner_phone_of_tree:
        return jsonify({"error": "Forbidden: You can only add members to your own tree directly. Use proposals for other trees."}), 403

    req_data = request.json
    if not req_data: return jsonify({"error": "Missing request body"}), 400
    new_member_info = req_data.get("member_data")
    confirm_conflict = req_data.get("confirm_conflict", False)
    if not new_member_info or not new_member_info.get("name"):
        return jsonify({"error": "Missing member data or name"}), 400

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data)

    conflicting_members = find_person_by_name(tree_data, new_member_info["name"])
    if conflicting_members and not confirm_conflict:
        return jsonify({
            "error": "Name conflict",
            "message": f"A member named '{new_member_info['name']}' already exists.",
            "conflicting_members": [{"id": m["id"], "name": m["name"]} for m in conflicting_members]
        }), 409

    new_member_id = generate_unique_id()
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    norm_new_phone = normalize_phone(new_member_info.get("phone"))
    if new_member_info.get("phone") and not norm_new_phone:
        return jsonify({"error": "Invalid phone format for new member"}), 400

    new_member_entry = deepcopy(DEFAULT_MEMBER_STRUCTURE)
    new_member_entry.update({
        "id": new_member_id,
        "name": new_member_info["name"].strip(),
        "dob": new_member_info.get("dob", "").strip(),
        "dod": new_member_info.get("dod", "").strip(),
        "gender": new_member_info.get("gender", "Unknown"),
        "phone": norm_new_phone or "",
        "totem": new_member_info.get("totem", "").strip(),
        "created_at": current_time,
        "created_by": current_user_phone, # Action performed by authenticated user
        "last_edited_at": current_time,
        "last_edited_by": current_user_phone,
        "stories": []
    })
    tree_data.setdefault("family_members", []).append(new_member_entry)

    save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
    if not save_success:
        return jsonify({"error": "Failed to save new member", "detail": save_error}), 500
    
    response_message = "Member added successfully."
    if save_error: response_message += f" Warning: {save_error}"
    return jsonify({"message": response_message, "member": new_member_entry}), 201


@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>', methods=['PUT'])
@token_required
@phone_required
def edit_member_api(owner_phone_of_tree, member_id):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    if current_user_phone != norm_owner_phone_of_tree:
        return jsonify({"error": "Forbidden: You can only edit members in your own tree directly. Use proposals."}), 403

    req_data = request.json
    if not req_data: return jsonify({"error": "Missing request body"}), 400
    updated_info = req_data.get("member_data")
    confirm_conflict = req_data.get("confirm_conflict", False)
    if not updated_info: return jsonify({"error": "Missing member_data in request"}), 400

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data)
    member_to_edit = find_person_by_id(tree_data, member_id)
    if not member_to_edit: return jsonify({"error": "Member not found"}), 404

    is_me_node_of_owner = (member_id == "Me" and member_to_edit.get("phone") == norm_owner_phone_of_tree)
    if is_me_node_of_owner and "phone" in updated_info and normalize_phone(updated_info["phone"]) != norm_owner_phone_of_tree:
        return jsonify({"error": "Cannot change the phone number of the owner's 'Me' node."}), 403
    if is_me_node_of_owner and "name" in updated_info and updated_info["name"] != member_to_edit.get("name"):
        # Allow 'Me' node name change if it's done via profile update, but direct edit here might be restricted
        # For now, let's assume profile endpoint handles 'Me' node name better.
        # If name is part of profile sync, this direct edit might be redundant or conflicting.
        # For simplicity, we allow it here but it should be consistent with profile logic.
        pass


    new_name = updated_info.get("name", member_to_edit.get("name")).strip()
    if new_name != member_to_edit.get("name"): # Check if name actually changed
        conflicting_members = [
            m for m in tree_data.get("family_members", [])
            if m["id"] != member_id and m.get("name", "").strip().lower() == new_name.lower()
        ]
        if conflicting_members and not confirm_conflict:
            return jsonify({
                "error": "Name conflict on edit",
                "message": f"Changing name to '{new_name}' would conflict with an existing member.",
                "conflicting_members": [{"id": m["id"], "name": m["name"]} for m in conflicting_members]
            }), 409

    something_changed = False
    editable_fields = ["name", "dob", "dod", "gender", "totem", "phone"]

    for field in editable_fields:
        if field in updated_info:
            new_value_raw = updated_info[field]
            new_value = new_value_raw.strip() if isinstance(new_value_raw, str) else new_value_raw
            
            if field == "phone":
                if is_me_node_of_owner and new_value != norm_owner_phone_of_tree : # Already checked but good to be safe
                    continue # Skip phone change for owner's 'Me' node here
                norm_new_phone = normalize_phone(new_value)
                if new_value and not norm_new_phone: return jsonify({"error": f"Invalid phone format for field {field}"}), 400
                new_value = norm_new_phone or ""
            
            if member_to_edit.get(field) != new_value:
                member_to_edit[field] = new_value
                something_changed = True
    
    if something_changed:
        member_to_edit["last_edited_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        member_to_edit["last_edited_by"] = current_user_phone
        save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save updated member", "detail": save_error}), 500
        
        response_message = "Member updated successfully."
        if save_error: response_message += f" Warning: {save_error}"
        return jsonify({"message": response_message, "member": member_to_edit}), 200
    else:
        return jsonify({"message": "No changes detected for member.", "member": member_to_edit}), 200


@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>', methods=['DELETE'])
@token_required
@phone_required
def delete_member_api(owner_phone_of_tree, member_id):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    if current_user_phone != norm_owner_phone_of_tree:
        return jsonify({"error": "Forbidden: You can only delete members from your own tree directly."}), 403

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500

    member_to_check = find_person_by_id(tree_data, member_id)
    if not member_to_check: return jsonify({"error": "Member not found"}), 404
    if member_id == "Me" and member_to_check.get("phone") == norm_owner_phone_of_tree:
        return jsonify({"error": "Cannot delete the owner's 'Me' user node."}), 403
    
    original_tree_data_for_save = deepcopy(tree_data)
    member_index = next((i for i, p in enumerate(tree_data.get("family_members", [])) if p.get("id") == member_id), -1)
    # Already checked if member exists, so index should be found
    
    deleted_member_name = tree_data["family_members"][member_index].get("name", "Unknown")
    del tree_data["family_members"][member_index]
    tree_data["relationships"] = [
        r for r in tree_data.get("relationships", [])
        if isinstance(r,dict) and r.get("from_id") != member_id and r.get("to_id") != member_id
    ]

    save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
    if not save_success:
        return jsonify({"error": "Failed to save after deleting member", "detail": save_error}), 500
    
    response_message = f"Member '{deleted_member_name}' deleted successfully."
    if save_error: response_message += f" Warning: {save_error}"
    return jsonify({"message": response_message}), 200


# --- Relationship Management Endpoints (Protected) ---
@app.route('/tree/<string:owner_phone_of_tree>/relationships', methods=['POST'])
@token_required
@phone_required
def add_relationship_api(owner_phone_of_tree):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    if current_user_phone != norm_owner_phone_of_tree:
        return jsonify({"error": "Forbidden: You can only add relationships to your own tree directly."}), 403

    req_data = request.json
    if not req_data or not all(k in req_data for k in ["from_id", "to_id", "type"]):
        return jsonify({"error": "Missing from_id, to_id, or type in request body"}), 400

    from_id, to_id, rel_type = req_data["from_id"], req_data["to_id"], req_data["type"]
    valid_rel_types = ['parent', 'spouse', 'sibling']
    if rel_type not in valid_rel_types:
        return jsonify({"error": f"Invalid relationship type. Must be one of {valid_rel_types}"}), 400
    if from_id == to_id:
        return jsonify({"error": "Cannot define a relationship between a person and themselves."}), 400

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data)
    if not find_person_by_id(tree_data, from_id) or not find_person_by_id(tree_data, to_id):
        return jsonify({"error": "One or both persons in the relationship not found."}), 404

    relationships = tree_data.setdefault("relationships", [])
    exists = False
    for rel in relationships:
        if not isinstance(rel, dict): continue
        rf, rt, rtype_existing = rel.get('from_id'), rel.get('to_id'), rel.get('type')
        if rel_type == 'parent' and rtype_existing == 'parent' and rf == from_id and rt == to_id: exists = True; break
        if rel_type in ['spouse', 'sibling'] and rtype_existing == rel_type and frozenset([rf, rt]) == frozenset([from_id, to_id]): exists = True; break
    if exists:
        return jsonify({"error": "This relationship already exists."}), 409

    new_rel_entry = {"from_id": from_id, "to_id": to_id, "type": rel_type}
    relationships.append(new_rel_entry)

    save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
    if not save_success:
        return jsonify({"error": "Failed to save new relationship", "detail": save_error}), 500
    
    response_message = "Relationship added successfully."
    if save_error: response_message += f" Warning: {save_error}"
    return jsonify({"message": response_message, "relationship": new_rel_entry}), 201


@app.route('/tree/<string:owner_phone_of_tree>/relationships/delete', methods=['POST'])
@token_required
@phone_required
def delete_relationship_api(owner_phone_of_tree):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    if current_user_phone != norm_owner_phone_of_tree:
        return jsonify({"error": "Forbidden: You can only delete relationships from your own tree directly."}), 403
        
    req_data = request.json
    if not req_data or not all(k in req_data for k in ["from_id", "to_id", "type"]):
        return jsonify({"error": "Missing from_id, to_id, or type in request body for deletion"}), 400
    
    del_from_id, del_to_id, del_rel_type = req_data["from_id"], req_data["to_id"], req_data["type"]

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data)
    relationships = tree_data.get("relationships", [])
    new_relationships = []
    deleted = False
    for rel in relationships:
        if not isinstance(rel, dict): new_relationships.append(rel); continue
        rf, rt, rtype = rel.get('from_id'), rel.get('to_id'), rel.get('type')
        is_match = False
        if del_rel_type == 'parent' and rtype == 'parent' and rf == del_from_id and rt == del_to_id: is_match = True
        elif del_rel_type in ['spouse', 'sibling'] and rtype == del_rel_type and frozenset([rf, rt]) == frozenset([del_from_id, del_to_id]): is_match = True
        
        if is_match: deleted = True
        else: new_relationships.append(rel)

    if not deleted: return jsonify({"error": "Relationship to delete not found."}), 404

    tree_data["relationships"] = new_relationships
    save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
    if not save_success:
        return jsonify({"error": "Failed to save after deleting relationship", "detail": save_error}), 500
    
    response_message = "Relationship deleted successfully."
    if save_error: response_message += f" Warning: {save_error}"
    return jsonify({"message": response_message}), 200


# --- Story Management Endpoints (Protected) ---
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>/stories', methods=['POST'])
@token_required
@phone_required
def add_story_api(owner_phone_of_tree, member_id):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    req_data = request.json
    if not req_data or "text" not in req_data or not req_data["text"].strip():
        return jsonify({"error": "Missing or empty 'text' for story"}), 400
    story_text = req_data["text"].strip()

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500

    original_tree_data_for_save = deepcopy(tree_data) # For save if current_user is owner
    member = find_person_by_id(tree_data, member_id)
    if not member: return jsonify({"error": "Member not found"}), 404

    new_story = {
        "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "text": story_text,
        "added_by": current_user_phone
    }
    member.setdefault("stories", []).append(new_story)
    member["last_edited_at"] = new_story["timestamp"]
    member["last_edited_by"] = current_user_phone

    if current_user_phone == norm_owner_phone_of_tree:
        # Owner adds story directly
        save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save story", "detail": save_error}), 500
        response_message = "Story added successfully."
        if save_error: response_message += f" Warning: {save_error}"
        return jsonify({"message": response_message, "story": new_story}), 201
    else:
        # Collaborator adds story: For simplicity, let's allow collaborators to add stories directly for now.
        # A more strict approach would be to use proposals for all changes.
        # Here, we allow stories to be added by non-owners as a contribution.
        # The story will have `added_by` set to the collaborator's phone.
        # Tree owner can delete it later if needed.
        save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save story (as collaborator)", "detail": save_error}), 500
        response_message = "Story added successfully (as collaborator)."
        if save_error: response_message += f" Warning: {save_error}"
        return jsonify({"message": response_message, "story": new_story}), 201


@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>/stories/<string:story_timestamp>', methods=['DELETE'])
@token_required
@phone_required
def delete_story_api(owner_phone_of_tree, member_id, story_timestamp):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data) # For save if current_user is owner
    member = find_person_by_id(tree_data, member_id)
    if not member: return jsonify({"error": "Member not found"}), 404

    stories = member.get("stories", [])
    story_to_delete_index = -1
    for i, story in enumerate(stories):
        if story.get("timestamp") == story_timestamp:
            # Authorization: Only story adder or tree owner can delete.
            can_delete = (story.get("added_by") == current_user_phone or current_user_phone == norm_owner_phone_of_tree)
            if not can_delete:
                return jsonify({"error": "Forbidden: You can only delete stories you added or if you are the tree owner."}), 403
            story_to_delete_index = i
            break
    
    if story_to_delete_index == -1:
        return jsonify({"error": "Story with given timestamp not found or you do not have permission to delete it."}), 404

    del member["stories"][story_to_delete_index]
    member["last_edited_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    member["last_edited_by"] = current_user_phone

    if current_user_phone == norm_owner_phone_of_tree:
        save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save after deleting story", "detail": save_error}), 500
        response_message = "Story deleted successfully."
        if save_error: response_message += f" Warning: {save_error}"
        return jsonify({"message": response_message}), 200
    else:
        # Similar to add story, direct delete is for owner. Collaborators use proposals.
        return jsonify({"error": "Forbidden: Use proposals to delete stories from trees you do not own (unless you added the story and it's a direct delete)."}), 403


# --- Collaboration Endpoints (Protected) ---
@app.route('/tree/<string:owner_phone_of_tree>/proposals', methods=['POST'])
@token_required
@phone_required
def propose_changes_api(owner_phone_of_tree):
    proposer_phone_from_token = flask_g.user["phone_number"] # This is the authenticated user making the proposal
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)

    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format in URL"}), 400
    if norm_owner_phone_of_tree == proposer_phone_from_token:
        return jsonify({"error": "Cannot propose changes to your own tree. Edit directly."}), 400

    req_data = request.json
    # The "proposer_phone" in req_data is redundant if we use token, but client might send it. We trust the token.
    if not req_data or "proposed_data" not in req_data:
        return jsonify({"error": "Missing proposed_data in request"}), 400
    
    proposed_data = req_data["proposed_data"]

    success, error = propose_changes(norm_owner_phone_of_tree, proposer_phone_from_token, proposed_data)
    if not success:
        return jsonify({"error": "Failed to propose changes", "detail": error}), 500
    return jsonify({"message": "Changes proposed successfully."}), 201


@app.route('/tree/my_proposals/pending', methods=['GET']) # Get pending proposals FOR the authenticated user's tree(s)
@token_required
@phone_required
def get_my_pending_changes_api():
    owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
    
    pending_data, error = load_pending_changes(owner_phone_from_token)
    if error:
        return jsonify({"error": "Could not load pending changes for your tree(s)", "detail": error}), 500
    return jsonify(pending_data), 200


@app.route('/tree/my_proposals/<string:proposer_phone_to_manage>/accept', methods=['POST'])
@token_required
@phone_required
def accept_changes_api(proposer_phone_to_manage):
    owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
    norm_proposer_phone = normalize_phone(proposer_phone_to_manage)
    if not norm_proposer_phone: return jsonify({"error": "Invalid proposer_phone_to_manage format"}), 400

    success, error = accept_changes(owner_phone_from_token, norm_proposer_phone)
    if not success:
        return jsonify({"error": "Failed to accept changes", "detail": error}), 500
    return jsonify({"message": "Changes accepted and merged successfully."}), 200


@app.route('/tree/my_proposals/<string:proposer_phone_to_manage>/reject', methods=['POST'])
@token_required
@phone_required
def reject_changes_api(proposer_phone_to_manage):
    owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
    norm_proposer_phone = normalize_phone(proposer_phone_to_manage)
    if not norm_proposer_phone: return jsonify({"error": "Invalid proposer_phone_to_manage format"}), 400

    success, error = reject_changes(owner_phone_from_token, norm_proposer_phone)
    if not success:
        return jsonify({"error": "Failed to reject changes", "detail": error}), 500
    return jsonify({"message": "Changes rejected successfully."}), 200

@app.route('/tree/my_proposals/<string:proposer_phone_to_review>/diff', methods=['GET'])
@token_required
@phone_required
def get_proposal_diff_api(proposer_phone_to_review):
    owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
    norm_proposer_phone = normalize_phone(proposer_phone_to_review)
    if not norm_proposer_phone: return jsonify({"error": "Invalid proposer_phone_to_review format"}), 400

    current_owner_data, load_err = load_tree_data(owner_phone_from_token)
    if load_err: return jsonify({"error": "Could not load current owner tree data", "detail": load_err}), 500

    pending_proposals, pend_err = load_pending_changes(owner_phone_from_token)
    if pend_err: return jsonify({"error": "Could not load pending proposals", "detail": pend_err}), 500
    
    # The keys in pending_proposals are already normalized proposer phones
    proposal_payload = pending_proposals.get(norm_proposer_phone) 
    if not proposal_payload or "tree_data" not in proposal_payload:
        return jsonify({"error": f"Proposal from {norm_proposer_phone} not found or invalid."}), 404
    
    proposed_data = proposal_payload["tree_data"]
    diff_summary_text = generate_diff_summary(current_owner_data, proposed_data)
    return jsonify({"diff_summary": diff_summary_text}), 200


# --- AI Feature Endpoints (Protected as they operate on user's tree or consume user quota) ---
@app.route('/ai/build_tree_from_text', methods=['POST'])
@token_required
def ai_build_tree_api():
    """
    Endpoint to analyze family description text and extract people and relationships.
    
    Request body:
    {
        "description": "Family description text here..."
    }
    
    Response:
    {
        "people": [...],
        "relationships": [...]
    }
    """
    if api_key_error: 
        return jsonify({"error": "Gemini API not configured"}), 503

    req_data = request.json
    if not req_data or "description" not in req_data:
        return jsonify({"error": "Missing description in request body"}), 400
    
    description = req_data["description"]
    
    if not description.strip():
        return jsonify({"error": "Description cannot be empty"}), 400

    ai_result, error = generate_tree_from_description_gemini(description)
    if error:
        return jsonify({"error": "AI analysis failed", "detail": error}), 500
    
    return jsonify(ai_result), 200



@app.route('/tree/<string:owner_phone_of_tree>/ai_merge_suggestions', methods=['POST'])
@token_required
@phone_required
def ai_merge_tree_api(owner_phone_of_tree):
    current_user_phone = flask_g.user["phone_number"]
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    if current_user_phone != norm_owner_phone_of_tree:
        return jsonify({"error": "Forbidden: You can only merge AI suggestions into your own tree."}), 403
    if api_key_error: return jsonify({"error": "Gemini API features disabled."}), 503


    req_data = request.json
    if not req_data or "ai_result" not in req_data:
        return jsonify({"error": "Missing ai_result in request body"}), 400
    
    ai_result_data = req_data["ai_result"]
    if not isinstance(ai_result_data, dict) or "people" not in ai_result_data or "relationships" not in ai_result_data:
        return jsonify({"error": "Invalid ai_result format"}), 400

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error: return jsonify({"error": "Could not load tree data for merge", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data)

    # (Merge logic from previous version - slightly adapted for current_user_phone)
    ai_people = ai_result_data.get("people", [])
    ai_rels = ai_result_data.get("relationships", [])
    valid_people = [p for p in ai_people if isinstance(p, dict) and p.get("name")]
    valid_rel_structs = [r for r in ai_rels if isinstance(r, dict) and all(k in r for k in ["person1_name", "person2_name", "type"])]
    ai_people_names = {p.get('name') for p in valid_people}
    valid_rels_final = [r for r in valid_rel_structs if r.get('person1_name') in ai_people_names and r.get('person2_name') in ai_people_names]

    if not valid_people and not valid_rels_final:
        return jsonify({"message": "No valid people or relationships from AI to merge."}), 200

    added_p_count, added_r_count, updated_p_names, skipped_p_names, skipped_r_desc = 0, 0, [], [], []
    id_map = {}
    current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    for person_ai in valid_people:
        ai_name = person_ai.get("name")
        if not ai_name: continue
        existing_people_with_name = find_person_by_name(tree_data, ai_name)
        existing_person = existing_people_with_name[0] if existing_people_with_name else None
        person_updated = False
        if existing_person:
            id_map[ai_name] = existing_person['id']
            for key in ['dob', 'dod', 'gender', 'totem']:
                if not existing_person.get(key) and person_ai.get(key):
                    if key == 'gender' and person_ai.get(key) == 'Unknown': continue
                    existing_person[key] = person_ai.get(key); person_updated = True
            if person_updated:
                existing_person["last_edited_at"] = current_time; existing_person["last_edited_by"] = current_user_phone
                updated_p_names.append(ai_name)
            else: skipped_p_names.append(ai_name + " (exists, no new info)")
        else:
            new_id = generate_unique_id(); id_map[ai_name] = new_id
            new_data = deepcopy(DEFAULT_MEMBER_STRUCTURE)
            valid_keys = ['name', 'dob', 'dod', 'gender', 'totem']
            new_data.update({k: v for k, v in person_ai.items() if k in valid_keys and v})
            new_data.update({"id": new_id, "created_at": current_time, "created_by": current_user_phone, 
                             "last_edited_at": current_time, "last_edited_by": current_user_phone})
            tree_data.setdefault("family_members", []).append(new_data)
            added_p_count += 1
    
    current_rels_list = tree_data.setdefault("relationships", [])
    existing_rel_set = set()
    for r_exist in current_rels_list: # Build set of existing relationships
        if not isinstance(r_exist, dict): continue
        from_id_e, to_id_e, r_type_e = r_exist.get('from_id'), r_exist.get('to_id'), r_exist.get('type')
        if not from_id_e or not to_id_e or not r_type_e: continue
        if r_type_e == 'parent': existing_rel_set.add( (from_id_e, to_id_e, r_type_e) )
        elif r_type_e in ['spouse', 'sibling']: existing_rel_set.add( (tuple(sorted((from_id_e, to_id_e))), r_type_e) )
    
    rel_type_map = {"parent_of":'parent', "spouse_of":'spouse', "sibling_of":'sibling'}
    for rel_ai in valid_rels_final:
        p1n, p2n, rta = rel_ai.get("person1_name"), rel_ai.get("person2_name"), rel_ai.get("type")
        id1, id2 = id_map.get(p1n), id_map.get(p2n)
        rti = rel_type_map.get(rta)
        if id1 and id2 and rti:
            exists, rel_key = False, None
            if rti == 'parent': rel_key = (id1, id2, rti)
            elif rti in ['spouse', 'sibling']: rel_key = (tuple(sorted((id1, id2))), rti)
            if rel_key and rel_key in existing_rel_set: exists = True
            if exists: skipped_r_desc.append(f"{p1n} {rta.replace('_',' ')} {p2n} (exists)")
            else:
                current_rels_list.append({"from_id": id1, "to_id": id2, "type": rti}); added_r_count += 1
                if rel_key: existing_rel_set.add(rel_key)
        else: skipped_r_desc.append(f"{p1n} {rta.replace('_',' ')} {p2n} (missing person ID or invalid type)")

    if added_p_count > 0 or added_r_count > 0 or updated_p_names:
        save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save merged AI data", "detail": save_error}), 500
        merge_summary_msg = f"AI Merge: Added {added_p_count} people, {added_r_count} relationships. Updated {len(updated_p_names)} people."
        if save_error: merge_summary_msg += f" Warning: {save_error}"
        return jsonify({"message": merge_summary_msg, "skipped_people": skipped_p_names, "skipped_relationships": skipped_r_desc}), 200
    else:
        return jsonify({"message": "No new information from AI to merge.", "skipped_people": skipped_p_names, "skipped_relationships": skipped_r_desc}), 200


@app.route('/tree/<string:owner_phone_of_tree>/quiz/generate', methods=['POST'])
@token_required # Quiz generation might be resource-intensive or use API quotas
def generate_quiz_api(owner_phone_of_tree):
    if api_key_error: return jsonify({"error": "Gemini API not configured"}), 503
    norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
    if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    # Authenticated user can generate quiz for any tree they can view.
    # The owner_phone_of_tree in URL specifies which tree's data to use.

    num_questions = request.json.get("num_questions", 5) if request.json else 5

    tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
    if load_error or not tree_data: return jsonify({"error": "Could not load tree data for quiz", "detail": load_error or "Tree data is empty"}), 500

    members = tree_data.get("family_members", [])
    relationships = tree_data.get("relationships", [])

    questions, error = generate_quiz_questions_gemini(members, relationships, num_questions=num_questions)
    if error: return jsonify({"error": "Failed to generate quiz questions", "detail": error}), 500
    if not questions or not isinstance(questions, list) or len(questions) == 0:
        return jsonify({"error": "AI returned no valid questions or in an unexpected format."}), 500
    
    valid_qs = [q for q in questions if isinstance(q, dict) and all(k in q for k in ['text','options','correct']) and isinstance(q['options'],list) and len(q['options']) > 1 and q['correct'] in q['options']]
    if not valid_qs:
        app.logger.error(f"AI Quiz Response Invalid Structure: {questions}")
        return jsonify({"error": "AI returned questions in an invalid structure."}), 500
        
    return jsonify(valid_qs), 200


# --- Settings & Profile Endpoints (Protected, for authenticated user's own tree) ---
@app.route('/user/me/settings', methods=['PUT'])
@token_required
@phone_required
def update_my_settings_api():
    current_user_phone = flask_g.user["phone_number"] # Settings are for the authenticated user's own tree
    
    req_data = request.json
    if not req_data: return jsonify({"error": "Missing request body for settings"}), 400

    tree_data, load_error = load_tree_data(current_user_phone) # Load user's own tree
    if load_error: return jsonify({"error": "Could not load tree data for settings", "detail": load_error}), 500
    
    original_tree_data_for_save = deepcopy(tree_data)
    settings = tree_data.setdefault("settings", deepcopy(DEFAULT_PROFILE["settings"]))
    metadata = tree_data.setdefault("metadata", deepcopy(DEFAULT_PROFILE["metadata"]))
    changed = False

    if "tree_name" in req_data and metadata.get("tree_name") != req_data["tree_name"]:
        metadata["tree_name"] = req_data["tree_name"]; changed = True
    if "theme" in req_data and settings.get("theme") != req_data["theme"]:
        settings["theme"] = req_data["theme"]; changed = True
    if "privacy" in req_data and settings.get("privacy") != req_data["privacy"]:
        settings["privacy"] = req_data["privacy"]; changed = True # Conceptual
    
    if changed:
        save_success, save_error = save_tree_data(current_user_phone, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save settings", "detail": save_error}), 500
        response_message = "Settings updated successfully."
        if save_error: response_message += f" Warning: {save_error}"
        return jsonify({"message": response_message, "settings": settings, "metadata": metadata}), 200
    else:
        return jsonify({"message": "No changes made to settings."}), 200


@app.route('/user/me/profile', methods=['PUT'])
@token_required
@phone_required
def update_my_profile_api():
    current_user_phone = flask_g.user["phone_number"] # Profile is for the authenticated user's own tree
    
    req_data = request.json
    if not req_data: return jsonify({"error": "Missing request body for profile"}), 400

    tree_data, load_error = load_tree_data(current_user_phone)
    if load_error: return jsonify({"error": "Could not load tree data for profile", "detail": load_error}), 500

    original_tree_data_for_save = deepcopy(tree_data)
    profile = tree_data.setdefault("profile", deepcopy(DEFAULT_PROFILE["profile"]))
    profile_updated, me_node_updated = False, False

    # Update profile section
    if "name" in req_data and profile.get("name") != req_data["name"]:
        profile["name"] = req_data["name"]; profile_updated = True
    if "dob" in req_data and profile.get("dob") != req_data["dob"]:
        profile["dob"] = req_data["dob"]; profile_updated = True
    if "gender" in req_data and profile.get("gender") != req_data["gender"]:
        profile["gender"] = req_data["gender"]; profile_updated = True
    
    # Sync relevant profile changes to the 'Me' node in family_members
    me_node = find_person_by_id(tree_data, "Me")
    if me_node and me_node.get("phone") == current_user_phone: # Ensure it's the correct 'Me' node
        # Use profile name for 'Me' node if available and different, else keep 'Me' or existing name
        me_node_name_update = profile.get("name") if profile.get("name") else me_node.get("name", "Me")
        if me_node.get("name") != me_node_name_update: me_node["name"] = me_node_name_update; me_node_updated = True
        
        if "dob" in req_data and me_node.get("dob") != req_data["dob"]:
             me_node["dob"] = req_data["dob"]; me_node_updated = True
        if "gender" in req_data and me_node.get("gender") != req_data["gender"]:
             me_node["gender"] = req_data["gender"]; me_node_updated = True
        
        if me_node_updated:
            me_node["last_edited_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            me_node["last_edited_by"] = current_user_phone
    elif not me_node:
        app.logger.warning(f"Could not find 'Me' node for owner {current_user_phone} to sync profile.")

    if profile_updated or me_node_updated:
        save_success, save_error = save_tree_data(current_user_phone, tree_data, original_tree_data_for_save)
        if not save_success:
            return jsonify({"error": "Failed to save profile updates", "detail": save_error}), 500
        response_message = "Profile updated successfully."
        if save_error: response_message += f" Warning: {save_error}"
        return jsonify({"message": response_message, "profile": profile, "me_node_updated": me_node_updated}), 200
    else:
        return jsonify({"message": "No changes made to profile."}), 200

# --- Family Timeline Endpoint (Public GET or @token_required if private) ---
@app.route('/tree/<string:owner_phone_of_tree>/timeline', methods=['GET'])
def get_timeline_api(owner_phone_of_tree):
    system_error_response = check_system_health()
    if system_error_response: return system_error_response
    norm_phone = normalize_phone(owner_phone_of_tree)
    if not norm_phone: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400

    tree_data, load_error = load_tree_data(norm_phone)
    if load_error or not tree_data: return jsonify({"error": "Could not load tree data for timeline", "detail": load_error or "Tree data is empty"}), 500

    members = tree_data.get("family_members", [])
    events = []
    valid_date_format = "%Y-%m-%d"
    for person in members:
        if not isinstance(person, dict): continue
        name = person.get("name", "Unknown")
        dob_str, dod_str = person.get("dob"), person.get("dod")
        try:
            if dob_str:
                datetime.strptime(dob_str, valid_date_format) # Validate format
                events.append({"date": dob_str, "type": "Birth", "desc": f"{name} born."})
            if dod_str:
                datetime.strptime(dod_str, valid_date_format) # Validate format
                events.append({"date": dod_str, "type": "Death", "desc": f"{name} passed."})
        except (ValueError, TypeError): pass
    
    try: events.sort(key=lambda x: x.get("date", "0000-00-00"))
    except Exception as e: app.logger.warning(f"Timeline sort error: {e}.")

    return jsonify(events), 200


# --------- Run the App ---------
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=7860, debug=True)