// mod async_db;
mod search_terms;

use std::collections::HashMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::time::Duration;

use log::{debug, error};
use search_terms::{OrderBy, SearchTerms};
use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use sqlx::{Row, Sqlite, Transaction};

use crate::note::{ContentChunk, LinkType, NoteContentData, NoteDetails};

fn row_to_note_entry(
    row: &sqlx::sqlite::SqliteRow,
) -> Result<(NoteEntryData, NoteContentData), DBError> {
    let path: String = row.try_get("path")?;
    let title: String = row.try_get("title")?;
    let size: i64 = row.try_get("size")?;
    let modified: i64 = row.try_get("modified")?;
    let hash: String = row.try_get("hash")?;

    let note_path = VaultPath::new(&path);
    let entry = NoteEntryData {
        path: note_path,
        size: size as u64,
        modified_secs: modified as u64,
    };
    let content = NoteContentData::new(title, hash.parse().unwrap_or(0));
    Ok((entry, content))
}

use super::error::DBError;

use super::{nfs::{NoteEntryData, PATH_SEPARATOR}, VaultPath};

const VERSION: &str = "0.4";
const DB_FILE: &str = "kimun.sqlite";

#[derive(Debug, Clone)]
pub(super) struct VaultDB {
    workspace_path: PathBuf,
    pool: SqlitePool,
}

#[derive(PartialEq)]
pub enum DBStatus {
    Ready,
    Outdated,
    NotValid,
    #[allow(dead_code)]
    FileNotFound,
}

impl DBStatus {
    pub fn is_ready(&self) -> bool {
        DBStatus::Ready.eq(self)
    }
}

impl Display for DBStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DBStatus::Ready => write!(f, "DB is Ready"),
            DBStatus::Outdated => write!(f, "DB is an old version, needs to be rebuilt"),
            DBStatus::NotValid => write!(f, "DB file is not valid"),
            DBStatus::FileNotFound => write!(f, "No DB file found"),
        }
    }
}

impl VaultDB {
    pub(super) async fn new<P: AsRef<Path>>(workspace_path: P) -> Result<Self, DBError> {
        let workspace_path = workspace_path.as_ref().to_owned();
        let db_path = workspace_path.join(DB_FILE);
        let connection_string = format!("sqlite:{}?mode=rwc", db_path.display());

        let pool = SqlitePoolOptions::new()
            .max_connections(5)
            .acquire_timeout(Duration::from_secs(30))
            .connect(&connection_string)
            .await?;

        Ok(Self {
            workspace_path,
            pool,
        })
    }

    pub fn pool(&self) -> &SqlitePool {
        &self.pool
    }

    pub fn get_db_path(&self) -> PathBuf {
        self.workspace_path.join(DB_FILE)
    }

    pub async fn check_db(&self) -> Result<DBStatus, DBError> {
        debug!("Checking the DB");

        let version: Option<String> =
            sqlx::query_scalar("SELECT value FROM appData WHERE name = 'version'")
                .fetch_optional(&self.pool)
                .await
                .or_else(|e| {
                    // If the table doesn't exist, return FileNotFound
                    if e.to_string().contains("no such table") {
                        return Ok(None);
                    }
                    Err(e)
                })?;

        match version {
            Some(v) => {
                debug!("DB Version: {}, current DB Version: {}", v, VERSION);
                if v == VERSION {
                    debug!("DB up to date");
                    Ok(DBStatus::Ready)
                } else {
                    debug!("DB outdated");
                    Ok(DBStatus::Outdated)
                }
            }
            None => {
                debug!("DB not valid or not found");
                Ok(DBStatus::NotValid)
            }
        }
    }

    pub async fn close(&self) -> Result<(), DBError> {
        self.pool.close().await;
        Ok(())
    }
}

/// Deletes all tables and recreates them
pub async fn init_db(pool: &SqlitePool) -> Result<(), DBError> {
    debug!("Deleting DB");
    delete_db(pool).await?;
    debug!("Creating Tables");
    create_tables(pool).await
}

async fn delete_db(pool: &SqlitePool) -> Result<(), DBError> {
    let rows = sqlx::query("SELECT name FROM sqlite_schema WHERE type = 'table'")
        .fetch_all(pool)
        .await?;

    let mut tables = vec![];
    for row in rows {
        let table_name: String = row.try_get("name")?;
        tables.push(table_name);
    }

    for table in tables {
        // Can't use params for tables or columns, so we use format!
        let drop_query = format!("DROP TABLE '{}'", table);
        match sqlx::query(&drop_query).execute(pool).await {
            Ok(_) => {}
            Err(e) => {
                if table.contains("_") {
                    // Some virtual tables are automatically deleted
                    debug!("Error for table {}: {}", table, e);
                } else {
                    return Err(DBError::DBError(e));
                }
            }
        }
    }

    sqlx::query("VACUUM").execute(pool).await?;
    Ok(())
}

async fn create_tables(pool: &SqlitePool) -> Result<(), DBError> {
    let mut tx = pool.begin().await?;

    sqlx::query(
        "CREATE TABLE appData (
            name TEXT PRIMARY KEY,
            value TEXT
        )",
    )
    .execute(&mut *tx)
    .await?;

    sqlx::query("INSERT INTO appData (name, value) VALUES (?, ?)")
        .bind("version")
        .bind(VERSION)
        .execute(&mut *tx)
        .await?;

    // Storing hash as a string, as SQLite doesn't like
    // unsigned 64bit integers, alternatively we could
    // have used signed numbers by subtracting the half
    // of the max value
    sqlx::query(
        "CREATE TABLE notes (
            path TEXT PRIMARY KEY,
            title TEXT,
            hash TEXT,
            size INTEGER,
            modified INTEGER,
            basePath TEXT,
            noteName TEXT
        )",
    )
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        "CREATE TABLE links (
            source TEXT,
            destination TEXT
        )",
    )
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        "CREATE INDEX backlinks
            ON links(destination)",
    )
    .execute(&mut *tx)
    .await?;

    sqlx::query(
        "CREATE VIRTUAL TABLE notesContent USING fts4(
            path,
            breadcrumb,
            text
        )",
    )
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;

    Ok(())
}

fn combine_conditions(positive: Vec<String>, negative: Vec<String>) -> Option<String> {
    match (positive.is_empty(), negative.is_empty()) {
        (true, true) => None,
        (false, true) => Some(positive.join(" OR ")),
        (true, false) => Some(negative.join(" AND ")),
        (false, false) => Some(format!(
            "({}) AND ({})",
            positive.join(" OR "),
            negative.join(" AND ")
        )),
    }
}

fn build_like_conditions(
    positive_terms: &[String],
    negative_terms: &[String],
    pos_condition_fn: impl Fn(usize) -> String,
    neg_condition_fn: impl Fn(usize) -> String,
    var_num: &mut usize,
    params: &mut Vec<String>,
    push_term_fn: impl Fn(&String) -> String,
) -> Option<String> {
    let mut positive_conditions = vec![];
    let mut negative_conditions = vec![];

    for term in positive_terms {
        if !term.is_empty() {
            positive_conditions.push(pos_condition_fn(*var_num));
            params.push(push_term_fn(term));
            *var_num += 1;
        }
    }

    for term in negative_terms {
        if !term.is_empty() {
            negative_conditions.push(neg_condition_fn(*var_num));
            params.push(push_term_fn(term));
            *var_num += 1;
        }
    }

    combine_conditions(positive_conditions, negative_conditions)
}

fn add_exclusion_conditions(
    excluded_terms: &[String],
    var_num: &mut usize,
    exclusion_conditions: &mut Vec<String>,
    params: &mut Vec<String>,
) {
    for excluded in excluded_terms {
        exclusion_conditions.push(format!(
            "notes.path NOT IN (SELECT DISTINCT notesContent.path FROM notesContent WHERE notesContent MATCH ?{})",
            var_num
        ));
        params.push(excluded.clone());
        *var_num += 1;
    }
}

fn build_search_sql_query_inner(search_terms: &SearchTerms) -> (String, Vec<String>) {
    let mut var_num = 1;
    let base_sql = "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path";
    let mut params = vec![];
    let mut queries = vec![];
    if !search_terms.terms.is_empty() || !search_terms.excluded_terms.is_empty() {
        if !search_terms.terms.is_empty() {
            // Positive content terms
            if search_terms.excluded_terms.is_empty() {
                // No exclusions: simple FTS4 MATCH
                let terms_sql = format!("{} WHERE notesContent MATCH ?{}", base_sql, var_num);
                queries.push(terms_sql);
                params.push(search_terms.terms.join(" "));
                var_num += 1;
            } else {
                // Mixed positive + exclusions: use NOT IN subqueries to avoid FTS4 path column bug
                let mut exclusion_conditions = vec![];
                add_exclusion_conditions(
                    &search_terms.excluded_terms,
                    &mut var_num,
                    &mut exclusion_conditions,
                    &mut params,
                );

                let terms_sql = format!(
                    "{} WHERE notesContent MATCH ?{} AND {}",
                    base_sql,
                    var_num,
                    exclusion_conditions.join(" AND ")
                );
                queries.push(terms_sql);
                params.push(search_terms.terms.join(" "));
                var_num += 1;
            }
        } else if !search_terms.excluded_terms.is_empty() {
            // Exclusion-only content query: FTS4 doesn't support pure exclusions
            // Use NOT IN approach with subquery for each excluded term
            let mut exclusion_conditions = vec![];
            add_exclusion_conditions(
                &search_terms.excluded_terms,
                &mut var_num,
                &mut exclusion_conditions,
                &mut params,
            );

            // Use base_sql to get all notes, then exclude matching ones
            let terms_sql = format!("{} WHERE {}", base_sql, exclusion_conditions.join(" AND "));
            queries.push(terms_sql);
        }
    }
    if !search_terms.breadcrumb.is_empty() || !search_terms.excluded_breadcrumb.is_empty() {
        if !search_terms.breadcrumb.is_empty() {
            if search_terms.excluded_breadcrumb.is_empty() {
                // Positive-only breadcrumb terms: use column-specific MATCH syntax
                let terms_sql = format!(
                    "{} WHERE notesContent.breadcrumb MATCH ?{}",
                    base_sql, var_num
                );
                queries.push(terms_sql);
                params.push(search_terms.breadcrumb.join(" "));
                var_num += 1;
            } else {
                // Positive breadcrumb terms with exclusions: use column-prefix syntax in notesContent MATCH
                let mut breadcrumb_parts = vec![];

                // Add positive breadcrumb terms with column prefix
                for breadcrumb in &search_terms.breadcrumb {
                    breadcrumb_parts.push(format!("breadcrumb: {}", breadcrumb));
                }

                // Add excluded breadcrumb terms with column prefix
                for excluded in &search_terms.excluded_breadcrumb {
                    breadcrumb_parts.push(format!("breadcrumb: -{}", excluded));
                }

                let terms_sql = format!("{} WHERE notesContent MATCH ?{}", base_sql, var_num);
                queries.push(terms_sql);
                params.push(breadcrumb_parts.join(" "));
                var_num += 1;
            }
        } else if !search_terms.excluded_breadcrumb.is_empty() {
            // Exclusion-only breadcrumb query: use NOT IN approach for breadcrumb column
            let mut exclusion_conditions = vec![];
            for excluded in &search_terms.excluded_breadcrumb {
                exclusion_conditions.push(format!(
                    "notes.path NOT IN (SELECT DISTINCT notesContent.path FROM notesContent WHERE notesContent MATCH ?{})",
                    var_num
                ));
                params.push(format!("breadcrumb: {}", excluded));
                var_num += 1;
            }

            let terms_sql = format!("{} WHERE {}", base_sql, exclusion_conditions.join(" AND "));
            queries.push(terms_sql);
        }
    }
    if !search_terms.filename.is_empty() || !search_terms.excluded_filename.is_empty() {
        if let Some(final_where) = build_like_conditions(
            &search_terms.filename,
            &search_terms.excluded_filename,
            |n| format!("notes.noteName LIKE ('%' || ?{} || '%')", n),
            |n| format!("notes.noteName NOT LIKE ('%' || ?{} || '%')", n),
            &mut var_num,
            &mut params,
            |t| t.clone(),
        ) {
            let terms_sql = format!("{} WHERE {}", base_sql, final_where);
            queries.push(terms_sql);
        }
    }
    if !search_terms.path.is_empty() || !search_terms.excluded_path.is_empty() {
        let mut positive_conditions = vec![];
        let mut negative_conditions = vec![];

        for path in &search_terms.path {
            if !path.is_empty() {
                match path.strip_suffix("/") {
                    Some(absolute) => {
                        positive_conditions.push(format!("notes.basePath = ('/' || ?{})", var_num));
                        params.push(absolute.to_string());
                    }
                    None => {
                        positive_conditions
                            .push(format!("notes.basePath LIKE ('/' || ?{} || '%')", var_num));
                        params.push(path.to_string());
                    }
                }
                var_num += 1;
            }
        }

        for excluded in &search_terms.excluded_path {
            if !excluded.is_empty() {
                match excluded.strip_suffix("/") {
                    Some(absolute) => {
                        negative_conditions
                            .push(format!("notes.basePath != ('/' || ?{})", var_num));
                        params.push(absolute.to_string());
                    }
                    None => {
                        negative_conditions.push(format!(
                            "notes.basePath NOT LIKE ('/' || ?{} || '%')",
                            var_num
                        ));
                        params.push(excluded.to_string());
                    }
                }
                var_num += 1;
            }
        }

        if let Some(final_where) = combine_conditions(positive_conditions, negative_conditions) {
            let terms_sql = format!("{} WHERE {}", base_sql, final_where);
            queries.push(terms_sql);
        }
    }

    if queries.is_empty() {
        debug!("No query provided");
        return (String::new(), vec![]);
    }

    let sql = queries.join(" INTERSECT ");

    (sql, params)
}

#[cfg(test)]
fn build_search_sql_query<S: AsRef<str>>(query: S) -> (String, Vec<String>) {
    let search_terms = SearchTerms::from_query_string(query);
    build_search_sql_query_inner(&search_terms)
}

pub async fn get_all_notes(
    pool: &SqlitePool,
) -> Result<Vec<(NoteEntryData, NoteContentData)>, DBError> {
    let query = "SELECT DISTINCT path, title, size, modified, hash, noteName FROM notes";

    let rows = sqlx::query(query).fetch_all(pool).await?;

    rows.iter().map(row_to_note_entry).collect()
}

pub async fn search_terms<S: AsRef<str>>(
    pool: &SqlitePool,
    search_query: S,
) -> Result<Vec<(NoteEntryData, NoteContentData)>, DBError> {
    let search_query = search_query.as_ref();
    let search_terms = SearchTerms::from_query_string(search_query);
    let (query, params) = build_search_sql_query_inner(&search_terms);
    let order_by = search_terms.order_by;

    if query.is_empty() {
        debug!("No query provided");
        return Ok(vec![]);
    }

    debug!("QUERY: {}", query);

    let mut sql_query = sqlx::query(&query);
    for param in params {
        sql_query = sql_query.bind(param);
    }

    let rows = sql_query.fetch_all(pool).await?;

    let mut result: Vec<(NoteEntryData, NoteContentData)> = rows
        .iter()
        .map(row_to_note_entry)
        .collect::<Result<_, _>>()?;

    if !order_by.is_empty() {
        result.sort_by(|(a_entry, a_content), (b_entry, b_content)| {
            for ob in &order_by {
                let ord = match ob {
                    OrderBy::Title { asc } => {
                        let cmp = a_content
                            .title
                            .to_lowercase()
                            .cmp(&b_content.title.to_lowercase());
                        if *asc {
                            cmp
                        } else {
                            cmp.reverse()
                        }
                    }
                    OrderBy::FileName { asc } => {
                        let cmp = a_entry.path.to_string().cmp(&b_entry.path.to_string());
                        if *asc {
                            cmp
                        } else {
                            cmp.reverse()
                        }
                    }
                };
                if ord != std::cmp::Ordering::Equal {
                    return ord;
                }
            }
            std::cmp::Ordering::Equal
        });
    }

    Ok(result)
}

async fn note_exists(pool: &SqlitePool, path: &VaultPath) -> Result<bool, DBError> {
    let sql = "SELECT count(*) FROM notes where path = ?";
    let count: i64 = sqlx::query_scalar(sql)
        .bind(path.to_string())
        .fetch_one(pool)
        .await?;

    match count {
        0 => Ok(false),
        1 => Ok(true),
        _ => Err(DBError::Other(format!(
            "Unexpected error, the DB contains more than one ({}) entry for path {}",
            count, path
        ))),
    }
}

pub async fn search_note_by_name<S: AsRef<str>>(
    pool: &SqlitePool,
    name: S,
) -> Result<Vec<(NoteEntryData, NoteContentData)>, DBError> {
    let name = name.as_ref().to_lowercase();
    let sql = "SELECT path, title, size, modified, hash, noteName FROM notes where noteName = ?";

    let rows = sqlx::query(sql).bind(&name).fetch_all(pool).await?;

    rows.iter().map(row_to_note_entry).collect()
}

pub async fn search_note_by_path(
    pool: &SqlitePool,
    path: &VaultPath,
) -> Result<Vec<(NoteEntryData, NoteContentData)>, DBError> {
    let sql = "SELECT path, title, size, modified, hash, noteName FROM notes where path = ?";
    let path_string = path.to_string();

    let rows = sqlx::query(sql).bind(&path_string).fetch_all(pool).await?;

    // Should always return one or zero
    rows.iter().map(row_to_note_entry).collect()
}

pub async fn get_notes(
    pool: &SqlitePool,
    path: &VaultPath,
    recursive: bool,
) -> Result<Vec<(NoteEntryData, NoteContentData)>, DBError> {
    let sql = if recursive {
        "SELECT path, title, size, modified, hash, noteName FROM notes where basePath LIKE (? || '%')"
    } else {
        "SELECT path, title, size, modified, hash, noteName FROM notes where basePath = ?"
    };

    let rows = sqlx::query(sql)
        .bind(path.to_string())
        .fetch_all(pool)
        .await?;

    rows.iter().map(row_to_note_entry).collect()
}

pub async fn get_backlinks(
    pool: &SqlitePool,
    path: &VaultPath,
) -> Result<Vec<(NoteEntryData, NoteContentData)>, DBError> {
    // Match notes that link to the full path OR by filename only (wikilinks stored without path)
    let sql = "SELECT DISTINCT n.path, n.title, n.size, n.modified, n.hash, n.noteName \
               FROM notes n \
               JOIN links l ON n.path = l.source \
               WHERE l.destination = ? OR l.destination = ?";

    let rows = sqlx::query(sql)
        .bind(path.to_string())
        .bind(path.get_name())
        .fetch_all(pool)
        .await?;

    rows.iter().map(row_to_note_entry).collect()
}

pub async fn get_notes_sections(
    pool: &SqlitePool,
    path: &VaultPath,
    recursive: bool,
) -> Result<HashMap<VaultPath, Vec<ContentChunk>>, DBError> {
    let mut result = HashMap::new();
    let (sql, bind_value) = if path.is_note() {
        // Exact note path
        (
            "SELECT path, breadcrumb, text FROM notesContent WHERE path = ?",
            path.to_string(),
        )
    } else if recursive {
        // All notes under this directory tree
        (
            "SELECT path, breadcrumb, text FROM notesContent WHERE path LIKE (? || '%')",
            path.to_string(),
        )
    } else {
        // Only notes directly in this directory (basePath join)
        ("SELECT nc.path, nc.breadcrumb, nc.text FROM notesContent nc JOIN notes n ON nc.path = n.path WHERE n.basePath = ?", path.to_string())
    };

    let rows = sqlx::query(sql).bind(bind_value).fetch_all(pool).await?;

    for row in rows {
        let path: String = row.try_get("path")?;
        let breadcrumb: String = row.try_get("breadcrumb")?;
        let text: String = row.try_get("text")?;

        let path = VaultPath::new(path);
        let breadcrumb = breadcrumb
            .split(">")
            .map(|e| e.to_string())
            .collect::<Vec<String>>();

        let chunk = ContentChunk { breadcrumb, text };
        let chunks = result.entry(path).or_insert_with(Vec::new);
        chunks.push(chunk);
    }

    Ok(result)
}

pub async fn insert_notes(
    tx: &mut Transaction<'_, Sqlite>,
    notes: &[(NoteEntryData, String)],
) -> Result<(), DBError> {
    if !notes.is_empty() {
        debug!("Inserting {} notes", notes.len());
        for (entry_data, text) in notes {
            let note_details = NoteDetails::new(&entry_data.path, text);
            insert_note(tx, entry_data, &note_details).await?;
        }
    }
    Ok(())
}

pub async fn update_notes(
    tx: &mut Transaction<'_, Sqlite>,
    notes: &[(NoteEntryData, String)],
) -> Result<(), DBError> {
    if !notes.is_empty() {
        debug!("Updating {} notes", notes.len());
        for (entry_data, text) in notes {
            let note_details = NoteDetails::new(&entry_data.path, text);
            update_note(tx, entry_data, &note_details).await?;
        }
    }
    Ok(())
}

pub async fn delete_notes(
    tx: &mut Transaction<'_, Sqlite>,
    paths: &[VaultPath],
) -> Result<(), DBError> {
    if !paths.is_empty() {
        for path in paths {
            delete_note(tx, path).await?;
        }
    }
    Ok(())
}

pub async fn save_note(
    pool: &SqlitePool,
    entry_data: &NoteEntryData,
    note_details: &NoteDetails,
) -> Result<(), DBError> {
    let exists = note_exists(pool, &entry_data.path).await?;
    let mut tx = pool.begin().await?;
    if exists {
        update_note(&mut tx, entry_data, note_details).await
    } else {
        insert_note(&mut tx, entry_data, note_details).await
    }?;
    tx.commit().await?;
    Ok(())
}

async fn insert_note(
    tx: &mut Transaction<'_, Sqlite>,
    entry_data: &NoteEntryData,
    note_details: &NoteDetails,
) -> Result<(), DBError> {
    let (parent_path, name) = entry_data.path.get_parent_path();
    let path_string = entry_data.path.to_string();
    let data = note_details.get_content_data();

    sqlx::query(
        "INSERT INTO notes (path, title, size, modified, hash, basePath, noteName) VALUES (?, ?, ?, ?, ?, ?, ?)"
    )
    .bind(&path_string)
    .bind(&data.title)
    .bind(entry_data.size as i64)
    .bind(entry_data.modified_secs as i64)
    .bind(data.hash.to_string())
    .bind(parent_path.to_string())
    .bind(&name)
    .execute(&mut **tx)
    .await
    .map_err(|e| {
        error!("Error inserting note: {}\nDetails: {}", e, note_details);
        DBError::DBError(e)
    })?;

    let (chunks, links) = note_details.get_chunks_and_links();
    for chunk in &chunks {
        let breadcrumb = chunk.get_breadcrumb();
        let chunk_text = &chunk.text;
        sqlx::query("INSERT INTO notesContent (path, breadcrumb, text) VALUES (?, ?, ?)")
            .bind(&path_string)
            .bind(&breadcrumb)
            .bind(chunk_text)
            .execute(&mut **tx)
            .await?;
    }

    for link in &links {
        if let LinkType::Note(path) = &link.ltype {
            sqlx::query("INSERT INTO links (source, destination) VALUES (?, ?)")
                .bind(&path_string)
                .bind(path.to_string())
                .execute(&mut **tx)
                .await?;
        }
    }

    Ok(())
}

async fn update_note(
    tx: &mut Transaction<'_, Sqlite>,
    entry_data: &NoteEntryData,
    note_details: &NoteDetails,
) -> Result<(), DBError> {
    let data = note_details.get_content_data();
    let title = data.title.clone();
    let hash = data.hash.to_string();
    let path_string = entry_data.path.to_string();

    sqlx::query("UPDATE notes SET title = ?, size = ?, modified = ?, hash = ? WHERE path = ?")
        .bind(&title)
        .bind(entry_data.size as i64)
        .bind(entry_data.modified_secs as i64)
        .bind(&hash)
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    sqlx::query("DELETE FROM notesContent WHERE path = ?")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    let (chunks, links) = note_details.get_chunks_and_links();
    for chunk in &chunks {
        let breadcrumb = chunk.get_breadcrumb();
        let chunk_text = &chunk.text;
        sqlx::query("INSERT INTO notesContent (path, breadcrumb, text) VALUES (?, ?, ?)")
            .bind(&path_string)
            .bind(&breadcrumb)
            .bind(chunk_text)
            .execute(&mut **tx)
            .await?;
    }

    sqlx::query("DELETE FROM links WHERE source = ?")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    for link in &links {
        if let LinkType::Note(path) = &link.ltype {
            sqlx::query("INSERT INTO links (source, destination) VALUES (?, ?)")
                .bind(&path_string)
                .bind(path.to_string())
                .execute(&mut **tx)
                .await?;
        }
    }

    Ok(())
}

async fn delete_note(tx: &mut Transaction<'_, Sqlite>, path: &VaultPath) -> Result<(), DBError> {
    let path_string = path.to_string();

    sqlx::query("DELETE FROM notes WHERE path = ?")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    sqlx::query("DELETE FROM notesContent WHERE path = ?")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    sqlx::query("DELETE FROM links WHERE source = ?")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    sqlx::query("DELETE FROM links WHERE destination = ?")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    Ok(())
}

pub async fn rename_note(
    tx: &mut Transaction<'_, Sqlite>,
    from: &VaultPath,
    to: &VaultPath,
) -> Result<(), DBError> {
    let old_note_name = from.get_name();
    let (new_base_path, new_note_name) = to.get_parent_path();

    sqlx::query("UPDATE notes SET path = ?, basePath = ?, noteName = ? WHERE path = ?")
        .bind(to.to_string())
        .bind(new_base_path.to_string())
        .bind(&new_note_name)
        .bind(from.to_string())
        .execute(&mut **tx)
        .await?;

    sqlx::query("UPDATE notesContent SET path = ? WHERE path = ?")
        .bind(to.to_string())
        .bind(from.to_string())
        .execute(&mut **tx)
        .await?;

    sqlx::query("UPDATE links SET source = ? WHERE source = ?")
        .bind(to.to_string())
        .bind(from.to_string())
        .execute(&mut **tx)
        .await?;

    sqlx::query("UPDATE links SET destination = ? WHERE destination = ?")
        .bind(to.to_string())
        .bind(from.to_string())
        .execute(&mut **tx)
        .await?;

    // Update links that reference the note by filename only (wikilinks without path)
    sqlx::query("UPDATE links SET destination = ? WHERE destination = ?")
        .bind(&new_note_name)
        .bind(&old_note_name)
        .execute(&mut **tx)
        .await?;

    Ok(())
}

pub async fn rename_directory(
    tx: &mut Transaction<'_, Sqlite>,
    from: &VaultPath,
    to: &VaultPath,
) -> Result<(), DBError> {
    let from = {
        let s = from.to_string();
        if s.ends_with(PATH_SEPARATOR) {
            s
        } else {
            s + &PATH_SEPARATOR.to_string()
        }
    };
    let to = {
        let s = to.to_string();
        if s.ends_with(PATH_SEPARATOR) {
            s
        } else {
            s + &PATH_SEPARATOR.to_string()
        }
    };

    let notes_sql = "UPDATE notes SET path = ? || SUBSTR(path, LENGTH(?) + 1), basePath = ? || SUBSTR(basePath, LENGTH(?) + 1) WHERE basePath LIKE (? || '%')";
    sqlx::query(notes_sql)
        .bind(&to)
        .bind(&from)
        .bind(&to)
        .bind(&from)
        .bind(&from)
        .execute(&mut **tx)
        .await?;

    sqlx::query("UPDATE notesContent SET path = ? || SUBSTR(path, LENGTH(?) + 1) WHERE path LIKE (? || '%')")
        .bind(&to)
        .bind(&from)
        .bind(&from)
        .execute(&mut **tx)
        .await?;

    sqlx::query(
        "UPDATE links SET source = ? || SUBSTR(source, LENGTH(?) + 1) WHERE source LIKE (? || '%')",
    )
    .bind(&to)
    .bind(&from)
    .bind(&from)
    .execute(&mut **tx)
    .await?;

    sqlx::query("UPDATE links SET destination = ? || SUBSTR(destination, LENGTH(?) + 1) WHERE destination LIKE (? || '%')")
        .bind(&to)
        .bind(&from)
        .bind(&from)
        .execute(&mut **tx)
        .await?;

    Ok(())
}

pub async fn delete_directories(
    tx: &mut Transaction<'_, Sqlite>,
    directories: &[VaultPath],
) -> Result<(), DBError> {
    if !directories.is_empty() {
        for directory in directories {
            delete_directory(tx, directory).await?;
        }
    }
    Ok(())
}

async fn delete_directory(
    tx: &mut Transaction<'_, Sqlite>,
    directory_path: &VaultPath,
) -> Result<(), DBError> {
    let path_string = directory_path.to_string();

    sqlx::query("DELETE FROM notes WHERE path LIKE (? || '%')")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    sqlx::query("DELETE FROM notesContent WHERE path LIKE (? || '%')")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    sqlx::query("DELETE FROM links WHERE source LIKE (? || '%')")
        .bind(&path_string)
        .execute(&mut **tx)
        .await?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_search_terms_query_empty() {
        let (sql, params) = build_search_sql_query("");
        assert_eq!(sql, "");
        assert_eq!(params.len(), 0);
    }

    #[test]
    fn test_search_terms_query_simple_terms() {
        let (sql, params) = build_search_sql_query("foo bar");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "foo bar");
    }

    #[test]
    fn test_search_terms_query_single_term() {
        let (sql, params) = build_search_sql_query("keyword");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_breadcrumb_only() {
        let (sql, params) = build_search_sql_query(">heading");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "heading");
    }

    #[test]
    fn test_search_terms_query_breadcrumb_with_in() {
        let (sql, params) = build_search_sql_query("in:section");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "section");
    }

    #[test]
    fn test_search_terms_query_multiple_breadcrumbs() {
        let (sql, params) = build_search_sql_query(">heading1 in:heading2");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "heading1 heading2");
    }

    #[test]
    fn test_search_terms_query_path_only() {
        let (sql, params) = build_search_sql_query("@filename");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?1 || '%')"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "filename");
    }

    #[test]
    fn test_search_terms_query_path_with_at() {
        let (sql, params) = build_search_sql_query("at:directory");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?1 || '%')"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "directory");
    }

    #[test]
    fn test_search_terms_query_multiple_paths() {
        let (sql, params) = build_search_sql_query("@file1 at:file2");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?1 || '%') OR notes.noteName LIKE ('%' || ?2 || '%')"
        );
        assert_eq!(params.len(), 2);
        assert_eq!(params[0], "file1");
        assert_eq!(params[1], "file2");
    }

    #[test]
    fn test_search_terms_query_terms_and_breadcrumb() {
        let (sql, params) = build_search_sql_query("keyword >section");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?2"
        );
        assert_eq!(params.len(), 2);
        assert_eq!(params[0], "keyword");
        assert_eq!(params[1], "section");
    }

    #[test]
    fn test_search_terms_query_terms_and_path() {
        let (sql, params) = build_search_sql_query("keyword @file");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?2 || '%')"
        );
        assert_eq!(params.len(), 2);
        assert_eq!(params[0], "keyword");
        assert_eq!(params[1], "file");
    }

    #[test]
    fn test_search_terms_query_breadcrumb_and_path() {
        let (sql, params) = build_search_sql_query(">heading @file");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?1 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?2 || '%')"
        );
        assert_eq!(params.len(), 2);
        assert_eq!(params[0], "heading");
        assert_eq!(params[1], "file");
    }

    #[test]
    fn test_search_terms_query_all_combined() {
        let (sql, params) = build_search_sql_query("keyword >heading @file");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?2 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?3 || '%')"
        );
        assert_eq!(params.len(), 3);
        assert_eq!(params[0], "keyword");
        assert_eq!(params[1], "heading");
        assert_eq!(params[2], "file");
    }

    #[test]
    fn test_search_terms_query_quoted_terms() {
        let (sql, params) = build_search_sql_query("\"exact phrase\" keyword");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "exact phrase keyword");
    }

    #[test]
    fn test_search_terms_query_order_by_title_asc() {
        let (sql, params) = build_search_sql_query("keyword or:title");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_order_by_title_desc() {
        let (sql, params) = build_search_sql_query("keyword or:-title");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_order_by_filename_asc() {
        let (sql, params) = build_search_sql_query("keyword or:filename");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_order_by_file_shorthand() {
        let (sql, params) = build_search_sql_query("keyword or:f");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_order_by_title_shorthand() {
        let (sql, params) = build_search_sql_query("keyword or:t");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_multiple_order_by() {
        let (sql, params) = build_search_sql_query("keyword ^title ^-filename");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_complex_with_order() {
        let (sql, params) = build_search_sql_query("keyword >section @file ^title");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?2 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notes.noteName LIKE ('%' || ?3 || '%')"
        );
        assert_eq!(params.len(), 3);
        assert_eq!(params[0], "keyword");
        assert_eq!(params[1], "section");
        assert_eq!(params[2], "file");
    }

    #[test]
    fn test_search_terms_query_only_order_by() {
        let (sql, params) = build_search_sql_query("^title");
        assert_eq!(sql, "");
        assert_eq!(params.len(), 0);
    }

    #[test]
    fn test_search_terms_query_invalid_order_by_field() {
        let (sql, params) = build_search_sql_query("keyword ^invalid");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1"
        );
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "keyword");
    }

    #[test]
    fn test_search_terms_query_whitespace_handling() {
        let (sql, params) = build_search_sql_query("  keyword   >section  ");
        assert_eq!(
            sql,
            "SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent MATCH ?1 INTERSECT SELECT DISTINCT notes.path as path, title, size, modified, hash, noteName FROM notesContent JOIN notes ON notesContent.path = notes.path WHERE notesContent.breadcrumb MATCH ?2"
        );
        assert_eq!(params.len(), 2);
        assert_eq!(params[0], "keyword");
        assert_eq!(params[1], "section");
    }

    #[test]
    fn test_fts4_mixed_exclusion_sql_generation() {
        let (sql, params) = build_search_sql_query("meeting -cancelled");

        // Should use NOT IN subquery approach instead of FTS4 native exclusion
        assert!(sql.contains("notesContent MATCH"));
        assert!(sql.contains("NOT IN"));
        assert!(sql.contains(
            "SELECT DISTINCT notesContent.path FROM notesContent WHERE notesContent MATCH"
        ));
        // params: first is the excluded term (NOT IN subquery), second is the positive term
        assert_eq!(params.len(), 2);
        assert!(params.contains(&"cancelled".to_string()));
        assert!(params.contains(&"meeting".to_string()));

        assert!(sql.contains("SELECT DISTINCT"));
    }

    #[test]
    fn test_exclusion_only_sql_generation() {
        // Critical test: exclusion-only queries MUST use NOT IN, not pure FTS4 MATCH
        let (sql, params) = build_search_sql_query("-cancelled");

        // Should NOT contain pure FTS4 exclusion (which is invalid)
        assert!(!sql.contains("MATCH \"-cancelled\""));
        // Should use NOT IN subquery approach
        assert!(sql.contains("NOT IN"));
        assert!(sql.contains(
            "SELECT DISTINCT notesContent.path FROM notesContent WHERE notesContent MATCH"
        ));
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "cancelled");
    }

    #[test]
    fn test_breadcrumb_exclusion_sql_generation() {
        let (sql, params) = build_search_sql_query(">project >-draft");

        assert!(sql.contains("notesContent MATCH"));
        assert_eq!(params.len(), 1);
        assert_eq!(params[0], "breadcrumb: project breadcrumb: -draft");
    }

    #[test]
    fn test_like_exclusion_sql_generation() {
        let (sql, params) = build_search_sql_query("@2024 @-draft");

        // Should generate filename query with positive and negative conditions
        assert!(sql.contains("notes.noteName LIKE"));
        assert!(sql.contains("notes.noteName NOT LIKE"));
        assert!(params.contains(&"2024".to_string()));
        assert!(params.contains(&"draft".to_string()));
    }

    #[test]
    fn test_exclusion_only_like_query() {
        let (sql, params) = build_search_sql_query("@-draft @-temp");

        // Exclusion-only should still generate valid WHERE clause
        assert!(sql.contains("notes.noteName NOT LIKE"));
        assert!(!sql.contains("notes.noteName LIKE ('%'")); // No positive conditions
        assert_eq!(params.len(), 2);
    }

    #[test]
    fn test_path_exclusion_sql_generation() {
        let (sql, params) = build_search_sql_query("/projects /-archive");

        assert!(sql.contains("notes.basePath LIKE"));
        assert!(sql.contains("notes.basePath NOT LIKE"));
        assert!(params.contains(&"projects".to_string()));
        assert!(params.contains(&"archive".to_string()));
    }

    #[test]
    fn test_exclusion_only_path_query() {
        let (sql, params) = build_search_sql_query("/-draft /-temp");

        assert!(sql.contains("notes.basePath NOT LIKE"));
        assert!(!sql.contains("notes.basePath LIKE ('/'"));
        assert_eq!(params.len(), 2);
    }
}
