// ============================================================
// PERSONAL NOTES ROUTES — per-user scratchpad, rich-text body.
// Pure CRUD, auth-gated. Body + title encrypted at rest (same
// crypto helper as user_memories so a row dump stays useless
// without the app key).
// ============================================================

var express = require('express');
var router = express.Router();
var db = require('../db/database');
var { authMiddleware } = require('../middleware/auth');
var logger = require('../utils/logger');
var cryptoUtil = require('../utils/crypto');
var { callAI } = require('../utils/ai');
var { wrapUserText, INJECTION_GUARD } = require('../utils/promptSafe');
var { marked } = require('marked');

// Convert AI output to clean HTML for Tiptap. Tiptap's setContent
// only handles HTML — markdown comes through as literal text. Real-
// world models ignore "output HTML only" prompts ~25% of the time
// and return mixed markdown/HTML. We normalise both:
//   • Body looks like HTML (has tags) → return as-is.
//   • Body looks like markdown OR plain text → run through marked.
function toHtmlBody(s) {
  if (!s) return '';
  var t = String(s).trim();
  // Strip ```html / ``` code fences a model sometimes wraps the body in.
  t = t.replace(/^```(?:html|markdown|md)?\s*/i, '').replace(/\s*```$/i, '');
  // Already HTML if there's a real tag.
  if (/<(p|div|h[1-6]|ul|ol|li|strong|em|b|i|u|s|a|blockquote|code|pre|br|hr|span)[\s>]/i.test(t)) return t;
  // Markdown → HTML. marked handles plain text fine — paragraphs become <p>.
  try { return marked.parse(t, { breaks: true }); }
  catch (e) {
    // Last-ditch: wrap the raw text in <p> so the editor at least shows it.
    return '<p>' + t.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>') + '</p>';
  }
}

router.use(authMiddleware);

var MAX_TITLE = 200;
var MAX_BODY  = 50000; // 50 KB of rich-text HTML is plenty for a clinical note
var MAX_NOTES_PER_USER = 500;

function decryptRow(row) {
  if (!row) return row;
  try { row.title = cryptoUtil.decryptString(row.title); } catch (e) {}
  try { row.body  = cryptoUtil.decryptString(row.body);  } catch (e) {}
  return row;
}

// ── GET list (active only — items in trash are filtered out) ────
router.get('/notes', async function (req, res) {
  try {
    var rows = await db.all(
      'SELECT id, title, body, created_at, updated_at FROM personal_notes WHERE user_id = $1 AND deleted_at IS NULL ORDER BY updated_at DESC',
      [req.user.id]
    );
    rows.forEach(decryptRow);
    res.json({ success: true, notes: rows });
  } catch (e) {
    logger.error('GET /notes', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── GET trash list — items the user soft-deleted, newest-deleted first ──
router.get('/notes/trash', async function (req, res) {
  try {
    var rows = await db.all(
      'SELECT id, title, body, created_at, updated_at, deleted_at FROM personal_notes WHERE user_id = $1 AND deleted_at IS NOT NULL ORDER BY deleted_at DESC',
      [req.user.id]
    );
    rows.forEach(decryptRow);
    res.json({ success: true, notes: rows });
  } catch (e) {
    logger.error('GET /notes/trash', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── GET one ─────────────────────────────────────────────────
router.get('/notes/:id', async function (req, res) {
  try {
    var row = await db.get(
      'SELECT id, title, body, created_at, updated_at FROM personal_notes WHERE id = $1 AND user_id = $2',
      [req.params.id, req.user.id]
    );
    if (!row) return res.status(404).json({ error: 'Note not found' });
    res.json({ success: true, note: decryptRow(row) });
  } catch (e) {
    logger.error('GET /notes/:id', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── POST create ─────────────────────────────────────────────
router.post('/notes', async function (req, res) {
  try {
    var title = (req.body.title || '').trim();
    var body  = (req.body.body  || '').trim();
    if (!title) return res.status(400).json({ error: 'Title required' });

    var count = await db.get('SELECT COUNT(*) as cnt FROM personal_notes WHERE user_id = $1', [req.user.id]);
    if (count && parseInt(count.cnt) >= MAX_NOTES_PER_USER) {
      return res.status(400).json({ error: 'Maximum ' + MAX_NOTES_PER_USER + ' notes per user' });
    }

    var result = await db.run(
      'INSERT INTO personal_notes (user_id, title, body) VALUES ($1, $2, $3) RETURNING id',
      [
        req.user.id,
        cryptoUtil.encryptString(title.substring(0, MAX_TITLE)),
        cryptoUtil.encryptString(body.substring(0, MAX_BODY)),
      ]
    );
    res.json({ success: true, id: result.lastInsertRowid });
    logger.audit(req.user.id, 'create_note', 'Created personal note', req, { category: 'notes' });
  } catch (e) {
    logger.error('POST /notes', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── PUT update ──────────────────────────────────────────────
router.put('/notes/:id', async function (req, res) {
  try {
    var title = (req.body.title || '').trim();
    var body  = (req.body.body  || '').trim();
    if (!title) return res.status(400).json({ error: 'Title required' });

    var existing = await db.get('SELECT id FROM personal_notes WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
    if (!existing) return res.status(404).json({ error: 'Note not found' });

    await db.run(
      'UPDATE personal_notes SET title = $1, body = $2, updated_at = NOW() WHERE id = $3 AND user_id = $4',
      [
        cryptoUtil.encryptString(title.substring(0, MAX_TITLE)),
        cryptoUtil.encryptString(body.substring(0, MAX_BODY)),
        req.params.id,
        req.user.id,
      ]
    );
    res.json({ success: true });
  } catch (e) {
    logger.error('PUT /notes/:id', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── DELETE = soft-delete (move to trash) ───────────────────────
// Sets deleted_at = NOW(); the row stays in personal_notes but is
// filtered out of the active list. Use POST /notes/:id/restore to
// undo, or DELETE /notes/:id?hard=1 to actually remove.
router.delete('/notes/:id', async function (req, res) {
  try {
    var hard = req.query.hard === '1' || req.query.hard === 'true';
    if (hard) {
      // Hard-delete only items already in trash, so a UI bug can't
      // accidentally erase an active note.
      await db.run(
        'DELETE FROM personal_notes WHERE id = $1 AND user_id = $2 AND deleted_at IS NOT NULL',
        [req.params.id, req.user.id]
      );
      logger.audit(req.user.id, 'delete_note_hard', 'Permanently deleted personal note', req, { category: 'notes' });
    } else {
      await db.run(
        'UPDATE personal_notes SET deleted_at = NOW() WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL',
        [req.params.id, req.user.id]
      );
      logger.audit(req.user.id, 'delete_note', 'Moved personal note to trash', req, { category: 'notes' });
    }
    res.json({ success: true });
  } catch (e) {
    logger.error('DELETE /notes/:id', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── POST /notes/:id/restore — pull a note out of trash ─────────
router.post('/notes/:id/restore', async function (req, res) {
  try {
    var existing = await db.get(
      'SELECT id FROM personal_notes WHERE id = $1 AND user_id = $2 AND deleted_at IS NOT NULL',
      [req.params.id, req.user.id]
    );
    if (!existing) return res.status(404).json({ error: 'Note not in trash' });

    await db.run(
      'UPDATE personal_notes SET deleted_at = NULL, updated_at = NOW() WHERE id = $1 AND user_id = $2',
      [req.params.id, req.user.id]
    );
    res.json({ success: true });
    logger.audit(req.user.id, 'restore_note', 'Restored personal note from trash', req, { category: 'notes' });
  } catch (e) {
    logger.error('POST /notes/:id/restore', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── POST /notes/trash/empty — hard-delete every trashed note for the user ─
router.post('/notes/trash/empty', async function (req, res) {
  try {
    var result = await db.run(
      'DELETE FROM personal_notes WHERE user_id = $1 AND deleted_at IS NOT NULL',
      [req.user.id]
    );
    res.json({ success: true, removed: result.changes || 0 });
    logger.audit(req.user.id, 'empty_notes_trash', 'Emptied notes trash', req, { category: 'notes' });
  } catch (e) {
    logger.error('POST /notes/trash/empty', e.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

// ── POST /api/notes/from-voice ──────────────────────────────
// Takes a raw voice transcript (produced by the shared
// /api/transcribe endpoint — whichever STT provider the admin
// configured, not user-selectable) and asks the AI to produce
// a clean, well-structured personal note: one short title
// followed by rich-text body HTML. Returned as-is to the client
// which drops it straight into the editor. The client stays
// in control of Save — this endpoint never touches the DB.
router.post('/notes/from-voice', async function (req, res) {
  try {
    var transcript = (req.body.transcript || '').trim();
    if (!transcript) return res.status(400).json({ error: 'No transcript provided' });

    var systemPrompt =
      'You turn a voice dictation into a clean, well-structured personal note.\n' +
      'The note may be anything: a clinical observation, a shopping list, a reminder, a travel idea, a journal entry. Match the dictation — do not assume it is medical and do not impose clinical structure (assessment/plan, SOAP, etc.) on non-clinical content.\n' +
      'Output STRICT JSON only — no preamble, no code fences, no commentary.\n' +
      'Shape: {"title": "<short descriptive title, max 80 chars>", "body": "<HTML body>"}.\n' +
      'The body must be HTML using only these tags: <p>, <h2>, <h3>, <strong>, <em>, <u>, <ul>, <ol>, <li>, <blockquote>, <code>, <a href="…">, <br>.\n' +
      'Tone follows the dictation: concise clinical prose if clinical, plain prose or a list if casual. Preserve every fact the speaker dictated — never invent, never drop.\n' +
      'If the dictation is fragmentary, still produce a title and a clear body. Return pure JSON.' +
      INJECTION_GUARD;

    var userContent =
      'Voice dictation to convert into a personal note:\n' +
      wrapUserText('dictation', transcript);

    // Model: prefer the client's selection (validated against the admin
    // allow-list inside callAI), fall back to the admin-configured default,
    // then to the LITELLM_DEFAULT_MODEL env var. Passing an empty string
    // to LiteLLM returns a 400, so we guard against that.
    var requested = (req.body.model || '').trim();
    var adminDefault = '';
    try { adminDefault = await db.getSetting('models.default'); } catch (e) {}
    var fallback = (adminDefault || process.env.LITELLM_DEFAULT_MODEL || '').trim();
    var model = requested || fallback;
    var opts = { maxTokens: 4000 };
    if (model) opts.model = model;

    var result = await callAI([
      { role: 'system', content: systemPrompt },
      { role: 'user',   content: userContent }
    ], opts);

    // Models sometimes wrap JSON in ```json fences, prepend "Here you go:",
    // or close the JSON early. Strip leading prose and any trailing text
    // after the last } to recover.
    var raw = (result.content || '').trim();
    raw = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '');
    var jsonStart = raw.indexOf('{');
    if (jsonStart > 0) raw = raw.substring(jsonStart);
    var jsonEnd = raw.lastIndexOf('}');
    if (jsonEnd > -1 && jsonEnd < raw.length - 1) raw = raw.substring(0, jsonEnd + 1);

    var parsed = null;
    try { parsed = JSON.parse(raw); } catch (e) { /* fallback below */ }

    var title, body;
    if (parsed && (parsed.title || parsed.body)) {
      title = String(parsed.title || '').trim();
      body  = toHtmlBody(parsed.body || '');
    } else {
      // Model didn't cooperate at all — treat the whole response as the
      // body and derive a title from the first sentence of the transcript.
      title = transcript.split(/[.\n]/)[0].trim().substring(0, 80) || 'Voice note';
      body  = toHtmlBody(result.content || transcript);
    }

    // Final guards: never send back an empty body — at minimum show the
    // raw transcript wrapped in <p> so the user can edit it manually.
    if (!body || !body.replace(/<[^>]+>/g, '').trim()) {
      body = '<p>' + String(transcript).replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</p>';
    }
    if (!title) title = 'Voice note';

    res.json({
      success: true,
      title: title.substring(0, 200),
      body: body,
      model: result.model,
    });
    logger.audit(req.user.id, 'note_from_voice', 'AI-generated note from voice', req, { category: 'notes' });
  } catch (err) {
    logger.error('POST /notes/from-voice', err.message);
    res.status(500).json({ error: 'Request failed' });
  }
});

module.exports = router;
