smarana 0.3.2

An extensible note taking system for typst.
use rusqlite::{params, Connection, Result};
use serde::Serialize;
use std::path::{Path, PathBuf};

use crate::frontparse::{parse_frontmatter, Frontmatter};

#[derive(Debug, Serialize)]
pub struct Note {
    pub filename: String,
    pub title: String,
    pub summary: Option<String>,
    pub date: Option<String>,
    pub time: Option<String>,
    pub tags: Option<Vec<String>>,
    pub content: String,
}

fn db_path(notebook_path: &Path) -> PathBuf {
    notebook_path.join(".smarana").join("index.db")
}

/// Initializes the SQLite database and its schema.
pub fn init_db(notebook_path: &Path) -> Result<()> {
    let path = db_path(notebook_path);
    let conn = Connection::open(&path)?;

    conn.execute(
        "CREATE TABLE IF NOT EXISTS notes (
            filename TEXT PRIMARY KEY,
            title TEXT NOT NULL,
            summary TEXT,
            date TEXT,
            time TEXT,
            tags TEXT,
            content TEXT NOT NULL DEFAULT ''
        )",
        [],
    )?;

    Ok(())
}

/// Drops and recreates the database schema.
pub fn init_db_force(notebook_path: &Path) -> Result<()> {
    let path = db_path(notebook_path);
    let conn = Connection::open(&path)?;
    conn.execute("DROP TABLE IF EXISTS notes", [])?;
    init_db(notebook_path)
}

/// Upserts a note using extracted frontmatter variables and full content
pub fn upsert_note(notebook_path: &Path, filename: &str, fm: &Frontmatter, content: &str) -> Result<()> {
    let path = db_path(notebook_path);
    let conn = Connection::open(&path)?;

    let title = fm.title.as_deref().unwrap_or("null");
    
    // Store tags as JSON array string
    let tags_json = if let Some(tags) = &fm.tags {
        serde_json::to_string(tags).ok()
    } else {
        None
    };

    conn.execute(
        "INSERT INTO notes (filename, title, summary, date, time, tags, content) 
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
         ON CONFLICT(filename) DO UPDATE SET
         title=excluded.title,
         summary=excluded.summary,
         date=excluded.date,
         time=excluded.time,
         tags=excluded.tags,
         content=excluded.content",
        params![
            filename,
            title,
            fm.summary,
            fm.date,
            fm.time,
            tags_json,
            content
        ],
    )?;

    Ok(())
}

/// Reads a .typ file natively and upserts its explicit properties.
pub fn sync_file(notebook_path: &Path, file_path: &Path) {
    if let Ok(content) = std::fs::read_to_string(file_path) {
        let rel_path = file_path.strip_prefix(notebook_path).unwrap_or(file_path).to_string_lossy().to_string();
        let normalized_path = rel_path.replace("\\", "/");
        
        let fm = parse_frontmatter(&content, &normalized_path);
        
        if let Err(e) = upsert_note(notebook_path, &normalized_path, &fm, &content) {
            eprintln!("Failed to register note '{}': {}", normalized_path, e);
        }
    }
}

/// Scans exclusively via `.typ` in the notebook root.
pub fn sync_all(notebook_path: &Path) {
    // Ensure we have the latest schema for a full sync
    if let Err(e) = init_db_force(notebook_path) {
        eprintln!("Failed to initialize database schema: {}", e);
        return;
    }

    if let Ok(entries) = std::fs::read_dir(notebook_path) {
        for entry in entries.filter_map(|e| e.ok()) {
            let path = entry.path();
            if path.is_file() {
                if let Some(ext) = path.extension() {
                    if ext == "typ" {
                        // Ignore internal configurations silently nested beneath notebook
                        if path.to_string_lossy().contains(".smarana") {
                            continue;
                        }
                        sync_file(notebook_path, &path);
                    }
                }
            }
        }
    }
}

/// Lists all notes structured explicitly for `-j` native JSON conversions!
pub fn list_notes(notebook_path: &Path) -> Result<Vec<Note>> {
    let path = db_path(notebook_path);
    let conn = Connection::open(&path)?;

    // We can order by date DESC
    let mut stmt = conn.prepare("SELECT filename, title, summary, date, time, tags, content FROM notes ORDER BY date DESC, time DESC")?;
    let note_iter = stmt.query_map([], |row| {
        let tags_str: Option<String> = row.get(5)?;
        let tags = if let Some(ts) = tags_str {
            serde_json::from_str(&ts).ok()
        } else {
            None
        };

        Ok(Note {
            filename: row.get(0)?,
            title: row.get(1)?,
            summary: row.get(2)?,
            date: row.get(3)?,
            time: row.get(4)?,
            tags,
            content: row.get(6)?
        })
    })?;

    let mut notes = Vec::new();
    for note in note_iter {
        notes.push(note?);
    }

    Ok(notes)
}