File size: 23,139 Bytes
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed60199
821f6b7
ed60199
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ed12ec7
c72f6c6
 
 
 
 
 
 
 
821f6b7
 
 
 
 
 
 
bda66bb
 
 
 
 
 
 
 
 
 
 
 
 
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c72f6c6
 
 
 
a1a42d4
 
 
 
 
 
 
 
 
 
 
c72f6c6
821f6b7
 
 
a1a42d4
 
 
 
 
 
 
 
 
 
 
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c79dbf
821f6b7
 
2c79dbf
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb08066
821f6b7
 
 
 
 
 
 
2c79dbf
821f6b7
 
 
 
 
 
eb08066
821f6b7
 
 
 
eb08066
 
 
 
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd7a12c
821f6b7
cd7a12c
 
 
 
 
821f6b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import initSqlJs from 'sql.js/dist/sql-wasm.js';
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url';
import { DEFAULT_STYLE_PROFILE } from './default-style.js';

class Database {
  constructor() {
    this.db = null;
    this.isReady = false;
    this.initPromise = this.init();
  }

  async init() {
    try {
      // Use the Vite-resolved URL for the WASM binary
      // This is the most reliable way to get the correct path in all environments
      const initPromise = initSqlJs({
        locateFile: () => wasmUrl
      });
      // Add a timeout to prevent hanging
      const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('initSqlJs timeout')), 30000));
      const SQL = await Promise.race([initPromise, timeoutPromise]);
      console.log('sql.js loaded from:', wasmUrl);
      
      const savedData = localStorage.getItem('schoolmind_db');
      if (savedData) {
        try {
          let uInt8Array;
          if (savedData.startsWith('SQLITE:')) {
            const base64 = savedData.substring(7);
            const binaryString = atob(base64);
            const len = binaryString.length;
            uInt8Array = new Uint8Array(len);
            for (let i = 0; i < len; i++) {
              uInt8Array[i] = binaryString.charCodeAt(i);
            }
          } else {
            uInt8Array = new Uint8Array(savedData.split(',').map(Number));
          }
          this.db = new SQL.Database(uInt8Array);
        } catch(e) {
          console.error("Corrupted DB in localStorage, clearing...", e);
          localStorage.removeItem('schoolmind_db');
          this.db = new SQL.Database();
          this.createSchema();
        }
        
        // Auto-Fixes and Migrations
        try {
          // Auto-Fix for the wrong model string from the first version
          this.db.run("UPDATE settings SET value = 'gemini-1.5-flash' WHERE key = 'model' AND value = 'gemini-3-flash'");
          // Auto-Fix for deprecated or rate-limited OpenRouter models -> openrouter/free
          this.db.run("UPDATE settings SET value = 'openrouter-openrouter/free' WHERE key = 'model' AND (value = 'openrouter-meta-llama/llama-3.1-8b-instruct:free' OR value = 'openrouter-meta-llama/llama-3.3-70b-instruct:free')");
          // Auto-Fix for stiff bullet point style profile
          this.db.run("UPDATE settings SET value = 'Passe deinen Schreibstil, die Ausführlichkeit und das Format dynamisch an die jeweilige Frage an. Antworte natürlich und frei. Vermeide ein starres Stichpunkt-Muster, es sei denn, es wird explizit gefordert oder ist für eine Übersicht sehr sinnvoll. Formuliere auf Oberstufen-Niveau.' WHERE key = 'style_profile' AND (value LIKE '%nutze Bulletpoints%' OR value LIKE '%PDF-Überschrift%')");
        } catch (e) {
          // Ignore errors if settings table doesn't exist yet or other schema mismatch
        }
        
        this.saveToStorage();
      } else {
        this.db = new SQL.Database();
      }
      
      // Ensure all tables exist with their latest schema definitions (catches cases where createSchema was old)
      this.createSchema();
      // Migrations for existing tables that might have been created without newer columns
      const migrations = [
        "ALTER TABLE flashcard_decks ADD COLUMN source_lang TEXT",
        "ALTER TABLE flashcard_decks ADD COLUMN target_lang TEXT",
        "ALTER TABLE flashcards ADD COLUMN image_url TEXT"
      ];
      
      for (const m of migrations) {
        try {
          this.db.run(m);
        } catch (e) {
          // Ignore, column likely already exists
        }
      }

      this.saveToStorage();
      this.isReady = true;
      console.log('SQLite Database initialized');
    } catch (err) {
      console.error('Failed to initialize SQLite', err);
    }
  }

  saveToStorage() {
    if (!this.db) return;
    try {
      const data = this.db.export();
      let binary = '';
      const len = data.byteLength;
      // Chunk processing to avoid Maximum call stack size exceeded if we used String.fromCharCode.apply
      for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(data[i]);
      }
      const b64 = btoa(binary);
      localStorage.setItem('schoolmind_db', 'SQLITE:' + b64);
    } catch (e) {
      console.error("Storage save failed, quota likely exceeded", e);
      if (e.name === 'QuotaExceededError' || e.message.includes('quota')) {
        alert("Achtung: Der lokale Speicher deines Browsers ist voll! Bitte lösche alte Chats oder Bilder, da dein Fortschritt sonst nicht mehr gespeichert werden kann.");
      }
    }
  }

  createSchema() {
    const schema = `

      CREATE TABLE IF NOT EXISTS profiles (

        id TEXT PRIMARY KEY,

        name TEXT,

        school TEXT,

        grade TEXT,

        subjects TEXT,

        context_info TEXT,

        is_anonymous INTEGER DEFAULT 0,

        is_active INTEGER DEFAULT 0

      );



      CREATE TABLE IF NOT EXISTS settings (

        key TEXT PRIMARY KEY,

        value TEXT

      );



      CREATE TABLE IF NOT EXISTS conversations (

        id TEXT PRIMARY KEY,

        title TEXT,

        emoji TEXT,

        preview TEXT,

        model_used TEXT,

        created_at TEXT,

        updated_at TEXT

      );



      CREATE TABLE IF NOT EXISTS messages (

        id TEXT PRIMARY KEY,

        conversation_id TEXT,

        role TEXT,

        content TEXT,

        model_used TEXT,

        style_used TEXT,

        created_at TEXT,

        FOREIGN KEY(conversation_id) REFERENCES conversations(id)

      );



      CREATE TABLE IF NOT EXISTS feedback_rules (

        id TEXT PRIMARY KEY,

        rule_text TEXT,

        source_feedback TEXT,

        is_active INTEGER DEFAULT 1,

        created_at TEXT

      );

      CREATE TABLE IF NOT EXISTS folder_metadata (

        path TEXT PRIMARY KEY,

        emoji TEXT,

        color TEXT

      );

      CREATE TABLE IF NOT EXISTS trash_metadata (

        id TEXT PRIMARY KEY,

        original_path TEXT,

        is_dir INTEGER,

        deleted_at TEXT

      );



      CREATE TABLE IF NOT EXISTS calendar_events (

        id TEXT PRIMARY KEY,

        title TEXT,

        type TEXT,

        date TEXT,

        time TEXT,

        subject TEXT,

        description TEXT,

        created_at TEXT

      );



      CREATE TABLE IF NOT EXISTS flashcard_decks (

        id TEXT PRIMARY KEY,

        title TEXT,

        description TEXT,

        color TEXT,

        source_lang TEXT,

        target_lang TEXT,

        created_at TEXT,

        updated_at TEXT

      );



      CREATE TABLE IF NOT EXISTS flashcards (

        id TEXT PRIMARY KEY,

        deck_id TEXT,

        front TEXT,

        back TEXT,

        image_url TEXT,

        next_review TEXT,

        ease_factor REAL,

        interval INTEGER,

        repetitions INTEGER,

        created_at TEXT,

        updated_at TEXT,

        FOREIGN KEY(deck_id) REFERENCES flashcard_decks(id) ON DELETE CASCADE

      );

    `;
    const statements = schema.split(';').filter(s => s.trim().length > 0);
    for (const stmt of statements) {
      try {
        this.db.run(stmt + ';');
      } catch (e) {
        console.error("Error creating table:", e, stmt);
      }
    }
    
    // Seed default settings
    this.db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('model', 'gemini-3.5-flash')");
    this.db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('style_profile', ?)", [DEFAULT_STYLE_PROFILE]);
    
    // Migration: Rebuild previews to new 150 character limit and clean out SiriHero tags
    try {
      if (this.isReady) {
        const convos = this.exec("SELECT id FROM conversations");
        for (const row of convos) {
          const cid = row.id;
          const lastMsg = this.exec("SELECT content FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT 1", [cid]);
          if (lastMsg.length > 0) {
            let cleanContent = lastMsg[0].content.replace(/!\[.*?\]\((data:image\/[^)]+)\)/g, '[Bild] ');
            cleanContent = cleanContent.replace(/!\[(?:SiriHero|KnowledgeCard)[^\]]*\]\([^)]*\)/gi, '');
            cleanContent = cleanContent.replace(/!(?:SiriHero|KnowledgeCard):[^\n]+/gi, '');
            cleanContent = cleanContent.replace(/[*_#`\[\]]/g, '').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim();
            const preview = cleanContent.length > 150 ? cleanContent.substring(0, 150) + '...' : cleanContent;
            this.exec("UPDATE conversations SET preview = ? WHERE id = ?", [preview, cid]);
          }
        }
      }
    } catch(e) { console.error('Preview Migration failed', e); }
    
    this.saveToStorage();
  }

  // --- Utility Methods ---

  exec(query, params = []) {
    if (!this.isReady) throw new Error("DB not ready");
    const stmt = this.db.prepare(query);
    stmt.bind(params);
    const results = [];
    while (stmt.step()) {
      results.push(stmt.getAsObject());
    }
    stmt.free();
    if (!query.trim().toUpperCase().startsWith("SELECT")) {
      this.saveToStorage();
    }
    return results;
  }

  // Settings
  getSetting(key) {
    try {
      const res = this.exec("SELECT value FROM settings WHERE key = ?", [key]);
      return res.length > 0 ? res[0].value : null;
    } catch (e) {
      console.error("Failed to read setting, attempting auto-fix...", key, e);
      try {
        this.db.run("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);");
        this.db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('model', 'gemini-3.5-flash')");
        this.db.run("INSERT OR IGNORE INTO settings (key, value) VALUES ('style_profile', 'Standard')");
        const res = this.exec("SELECT value FROM settings WHERE key = ?", [key]);
        return res.length > 0 ? res[0].value : null;
      } catch (innerErr) {
        console.error("Auto-fix failed:", innerErr);
        return null;
      }
    }
  }

  setSetting(key, value) {
    try {
      this.exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [key, value]);
    } catch (e) {
      console.error("Failed to write setting, attempting auto-fix...", key, e);
      try {
        this.db.run("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT);");
        this.exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [key, value]);
      } catch (innerErr) {
        console.error("Auto-fix failed:", innerErr);
      }
    }
  }

  // Profiles
  getActiveProfile() {
    const res = this.exec("SELECT * FROM profiles WHERE is_active = 1");
    return res.length > 0 ? res[0] : null;
  }

  // Conversations
  getConversations() {
    return this.exec("SELECT * FROM conversations ORDER BY updated_at DESC");
  }

  createConversation(id, title, emoji) {
    const now = new Date().toISOString();
    this.exec(
      "INSERT INTO conversations (id, title, emoji, preview, model_used, created_at, updated_at) VALUES (?, ?, ?, '', '', ?, ?)",
      [id, title, emoji, now, now]
    );
  }

  updateConversationTitle(id, newTitle) {
    const now = new Date().toISOString();
    this.exec("UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?", [newTitle, now, id]);
  }

  deleteConversation(id) {
    this.exec("DELETE FROM messages WHERE conversation_id = ?", [id]);
    this.exec("DELETE FROM conversations WHERE id = ?", [id]);
  }

  addMessage(id, conversation_id, role, content, model_used, style_used) {
    const now = new Date().toISOString();
    this.exec(
      "INSERT INTO messages (id, conversation_id, role, content, model_used, style_used, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
      [id, conversation_id, role, content, model_used, style_used, now]
    );
    // Update conversation preview. Remove base64 image markdown first
    let cleanContent = content.replace(/!\[.*?\]\((data:image\/[^)]+)\)/g, '[Bild] ');
    
    // Remove specific AI components like SiriHero and KnowledgeCard
    cleanContent = cleanContent.replace(/!\[(?:SiriHero|KnowledgeCard)[^\]]*\]\([^)]*\)/gi, '');
    cleanContent = cleanContent.replace(/!(?:SiriHero|KnowledgeCard):[^\n]+/gi, '');
    
    // Strip common markdown
    cleanContent = cleanContent.replace(/[*_#`\\[\\]]/g, '').replace(/\\n/g, ' ').replace(/\\s+/g, ' ').trim();

    const preview = cleanContent.length > 150 ? cleanContent.substring(0, 150) + '...' : cleanContent;
    this.exec("UPDATE conversations SET preview = ?, updated_at = ? WHERE id = ?", [preview, now, conversation_id]);
  }

  getMessages(conversation_id) {
    return this.exec("SELECT * FROM messages WHERE conversation_id = ? ORDER BY created_at ASC", [conversation_id]);
  }

  searchMessages(query) {
    if (!query || !query.trim()) return [];
    const likeQuery = `%${query}%`;
    const sql = `

      SELECT m.id as message_id, m.conversation_id, m.content, m.role, m.created_at,

             c.title, c.emoji, c.updated_at

      FROM messages m

      JOIN conversations c ON m.conversation_id = c.id

      WHERE m.content LIKE ? COLLATE NOCASE

      ORDER BY c.updated_at DESC, m.created_at DESC

    `;
    const rows = this.exec(sql, [likeQuery]);
    
    // Group all matches by conversation
    const grouped = new Map();
    for (const row of rows) {
      if (!grouped.has(row.conversation_id)) {
        grouped.set(row.conversation_id, {
          conversation_id: row.conversation_id,
          title: row.title,
          emoji: row.emoji,
          updated_at: row.updated_at,
          matches: []
        });
      }
      grouped.get(row.conversation_id).matches.push({
        message_id: row.message_id,
        content: row.content,
        role: row.role,
        created_at: row.created_at
      });
    }
    return Array.from(grouped.values());
  }

  // Feedback Rules
  getActiveFeedbackRules() {
    return this.exec("SELECT rule_text FROM feedback_rules WHERE is_active = 1");
  }

  addFeedbackRule(id, rule_text, source_feedback) {
    const now = new Date().toISOString();
    this.exec(
      "INSERT INTO feedback_rules (id, rule_text, source_feedback, is_active, created_at) VALUES (?, ?, ?, 1, ?)",
      [id, rule_text, source_feedback, now]
    );
  }
  // --- Folder Metadata Operations ---

  getFolderMetadata(path) {
    if (!this.db || !path) return null;
    const stmt = this.db.prepare("SELECT emoji, color FROM folder_metadata WHERE path = ?");
    stmt.bind([path]);
    if (stmt.step()) {
      const res = stmt.getAsObject();
      stmt.free();
      return res;
    }
    stmt.free();
    return null;
  }

  saveFolderMetadata(path, emoji, color) {
    if (!this.db || !path) return;
    try {
      this.exec("INSERT OR REPLACE INTO folder_metadata (path, emoji, color) VALUES (?, ?, ?)", [path, emoji || '', color || '#4285f4']);
    } catch (e) {
      console.error("saveFolderMetadata error:", e);
    }
  }

  deleteFolderMetadata(path) {
    if (!this.db || !path) return;
    // Delete this folder's metadata and any nested folders metadata
    try {
      this.exec("DELETE FROM folder_metadata WHERE path = ? OR path LIKE ?", [path, `${path}/%`]);
    } catch (e) {
      console.error("deleteFolderMetadata error:", e);
    }
  }

  // --- Trash Operations ---
  
  getTrashItems() {
    if (!this.db) return [];
    const res = this.exec("SELECT * FROM trash_metadata ORDER BY deleted_at DESC");
    return res;
  }

  addTrashItem(id, originalPath, isDir) {
    const now = new Date().toISOString();
    this.exec(
      "INSERT INTO trash_metadata (id, original_path, is_dir, deleted_at) VALUES (?, ?, ?, ?)",
      [id, originalPath, isDir ? 1 : 0, now]
    );
  }
  removeTrashItem(id) {
    if (!this.db) return;
    this.exec("DELETE FROM trash_metadata WHERE id = ?", [id]);
  }
  // --- FLASHCARDS & ANKI ---
  
  getDecks() {
    try {
      const decks = this.exec("SELECT * FROM flashcard_decks ORDER BY created_at DESC");
      
      // Get due cards count for each deck
      const now = new Date().toISOString();
      decks.forEach(deck => {
        const countRes = this.exec("SELECT COUNT(*) as count FROM flashcards WHERE deck_id = ? AND next_review <= ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%'", [deck.id, now]);
        deck.due_count = countRes.length > 0 ? countRes[0].count : 0;
        
        const totalRes = this.exec("SELECT COUNT(*) as count FROM flashcards WHERE deck_id = ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%'", [deck.id]);
        deck.total_count = totalRes.length > 0 ? totalRes[0].count : 0;
      });
      
      return decks;
    } catch(e) {
      console.error(e);
      return [];
    }
  }

  createDeck(title, description = '', color = '#8a53e6', sourceLang = 'Deutsch', targetLang = 'Englisch') {
    const id = 'deck_' + Date.now() + Math.random().toString(36).substring(2, 9);
    const now = new Date().toISOString();
    this.exec(
      "INSERT INTO flashcard_decks (id, title, description, color, source_lang, target_lang, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
      [id, title, description, color, sourceLang, targetLang, now, now]
    );
    return { id, title, description, color, source_lang: sourceLang, target_lang: targetLang, created_at: now, updated_at: now, due_count: 0, total_count: 0 };
  }
  
  updateDeckTitle(deckId, newTitle) {
    const now = new Date().toISOString();
    this.exec("UPDATE flashcard_decks SET title = ?, updated_at = ? WHERE id = ?", [newTitle, now, deckId]);
  }
  
  updateDeckLanguages(deckId, sourceLang, targetLang) {
    const now = new Date().toISOString();
    this.exec("UPDATE flashcard_decks SET source_lang = ?, target_lang = ?, updated_at = ? WHERE id = ?", [sourceLang, targetLang, now, deckId]);
  }
  
  deleteDeck(deckId) {
    this.exec("DELETE FROM flashcards WHERE deck_id = ?", [deckId]);
    this.exec("DELETE FROM flashcard_decks WHERE id = ?", [deckId]);
  }

  addFlashcard(deckId, front, back, imageUrl = null) {
    const id = 'card_' + Date.now() + Math.random().toString(36).substring(2, 9);
    const now = new Date().toISOString();
    // Default Anki SM-2 initial values
    this.exec(
      `INSERT INTO flashcards 

      (id, deck_id, front, back, image_url, next_review, ease_factor, interval, repetitions, created_at, updated_at) 

      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [id, deckId, front, back, imageUrl, now, 2.5, 0, 0, now, now]
    );
    return id;
  }
  
  getDueCards(deckId) {
    const now = new Date().toISOString();
    return this.exec(
      "SELECT * FROM flashcards WHERE deck_id = ? AND next_review <= ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%' ORDER BY next_review ASC",
      [deckId, now]
    );
  }

  getAllCards(deckId) {
    // For free practice, shuffle them or just order randomly
    return this.exec(
      "SELECT * FROM flashcards WHERE deck_id = ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%' ORDER BY RANDOM()",
      [deckId]
    );
  }

  getCardsForDeck(deckId) {
    return this.exec(
      "SELECT * FROM flashcards WHERE deck_id = ? AND back NOT LIKE '%<!-- PENDING_CORRECTION -->%' ORDER BY created_at DESC",
      [deckId]
    );
  }

  getPendingCards() {
    return this.exec("SELECT * FROM flashcards WHERE back LIKE '%<!-- PENDING_CORRECTION -->%' OR back LIKE '%<!-- NEEDS_ENRICHMENT -->%'");
  }

  searchFlashcards(query) {
    return this.exec(`

      SELECT f.*, d.title as deck_title, d.color as deck_color

      FROM flashcards f

      JOIN flashcard_decks d ON f.deck_id = d.id

      WHERE f.front LIKE ? OR f.back LIKE ?

      ORDER BY f.created_at DESC

      LIMIT 50

    `, [`%${query}%`, `%${query}%`]);
  }

  deleteFlashcard(cardId) {
    this.exec("DELETE FROM flashcards WHERE id = ?", [cardId]);
  }
  
  editFlashcard(cardId, front, back, imageUrl = null) {
    const now = new Date().toISOString();
    if (imageUrl !== null) {
      this.exec("UPDATE flashcards SET front = ?, back = ?, image_url = ?, updated_at = ? WHERE id = ?", [front, back, imageUrl, now, cardId]);
    } else {
      this.exec("UPDATE flashcards SET front = ?, back = ?, updated_at = ? WHERE id = ?", [front, back, now, cardId]);
    }
  }
  
  // SM-2 Algorithm update
  updateCardProgress(cardId, quality) {
    // quality: 0 (Again), 1 (Hard), 2 (Good), 3 (Easy)
    const res = this.exec("SELECT ease_factor, interval, repetitions FROM flashcards WHERE id = ?", [cardId]);
    if (res.length === 0) return;
    
    let { ease_factor, interval, repetitions } = res[0];
    
    if (quality === 0) {
      repetitions = 0;
      interval = 1; // 1 day
      ease_factor = Math.max(1.3, ease_factor - 0.20);
    } else {
      if (repetitions === 0) {
        interval = 1;
      } else if (repetitions === 1) {
        interval = 6;
      } else {
        if (quality === 1) { // Hard
          interval = Math.round(interval * 1.2);
          ease_factor = Math.max(1.3, ease_factor - 0.15);
        } else if (quality === 2) { // Good
          interval = Math.round(interval * ease_factor);
        } else if (quality === 3) { // Easy
          interval = Math.round(interval * ease_factor * 1.3);
          ease_factor += 0.15;
        }
      }
      repetitions++;
    }
    
    // Calculate next review date
    const next_review = new Date();
    next_review.setDate(next_review.getDate() + interval);
    const next_review_iso = next_review.toISOString();
    const now = new Date().toISOString();
    
    this.exec(
      "UPDATE flashcards SET next_review = ?, ease_factor = ?, interval = ?, repetitions = ?, updated_at = ? WHERE id = ?",
      [next_review_iso, ease_factor, interval, repetitions, now, cardId]
    );
  }

  // --- Calendar ---
  getCalendarEvents() {
    return this.exec("SELECT * FROM calendar_events ORDER BY date ASC, time ASC");
  }

  getUpcomingEvents(limit = 4) {
    const today = new Date().toISOString().split('T')[0];
    return this.exec("SELECT * FROM calendar_events WHERE date >= ? ORDER BY date ASC, time ASC LIMIT ?", [today, limit]);
  }

  addCalendarEvent(title, type, date, time, subject, description) {
    const id = 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
    const now = new Date().toISOString();
    this.exec(
      "INSERT INTO calendar_events (id, title, type, date, time, subject, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
      [id, title, type, date, time, subject, description, now]
    );
    return id;
  }

  deleteCalendarEvent(id) {
    this.exec("DELETE FROM calendar_events WHERE id = ?", [id]);
  }

}

export const db = new Database();