File size: 60,595 Bytes
65d68f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import {
  ApiError,
  apiGet,
  apiPost,
} from "@/lib/api/client";
import { isLocalResourceId, withDemoFallback } from "@/lib/api/mode";
import type {
  ApiDocument,
  BrainAskPayload,
  BrainAskResponse,
  BrainGenerationResponse,
  BrainGeneratePayload,
  BrainLastNightPlanOutput,
  BrainAnswerCorrectionOutput,
  BrainPyqAnalysisOutput,
  BrainSourcesListResponse,
  BrainSourceResponse,
  BrainVideoPlanPayload,
  BrainVideoPlanResponse,
  BrainExamAnswerOutput,
  DocumentChunk,
  FlashcardResponseItem,
  NotesOutput,
  QuizQuestionResponse,
  StudyPathGeneratePayload,
  BrainStudyPathResponse,
  StudyProfilePayload,
  StudyProfileResponse,
  StudyRequestAnalyzePayload,
  StudyRequestAnalysis,
  StudentLevel,
  TextSourcePayload,
  VideoMode,
} from "@/lib/api/types";
import { demoUser } from "@/lib/frontend-demo-data";
import {
  getLocalDocuments,
  retrieveLocalDocumentChunks,
  saveLocalDocument,
} from "@/lib/frontend-local-store";
const OFFLINE_EVIDENCE_LABEL = "Offline preview — not connected to DocDoe AI";
const OFFLINE_TRUST_NOTE =
  "Offline preview only. Connect to the DocDoe backend for source-grounded AI answers.";

function safeTitle(value: string) {
  return value
    .trim()
    .replace(/\s+/g, " ")
    .replace(/\b\w/g, (letter) => letter.toUpperCase());
}

function containsAny(text: string, patterns: string[]) {
  return patterns.some((pattern) => text.includes(pattern));
}

function inferSubject(text: string) {
  const subjects = [
    "physics",
    "chemistry",
    "biology",
    "maths",
    "mathematics",
    "computer science",
    "english",
    "social science",
  ];
  const found = subjects.find((subject) => text.includes(subject));
  if (!found) return null;
  return found === "maths" ? "Maths" : safeTitle(found);
}

function inferTopic(rawText: string, lowerText: string, subject: string | null) {
  const knownTopics = [
    "electromagnetic induction",
    "photosynthesis",
    "organic chemistry",
    "trigonometry",
    "python basics",
    "current electricity",
    "life processes",
    "chemical reactions",
    "faraday's law",
    "lenz's law",
  ];
  const known = knownTopics.find((topic) => lowerText.includes(topic));
  if (known) return safeTitle(known);

  const afterSubject = subject ? rawText.split(new RegExp(subject, "i"))[1]?.trim() : "";
  const candidate = (afterSubject || rawText)
    .replace(/\b(exam|tomorrow|today|need|want|a\+|full score|good marks|pass|study|prepare|class|kerala|cbse|sslc|jee|neet)\b/gi, " ")
    .replace(/[^\w\s+.-]/g, " ")
    .replace(/\s+/g, " ")
    .trim();
  if (candidate.length > 3 && !/^(asdf|test|hello|hi)$/i.test(candidate)) {
    return safeTitle(candidate.split(" ").slice(0, 5).join(" "));
  }
  return null;
}

function inferAnalysis(payload: StudyRequestAnalyzePayload): StudyRequestAnalysis {
  const rawText = payload.raw_text.trim();
  const lowerText = rawText.toLowerCase();
  const subject = inferSubject(lowerText);
  const topic = inferTopic(rawText, lowerText, subject);
  const exam = containsAny(lowerText, ["kerala", "+2", "hse"])
    ? "Kerala +2"
    : containsAny(lowerText, ["sslc"])
      ? "SSLC"
      : containsAny(lowerText, ["cbse"])
        ? "CBSE"
        : containsAny(lowerText, ["jee"])
          ? "JEE"
          : containsAny(lowerText, ["neet"])
            ? "NEET"
            : null;
  const grade = containsAny(lowerText, ["+2", "class 12", "12th"]) ? "12" : containsAny(lowerText, ["+1", "class 11", "11th"]) ? "11" : null;
  const board = exam?.includes("Kerala") ? "Kerala HSE" : exam === "CBSE" ? "CBSE" : null;
  const goal = containsAny(lowerText, ["a+", "a plus"]) ? "A+" : containsAny(lowerText, ["full score", "rank", "top score"]) ? "Full score" : containsAny(lowerText, ["pass"]) ? "Pass" : "Good marks";
  const timeLeft = containsAny(lowerText, ["tomorrow"]) ? "tomorrow" : containsAny(lowerText, ["today", "1 hour"]) ? "1 hour" : containsAny(lowerText, ["3 hours"]) ? "3 hours" : containsAny(lowerText, ["5 hours"]) ? "5 hours" : containsAny(lowerText, ["7 days", "week"]) ? "7 days" : null;
  const level: StudentLevel | null = containsAny(lowerText, ["beginner", "basics", "from zero"])
    ? "beginner"
    : containsAny(lowerText, ["advanced", "full-score", "rank"])
      ? "advanced"
      : null;
  const language = containsAny(lowerText, ["malayalam"]) ? "Malayalam + English" : containsAny(lowerText, ["hindi"]) ? "Hindi + English" : null;
  const primaryNeed = containsAny(lowerText, ["video"]) ? "study_video" : containsAny(lowerText, ["quiz"]) ? "quiz_me" : containsAny(lowerText, ["last night", "tomorrow"]) ? "last_night_plan" : "what_to_study";
  const missingFields = [
    exam ? null : "exam",
    subject ? null : "subject",
    topic ? null : "topic",
    timeLeft ? null : "time_left",
    level ? null : "level",
    language ? null : "language_preference",
  ].filter((field): field is string => Boolean(field));
  const confidence = Math.max(0.35, Math.min(0.92, 1 - missingFields.length * 0.11));

  return {
    raw_text: rawText,
    exam,
    board,
    grade,
    subject,
    chapter: topic,
    topic,
    goal,
    time_left: timeLeft,
    level,
    language_preference: language,
    primary_need: primaryNeed,
    confidence,
    missing_fields: missingFields,
    warnings: [
      ...(confidence < 0.55 ? ["DocDoe needs a clearer exam, subject, or chapter before building your path."] : []),
      "PYQ data is not available yet. DocDoe will use your source, topic, goal, and time.",
    ],
    syllabus_status: payload.syllabus_text?.trim() ? "provided" : "not_provided",
    pyq_status: "unavailable",
  };
}

export function brainSourceToApiDocument(source: BrainSourceResponse): ApiDocument {
  return {
    id: source.id,
    user_id: source.user_id,
    title: source.title,
    file_name: source.file_name,
    file_type: source.file_type || source.source_type,
    subject: source.subject,
    chapter: source.chapter,
    syllabus: source.syllabus,
    status: source.status,
    extracted_text: source.preview_text,
    chunk_count: source.chunk_count,
    created_at: source.created_at,
  };
}

function apiDocumentToBrainSource(document: ApiDocument, sourceType = "syllabus_text"): BrainSourceResponse {
  return {
    id: document.id,
    user_id: document.user_id,
    title: document.title,
    source_type: sourceType,
    status: document.status,
    file_name: document.file_name,
    file_type: document.file_type,
    subject: document.subject,
    chapter: document.chapter,
    syllabus: document.syllabus,
    word_count: Math.round((document.extracted_text_length ?? 0) / 5),
    chunk_count: document.chunk_count,
    preview_text: null, // Use /documents/{id}/preview endpoint for preview text
    created_at: document.created_at,
    updated_at: document.created_at,
  };
}

function localTextSource(payload: TextSourcePayload): BrainSourceResponse {
  const now = new Date().toISOString();
  const document: ApiDocument = {
    id: `doc-local-text-${Date.now()}`,
    user_id: demoUser.id,
    title: payload.title || payload.chapter || "Study source",
    file_name: payload.title || payload.chapter || "Study source",
    file_type: payload.source_type,
    subject: payload.subject ?? "Study",
    chapter: payload.chapter ?? payload.title,
    syllabus: payload.syllabus ?? "Study plan",
    status: "ready",
    extracted_text: payload.text,
    chunk_count: Math.max(1, Math.ceil(payload.text.length / 420)),
    created_at: now,
  };
  return apiDocumentToBrainSource(saveLocalDocument(document), payload.source_type);
}

function fallbackProfile(payload: StudyProfilePayload): StudyProfileResponse {
  const now = new Date().toISOString();
  return {
    id: `sp-local-${Date.now()}`,
    user_id: demoUser.id,
    exam: payload.exam ?? null,
    board: payload.board ?? null,
    grade: payload.grade ?? null,
    subject: payload.subject ?? null,
    chapter: payload.chapter ?? null,
    topic: payload.topic ?? null,
    goal: payload.goal ?? null,
    time_left: payload.time_left ?? null,
    level: payload.level ?? "intermediate",
    language_preference: payload.language_preference ?? "English",
    primary_need: payload.primary_need ?? "what_to_study",
    weak_areas: payload.weak_areas ?? [],
    onboarding_completed: payload.onboarding_completed ?? false,
    extra: payload.extra ?? {},
    created_at: now,
    updated_at: now,
  };
}

function planDurationMinutes(timeLeft: string | null | undefined) {
  if (!timeLeft) return 300;
  const value = timeLeft.toLowerCase();
  if (value.includes("1")) return 60;
  if (value.includes("3")) return 180;
  if (value.includes("5") || value.includes("tomorrow")) return 300;
  if (value.includes("7")) return 420;
  return 600;
}

function fallbackStudyPath(payload: StudyPathGeneratePayload): BrainStudyPathResponse {
  const topic = payload.topic || payload.syllabus_text?.split(/\s+/).slice(0, 4).join(" ") || "your chapter";
  const duration = planDurationMinutes(payload.time_left);
  const blocks = [
    {
      title: `${topic} meaning`,
      reason_type: "concept",
      task: "Read the core definition, then write it once in your own words.",
      expected_output: "One crisp definition and three keywords.",
      actions: ["Read", "Underline keywords", "Write a 2-mark answer"],
    },
    {
      title: "Scoring points",
      reason_type: "exam",
      task: "Convert the chapter into mark-wise bullet answers.",
      expected_output: "1-mark, 2-mark, and 4-mark answer frames.",
      actions: ["Make notes", "Practice answer", "Self-check"],
    },
    {
      title: "Final active recall",
      reason_type: "memory",
      task: "Close the source and answer five quick questions.",
      expected_output: "A short checklist of weak points to revise.",
      actions: ["Quiz", "Revise", "Repeat mistakes"],
    },
  ];

  return {
    title: `${safeTitle(topic)} study path`,
    exam: payload.exam ?? null,
    subject: payload.subject ?? null,
    topic,
    goal: payload.goal ?? "Good marks",
    time_left: payload.time_left ?? "5 hours",
    level: payload.level ?? "intermediate",
    readiness_score: 0.66,
    next_best_action: `Start with ${topic}, then generate notes and a short quiz.`,
    study_timeline: blocks.map((block, index) => ({
      order: index + 1,
      title: block.title,
      duration_minutes: Math.max(20, Math.round(duration / blocks.length)),
      reason: index === 0
        ? `This locks the meaning before exam answers.`
        : index === 1
          ? `Marks come from structure, exact keywords, and clear points.`
          : `Active recall shows what still needs revision.`,
      reason_type: block.reason_type,
      task: block.task,
      expected_output: block.expected_output,
      actions: block.actions,
      source_basis: payload.source_ids?.length ? "selected_sources" : payload.source_id ? "selected_source" : "study_profile",
    })),
    weak_areas: ["concept clarity", "answer writing"],
    easy_marks: [`1-mark definition of ${topic}`, "Keyword-only short answers"],
    danger_areas: ["Writing long paragraphs instead of mark-wise points"],
    revision_keywords: ["definition", "keywords", "diagram", "example"],
    source_basis: payload.source_ids?.length ? "user_sources" : payload.source_id ? "user_source" : "study_profile",
    trust_notes: [
      OFFLINE_TRUST_NOTE,
      payload.source_ids?.length
        ? `Using ${payload.source_ids.length} selected sources for this path.`
        : payload.source_id
          ? "Using your selected source for this path."
          : "No source uploaded yet. DocDoe is using your study setup.",
    ],
    evidence_label: OFFLINE_EVIDENCE_LABEL,
    analysis: {},
  };
}

const TOPIC_KNOWLEDGE: Record<string, { definition: string; keyPoints: string[]; formula?: string; diagram?: string; examTip: string; realLife: string; mistakes: string[] }> = {
  "electromagnetic induction": {
    definition: "Electromagnetic induction is the production of an electromotive force (EMF) across an electrical conductor in a changing magnetic field. When the magnetic flux through a circuit changes, an EMF is induced in it.",
    keyPoints: [
      "Faraday's First Law: Whenever the magnetic flux linked with a circuit changes, an EMF is induced in the circuit.",
      "Faraday's Second Law: The magnitude of induced EMF is equal to the rate of change of magnetic flux, e = -dΦ/dt.",
      "Lenz's Law: The direction of induced EMF opposes the change in flux that causes it (conservation of energy).",
      "Motional EMF: When a conductor moves in a magnetic field, EMF = Bvl (B = field, v = velocity, l = length).",
      "Self-inductance (L): EMF induced in a coil due to change in its own current, e = -L(dI/dt).",
      "Mutual inductance (M): EMF induced in one coil due to change in current in a nearby coil.",
    ],
    formula: "e = -N(dΦ/dt) where Φ = B·A·cos(θ)",
    diagram: "Draw a coil connected to a galvanometer, with a bar magnet moving toward it. Label: coil turns (N), magnetic field lines, direction of induced current, galvanometer deflection.",
    examTip: "In Kerala +2 boards, Faraday's law appears as a 4-mark or 5-mark question almost every year. Always write the mathematical expression with the negative sign (Lenz's law).",
    realLife: "Electric generators at power stations use electromagnetic induction — rotating coils in magnetic fields produce the electricity that powers your home.",
    mistakes: ["Forgetting the negative sign in Faraday's law (it represents Lenz's law)", "Confusing magnetic flux (Φ = BA cos θ) with magnetic field (B)", "Not drawing the diagram for AC generator questions"],
  },
  "faraday": {
    definition: "Faraday's Law of Electromagnetic Induction states that the induced EMF in a circuit is directly proportional to the rate of change of magnetic flux through the circuit. Mathematically, e = -N(dΦ/dt), where N is the number of turns and Φ is the magnetic flux.",
    keyPoints: [
      "First Law: A change in magnetic flux through a circuit induces an EMF.",
      "Second Law: The induced EMF equals the negative rate of change of flux linkage: e = -N(dΦ/dt).",
      "The negative sign comes from Lenz's Law — the induced current opposes the change causing it.",
      "Magnetic flux Φ = B·A·cos(θ), where B is field strength, A is area, θ is angle between B and normal to A.",
      "More turns (N) = larger induced EMF. Faster flux change = larger EMF.",
      "This is the principle behind generators, transformers, and induction cooktops.",
    ],
    formula: "e = -N(dΦ/dt), where Φ = BAcos(θ)",
    examTip: "For a 5-mark answer: (1) State the law, (2) Write the formula, (3) Explain each symbol, (4) Mention Lenz's law connection, (5) Give one application.",
    realLife: "When you swipe a credit card, the changing magnetic stripe induces tiny currents in the reader — that's Faraday's law in action.",
    mistakes: ["Writing e = N(dΦ/dt) without the minus sign", "Not specifying what Φ represents", "Confusing EMF with current"],
  },
  "lenz": {
    definition: "Lenz's Law states that the direction of the induced current is such that it opposes the change in magnetic flux that produced it. This is a consequence of the conservation of energy.",
    keyPoints: [
      "The induced current creates its own magnetic field that opposes the original change.",
      "If flux is increasing → induced current creates opposing field. If decreasing → supporting field.",
      "Lenz's law explains the negative sign in Faraday's law: e = -N(dΦ/dt).",
      "Without Lenz's law, energy could be created from nothing — violating conservation of energy.",
      "The opposing nature means you must do work to maintain the change, converting mechanical → electrical energy.",
    ],
    examTip: "Board exams often ask: 'Explain the significance of the negative sign in Faraday's law.' Your answer should clearly connect it to Lenz's law and conservation of energy.",
    realLife: "When you drop a magnet through a copper tube, it falls slowly because the induced currents oppose its motion — that's Lenz's law slowing it down.",
    mistakes: ["Saying Lenz's law opposes the flux instead of opposing the CHANGE in flux", "Not connecting it to energy conservation"],
  },
  "ac generator": {
    definition: "An AC generator (alternator) converts mechanical energy into alternating current electrical energy using the principle of electromagnetic induction. A coil rotating in a uniform magnetic field produces a sinusoidal EMF.",
    keyPoints: [
      "Main parts: Armature coil, field magnets, slip rings, brushes, and axle.",
      "Working principle: When the coil rotates in the magnetic field, the flux changes continuously, inducing an alternating EMF.",
      "EMF equation: e = NBAω sin(ωt), where ω = angular velocity.",
      "Peak EMF (e₀) = NBAω occurs when the coil is parallel to the field (θ = 90°).",
      "EMF is zero when the coil is perpendicular to the field (θ = 0°, 180°).",
      "One full rotation = one complete AC cycle.",
    ],
    formula: "e = NBAω sin(ωt) = e₀ sin(ωt)",
    diagram: "Draw a rectangular coil ABCD between two pole pieces N and S. Show slip rings R₁, R₂ connected to the coil ends, brushes B₁, B₂ pressing against rings, and external load. Label the axis of rotation.",
    examTip: "AC generator is a strong practice topic. Draw and label the diagram clearly, then explain the working step by step.",
    realLife: "Hydroelectric dams use massive AC generators where water turbines spin the coil to produce electricity for cities.",
    mistakes: ["Confusing slip rings (AC) with split-ring commutator (DC)", "Forgetting to label the diagram", "Not writing the condition for maximum and zero EMF"],
  },
  "self inductance": {
    definition: "Self-inductance is the property of a coil by which it opposes any change in current flowing through it by inducing an EMF in itself. The self-induced EMF is e = -L(dI/dt), where L is the inductance in Henry.",
    keyPoints: [
      "When current through a coil changes, the magnetic flux linked with the coil changes, inducing a back EMF.",
      "Self-inductance L = NΦ/I for a solenoid: L = μ₀n²Al (n = turns/length, A = area, l = length).",
      "Energy stored in an inductor: U = ½LI².",
      "SI unit of inductance is Henry (H). 1 H = 1 V·s/A.",
      "Self-inductance is the electrical analogue of inertia in mechanics.",
    ],
    formula: "L = μ₀n²Al, e = -L(dI/dt), U = ½LI²",
    examTip: "When asked about self-inductance, always mention the analogy with inertia — examiners look for conceptual understanding.",
    realLife: "The spark you see when unplugging an appliance is caused by self-inductance — the coil tries to maintain current flow.",
    mistakes: ["Confusing self-inductance with mutual inductance", "Forgetting that L depends on the geometry of the coil, not the current"],
  },
  "mutual inductance": {
    definition: "Mutual inductance is the property by which a change in current in one coil induces an EMF in a neighboring coil. If current I₁ in coil 1 produces flux Φ₂ in coil 2, then M = N₂Φ₂/I₁.",
    keyPoints: [
      "Mutual inductance depends on: geometry, number of turns, distance between coils, and the medium.",
      "The induced EMF in coil 2: e₂ = -M(dI₁/dt).",
      "M₁₂ = M₂₁ = M (reciprocity theorem).",
      "For two coaxial solenoids: M = μ₀n₁n₂Al.",
      "Transformers work on the principle of mutual inductance.",
    ],
    formula: "M = μ₀n₁n₂Al, e₂ = -M(dI₁/dt)",
    examTip: "Mutual inductance questions often connect to transformers. Mention the transformer principle when discussing M.",
    realLife: "Wireless phone chargers use mutual inductance — a coil in the pad induces current in a coil inside your phone.",
    mistakes: ["Assuming M depends on current (it depends only on geometry)", "Not mentioning the reciprocity theorem"],
  },
  "eddy currents": {
    definition: "Eddy currents are loops of electric current induced within conductors by a changing magnetic field. They flow in closed loops within the conductor, perpendicular to the magnetic field.",
    keyPoints: [
      "Caused by changing magnetic flux through a bulk conductor (not just a wire loop).",
      "They produce heating (I²R losses) and opposing magnetic effects.",
      "Applications: electromagnetic braking, induction heating, speedometers, metal detectors.",
      "To reduce eddy current losses, cores are laminated (thin insulated sheets).",
      "Eddy currents follow Lenz's law — they oppose the change causing them.",
    ],
    examTip: "Usually a 1-mark or 2-mark question. Know 3 applications and the lamination technique.",
    realLife: "Induction cooktops heat pans using eddy currents — the changing magnetic field from the coil induces currents in the pan's metal base.",
    mistakes: ["Not mentioning lamination as the method to reduce eddy currents", "Confusing eddy currents with regular induced currents in a wire"],
  },
  "transformer": {
    definition: "A transformer is a device that transfers electrical energy between two circuits through mutual inductance. It can step up or step down AC voltage. Vs/Vp = Ns/Np = Ip/Is.",
    keyPoints: [
      "Works only with AC (needs changing flux for induction).",
      "Step-up: Ns > Np → voltage increases, current decreases.",
      "Step-down: Ns < Np → voltage decreases, current increases.",
      "Ideal transformer: VpIp = VsIs (power conserved).",
      "Core is laminated soft iron to reduce eddy current losses.",
      "Energy losses: copper loss (I²R), eddy currents, hysteresis, flux leakage.",
    ],
    formula: "Vs/Vp = Ns/Np, efficiency = (Ps/Pp) × 100%",
    examTip: "Always mention the ideal transformer equation AND the energy losses. Examiners give marks for mentioning at least 3 types of losses.",
    realLife: "The large boxes on electric poles are transformers stepping down 11kV to 230V for your home.",
    mistakes: ["Saying transformers work with DC", "Forgetting to mention lamination for reducing losses"],
  },
};

function findTopicMatch(question: string, sourceContext?: string): { definition: string; keyPoints: string[]; formula?: string; diagram?: string; examTip: string; realLife: string; mistakes: string[] } | null {
  const q = question.toLowerCase();
  for (const [key, data] of Object.entries(TOPIC_KNOWLEDGE)) {
    if (q.includes(key)) return data;
  }
  if (q.includes("emf") || q.includes("flux")) return TOPIC_KNOWLEDGE["electromagnetic induction"];
  if (q.includes("generator") || q.includes("alternator")) return TOPIC_KNOWLEDGE["ac generator"];
  if (q.includes("inductor") || q.includes("self induct") || q.includes("back emf")) return TOPIC_KNOWLEDGE["self inductance"];
  if (q.includes("mutual") || q.includes("two coil")) return TOPIC_KNOWLEDGE["mutual inductance"];
  if (q.includes("eddy")) return TOPIC_KNOWLEDGE["eddy currents"];
  if (q.includes("step up") || q.includes("step down") || q.includes("transformer")) return TOPIC_KNOWLEDGE["transformer"];
  if (sourceContext) {
    const sc = sourceContext.toLowerCase();
    for (const [key, data] of Object.entries(TOPIC_KNOWLEDGE)) {
      if (sc.includes(key)) return data;
    }
    if (sc.includes("electromagnetic") || sc.includes("induction")) return TOPIC_KNOWLEDGE["electromagnetic induction"];
  }
  return null;
}

function classifyQuestion(question: string): "explain" | "formula" | "diagram" | "exam_answer" | "mistakes" | "real_life" | "summarize" | "general" {
  const q = question.toLowerCase();
  if (q.includes("formula") || q.includes("equation") || q.includes("derive") || q.includes("mathematical")) return "formula";
  if (q.includes("diagram") || q.includes("draw") || q.includes("label") || q.includes("figure")) return "diagram";
  if (q.includes("mistake") || q.includes("error") || q.includes("wrong") || q.includes("avoid")) return "mistakes";
  if (q.includes("mark") || q.includes("exam") || q.includes("answer format") || q.includes("board")) return "exam_answer";
  if (q.includes("real") || q.includes("life") || q.includes("example") || q.includes("daily") || q.includes("application")) return "real_life";
  if (q.includes("summar") || q.includes("brief") || q.includes("short") || q.includes("points") || q.includes("key")) return "summarize";
  if (q.includes("explain") || q.includes("what is") || q.includes("define") || q.includes("concept") || q.includes("important") || q.includes("simply")) return "explain";
  return "general";
}

function fallbackAsk(payload: BrainAskPayload): BrainAskResponse {
  const sourceIds = payload.source_ids?.length ? payload.source_ids : payload.source_id ? [payload.source_id] : [];
  const chunks: DocumentChunk[] = sourceIds
    .filter((id) => isLocalResourceId(id))
    .flatMap((id) => retrieveLocalDocumentChunks(id, payload.question, 2).chunks);
  const primaryChunk = chunks[0];
  const sourceTitles = sourceIds.map((id) => getLocalDocuments().find((doc) => doc.id === id)?.title).filter((title): title is string => Boolean(title));

  const topic = findTopicMatch(payload.question, sourceTitles.join(" "));
  const questionType = classifyQuestion(payload.question);

  let answer: string;
  let sections: Array<{ title: string; body: string }>;
  let suggestedActions: string[];

  if (topic) {
    switch (questionType) {
      case "explain":
        answer = topic.definition;
        sections = [
          { title: "Key concepts", body: topic.keyPoints.slice(0, 4).join("\n") },
          ...(topic.formula ? [{ title: "Formula", body: topic.formula }] : []),
          { title: "Exam tip", body: topic.examTip },
        ];
        suggestedActions = ["Ask for diagram help", "Generate a quiz on this", "Explain with real-life example"];
        break;
      case "formula":
        answer = topic.formula
          ? `The key formula is: ${topic.formula}. ${topic.keyPoints.find((p) => p.toLowerCase().includes("formula") || p.toLowerCase().includes("equation")) || topic.keyPoints[1] || ""}`
          : topic.definition;
        sections = [
          ...(topic.formula ? [{ title: "Formula", body: topic.formula }] : []),
          { title: "How to apply", body: topic.keyPoints.filter((p) => p.includes("=") || p.includes("where")).join("\n") || topic.keyPoints.slice(0, 3).join("\n") },
          { title: "Common mistakes", body: topic.mistakes.join("\n") },
        ];
        suggestedActions = ["Show me worked examples", "What are common numerical problems?", "Explain the derivation"];
        break;
      case "diagram":
        answer = topic.diagram || `For ${payload.question.slice(0, 60)}, you'll need a clear labelled diagram. ${topic.keyPoints[0]}`;
        sections = [
          ...(topic.diagram ? [{ title: "Diagram instructions", body: topic.diagram }] : []),
          { title: "Key labels to include", body: topic.keyPoints.slice(0, 3).join("\n") },
          { title: "Examiner note", body: topic.examTip },
        ];
        suggestedActions = ["What marks does the diagram carry?", "Explain the working from the diagram", "Common diagram mistakes"];
        break;
      case "exam_answer":
        answer = `Here's how to structure your exam answer: Start with the definition, then the mathematical expression, followed by key points. ${topic.examTip}`;
        sections = [
          { title: "Definition (1-2 marks)", body: topic.definition },
          ...(topic.formula ? [{ title: "Formula (1 mark)", body: topic.formula }] : []),
          { title: "Key points to include", body: topic.keyPoints.slice(0, 4).join("\n") },
          { title: "Avoid these mistakes", body: topic.mistakes.join("\n") },
        ];
        suggestedActions = ["Write a full 5-mark answer", "What keywords get marks?", "Show marking scheme"];
        break;
      case "mistakes":
        answer = `Here are the most common mistakes students make: ${topic.mistakes[0]}. Knowing these can help you avoid losing marks.`;
        sections = [
          { title: "Common mistakes", body: topic.mistakes.map((m, i) => `${i + 1}. ${m}`).join("\n") },
          { title: "How to avoid them", body: topic.examTip },
          { title: "Quick check", body: topic.keyPoints[0] },
        ];
        suggestedActions = ["Give me a quiz to test this", "What are examiner expectations?", "Show the correct approach"];
        break;
      case "real_life":
        answer = topic.realLife;
        sections = [
          { title: "Real-world connection", body: topic.realLife },
          { title: "The science behind it", body: topic.keyPoints.slice(0, 2).join("\n") },
          { title: "Why this matters for exams", body: "Real-life examples are often asked as 1-mark or 2-mark questions. Examiners love when you connect theory to applications." },
        ];
        suggestedActions = ["Give more applications", "Now explain the theory", "Create flashcards"];
        break;
      case "summarize":
        answer = `Quick summary: ${topic.definition.split(". ").slice(0, 2).join(". ")}.`;
        sections = [
          { title: "Key points", body: topic.keyPoints.join("\n") },
          ...(topic.formula ? [{ title: "Important formula", body: topic.formula }] : []),
          { title: "Exam tip", body: topic.examTip },
        ];
        suggestedActions = ["Explain in more detail", "Create flashcards from this", "Give me practice questions"];
        break;
      default:
        answer = topic.definition;
        sections = [
          { title: "Key concepts", body: topic.keyPoints.slice(0, 3).join("\n") },
          ...(topic.formula ? [{ title: "Formula", body: topic.formula }] : []),
          { title: "Exam focus", body: topic.examTip },
        ];
        suggestedActions = ["Explain deeper", "Show diagram", "Write exam answer"];
    }
  } else if (primaryChunk?.chunk_text) {
    answer = `From your material: ${primaryChunk.chunk_text}`;
    sections = [
      { title: "From your source", body: primaryChunk.chunk_text },
      { title: "Study approach", body: "Read this section carefully, underline keywords, and try to write the main idea in one sentence.\nThen check if you can recall it without looking." },
    ];
    suggestedActions = ["Summarize this section", "Create a quiz from this", "Explain this simply"];
  } else {
    const questionTopic = payload.question.replace(/[?.,!]/g, "").trim();
    answer = `DocDoe needs a live backend connection to answer "${questionTopic.slice(0, 80)}".`;
    sections = [
      {
        title: "Connect the knowledge brain",
        body: "Sign in and ensure the DocDoe backend is running. The AI engine retrieves your uploaded sources, applies exam intelligence, and generates grounded answers.",
      },
      {
        title: "Source-grounded mode",
        body: "Upload a textbook chapter, notes, or question paper. DocDoe retrieves relevant chunks and answers from your material — not generic templates.",
      },
      {
        title: "Offline preview limits",
        body: "Without the backend, only cached physics scaffolds are available. Real production answers require the connected AI pipeline.",
      },
    ];
    suggestedActions = ["Upload your textbook PDF", "Select an uploaded source", "Retry after signing in"];
  }

  return {
    answer,
    sections,
    citations: chunks.length > 0
      ? chunks.slice(0, 5).map((chunk) => ({
          chunk_id: chunk.id,
          chunk_index: chunk.chunk_index,
          heading: chunk.heading,
          page_number: chunk.page_number,
          snippet: chunk.chunk_text.slice(0, 140),
          source_id: chunk.document_id,
          source_title: sourceTitles[0] ?? null,
        }))
      : sourceTitles.map((title) => ({
          chunk_id: null,
          chunk_index: null,
          heading: title,
          page_number: null,
          snippet: null,
          source_id: null,
          source_title: title,
        })),
    suggested_actions: suggestedActions,
    trust_note: sourceIds.length
      ? `${OFFLINE_TRUST_NOTE} Showing local source excerpt.`
      : topic
        ? `${OFFLINE_TRUST_NOTE} Showing cached topic scaffold.`
        : OFFLINE_TRUST_NOTE,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

function fallbackExamAnswer(payload: BrainGeneratePayload): BrainGenerationResponse<BrainExamAnswerOutput> {
  const topic = payload.topic || "this topic";
  const output: BrainExamAnswerOutput = {
    one_mark: [{ question: `Define ${topic}.`, answer: `${topic} is the core idea from this chapter, written in one crisp line.` }],
    two_mark: [{ question: `Explain ${topic} in two points.`, answer: "Write the definition, then add one keyword-based example." }],
    four_mark: [{ question: `Write a 4-mark answer on ${topic}.`, answer: "Definition, principle, keyword explanation, and one example.", diagram_needed: true }],
    six_mark: [{ question: `Prepare a long answer for ${topic}.`, answer: "Start with definition, add law/principle, explain steps, include diagram, finish with application.", diagram_needed: true }],
    keywords_to_underline: ["definition", "principle", "diagram", "example"],
    diagram_required: true,
    common_mistake: "Writing long story answers instead of clean mark-wise points.",
    examiner_tip: "Marks come from exact keywords, neat structure, and a labelled diagram when needed.",
    answer_writing_formula: ["Definition", "Principle", "Steps", "Diagram", "Application"],
    trust_note: payload.source_ids?.length ? `Using ${payload.source_ids.length} selected sources.` : "Using your selected topic and source.",
  };
  return {
    type: "exam_answer",
    topic,
    output,
    trust_note: output.trust_note,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

function fallbackAnswerCorrection(payload: BrainGeneratePayload): BrainGenerationResponse<BrainAnswerCorrectionOutput> {
  const question = String(payload.options?.question || payload.topic || "this question");
  const marks = Number(payload.options?.marks || 5) || 5;
  const output: BrainAnswerCorrectionOutput = {
    syllabus_position: payload.subject ? `${payload.subject} -> ${question}` : question,
    mark_scheme_assumption: `Assuming this is a ${marks}-mark answer.`,
    score: `Estimated score: not available offline/${marks}`,
    what_correct: ["Offline preview cannot grade the pasted answer reliably."],
    marks_lost: ["Connect to the live backend to calculate marks lost from the answer."],
    missing_keywords: ["exact keywords", "marking points", "board format"],
    corrected_board_answer: "Connect to DocDoe AI to rewrite this in board-answer format.",
    how_to_improve: ["Use the live backend for source-grounded answer correction."],
    quick_retry_task: "Connect to the backend and run Correct my answer again.",
    source_truth: OFFLINE_TRUST_NOTE,
    source_evidence: [],
    assumed_marks: marks,
    official_scheme_available: false,
  };
  return {
    type: "answer_correction",
    topic: question,
    output,
    trust_note: OFFLINE_TRUST_NOTE,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

function fallbackNotes(payload: BrainGeneratePayload): BrainGenerationResponse<NotesOutput> {
  const topic = payload.topic || "this topic";
  return {
    type: "notes",
    topic,
    output: {
      title: `${topic} scoring notes`,
      summary: `DocDoe shaped ${topic} into exam-focused notes.`,
      student_level_summary: `This is tuned for a ${payload.level ?? "intermediate"} student.`,
      must_learn_first: [`Meaning of ${topic}`, "One clean example", "A short exam answer frame"],
      key_points: [`Meaning of ${topic}`, "Important keywords", "Exam answer structure", "One diagram, formula, or example if the chapter supports it"],
      important_definitions: [`Define ${topic} in one crisp line.`],
      exam_keywords: ["definition", "principle", "example", "diagram"],
      scoring_keywords: ["definition", "principle", "example", "diagram"],
      memory_tricks: ["Definition first, example second, keywords always."],
      exam_focus: ["Write short point-wise answers.", "Underline exact terms."],
      possible_exam_questions: [`Define ${topic}.`, `Write a short note on ${topic}.`, `Give one example or application of ${topic}.`],
      common_mistakes: ["Writing a long paragraph without keywords.", "Skipping the example or diagram when the question asks for it."],
      last_minute_revision: ["Revise definition", "Practice one 4-mark answer"],
      ten_minute_revision: ["Read the definition aloud.", "Write three keywords without looking.", "Answer one short question.", "Circle the mistake you usually make."],
      quick_check: [`Define ${topic}.`, "List three keywords."],
      next_study_action: `Turn ${topic} into a 5-question quiz, then revise only the wrong answers.`,
    },
    trust_note: payload.source_ids?.length
      ? `${OFFLINE_TRUST_NOTE} Using ${payload.source_ids.length} selected sources.`
      : OFFLINE_TRUST_NOTE,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

function fallbackQuiz(payload: BrainGeneratePayload): BrainGenerationResponse<{ quiz_title: string; questions: QuizQuestionResponse[] }> {
  const topic = payload.topic || "this topic";
  const requestedCount = Math.max(1, Math.min(Number(payload.options?.num_questions ?? payload.options?.question_count ?? 5) || 5, 15));
  const baseQuestions: QuizQuestionResponse[] = [
    {
      question: `What is the core meaning of ${topic}?`,
      type: "mcq",
      options: [
        "A crisp definition of the main idea",
        "A random example without keywords",
        "A long story answer",
        "A diagram label only",
      ],
      answer: "A crisp definition of the main idea",
      explanation: "Direct definitions are valuable when phrased clearly.",
      difficulty: "easy",
      common_trap: "Starting with examples before defining the term.",
      exam_tip: "Write the definition first, then one keyword.",
    },
    {
      question: `Which keywords should you underline for ${topic}?`,
      type: "mcq",
      options: [
        "Exact terms from the chapter",
        "Only filler sentences",
        "Unrelated examples",
        "Personal opinions",
      ],
      answer: "Exact terms from the chapter",
      explanation: "Keywords help examiners find the scoring points fast.",
      difficulty: "medium",
      common_trap: "Using casual words instead of textbook terms.",
      exam_tip: "Underline exact terms after writing the answer.",
    },
    {
      question: `Write a two-point answer for ${topic}.`,
      type: "mcq",
      options: [
        "Definition plus one clear exam point",
        "Only the chapter title",
        "Three unrelated facts",
        "A diagram without labels",
      ],
      answer: "Definition plus one clear exam point",
      explanation: "Two-mark answers need structure more than length.",
      difficulty: "medium",
      common_trap: "Writing one long sentence with no separate points.",
      exam_tip: "Use two bullets for two marks.",
    },
    {
      question: `What diagram or example can support ${topic}?`,
      type: "mcq",
      options: [
        "The simplest labelled diagram or class example",
        "An unlabelled rough sketch",
        "An unrelated picture",
        "No support at all",
      ],
      answer: "The simplest labelled diagram or class example",
      explanation: "Visual support helps when the chapter has a process or setup.",
      difficulty: "medium",
      common_trap: "Drawing without labels.",
      exam_tip: "Labels often carry marks in exams.",
    },
    {
      question: `What is a common mistake in ${topic}?`,
      type: "mcq",
      options: [
        "Skipping keywords and writing a story answer",
        "Using point-wise structure",
        "Underlining exact terms",
        "Adding a neat labelled diagram",
      ],
      answer: "Skipping keywords and writing a story answer",
      explanation: "Short, keyword-rich answers score better.",
      difficulty: "hard",
      common_trap: "Trying to write everything you remember.",
      exam_tip: "Write what earns marks first.",
    },
  ];
  const questions = Array.from({ length: requestedCount }, (_, index) => {
    const base = baseQuestions[index % baseQuestions.length];
    return index < baseQuestions.length
      ? base
      : {
          ...base,
          question: `${base.question} Practice ${index + 1}.`,
        };
  });
  return {
    type: "quiz",
    topic,
    output: {
      quiz_title: `${topic} quick test`,
      questions,
    },
    trust_note: payload.source_ids?.length
      ? `${OFFLINE_TRUST_NOTE} Using ${payload.source_ids.length} selected sources.`
      : OFFLINE_TRUST_NOTE,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
    is_fallback: true,
    generation_time_ms: 0,
    cache_hit: false,
  };
}

function fallbackFlashcards(payload: BrainGeneratePayload): BrainGenerationResponse<{ cards: FlashcardResponseItem[] }> {
  const topic = payload.topic || "this topic";
  return {
    type: "flashcards",
    topic,
    output: {
      cards: [
        { front: `Define ${topic}`, back: "A crisp one-line definition with exact keywords.", hint: "Start with the core process.", tag: "definition", memory_hook: "One line first, details later." },
        { front: "Exam answer order", back: "Definition > principle > keywords > example or diagram.", hint: "Structure scores marks.", tag: "answer_writing", memory_hook: "D-P-K-E." },
        { front: "What should you underline?", back: "Only the exact scoring words: definition terms, laws, formulas, labels, and named examples.", hint: "Think like an examiner.", tag: "keywords" },
        { front: "Common mistake", back: "Writing a long story answer without point-wise structure.", hint: "Length is not the same as marks.", tag: "avoid" },
        { front: "Two-mark frame", back: `Point 1: Define ${topic}. Point 2: Add one keyword-based example or application.`, hint: "Two points for two marks.", tag: "2_mark" },
        { front: "Four-mark frame", back: "Definition, principle, two key points, then example or diagram.", hint: "Use four scoring beats.", tag: "4_mark" },
        { front: "Final recall check", back: `Close the notes and say: meaning of ${topic}, three keywords, one example, one mistake to avoid.`, hint: "No looking.", tag: "recall" },
        { front: "When stuck", back: "Ask DocDoe for a simpler explanation, then quiz only the weak part.", hint: "Recover fast.", tag: "next_step" },
      ],
    },
    trust_note: payload.source_ids?.length
      ? `${OFFLINE_TRUST_NOTE} Using ${payload.source_ids.length} selected sources.`
      : OFFLINE_TRUST_NOTE,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

function fallbackLastNightPlan(payload: BrainGeneratePayload): BrainGenerationResponse<BrainLastNightPlanOutput> {
  const topic = payload.topic || "your chapter";
  const output: BrainLastNightPlanOutput = {
    time_blocks: [
      { label: "Chapter pass", minutes: 45 },
      { label: "Definitions + keywords", minutes: 35 },
      { label: "Exam answer practice", minutes: 60 },
      { label: "Quick self-test", minutes: 30 },
      { label: "Final revision", minutes: 20 },
    ],
    must_study: [`Core definition of ${topic}`, "Important keywords", "Mark-wise answer frames"],
    skip_now: ["Deep extra reading", "Unseen long derivations unless your teacher marked them"],
    easy_marks: ["Direct definitions", "Diagram labels", "Short keyword answers"],
    revision_keywords: ["definition", "formula", "diagram", "keywords"],
    final_checklist: ["Definition is crisp", "Keywords are ready", "One answer practiced", "Sleep plan is calm"],
    answer_writing_formula: ["Definition", "Keyword", "Point", "Example"],
    calming_note: "You do not need to master everything tonight. Lock the scoring parts first.",
    trust_note: "PYQ data is not available yet. DocDoe is using your source, topic, goal, and time.",
  };
  return {
    type: "last_night_plan",
    topic,
    output,
    trust_note: output.trust_note,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

function durationForVideoMode(mode: VideoMode) {
  if (mode === "quick_concept") return { range: "5 minutes", minutes: 5, planningOnly: false };
  if (mode === "exam_focus") return { range: "10 minutes", minutes: 10, planningOnly: false };
  if (mode === "deep_masterclass") return { range: "30 minutes", minutes: 30, planningOnly: false };
  return { range: "30 minutes", minutes: 30, planningOnly: false };
}

function fallbackVideoPlan(payload: BrainVideoPlanPayload): BrainVideoPlanResponse {
  const duration = durationForVideoMode(payload.video_mode);
  return {
    title: `${payload.topic} - ${payload.video_mode === "quick_concept" ? "Quick Concept" : payload.video_mode === "exam_focus" ? "Exam Focus" : payload.video_mode === "deep_masterclass" ? "Deep Masterclass" : "Full Chapter War Mode"}`,
    video_mode: payload.video_mode,
    target_duration_range: duration.range,
    estimated_duration_minutes: duration.minutes,
    render_supported: !duration.planningOnly,
    planning_only: duration.planningOnly,
    scenes: [
      {
        scene_type: "intro",
        title: `What is ${payload.topic}?`,
        narration: `Start with the simplest meaning of ${payload.topic}, then connect it to the exam question style.`,
        visual_direction: payload.teaching_style,
        on_screen_text: payload.topic,
        duration_seconds: 45,
        subtitle_segments: [`DocDoe explains ${payload.topic} from the basics.`],
        exam_tip: "Define the concept in one line before expanding.",
        pyq_connection: null,
      },
      {
        scene_type: "exam_focus",
        title: "Scoring points",
        narration: "Turn the concept into short mark-wise answers.",
        visual_direction: "clean_explainer",
        on_screen_text: "Definition -> Keywords -> Example",
        duration_seconds: 80,
        subtitle_segments: ["Use definition, keywords, and one clean example."],
        exam_tip: "Underline keywords in the final answer.",
        pyq_connection: null,
      },
      {
        scene_type: "worked_example",
        title: "One exam-style example",
        narration: "Show one short answer and point out exactly where marks are earned.",
        visual_direction: "split_answer_breakdown",
        on_screen_text: "Where the marks are",
        duration_seconds: 95,
        subtitle_segments: ["Definition earns the first mark.", "Keywords and example complete the answer."],
        exam_tip: "Practice one answer immediately after watching.",
        pyq_connection: null,
      },
      {
        scene_type: "recall",
        title: "Pause and recall",
        narration: "Ask the student to pause, close notes, and recall the definition, three keywords, and one mistake.",
        visual_direction: "clean_recall_screen",
        on_screen_text: "Pause: 3 keywords + 1 mistake",
        duration_seconds: 60,
        subtitle_segments: ["Pause the video.", "Say three keywords before continuing."],
        exam_tip: "Active recall beats rereading.",
        pyq_connection: null,
      },
    ],
    narration_style: payload.teaching_style,
    visual_style: payload.teaching_style === "anime_tutor" ? "anime_visual_explainer" : "clean_explainer",
    exam_focus: payload.video_mode !== "quick_concept",
    answer_cards: [
      { label: "Exam keywords", items: ["definition", "keywords", "diagram", "example"] },
      { label: "Answer frame", items: ["Start with the definition", "Add the principle or formula", "Use two short points", "End with an example"] },
    ],
    quiz_moments: ["Pause and answer one 2-mark question.", "Recall three keywords without looking.", "Spot the common mistake before the final recap."],
    revision_keywords: ["definition", "keywords", "example", "diagram", "common mistake"],
    duration_validation: {},
    trust_note: OFFLINE_TRUST_NOTE,
    evidence_label: OFFLINE_EVIDENCE_LABEL,
  };
}

export function analyzeStudyRequest(payload: StudyRequestAnalyzePayload) {
  if (!payload.raw_text.trim()) {
    throw new ApiError({
      message: "Please describe what you want to study.",
      status: 400,
      body: null,
    });
  }
  return withDemoFallback(
    () => apiPost<StudyRequestAnalysis, StudyRequestAnalyzePayload>("/study/analyze-request", payload),
    () => inferAnalysis(payload),
  );
}

export function saveStudyProfile(payload: StudyProfilePayload) {
  return withDemoFallback(
    () => apiPost<StudyProfileResponse, StudyProfilePayload>("/study-profile", payload),
    () => fallbackProfile(payload),
  );
}

export function getStudyProfile() {
  return withDemoFallback(
    () => apiGet<StudyProfileResponse>("/study-profile/me"),
    () => fallbackProfile({}),
  );
}

export function createTextSource(payload: TextSourcePayload) {
  return withDemoFallback(
    () => apiPost<BrainSourceResponse, TextSourcePayload>("/sources/text", payload),
    () => localTextSource(payload),
  );
}

export async function listBrainSourcesAsDocuments() {
  return withDemoFallback(
    async () => {
      const result = await apiGet<BrainSourcesListResponse>("/sources");
      return result.sources.map(brainSourceToApiDocument);
    },
    () => getLocalDocuments(),
  );
}

export function generateStudyPath(payload: StudyPathGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainStudyPathResponse, StudyPathGeneratePayload>("/study-path/generate", payload, { timeoutMs: 60000 }),
    () => fallbackStudyPath(payload),
  );
}

export function askDocDoe(payload: BrainAskPayload, options?: { signal?: AbortSignal }) {
  return withDemoFallback(
    () => apiPost<BrainAskResponse, BrainAskPayload>("/ask", payload, { timeoutMs: 60000, signal: options?.signal }),
    () => fallbackAsk(payload),
  );
}

/**
 * Stream /ask tokens via SSE for real-time StudyCast feel.
 * Uses fetch + reader (EventSource can't easily do POST+auth).
 */
export async function streamAskDocDoe(
  payload: BrainAskPayload,
  callbacks: {
    onDelta: (token: string) => void;
    onDone: (meta: { model: string; isFallback?: boolean }) => void;
    onError: (message: string) => void;
  },
  signal?: AbortSignal,
): Promise<void> {
  const authToken = (await import("@/lib/auth/auth-client")).getAuthToken();
  const streamSignal = AbortSignal.any([
    AbortSignal.timeout(120_000), ...(signal ? [signal] : []),
  ]);

  let response: Response;
  try {
    response = await fetch(`${(await import("@/lib/api/client")).getApiBaseUrl()}/ask/stream`, {
      method: "POST",
      signal: streamSignal,
      headers: {
        "Content-Type": "application/json",
        Accept: "text/event-stream",
        ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}),
      },
      body: JSON.stringify(payload),
    });
  } catch {
    if (streamSignal.aborted) {
      callbacks.onError(signal?.aborted ? "Response stopped." : "The response timed out. Please retry.");
      return;
    }
    callbacks.onError("Ask stream unreachable.");
    return;
  }
  if (!response.ok) {
    callbacks.onError("Could not stream ask answer.");
    return;
  }
  const reader = response.body?.getReader();
  if (!reader) {
    callbacks.onError("Stream unsupported.");
    return;
  }
  const decoder = new TextDecoder();
  let buffer = "";
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        callbacks.onError("The response ended before it was complete. Please retry.");
        return;
      }
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop() ?? "";
      for (const line of lines) {
        if (!line.startsWith("data: ")) continue;
        const raw = line.slice(6).trim();
        if (!raw) continue;
        try {
          const ev = JSON.parse(raw) as Record<string, unknown>;
          if (ev.error) {
            callbacks.onError(String(ev.error));
            return;
          }
          if (ev.delta) callbacks.onDelta(String(ev.delta));
          if (ev.done) {
            callbacks.onDone({ model: String(ev.model ?? "docdoe"), isFallback: !!ev.is_fallback });
            return;
          }
        } catch {}
      }
    }
  } catch {
    callbacks.onError(streamSignal.aborted
      ? (signal?.aborted ? "Response stopped." : "The response timed out. Please retry.")
      : "Ask stream lost. Please retry.");
  } finally {
    await reader.cancel().catch(() => undefined);
    reader.releaseLock();
  }
}

export function generateBrainExamAnswer(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<BrainExamAnswerOutput>, BrainGeneratePayload>("/generate/exam-answer", payload, { timeoutMs: 60000 }),
    () => fallbackExamAnswer(payload),
  );
}

export function generateBrainAnswerCorrection(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<BrainAnswerCorrectionOutput>, BrainGeneratePayload>("/generate/answer-correction", payload, { timeoutMs: 90000 }),
    () => fallbackAnswerCorrection(payload),
  );
}

export function generateBrainPyqAnalysis(payload: BrainGeneratePayload) {
  const output: BrainPyqAnalysisOutput = {
    summary: "Upload PYQ papers to analyze repeated patterns.",
    data_source_label: "No uploaded PYQ evidence",
    total_questions_found: 0,
    years_seen: [],
    year_count: 0,
    is_full_ten_year_analysis: false,
    coverage_note: "Upload PYQ papers to analyze repeated patterns.",
    pattern_note: "No uploaded PYQ questions matched this request.",
    repeated_topics: [],
    marks_distribution: [],
    answer_type_distribution: [],
    sample_questions: [],
    evidence_question_ids: [],
    confidence: 0,
    next_step: "Upload previous year question papers for this subject or chapter.",
  };
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<BrainPyqAnalysisOutput>, BrainGeneratePayload>("/generate/pyq-analysis", payload, { timeoutMs: 60000 }),
    () => ({
      type: "pyq_analysis",
      topic: payload.topic ?? "",
      output,
      trust_note: output.coverage_note,
      evidence_label: "No uploaded PYQ evidence",
      is_fallback: true,
    }),
  );
}

export function generateBrainNotes(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<NotesOutput>, BrainGeneratePayload>("/generate/notes", payload, { timeoutMs: 60000 }),
    () => fallbackNotes(payload),
  );
}

export function generateBrainQuiz(payload: BrainGeneratePayload) {
  const requestedCount = Number(payload.options?.num_questions ?? payload.options?.question_count ?? 5) || 5;
  const timeoutMs = requestedCount >= 10 ? 240000 : 90000;
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<{ quiz_title: string; questions: QuizQuestionResponse[] }>, BrainGeneratePayload>("/generate/quiz", payload, { timeoutMs }),
    () => fallbackQuiz(payload),
  );
}

export function generateBrainFlashcards(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<{ cards: FlashcardResponseItem[] }>, BrainGeneratePayload>("/generate/flashcards", payload, { timeoutMs: 60000 }),
    () => fallbackFlashcards(payload),
  );
}

export function generateBrainExamMode(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<BrainExamAnswerOutput>, BrainGeneratePayload>("/generate/exam-mode", payload, { timeoutMs: 60000 }),
    () => fallbackExamAnswer(payload),
  );
}

export function generateBrainLastNightPlan(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<BrainLastNightPlanOutput>, BrainGeneratePayload>("/generate/last-night-plan", payload, { timeoutMs: 60000 }),
    () => fallbackLastNightPlan(payload),
  );
}

export function generateBrainVideoPlan(payload: BrainVideoPlanPayload) {
  return withDemoFallback(
    () => apiPost<BrainVideoPlanResponse, BrainVideoPlanPayload>("/video-generator/plan", payload, { timeoutMs: 180000 }),
    () => fallbackVideoPlan(payload),
  );
}

export type DeepChapterOutput = {
  syllabus_position: string;
  chapter_title: string;
  what_this_chapter_is_about: string;
  why_students_struggle: string;
  learning_path: string[];
  prerequisite_topics: string[];
  deep_concept_sections: Array<{
    name: string;
    simple_explanation: string;
    deep_explanation: string;
    real_example: string;
    equation: string;
    why_it_matters: string;
    exam_use: string;
    common_mistake: string;
    practice_question: string;
  }>;
  derivations: Array<{
    name: string;
    syllabus_position: string;
    prerequisite: string;
    symbols: string[];
    assumptions: string[];
    steps: string[];
    final_formula: string;
    unit_check: string;
    common_mistakes: string[];
    board_answer_format: string;
  }>;
  formulas: Array<{ name: string; formula: string; meaning: string; unit: string; when_to_use: string; condition: string }>;
  numerical_patterns: Array<{ pattern_name: string; formula_used: string; method: string; example: string; common_mistakes: string[] }>;
  diagrams_needed: string[];
  pyq_patterns: Array<{ question: string; marks: string; year: string; topic: string }>;
  pyq_note: string;
  board_answer_keywords: string[];
  common_mistakes: string[];
  important_questions: string[];
  practice_questions: string[];
  quick_revision_box: string[];
  next_best_action: string;
  source_used: string;
};

export type VisualLessonOutput = {
  lesson_title: string;
  syllabus_position: string;
  learning_goal: string;
  visual_style: string;
  concept_flow: string[];
  derivation_frames: Array<{
    frame_number: number;
    teaching_purpose: string;
    visual_type: string;
    visual_description: string;
    on_screen_text: string;
    narration: string;
    why_this_helps: string;
    common_misunderstanding_fixed: string;
    exam_connection: string;
    duration_seconds: number;
  }>;
  diagram_frames: Array<{
    frame_number: number;
    teaching_purpose: string;
    visual_type: string;
    visual_description: string;
    on_screen_text: string;
    narration: string;
    why_this_helps: string;
    exam_connection: string;
    duration_seconds: number;
  }>;
  numerical_frames: Array<{
    frame_number: number;
    teaching_purpose: string;
    visual_type: string;
    visual_description: string;
    on_screen_text: string;
    narration: string;
    why_this_helps: string;
    exam_connection: string;
    duration_seconds: number;
  }>;
  recap_frames: Array<{
    frame_number: number;
    teaching_purpose: string;
    visual_type: string;
    visual_description: string;
    on_screen_text: string;
    narration: string;
    why_this_helps: string;
    exam_connection: string;
    duration_seconds: number;
  }>;
  practice_frames: Array<{
    frame_number: number;
    teaching_purpose: string;
    visual_type: string;
    visual_description: string;
    on_screen_text: string;
    narration: string;
    why_this_helps: string;
    exam_connection: string;
    duration_seconds: number;
  }>;
  total_estimated_seconds: number;
  total_frames: number;
  narration_script: string;
};

export type StudyIntentOutput = {
  raw_text: string;
  board: string;
  class_level: string;
  stream: string;
  subject: string;
  chapter: string;
  topic: string;
  goal: string;
  urgency: string;
  language: string;
  requested_mode: string;
  confidence: number;
  missing_fields: string[];
  clarification_question: string;
};

export function generateBrainDeepChapter(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<DeepChapterOutput>, BrainGeneratePayload>("/generate/deep-chapter", payload, { timeoutMs: 90000 }),
    () => ({ type: "deep_chapter", topic: payload.topic ?? "", output: {} as DeepChapterOutput, trust_note: "Offline preview.", is_fallback: true }),
  );
}

export function generateBrainVisualLesson(payload: BrainGeneratePayload) {
  return withDemoFallback(
    () => apiPost<BrainGenerationResponse<VisualLessonOutput>, BrainGeneratePayload>("/generate/visual-lesson", payload, { timeoutMs: 90000 }),
    () => ({ type: "visual_lesson", topic: payload.topic ?? "", output: {} as VisualLessonOutput, trust_note: "Offline preview.", is_fallback: true }),
  );
}