use crate::AppState;
use crate::os_detection;
use rusqlite::params;
use serde::{Deserialize, Serialize};
use tauri::{Manager, State};

#[derive(Debug, Serialize, Deserialize)]
pub struct Client {
    pub id: Option<i64>,
    pub name: String,
    pub email: Option<String>,
    pub address: Option<String>,
    pub company: Option<String>,
    pub phone: Option<String>,
    pub tax_id: Option<String>,
    pub payment_terms: Option<String>,
    pub notes: Option<String>,
    pub currency: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Project {
    pub id: Option<i64>,
    pub client_id: Option<i64>,
    pub name: String,
    pub hourly_rate: f64,
    pub color: String,
    pub archived: bool,
    pub budget_hours: Option<f64>,
    pub budget_amount: Option<f64>,
    pub rounding_override: Option<i32>,
    pub notes: Option<String>,
    pub currency: Option<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
    pub id: Option<i64>,
    pub project_id: i64,
    pub name: String,
    pub estimated_hours: Option<f64>,
    pub hourly_rate: Option<f64>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TimeEntry {
    pub id: Option<i64>,
    pub project_id: i64,
    pub task_id: Option<i64>,
    pub description: Option<String>,
    pub start_time: String,
    pub end_time: Option<String>,
    pub duration: i64,
    pub billable: Option<i64>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Invoice {
    pub id: Option<i64>,
    pub client_id: i64,
    pub invoice_number: String,
    pub date: String,
    pub due_date: Option<String>,
    pub subtotal: f64,
    pub tax_rate: f64,
    pub tax_amount: f64,
    pub discount: f64,
    pub total: f64,
    pub notes: Option<String>,
    pub status: String,
    pub template_id: Option<String>,
}

// Client commands
#[tauri::command]
pub fn get_clients(state: State<AppState>) -> Result<Vec<Client>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, name, email, address, company, phone, tax_id, payment_terms, notes, currency FROM clients ORDER BY name"
    ).map_err(|e| e.to_string())?;
    let clients = stmt.query_map([], |row| {
        Ok(Client {
            id: Some(row.get(0)?),
            name: row.get(1)?,
            email: row.get(2)?,
            address: row.get(3)?,
            company: row.get(4)?,
            phone: row.get(5)?,
            tax_id: row.get(6)?,
            payment_terms: row.get(7)?,
            notes: row.get(8)?,
            currency: row.get(9)?,
        })
    }).map_err(|e| e.to_string())?;
    clients.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_client(state: State<AppState>, client: Client) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO clients (name, email, address, company, phone, tax_id, payment_terms, notes, currency) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
        params![client.name, client.email, client.address, client.company, client.phone, client.tax_id, client.payment_terms, client.notes, client.currency],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_client(state: State<AppState>, client: Client) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE clients SET name = ?1, email = ?2, address = ?3, company = ?4, phone = ?5, tax_id = ?6, payment_terms = ?7, notes = ?8, currency = ?9 WHERE id = ?10",
        params![client.name, client.email, client.address, client.company, client.phone, client.tax_id, client.payment_terms, client.notes, client.currency, client.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn get_client_dependents(state: State<AppState>, client_id: i64) -> Result<serde_json::Value, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let project_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM projects WHERE client_id = ?1", params![client_id], |row| row.get(0)
    ).map_err(|e| e.to_string())?;
    let invoice_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM invoices WHERE client_id = ?1", params![client_id], |row| row.get(0)
    ).map_err(|e| e.to_string())?;
    let expense_count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM expenses WHERE client_id = ?1", params![client_id], |row| row.get(0)
    ).map_err(|e| e.to_string())?;
    Ok(serde_json::json!({
        "projects": project_count,
        "invoices": invoice_count,
        "expenses": expense_count
    }))
}

#[tauri::command]
pub fn delete_client(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    conn.execute_batch("BEGIN TRANSACTION").map_err(|e| e.to_string())?;

    let result = (|| -> Result<(), rusqlite::Error> {
        let project_ids: Vec<i64> = {
            let mut stmt = conn.prepare("SELECT id FROM projects WHERE client_id = ?1")?;
            let rows = stmt.query_map(params![id], |row| row.get(0))?;
            rows.filter_map(|r| r.ok()).collect()
        };

        for pid in &project_ids {
            conn.execute("DELETE FROM timeline_events WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM invoice_items WHERE time_entry_id IN (SELECT id FROM time_entries WHERE project_id = ?1)", params![pid])?;
            conn.execute("DELETE FROM entry_tags WHERE entry_id IN (SELECT id FROM time_entries WHERE project_id = ?1)", params![pid])?;
            conn.execute("DELETE FROM time_entries WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM tracked_apps WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM favorites WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM recurring_entries WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM entry_templates WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM timesheet_rows WHERE project_id = ?1", params![pid])?;
            conn.execute("DELETE FROM tasks WHERE project_id = ?1", params![pid])?;
        }

        conn.execute("DELETE FROM expenses WHERE client_id = ?1", params![id])?;
        conn.execute("DELETE FROM invoice_payments WHERE invoice_id IN (SELECT id FROM invoices WHERE client_id = ?1)", params![id])?;
        conn.execute("DELETE FROM invoice_items WHERE invoice_id IN (SELECT id FROM invoices WHERE client_id = ?1)", params![id])?;
        conn.execute("DELETE FROM invoices WHERE client_id = ?1", params![id])?;
        conn.execute("DELETE FROM recurring_invoices WHERE client_id = ?1", params![id])?;
        conn.execute("DELETE FROM projects WHERE client_id = ?1", params![id])?;
        conn.execute("DELETE FROM clients WHERE id = ?1", params![id])?;
        Ok(())
    })();

    match result {
        Ok(()) => {
            conn.execute_batch("COMMIT").map_err(|e| e.to_string())?;
            Ok(())
        }
        Err(e) => {
            conn.execute_batch("ROLLBACK").ok();
            Err(e.to_string())
        }
    }
}

// Project commands
#[tauri::command]
pub fn get_projects(state: State<AppState>) -> Result<Vec<Project>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency FROM projects ORDER BY name"
    ).map_err(|e| e.to_string())?;
    let projects = stmt.query_map([], |row| {
        Ok(Project {
            id: Some(row.get(0)?),
            client_id: row.get(1)?,
            name: row.get(2)?,
            hourly_rate: row.get(3)?,
            color: row.get(4)?,
            archived: row.get::<_, i32>(5)? != 0,
            budget_hours: row.get(6)?,
            budget_amount: row.get(7)?,
            rounding_override: row.get(8)?,
            notes: row.get(9)?,
            currency: row.get(10)?,
        })
    }).map_err(|e| e.to_string())?;
    projects.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_project(state: State<AppState>, project: Project) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO projects (client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
        params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.budget_hours, project.budget_amount, project.rounding_override, project.notes, project.currency],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_project(state: State<AppState>, project: Project) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE projects SET client_id = ?1, name = ?2, hourly_rate = ?3, color = ?4, archived = ?5, budget_hours = ?6, budget_amount = ?7, rounding_override = ?8, notes = ?9, currency = ?10 WHERE id = ?11",
        params![project.client_id, project.name, project.hourly_rate, project.color, project.archived as i32, project.budget_hours, project.budget_amount, project.rounding_override, project.notes, project.currency, project.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[derive(serde::Serialize)]
pub struct DependentCounts {
    pub time_entries: i64,
    pub favorites: i64,
    pub expenses: i64,
    pub recurring_entries: i64,
    pub timeline_events: i64,
    pub tasks: i64,
    pub tracked_apps: i64,
}

#[tauri::command]
pub fn get_project_dependents(state: State<AppState>, project_id: i64) -> Result<DependentCounts, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let count = |table: &str, col: &str| -> i64 {
        conn.query_row(
            &format!("SELECT COUNT(*) FROM {} WHERE {} = ?1", table, col),
            params![project_id],
            |row| row.get(0),
        ).unwrap_or(0)
    };
    Ok(DependentCounts {
        time_entries: count("time_entries", "project_id"),
        favorites: count("favorites", "project_id"),
        expenses: count("expenses", "project_id"),
        recurring_entries: count("recurring_entries", "project_id"),
        timeline_events: count("timeline_events", "project_id"),
        tasks: count("tasks", "project_id"),
        tracked_apps: count("tracked_apps", "project_id"),
    })
}

#[tauri::command]
pub fn delete_project(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    conn.execute_batch("BEGIN TRANSACTION").map_err(|e| e.to_string())?;

    let result = (|| -> Result<(), rusqlite::Error> {
        conn.execute("DELETE FROM timeline_events WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM invoice_items WHERE time_entry_id IN (SELECT id FROM time_entries WHERE project_id = ?1)", params![id])?;
        conn.execute(
            "DELETE FROM entry_tags WHERE entry_id IN (SELECT id FROM time_entries WHERE project_id = ?1)",
            params![id],
        )?;
        conn.execute("DELETE FROM time_entries WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM favorites WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM expenses WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM recurring_entries WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM tracked_apps WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM entry_templates WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM timesheet_rows WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM tasks WHERE project_id = ?1", params![id])?;
        conn.execute("DELETE FROM projects WHERE id = ?1", params![id])?;
        Ok(())
    })();

    match result {
        Ok(()) => {
            conn.execute_batch("COMMIT").map_err(|e| e.to_string())?;
            Ok(())
        }
        Err(e) => {
            conn.execute_batch("ROLLBACK").ok();
            Err(e.to_string())
        }
    }
}

// Task commands
#[tauri::command]
pub fn get_tasks(state: State<AppState>, project_id: i64) -> Result<Vec<Task>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare("SELECT id, project_id, name, estimated_hours, hourly_rate FROM tasks WHERE project_id = ?1 ORDER BY name").map_err(|e| e.to_string())?;
    let tasks = stmt.query_map(params![project_id], |row| {
        Ok(Task {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            name: row.get(2)?,
            estimated_hours: row.get(3)?,
            hourly_rate: row.get(4)?,
        })
    }).map_err(|e| e.to_string())?;
    tasks.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_task(state: State<AppState>, task: Task) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO tasks (project_id, name, estimated_hours, hourly_rate) VALUES (?1, ?2, ?3, ?4)",
        params![task.project_id, task.name, task.estimated_hours, task.hourly_rate],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn delete_task(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM entry_templates WHERE task_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM timesheet_rows WHERE task_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM recurring_entries WHERE task_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM tasks WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn update_task(state: State<AppState>, task: Task) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE tasks SET name = ?1, estimated_hours = ?2, hourly_rate = ?3 WHERE id = ?4",
        params![task.name, task.estimated_hours, task.hourly_rate, task.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

// Time entry commands
#[tauri::command]
pub fn get_time_entries(state: State<AppState>, start_date: Option<String>, end_date: Option<String>) -> Result<Vec<TimeEntry>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let query = match (start_date, end_date) {
        (Some(start), Some(end)) => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, task_id, description, start_time, end_time, duration, billable
                 FROM time_entries
                 WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)
                 ORDER BY start_time DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map(params![start, end], |row| {
                Ok(TimeEntry {
                    id: Some(row.get(0)?),
                    project_id: row.get(1)?,
                    task_id: row.get(2)?,
                    description: row.get(3)?,
                    start_time: row.get(4)?,
                    end_time: row.get(5)?,
                    duration: row.get(6)?,
                    billable: row.get(7)?,
                })
            }).map_err(|e| e.to_string())?;
            rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
        }
        _ => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, task_id, description, start_time, end_time, duration, billable
                 FROM time_entries ORDER BY start_time DESC LIMIT 100"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map([], |row| {
                Ok(TimeEntry {
                    id: Some(row.get(0)?),
                    project_id: row.get(1)?,
                    task_id: row.get(2)?,
                    description: row.get(3)?,
                    start_time: row.get(4)?,
                    end_time: row.get(5)?,
                    duration: row.get(6)?,
                    billable: row.get(7)?,
                })
            }).map_err(|e| e.to_string())?;
            rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
        }
    };
    Ok(query)
}

#[tauri::command]
pub fn create_time_entry(state: State<AppState>, entry: TimeEntry) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let week_start: String = conn.query_row(
        "SELECT date(?1, 'weekday 0', '-6 days')",
        params![entry.start_time],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    let locked: bool = conn.query_row(
        "SELECT COUNT(*) FROM timesheet_locks WHERE week_start = ?1",
        params![week_start],
        |row| row.get::<_, i64>(0),
    ).map_err(|e| e.to_string())? > 0;
    if locked {
        return Err("Cannot modify entries in a locked week".to_string());
    }
    conn.execute(
        "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration, billable)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
        params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration, entry.billable],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_time_entry(state: State<AppState>, entry: TimeEntry) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let week_start: String = conn.query_row(
        "SELECT date(?1, 'weekday 0', '-6 days')",
        params![entry.start_time],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    let locked: bool = conn.query_row(
        "SELECT COUNT(*) FROM timesheet_locks WHERE week_start = ?1",
        params![week_start],
        |row| row.get::<_, i64>(0),
    ).map_err(|e| e.to_string())? > 0;
    if locked {
        return Err("Cannot modify entries in a locked week".to_string());
    }
    conn.execute(
        "UPDATE time_entries SET project_id = ?1, task_id = ?2, description = ?3,
         start_time = ?4, end_time = ?5, duration = ?6, billable = ?7 WHERE id = ?8",
        params![entry.project_id, entry.task_id, entry.description, entry.start_time, entry.end_time, entry.duration, entry.billable, entry.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_time_entry(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let entry_start: String = conn.query_row(
        "SELECT start_time FROM time_entries WHERE id = ?1",
        params![id],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    let week_start: String = conn.query_row(
        "SELECT date(?1, 'weekday 0', '-6 days')",
        params![entry_start],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    let locked: bool = conn.query_row(
        "SELECT COUNT(*) FROM timesheet_locks WHERE week_start = ?1",
        params![week_start],
        |row| row.get::<_, i64>(0),
    ).map_err(|e| e.to_string())? > 0;
    if locked {
        return Err("Cannot modify entries in a locked week".to_string());
    }
    conn.execute("DELETE FROM invoice_items WHERE time_entry_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM entry_tags WHERE entry_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM time_entries WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
    Ok(())
}

// Reports
#[tauri::command]
pub fn get_reports(state: State<AppState>, start_date: String, end_date: String) -> Result<serde_json::Value, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    // Total hours
    let total: i64 = conn.query_row(
        "SELECT COALESCE(SUM(duration), 0) FROM time_entries
         WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)",
        params![start_date, end_date],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    // By project - include project_id so frontend can look up name/color/rate
    let mut stmt = conn.prepare(
        "SELECT p.id, p.name, p.color, SUM(t.duration) as total_duration
         FROM time_entries t
         JOIN projects p ON t.project_id = p.id
         WHERE date(t.start_time) >= date(?1) AND date(t.start_time) <= date(?2)
         GROUP BY p.id
         ORDER BY total_duration DESC"
    ).map_err(|e| e.to_string())?;

    let by_project: Vec<serde_json::Value> = stmt.query_map(params![start_date, end_date], |row| {
        Ok(serde_json::json!({
            "project_id": row.get::<_, i64>(0)?,
            "name": row.get::<_, String>(1)?,
            "color": row.get::<_, String>(2)?,
            "total_seconds": row.get::<_, i64>(3)?
        }))
    }).map_err(|e| e.to_string())?
    .collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;

    Ok(serde_json::json!({
        "totalSeconds": total,
        "byProject": by_project
    }))
}

// Invoice commands
#[tauri::command]
pub fn create_invoice(state: State<AppState>, invoice: Invoice) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
        params![invoice.client_id, invoice.invoice_number, invoice.date, invoice.due_date,
                invoice.subtotal, invoice.tax_rate, invoice.tax_amount, invoice.discount,
                invoice.total, invoice.notes, invoice.status, invoice.template_id],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn get_invoices(state: State<AppState>) -> Result<Vec<Invoice>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id
         FROM invoices ORDER BY date DESC"
    ).map_err(|e| e.to_string())?;
    let invoices = stmt.query_map([], |row| {
        Ok(Invoice {
            id: Some(row.get(0)?),
            client_id: row.get(1)?,
            invoice_number: row.get(2)?,
            date: row.get(3)?,
            due_date: row.get(4)?,
            subtotal: row.get(5)?,
            tax_rate: row.get(6)?,
            tax_amount: row.get(7)?,
            discount: row.get(8)?,
            total: row.get(9)?,
            notes: row.get(10)?,
            status: row.get(11)?,
            template_id: row.get(12)?,
        })
    }).map_err(|e| e.to_string())?;
    invoices.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn update_invoice(state: State<AppState>, invoice: Invoice) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE invoices SET client_id = ?1, invoice_number = ?2, date = ?3, due_date = ?4,
         subtotal = ?5, tax_rate = ?6, tax_amount = ?7, discount = ?8, total = ?9, notes = ?10, status = ?11, template_id = ?12
         WHERE id = ?13",
        params![invoice.client_id, invoice.invoice_number, invoice.date, invoice.due_date,
                invoice.subtotal, invoice.tax_rate, invoice.tax_amount, invoice.discount,
                invoice.total, invoice.notes, invoice.status, invoice.template_id, invoice.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_invoice(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM invoice_payments WHERE invoice_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM invoice_items WHERE invoice_id = ?1", params![id]).map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM invoices WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn update_invoice_template(state: State<AppState>, id: i64, template_id: String) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE invoices SET template_id = ?1 WHERE id = ?2",
        params![template_id, id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

// Invoice items
#[derive(Debug, Serialize, Deserialize)]
pub struct InvoiceItem {
    pub id: Option<i64>,
    pub invoice_id: i64,
    pub description: String,
    pub quantity: f64,
    pub rate: f64,
    pub amount: f64,
    pub time_entry_id: Option<i64>,
}

#[tauri::command]
pub fn get_invoice_items(state: State<AppState>, invoice_id: i64) -> Result<Vec<InvoiceItem>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, invoice_id, description, quantity, rate, amount, time_entry_id
         FROM invoice_items WHERE invoice_id = ?1 ORDER BY id"
    ).map_err(|e| e.to_string())?;
    let items = stmt.query_map(params![invoice_id], |row| {
        Ok(InvoiceItem {
            id: Some(row.get(0)?),
            invoice_id: row.get(1)?,
            description: row.get(2)?,
            quantity: row.get(3)?,
            rate: row.get(4)?,
            amount: row.get(5)?,
            time_entry_id: row.get(6)?,
        })
    }).map_err(|e| e.to_string())?;
    items.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_invoice_item(state: State<AppState>, item: InvoiceItem) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount, time_entry_id)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
        params![item.invoice_id, item.description, item.quantity, item.rate, item.amount, item.time_entry_id],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn delete_invoice_items(state: State<AppState>, invoice_id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM invoice_items WHERE invoice_id = ?1", params![invoice_id]).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn save_invoice_items_batch(
    state: State<AppState>,
    invoice_id: i64,
    items: Vec<serde_json::Value>,
) -> Result<Vec<i64>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("BEGIN TRANSACTION", []).map_err(|e| e.to_string())?;

    // Delete existing items first
    if let Err(e) = conn.execute("DELETE FROM invoice_items WHERE invoice_id = ?1", params![invoice_id]) {
        let _ = conn.execute("ROLLBACK", []);
        return Err(e.to_string());
    }

    let mut ids = Vec::new();
    for item in &items {
        let description = item.get("description").and_then(|v| v.as_str()).unwrap_or("");
        let quantity = item.get("quantity").and_then(|v| v.as_f64()).unwrap_or(0.0);
        let unit_price = item.get("unit_price").and_then(|v| v.as_f64()).unwrap_or(0.0);
        let amount = quantity * unit_price;
        let time_entry_id = item.get("time_entry_id").and_then(|v| v.as_i64());

        match conn.execute(
            "INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount, time_entry_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            params![invoice_id, description, quantity, unit_price, amount, time_entry_id],
        ) {
            Ok(_) => ids.push(conn.last_insert_rowid()),
            Err(e) => {
                let _ = conn.execute("ROLLBACK", []);
                return Err(format!("Failed to save item: {}", e));
            }
        }
    }

    conn.execute("COMMIT", []).map_err(|e| e.to_string())?;
    Ok(ids)
}

// Settings commands
#[tauri::command]
pub fn get_settings(state: State<AppState>) -> Result<std::collections::HashMap<String, String>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?;
    let settings = stmt.query_map([], |row| {
        Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
    }).map_err(|e| e.to_string())?;

    let mut result = std::collections::HashMap::new();
    for setting in settings {
        let (key, value) = setting.map_err(|e| e.to_string())?;
        result.insert(key, value);
    }
    Ok(result)
}

#[tauri::command]
pub fn update_settings(state: State<AppState>, key: String, value: String) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
        params![key, value],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

// Export all data as JSON
#[tauri::command]
pub fn export_data(state: State<AppState>) -> Result<serde_json::Value, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    let clients = {
        let mut stmt = conn.prepare("SELECT id, name, email, address, company, phone, tax_id, payment_terms, notes, currency FROM clients").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "name": row.get::<_, String>(1)?,
                "email": row.get::<_, Option<String>>(2)?,
                "address": row.get::<_, Option<String>>(3)?,
                "company": row.get::<_, Option<String>>(4)?,
                "phone": row.get::<_, Option<String>>(5)?,
                "tax_id": row.get::<_, Option<String>>(6)?,
                "payment_terms": row.get::<_, Option<String>>(7)?,
                "notes": row.get::<_, Option<String>>(8)?,
                "currency": row.get::<_, Option<String>>(9)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let projects = {
        let mut stmt = conn.prepare("SELECT id, client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency FROM projects").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "client_id": row.get::<_, Option<i64>>(1)?,
                "name": row.get::<_, String>(2)?,
                "hourly_rate": row.get::<_, f64>(3)?,
                "color": row.get::<_, String>(4)?,
                "archived": row.get::<_, i32>(5)? != 0,
                "budget_hours": row.get::<_, Option<f64>>(6)?,
                "budget_amount": row.get::<_, Option<f64>>(7)?,
                "rounding_override": row.get::<_, Option<i32>>(8)?,
                "notes": row.get::<_, Option<String>>(9)?,
                "currency": row.get::<_, Option<String>>(10)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let tasks = {
        let mut stmt = conn.prepare("SELECT id, project_id, name, estimated_hours, hourly_rate FROM tasks").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "name": row.get::<_, String>(2)?,
                "estimated_hours": row.get::<_, Option<f64>>(3)?,
                "hourly_rate": row.get::<_, Option<f64>>(4)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let time_entries = {
        let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, start_time, end_time, duration, billable FROM time_entries").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "task_id": row.get::<_, Option<i64>>(2)?,
                "description": row.get::<_, Option<String>>(3)?,
                "start_time": row.get::<_, String>(4)?,
                "end_time": row.get::<_, Option<String>>(5)?,
                "duration": row.get::<_, i64>(6)?,
                "billable": row.get::<_, Option<i64>>(7)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let tags = {
        let mut stmt = conn.prepare("SELECT id, name, color FROM tags").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "name": row.get::<_, String>(1)?,
                "color": row.get::<_, Option<String>>(2)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let entry_tags = {
        let mut stmt = conn.prepare("SELECT entry_id, tag_id FROM entry_tags").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "entry_id": row.get::<_, i64>(0)?,
                "tag_id": row.get::<_, i64>(1)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let invoices = {
        let mut stmt = conn.prepare("SELECT id, client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id FROM invoices").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "client_id": row.get::<_, i64>(1)?,
                "invoice_number": row.get::<_, String>(2)?,
                "date": row.get::<_, String>(3)?,
                "due_date": row.get::<_, Option<String>>(4)?,
                "subtotal": row.get::<_, f64>(5)?,
                "tax_rate": row.get::<_, f64>(6)?,
                "tax_amount": row.get::<_, f64>(7)?,
                "discount": row.get::<_, f64>(8)?,
                "total": row.get::<_, f64>(9)?,
                "notes": row.get::<_, Option<String>>(10)?,
                "status": row.get::<_, String>(11)?,
                "template_id": row.get::<_, Option<String>>(12)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let invoice_items = {
        let mut stmt = conn.prepare("SELECT id, invoice_id, description, quantity, rate, amount, time_entry_id FROM invoice_items").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "invoice_id": row.get::<_, i64>(1)?,
                "description": row.get::<_, String>(2)?,
                "quantity": row.get::<_, f64>(3)?,
                "rate": row.get::<_, f64>(4)?,
                "amount": row.get::<_, f64>(5)?,
                "time_entry_id": row.get::<_, Option<i64>>(6)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let tracked_apps = {
        let mut stmt = conn.prepare("SELECT id, project_id, exe_name, exe_path, display_name FROM tracked_apps").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "exe_name": row.get::<_, String>(2)?,
                "exe_path": row.get::<_, Option<String>>(3)?,
                "display_name": row.get::<_, Option<String>>(4)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let favorites = {
        let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, sort_order FROM favorites").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "task_id": row.get::<_, Option<i64>>(2)?,
                "description": row.get::<_, Option<String>>(3)?,
                "sort_order": row.get::<_, i32>(4)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let recurring_entries = {
        let mut stmt = conn.prepare("SELECT id, project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered FROM recurring_entries").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "task_id": row.get::<_, Option<i64>>(2)?,
                "description": row.get::<_, Option<String>>(3)?,
                "duration": row.get::<_, i64>(4)?,
                "recurrence_rule": row.get::<_, String>(5)?,
                "time_of_day": row.get::<_, Option<String>>(6)?,
                "mode": row.get::<_, Option<String>>(7)?,
                "enabled": row.get::<_, i32>(8)?,
                "last_triggered": row.get::<_, Option<String>>(9)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let expenses = {
        let mut stmt = conn.prepare("SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced FROM expenses").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "client_id": row.get::<_, Option<i64>>(2)?,
                "category": row.get::<_, Option<String>>(3)?,
                "description": row.get::<_, Option<String>>(4)?,
                "amount": row.get::<_, f64>(5)?,
                "date": row.get::<_, String>(6)?,
                "receipt_path": row.get::<_, Option<String>>(7)?,
                "invoiced": row.get::<_, i32>(8)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let timeline_events = {
        let mut stmt = conn.prepare("SELECT id, project_id, exe_name, exe_path, window_title, started_at, ended_at, duration FROM timeline_events").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "project_id": row.get::<_, i64>(1)?,
                "exe_name": row.get::<_, Option<String>>(2)?,
                "exe_path": row.get::<_, Option<String>>(3)?,
                "window_title": row.get::<_, Option<String>>(4)?,
                "started_at": row.get::<_, String>(5)?,
                "ended_at": row.get::<_, Option<String>>(6)?,
                "duration": row.get::<_, i64>(7)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let calendar_sources = {
        let mut stmt = conn.prepare("SELECT id, name, type, url, last_synced, sync_interval, enabled FROM calendar_sources").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "name": row.get::<_, String>(1)?,
                "source_type": row.get::<_, String>(2)?,
                "url": row.get::<_, Option<String>>(3)?,
                "last_synced": row.get::<_, Option<String>>(4)?,
                "sync_interval": row.get::<_, i32>(5)?,
                "enabled": row.get::<_, i32>(6)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let calendar_events = {
        let mut stmt = conn.prepare("SELECT id, source_id, uid, summary, start_time, end_time, duration, location FROM calendar_events").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "source_id": row.get::<_, i64>(1)?,
                "uid": row.get::<_, Option<String>>(2)?,
                "summary": row.get::<_, Option<String>>(3)?,
                "start_time": row.get::<_, Option<String>>(4)?,
                "end_time": row.get::<_, Option<String>>(5)?,
                "duration": row.get::<_, i64>(6)?,
                "location": row.get::<_, Option<String>>(7)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let timesheet_locks = {
        let mut stmt = conn.prepare("SELECT id, week_start, status, locked_at FROM timesheet_locks").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "week_start": row.get::<_, String>(1)?,
                "status": row.get::<_, Option<String>>(2)?,
                "locked_at": row.get::<_, Option<String>>(3)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let entry_templates = {
        let mut stmt = conn.prepare("SELECT id, name, project_id, task_id, description, duration, billable FROM entry_templates").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "name": row.get::<_, String>(1)?,
                "project_id": row.get::<_, i64>(2)?,
                "task_id": row.get::<_, Option<i64>>(3)?,
                "description": row.get::<_, Option<String>>(4)?,
                "duration": row.get::<_, i64>(5)?,
                "billable": row.get::<_, i32>(6)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let timesheet_rows = {
        let mut stmt = conn.prepare("SELECT id, week_start, project_id, task_id, sort_order FROM timesheet_rows").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "week_start": row.get::<_, String>(1)?,
                "project_id": row.get::<_, i64>(2)?,
                "task_id": row.get::<_, Option<i64>>(3)?,
                "sort_order": row.get::<_, i32>(4)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let invoice_payments = {
        let mut stmt = conn.prepare("SELECT id, invoice_id, amount, date, method, notes, created_at FROM invoice_payments").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "invoice_id": row.get::<_, i64>(1)?,
                "amount": row.get::<_, f64>(2)?,
                "date": row.get::<_, String>(3)?,
                "method": row.get::<_, Option<String>>(4)?,
                "notes": row.get::<_, Option<String>>(5)?,
                "created_at": row.get::<_, Option<String>>(6)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let recurring_invoices = {
        let mut stmt = conn.prepare("SELECT id, client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled, created_at FROM recurring_invoices").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "client_id": row.get::<_, i64>(1)?,
                "template_id": row.get::<_, Option<String>>(2)?,
                "line_items_json": row.get::<_, String>(3)?,
                "tax_rate": row.get::<_, f64>(4)?,
                "discount": row.get::<_, f64>(5)?,
                "notes": row.get::<_, Option<String>>(6)?,
                "recurrence_rule": row.get::<_, String>(7)?,
                "next_due_date": row.get::<_, String>(8)?,
                "enabled": row.get::<_, i32>(9)?,
                "created_at": row.get::<_, Option<String>>(10)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    let settings = {
        let mut stmt = conn.prepare("SELECT key, value FROM settings").map_err(|e| e.to_string())?;
        let rows: Vec<serde_json::Value> = stmt.query_map([], |row| {
            Ok(serde_json::json!({
                "key": row.get::<_, String>(0)?,
                "value": row.get::<_, Option<String>>(1)?
            }))
        }).map_err(|e| e.to_string())?.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;
        rows
    };

    Ok(serde_json::json!({
        "clients": clients,
        "projects": projects,
        "tasks": tasks,
        "time_entries": time_entries,
        "tags": tags,
        "entry_tags": entry_tags,
        "invoices": invoices,
        "invoice_items": invoice_items,
        "invoice_payments": invoice_payments,
        "recurring_invoices": recurring_invoices,
        "tracked_apps": tracked_apps,
        "favorites": favorites,
        "recurring_entries": recurring_entries,
        "expenses": expenses,
        "timeline_events": timeline_events,
        "calendar_sources": calendar_sources,
        "calendar_events": calendar_events,
        "timesheet_locks": timesheet_locks,
        "entry_templates": entry_templates,
        "timesheet_rows": timesheet_rows,
        "settings": settings
    }))
}

// Clear all data
#[tauri::command]
pub fn clear_all_data(state: State<AppState>) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute_batch(
        "DELETE FROM entry_tags;
         DELETE FROM invoice_payments;
         DELETE FROM invoice_items;
         DELETE FROM recurring_invoices;
         DELETE FROM invoices;
         DELETE FROM favorites;
         DELETE FROM recurring_entries;
         DELETE FROM entry_templates;
         DELETE FROM timesheet_rows;
         DELETE FROM timesheet_locks;
         DELETE FROM timeline_events;
         DELETE FROM expenses;
         DELETE FROM tracked_apps;
         DELETE FROM time_entries;
         DELETE FROM tasks;
         DELETE FROM projects;
         DELETE FROM clients;
         DELETE FROM tags;
         DELETE FROM calendar_events;
         DELETE FROM calendar_sources;"
    ).map_err(|e| e.to_string())?;
    Ok(())
}

// Tracked Apps struct
#[derive(Debug, Serialize, Deserialize)]
pub struct TrackedApp {
    pub id: Option<i64>,
    pub project_id: i64,
    pub exe_name: String,
    pub exe_path: Option<String>,
    pub display_name: Option<String>,
}

// OS Detection commands
#[tauri::command]
pub fn get_idle_seconds() -> Result<u64, String> {
    Ok(os_detection::get_system_idle_seconds())
}

#[tauri::command]
pub fn get_visible_windows() -> Result<Vec<os_detection::WindowInfo>, String> {
    Ok(os_detection::enumerate_visible_windows())
}

#[tauri::command]
pub fn get_running_processes() -> Result<Vec<os_detection::WindowInfo>, String> {
    Ok(os_detection::enumerate_running_processes())
}

// Tracked Apps CRUD commands
#[tauri::command]
pub fn get_tracked_apps(state: State<AppState>, project_id: i64) -> Result<Vec<TrackedApp>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, project_id, exe_name, exe_path, display_name FROM tracked_apps WHERE project_id = ?1 ORDER BY display_name"
    ).map_err(|e| e.to_string())?;
    let apps = stmt.query_map(params![project_id], |row| {
        Ok(TrackedApp {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            exe_name: row.get(2)?,
            exe_path: row.get(3)?,
            display_name: row.get(4)?,
        })
    }).map_err(|e| e.to_string())?;
    apps.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn add_tracked_app(state: State<AppState>, app: TrackedApp) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO tracked_apps (project_id, exe_name, exe_path, display_name) VALUES (?1, ?2, ?3, ?4)",
        params![app.project_id, app.exe_name, app.exe_path, app.display_name],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn remove_tracked_app(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM tracked_apps WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
    Ok(())
}

// Tag structs and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct Tag {
    pub id: Option<i64>,
    pub name: String,
    pub color: String,
}

#[tauri::command]
pub fn get_tags(state: State<AppState>) -> Result<Vec<Tag>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare("SELECT id, name, color FROM tags ORDER BY name")
        .map_err(|e| e.to_string())?;
    let tags = stmt.query_map([], |row| {
        Ok(Tag {
            id: Some(row.get(0)?),
            name: row.get(1)?,
            color: row.get(2)?,
        })
    }).map_err(|e| e.to_string())?;
    tags.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_tag(state: State<AppState>, tag: Tag) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO tags (name, color) VALUES (?1, ?2)",
        params![tag.name, tag.color],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_tag(state: State<AppState>, tag: Tag) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE tags SET name = ?1, color = ?2 WHERE id = ?3",
        params![tag.name, tag.color, tag.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_tag(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM entry_tags WHERE tag_id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM tags WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn get_entry_tags(state: State<AppState>, entry_id: i64) -> Result<Vec<Tag>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT t.id, t.name, t.color FROM tags t
         JOIN entry_tags et ON t.id = et.tag_id
         WHERE et.entry_id = ?1 ORDER BY t.name"
    ).map_err(|e| e.to_string())?;
    let tags = stmt.query_map(params![entry_id], |row| {
        Ok(Tag {
            id: Some(row.get(0)?),
            name: row.get(1)?,
            color: row.get(2)?,
        })
    }).map_err(|e| e.to_string())?;
    tags.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn set_entry_tags(state: State<AppState>, entry_id: i64, tag_ids: Vec<i64>) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM entry_tags WHERE entry_id = ?1", params![entry_id])
        .map_err(|e| e.to_string())?;
    for tag_id in tag_ids {
        conn.execute(
            "INSERT INTO entry_tags (entry_id, tag_id) VALUES (?1, ?2)",
            params![entry_id, tag_id],
        ).map_err(|e| e.to_string())?;
    }
    Ok(())
}

#[tauri::command]
pub fn get_project_budget_status(state: State<AppState>, project_id: i64, today: String) -> Result<serde_json::Value, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    let total_seconds: i64 = conn.query_row(
        "SELECT COALESCE(SUM(duration), 0) FROM time_entries WHERE project_id = ?1",
        params![project_id],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    let project_row: (Option<f64>, Option<f64>, f64) = conn.query_row(
        "SELECT budget_hours, budget_amount, hourly_rate FROM projects WHERE id = ?1",
        params![project_id],
        |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
    ).map_err(|e| e.to_string())?;

    let hours_used = total_seconds as f64 / 3600.0;
    let amount_used = hours_used * project_row.2;

    // Daily average based on last 14 days
    let fourteen_days_ago: String = conn.query_row(
        "SELECT date(?1, '-14 days')",
        params![today],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    let recent_seconds: i64 = conn.query_row(
        "SELECT COALESCE(SUM(duration), 0) FROM time_entries
         WHERE project_id = ?1 AND date(start_time) >= date(?2)",
        params![project_id, fourteen_days_ago],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    let daily_avg_hours = (recent_seconds as f64 / 3600.0) / 14.0;
    let hours_remaining = project_row.0.map(|b| (b - hours_used).max(0.0));
    let est_completion_days = hours_remaining.map(|r| {
        if daily_avg_hours > 0.0 { (r / daily_avg_hours).ceil() } else { -1.0 }
    });
    let pace = hours_remaining.map(|remaining| {
        if remaining <= 0.0 { "complete" }
        else if daily_avg_hours <= 0.0 { "behind" }
        else {
            let expected_days = remaining / daily_avg_hours;
            if expected_days <= 7.0 { "ahead" }
            else if expected_days <= 30.0 { "on_track" }
            else { "behind" }
        }
    });

    Ok(serde_json::json!({
        "hours_used": hours_used,
        "amount_used": amount_used,
        "budget_hours": project_row.0,
        "budget_amount": project_row.1,
        "percent_hours": project_row.0.map(|b| if b > 0.0 { (hours_used / b) * 100.0 } else { 0.0 }),
        "percent_amount": project_row.1.map(|b| if b > 0.0 { (amount_used / b) * 100.0 } else { 0.0 }),
        "daily_average_hours": daily_avg_hours,
        "estimated_completion_days": est_completion_days,
        "hours_remaining": hours_remaining,
        "pace": pace
    }))
}

// Favorite structs and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct Favorite {
    pub id: Option<i64>,
    pub project_id: i64,
    pub task_id: Option<i64>,
    pub description: Option<String>,
    pub sort_order: i32,
}

#[tauri::command]
pub fn get_favorites(state: State<AppState>) -> Result<Vec<Favorite>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, project_id, task_id, description, sort_order FROM favorites ORDER BY sort_order"
    ).map_err(|e| e.to_string())?;
    let favs = stmt.query_map([], |row| {
        Ok(Favorite {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            task_id: row.get(2)?,
            description: row.get(3)?,
            sort_order: row.get(4)?,
        })
    }).map_err(|e| e.to_string())?;
    favs.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_favorite(state: State<AppState>, fav: Favorite) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO favorites (project_id, task_id, description, sort_order) VALUES (?1, ?2, ?3, ?4)",
        params![fav.project_id, fav.task_id, fav.description, fav.sort_order],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn delete_favorite(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM favorites WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn reorder_favorites(state: State<AppState>, ids: Vec<i64>) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    for (i, id) in ids.iter().enumerate() {
        conn.execute(
            "UPDATE favorites SET sort_order = ?1 WHERE id = ?2",
            params![i as i32, id],
        ).map_err(|e| e.to_string())?;
    }
    Ok(())
}

// Recurring entry struct and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct RecurringEntry {
    pub id: Option<i64>,
    pub project_id: i64,
    pub task_id: Option<i64>,
    pub description: Option<String>,
    pub duration: i64,
    pub recurrence_rule: String,
    pub time_of_day: String,
    pub mode: String,
    pub enabled: Option<i64>,
    pub last_triggered: Option<String>,
}

#[tauri::command]
pub fn get_recurring_entries(state: State<AppState>) -> Result<Vec<RecurringEntry>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered
         FROM recurring_entries ORDER BY created_at"
    ).map_err(|e| e.to_string())?;
    let entries = stmt.query_map([], |row| {
        Ok(RecurringEntry {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            task_id: row.get(2)?,
            description: row.get(3)?,
            duration: row.get(4)?,
            recurrence_rule: row.get(5)?,
            time_of_day: row.get(6)?,
            mode: row.get(7)?,
            enabled: row.get(8)?,
            last_triggered: row.get(9)?,
        })
    }).map_err(|e| e.to_string())?;
    entries.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_recurring_entry(state: State<AppState>, entry: RecurringEntry) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO recurring_entries (project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
        params![entry.project_id, entry.task_id, entry.description, entry.duration,
                entry.recurrence_rule, entry.time_of_day, entry.mode, entry.enabled.unwrap_or(1)],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_recurring_entry(state: State<AppState>, entry: RecurringEntry) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE recurring_entries SET project_id = ?1, task_id = ?2, description = ?3, duration = ?4,
         recurrence_rule = ?5, time_of_day = ?6, mode = ?7, enabled = ?8 WHERE id = ?9",
        params![entry.project_id, entry.task_id, entry.description, entry.duration,
                entry.recurrence_rule, entry.time_of_day, entry.mode, entry.enabled, entry.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_recurring_entry(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM recurring_entries WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn update_recurring_last_triggered(state: State<AppState>, id: i64, last_triggered: String) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE recurring_entries SET last_triggered = ?1 WHERE id = ?2",
        params![last_triggered, id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

// Expense struct and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct Expense {
    pub id: Option<i64>,
    pub project_id: i64,
    pub client_id: Option<i64>,
    pub category: String,
    pub description: Option<String>,
    pub amount: f64,
    pub date: String,
    pub receipt_path: Option<String>,
    pub invoiced: Option<i64>,
}

#[tauri::command]
pub fn get_expenses(state: State<AppState>, project_id: Option<i64>, start_date: Option<String>, end_date: Option<String>) -> Result<Vec<Expense>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    let map_row = |row: &rusqlite::Row| -> rusqlite::Result<Expense> {
        Ok(Expense {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            client_id: row.get(2)?,
            category: row.get(3)?,
            description: row.get(4)?,
            amount: row.get(5)?,
            date: row.get(6)?,
            receipt_path: row.get(7)?,
            invoiced: row.get(8)?,
        })
    };

    let results: Vec<Expense> = match (project_id, &start_date, &end_date) {
        (Some(pid), Some(start), Some(end)) => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced
                 FROM expenses WHERE project_id = ?1 AND date >= ?2 AND date <= ?3 ORDER BY date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map(params![pid, start, end], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
        (Some(pid), _, _) => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced
                 FROM expenses WHERE project_id = ?1 ORDER BY date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map(params![pid], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
        (_, Some(start), Some(end)) => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced
                 FROM expenses WHERE date >= ?1 AND date <= ?2 ORDER BY date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map(params![start, end], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
        _ => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced
                 FROM expenses ORDER BY date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map([], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
    };

    Ok(results)
}

#[tauri::command]
pub fn create_expense(state: State<AppState>, expense: Expense) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO expenses (project_id, client_id, category, description, amount, date, receipt_path, invoiced)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
        params![expense.project_id, expense.client_id, expense.category, expense.description,
                expense.amount, expense.date, expense.receipt_path, expense.invoiced.unwrap_or(0)],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_expense(state: State<AppState>, expense: Expense) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE expenses SET project_id = ?1, client_id = ?2, category = ?3, description = ?4,
         amount = ?5, date = ?6, receipt_path = ?7, invoiced = ?8 WHERE id = ?9",
        params![expense.project_id, expense.client_id, expense.category, expense.description,
                expense.amount, expense.date, expense.receipt_path, expense.invoiced, expense.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_expense(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM expenses WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn get_uninvoiced_expenses(state: State<AppState>, project_id: Option<i64>, client_id: Option<i64>) -> Result<Vec<Expense>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    let map_row = |row: &rusqlite::Row| -> rusqlite::Result<Expense> {
        Ok(Expense {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            client_id: row.get(2)?,
            category: row.get(3)?,
            description: row.get(4)?,
            amount: row.get(5)?,
            date: row.get(6)?,
            receipt_path: row.get(7)?,
            invoiced: row.get(8)?,
        })
    };

    let results: Vec<Expense> = match (project_id, client_id) {
        (Some(pid), _) => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced
                 FROM expenses WHERE invoiced = 0 AND project_id = ?1 ORDER BY date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map(params![pid], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
        (_, Some(cid)) => {
            let mut stmt = conn.prepare(
                "SELECT e.id, e.project_id, e.client_id, e.category, e.description, e.amount, e.date, e.receipt_path, e.invoiced
                 FROM expenses e JOIN projects p ON e.project_id = p.id
                 WHERE e.invoiced = 0 AND p.client_id = ?1 ORDER BY e.date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map(params![cid], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
        _ => {
            let mut stmt = conn.prepare(
                "SELECT id, project_id, client_id, category, description, amount, date, receipt_path, invoiced
                 FROM expenses WHERE invoiced = 0 ORDER BY date DESC"
            ).map_err(|e| e.to_string())?;
            let rows = stmt.query_map([], map_row)
                .map_err(|e| e.to_string())?
                .collect::<Result<Vec<_>, _>>()
                .map_err(|e| e.to_string())?;
            rows
        },
    };

    Ok(results)
}

#[tauri::command]
pub fn mark_expenses_invoiced(state: State<AppState>, ids: Vec<i64>) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    for id in ids {
        conn.execute("UPDATE expenses SET invoiced = 1 WHERE id = ?1", params![id])
            .map_err(|e| e.to_string())?;
    }
    Ok(())
}

// Timeline event struct and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct TimelineEvent {
    pub id: Option<i64>,
    pub project_id: i64,
    pub exe_name: Option<String>,
    pub exe_path: Option<String>,
    pub window_title: Option<String>,
    pub started_at: String,
    pub ended_at: Option<String>,
    pub duration: Option<i64>,
}

#[tauri::command]
pub fn get_timeline_events(state: State<AppState>, project_id: i64, date: String) -> Result<Vec<TimelineEvent>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, project_id, exe_name, exe_path, window_title, started_at, ended_at, duration
         FROM timeline_events WHERE project_id = ?1 AND date(started_at) = ?2 ORDER BY started_at ASC"
    ).map_err(|e| e.to_string())?;
    let events = stmt.query_map(params![project_id, date], |row| {
        Ok(TimelineEvent {
            id: Some(row.get(0)?),
            project_id: row.get(1)?,
            exe_name: row.get(2)?,
            exe_path: row.get(3)?,
            window_title: row.get(4)?,
            started_at: row.get(5)?,
            ended_at: row.get(6)?,
            duration: row.get(7)?,
        })
    }).map_err(|e| e.to_string())?;
    events.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_timeline_event(state: State<AppState>, event: TimelineEvent) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO timeline_events (project_id, exe_name, exe_path, window_title, started_at, ended_at, duration)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
        params![event.project_id, event.exe_name, event.exe_path, event.window_title,
                event.started_at, event.ended_at, event.duration.unwrap_or(0)],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_timeline_event_ended(state: State<AppState>, id: i64, ended_at: String, duration: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE timeline_events SET ended_at = ?1, duration = ?2 WHERE id = ?3",
        params![ended_at, duration, id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_timeline_events(state: State<AppState>, project_id: i64, date: String) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "DELETE FROM timeline_events WHERE project_id = ?1 AND date(started_at) = ?2",
        params![project_id, date],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn clear_all_timeline_data(state: State<AppState>) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM timeline_events", []).map_err(|e| e.to_string())?;
    Ok(())
}

// Goals command
#[tauri::command]
pub fn get_goal_progress(state: State<AppState>, today: String) -> Result<serde_json::Value, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    // Today's total seconds
    let today_seconds: i64 = conn.query_row(
        "SELECT COALESCE(SUM(duration), 0) FROM time_entries WHERE date(start_time) = date(?1)",
        params![today],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    // This week's total seconds (Monday to Sunday, ISO week)
    let week_seconds: i64 = conn.query_row(
        "SELECT COALESCE(SUM(duration), 0) FROM time_entries
         WHERE strftime('%Y-%W', start_time) = strftime('%Y-%W', ?1)",
        params![today],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    // Streak: count consecutive days with entries going backwards from today
    let mut streak_days: i64 = 0;
    let mut check_date = today.clone();
    loop {
        let has_entry: i64 = conn.query_row(
            "SELECT COUNT(*) FROM time_entries WHERE date(start_time) = date(?1)",
            params![check_date],
            |row| row.get(0),
        ).map_err(|e| e.to_string())?;
        if has_entry > 0 {
            streak_days += 1;
            // Go to previous day
            check_date = conn.query_row(
                "SELECT date(?1, '-1 day')",
                params![check_date],
                |row| row.get::<_, String>(0),
            ).map_err(|e| e.to_string())?;
        } else {
            break;
        }
    }

    Ok(serde_json::json!({
        "today_seconds": today_seconds,
        "week_seconds": week_seconds,
        "streak_days": streak_days
    }))
}

// Profitability report command - includes expenses for net profit
#[tauri::command]
pub fn get_profitability_report(state: State<AppState>, start_date: String, end_date: String) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    let mut stmt = conn.prepare(
        "SELECT p.id, p.name, p.color, p.hourly_rate, p.budget_hours, p.budget_amount,
                c.name as client_name,
                COALESCE(SUM(t.duration), 0) as total_seconds
         FROM projects p
         LEFT JOIN clients c ON p.client_id = c.id
         LEFT JOIN time_entries t ON t.project_id = p.id
             AND date(t.start_time) >= date(?1)
             AND date(t.start_time) <= date(?2)
         GROUP BY p.id
         ORDER BY total_seconds DESC"
    ).map_err(|e| e.to_string())?;

    let rows: Vec<serde_json::Value> = stmt.query_map(params![start_date, end_date], |row| {
        let project_id: i64 = row.get(0)?;
        let total_seconds: i64 = row.get(7)?;
        let hourly_rate: f64 = row.get(3)?;
        let hours = total_seconds as f64 / 3600.0;
        let revenue = hours * hourly_rate;
        let budget_hours: Option<f64> = row.get(4)?;
        let budget_amount: Option<f64> = row.get(5)?;

        Ok(serde_json::json!({
            "project_id": project_id,
            "project_name": row.get::<_, String>(1)?,
            "color": row.get::<_, String>(2)?,
            "hourly_rate": hourly_rate,
            "client_name": row.get::<_, Option<String>>(6)?,
            "total_seconds": total_seconds,
            "total_hours": hours,
            "revenue": revenue,
            "budget_hours": budget_hours,
            "budget_amount": budget_amount,
            "budget_used_pct": budget_hours.map(|b| if b > 0.0 { (hours / b) * 100.0 } else { 0.0 }),
            "percent_amount": budget_amount.map(|b| if b > 0.0 { (revenue / b) * 100.0 } else { 0.0 })
        }))
    }).map_err(|e| e.to_string())?
    .collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?;

    // Add expense totals per project for the date range
    let mut result: Vec<serde_json::Value> = Vec::new();
    for mut row in rows {
        let pid = row["project_id"].as_i64().unwrap_or(0);
        let expense_total: f64 = conn.query_row(
            "SELECT COALESCE(SUM(amount), 0) FROM expenses WHERE project_id = ?1 AND date >= ?2 AND date <= ?3",
            params![pid, start_date, end_date],
            |r| r.get(0),
        ).unwrap_or(0.0);
        let revenue = row["revenue"].as_f64().unwrap_or(0.0);
        row.as_object_mut().unwrap().insert("expenses".into(), serde_json::json!(expense_total));
        row.as_object_mut().unwrap().insert("net_profit".into(), serde_json::json!(revenue - expense_total));
        result.push(row);
    }

    Ok(result)
}

// Timesheet data command
#[tauri::command]
pub fn get_timesheet_data(state: State<AppState>, week_start: String) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    // Get the 7 days of the week
    let mut days: Vec<String> = Vec::new();
    for i in 0..7 {
        let day: String = conn.query_row(
            &format!("SELECT date(?1, '+{} days')", i),
            params![week_start],
            |row| row.get(0),
        ).map_err(|e| e.to_string())?;
        days.push(day);
    }

    let mut stmt = conn.prepare(
        "SELECT p.id, p.name, p.color, t2.id as task_id, t2.name as task_name,
                date(te.start_time) as entry_date, COALESCE(SUM(te.duration), 0) as total_seconds
         FROM time_entries te
         JOIN projects p ON te.project_id = p.id
         LEFT JOIN tasks t2 ON te.task_id = t2.id
         WHERE date(te.start_time) >= date(?1)
           AND date(te.start_time) < date(?1, '+7 days')
         GROUP BY p.id, t2.id, date(te.start_time)
         ORDER BY p.name, t2.name"
    ).map_err(|e| e.to_string())?;

    let raw_rows: Vec<(i64, String, String, Option<i64>, Option<String>, String, i64)> = stmt
        .query_map(params![week_start], |row| {
            Ok((
                row.get(0)?,
                row.get(1)?,
                row.get(2)?,
                row.get(3)?,
                row.get(4)?,
                row.get(5)?,
                row.get(6)?,
            ))
        })
        .map_err(|e| e.to_string())?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())?;

    // Group by project+task
    let mut grouped: std::collections::HashMap<(i64, Option<i64>), serde_json::Value> = std::collections::HashMap::new();
    for (project_id, project_name, color, task_id, task_name, entry_date, total_seconds) in &raw_rows {
        let key = (*project_id, *task_id);
        let entry = grouped.entry(key).or_insert_with(|| {
            serde_json::json!({
                "project_id": project_id,
                "project_name": project_name,
                "color": color,
                "task_id": task_id,
                "task_name": task_name,
                "days": vec![0i64; 7]
            })
        });
        // Find which day index this entry_date corresponds to
        if let Some(day_idx) = days.iter().position(|d| d == entry_date) {
            if let Some(arr) = entry.get_mut("days").and_then(|d| d.as_array_mut()) {
                arr[day_idx] = serde_json::json!(total_seconds);
            }
        }
    }

    Ok(grouped.into_values().collect())
}

// Import entries command (CSV-style: individual entries)
#[tauri::command]
pub fn import_entries(state: State<AppState>, entries: Vec<serde_json::Value>) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut count: i64 = 0;

    for entry in entries {
        let project_name = entry.get("project").and_then(|v| v.as_str()).unwrap_or("Imported");
        let description = entry.get("description").and_then(|v| v.as_str()).unwrap_or("");
        let start_time = entry.get("start_time").and_then(|v| v.as_str()).unwrap_or("");
        let end_time = entry.get("end_time").and_then(|v| v.as_str());
        let duration = entry.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
        let billable = entry.get("billable").and_then(|v| v.as_i64()).unwrap_or(1);

        if start_time.is_empty() {
            continue;
        }

        // Find or create project
        let project_id: i64 = match conn.query_row(
            "SELECT id FROM projects WHERE name = ?1",
            params![project_name],
            |row| row.get(0),
        ) {
            Ok(id) => id,
            Err(_) => {
                conn.execute(
                    "INSERT INTO projects (name, hourly_rate, color, archived) VALUES (?1, 0, '#F59E0B', 0)",
                    params![project_name],
                ).map_err(|e| e.to_string())?;
                conn.last_insert_rowid()
            }
        };

        conn.execute(
            "INSERT INTO time_entries (project_id, description, start_time, end_time, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
            params![project_id, description, start_time, end_time, duration, billable],
        ).map_err(|e| e.to_string())?;
        count += 1;
    }

    Ok(count)
}

// Import full JSON data (clients, projects, tasks, entries, tags)
#[tauri::command]
pub fn import_json_data(state: State<AppState>, data: serde_json::Value) -> Result<serde_json::Value, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut counts = serde_json::json!({
        "clients": 0,
        "projects": 0,
        "tasks": 0,
        "entries": 0,
        "tags": 0
    });

    // Import clients
    if let Some(clients) = data.get("clients").and_then(|v| v.as_array()) {
        for client in clients {
            let name = client.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if name.is_empty() { continue; }
            let email = client.get("email").and_then(|v| v.as_str());
            let address = client.get("address").and_then(|v| v.as_str());
            let company = client.get("company").and_then(|v| v.as_str());
            let phone = client.get("phone").and_then(|v| v.as_str());
            let tax_id = client.get("tax_id").and_then(|v| v.as_str());
            let payment_terms = client.get("payment_terms").and_then(|v| v.as_str());
            let notes = client.get("notes").and_then(|v| v.as_str());
            let currency = client.get("currency").and_then(|v| v.as_str());
            conn.execute(
                "INSERT OR IGNORE INTO clients (name, email, address, company, phone, tax_id, payment_terms, notes, currency) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
                params![name, email, address, company, phone, tax_id, payment_terms, notes, currency],
            ).map_err(|e| e.to_string())?;
            counts["clients"] = serde_json::json!(counts["clients"].as_i64().unwrap_or(0) + 1);
        }
    }

    // Import projects
    if let Some(projects) = data.get("projects").and_then(|v| v.as_array()) {
        for project in projects {
            let name = project.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if name.is_empty() { continue; }
            let client_id = project.get("client_id").and_then(|v| v.as_i64());
            let hourly_rate = project.get("hourly_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let color = project.get("color").and_then(|v| v.as_str()).unwrap_or("#F59E0B");
            let archived = if project.get("archived").and_then(|v| v.as_bool()).unwrap_or(false) { 1 } else { 0 };
            let budget_hours = project.get("budget_hours").and_then(|v| v.as_f64());
            let budget_amount = project.get("budget_amount").and_then(|v| v.as_f64());
            let rounding_override = project.get("rounding_override").and_then(|v| v.as_i64()).map(|v| v as i32);
            let notes = project.get("notes").and_then(|v| v.as_str());
            let currency = project.get("currency").and_then(|v| v.as_str());
            conn.execute(
                "INSERT OR IGNORE INTO projects (client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
                params![client_id, name, hourly_rate, color, archived, budget_hours, budget_amount, rounding_override, notes, currency],
            ).map_err(|e| e.to_string())?;
            counts["projects"] = serde_json::json!(counts["projects"].as_i64().unwrap_or(0) + 1);
        }
    }

    // Import tags
    if let Some(tags) = data.get("tags").and_then(|v| v.as_array()) {
        for tag in tags {
            let name = tag.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if name.is_empty() { continue; }
            let color = tag.get("color").and_then(|v| v.as_str()).unwrap_or("#6B7280");
            conn.execute(
                "INSERT OR IGNORE INTO tags (name, color) VALUES (?1, ?2)",
                params![name, color],
            ).map_err(|e| e.to_string())?;
            counts["tags"] = serde_json::json!(counts["tags"].as_i64().unwrap_or(0) + 1);
        }
    }

    // Import tasks
    if let Some(tasks) = data.get("tasks").and_then(|v| v.as_array()) {
        for task in tasks {
            let name = task.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if name.is_empty() { continue; }
            let project_id = task.get("project_id").and_then(|v| v.as_i64());
            if let Some(pid) = project_id {
                let estimated_hours = task.get("estimated_hours").and_then(|v| v.as_f64());
                let hourly_rate = task.get("hourly_rate").and_then(|v| v.as_f64());
                conn.execute(
                    "INSERT OR IGNORE INTO tasks (project_id, name, estimated_hours, hourly_rate) VALUES (?1, ?2, ?3, ?4)",
                    params![pid, name, estimated_hours, hourly_rate],
                ).map_err(|e| e.to_string())?;
                counts["tasks"] = serde_json::json!(counts["tasks"].as_i64().unwrap_or(0) + 1);
            }
        }
    }

    // Import time entries
    if let Some(entries) = data.get("time_entries").and_then(|v| v.as_array()) {
        for entry in entries {
            let start_time = entry.get("start_time").and_then(|v| v.as_str()).unwrap_or("");
            if start_time.is_empty() { continue; }
            let project_id = entry.get("project_id").and_then(|v| v.as_i64());
            if let Some(pid) = project_id {
                let task_id = entry.get("task_id").and_then(|v| v.as_i64());
                let description = entry.get("description").and_then(|v| v.as_str());
                let end_time = entry.get("end_time").and_then(|v| v.as_str());
                let duration = entry.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
                let billable = entry.get("billable").and_then(|v| v.as_i64()).unwrap_or(1);

                conn.execute(
                    "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
                    params![pid, task_id, description, start_time, end_time, duration, billable],
                ).map_err(|e| e.to_string())?;
                counts["entries"] = serde_json::json!(counts["entries"].as_i64().unwrap_or(0) + 1);
            }
        }
    }

    // Import entry_tags
    if let Some(et_list) = data.get("entry_tags").and_then(|v| v.as_array()) {
        for et in et_list {
            let entry_id = et.get("entry_id").and_then(|v| v.as_i64());
            let tag_id = et.get("tag_id").and_then(|v| v.as_i64());
            if let (Some(eid), Some(tid)) = (entry_id, tag_id) {
                conn.execute(
                    "INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?1, ?2)",
                    params![eid, tid],
                ).ok();
            }
        }
    }

    // Import invoices
    if let Some(invoices) = data.get("invoices").and_then(|v| v.as_array()) {
        for inv in invoices {
            let client_id = inv.get("client_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let invoice_number = inv.get("invoice_number").and_then(|v| v.as_str()).unwrap_or("");
            if invoice_number.is_empty() { continue; }
            let date = inv.get("date").and_then(|v| v.as_str()).unwrap_or("");
            let due_date = inv.get("due_date").and_then(|v| v.as_str());
            let subtotal = inv.get("subtotal").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let tax_rate = inv.get("tax_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let tax_amount = inv.get("tax_amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let discount = inv.get("discount").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let total = inv.get("total").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let notes = inv.get("notes").and_then(|v| v.as_str());
            let status = inv.get("status").and_then(|v| v.as_str()).unwrap_or("draft");
            let template_id = inv.get("template_id").and_then(|v| v.as_str());
            conn.execute(
                "INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
                params![client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id],
            ).ok();
        }
    }

    // Import invoice_items
    if let Some(items) = data.get("invoice_items").and_then(|v| v.as_array()) {
        for item in items {
            let invoice_id = item.get("invoice_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let description = item.get("description").and_then(|v| v.as_str()).unwrap_or("");
            let quantity = item.get("quantity").and_then(|v| v.as_f64()).unwrap_or(1.0);
            let rate = item.get("rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let amount = item.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let time_entry_id = item.get("time_entry_id").and_then(|v| v.as_i64());
            conn.execute(
                "INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount, time_entry_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                params![invoice_id, description, quantity, rate, amount, time_entry_id],
            ).ok();
        }
    }

    // Import tracked_apps
    if let Some(apps) = data.get("tracked_apps").and_then(|v| v.as_array()) {
        for app in apps {
            let project_id = app.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let exe_name = app.get("exe_name").and_then(|v| v.as_str()).unwrap_or("");
            if exe_name.is_empty() { continue; }
            let exe_path = app.get("exe_path").and_then(|v| v.as_str());
            let display_name = app.get("display_name").and_then(|v| v.as_str());
            conn.execute(
                "INSERT INTO tracked_apps (project_id, exe_name, exe_path, display_name) VALUES (?1, ?2, ?3, ?4)",
                params![project_id, exe_name, exe_path, display_name],
            ).ok();
        }
    }

    // Import favorites
    if let Some(favs) = data.get("favorites").and_then(|v| v.as_array()) {
        for fav in favs {
            let project_id = fav.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let task_id = fav.get("task_id").and_then(|v| v.as_i64());
            let description = fav.get("description").and_then(|v| v.as_str());
            let sort_order = fav.get("sort_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
            conn.execute(
                "INSERT INTO favorites (project_id, task_id, description, sort_order) VALUES (?1, ?2, ?3, ?4)",
                params![project_id, task_id, description, sort_order],
            ).ok();
        }
    }

    // Import recurring_entries
    if let Some(recs) = data.get("recurring_entries").and_then(|v| v.as_array()) {
        for rec in recs {
            let project_id = rec.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let task_id = rec.get("task_id").and_then(|v| v.as_i64());
            let description = rec.get("description").and_then(|v| v.as_str());
            let duration = rec.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
            let recurrence_rule = rec.get("recurrence_rule").and_then(|v| v.as_str()).unwrap_or("");
            if recurrence_rule.is_empty() { continue; }
            let time_of_day = rec.get("time_of_day").and_then(|v| v.as_str());
            let mode = rec.get("mode").and_then(|v| v.as_str());
            let enabled = rec.get("enabled").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
            let last_triggered = rec.get("last_triggered").and_then(|v| v.as_str());
            conn.execute(
                "INSERT INTO recurring_entries (project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
                params![project_id, task_id, description, duration, recurrence_rule, time_of_day, mode, enabled, last_triggered],
            ).ok();
        }
    }

    // Import expenses
    if let Some(exps) = data.get("expenses").and_then(|v| v.as_array()) {
        for exp in exps {
            let project_id = exp.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let client_id = exp.get("client_id").and_then(|v| v.as_i64());
            let category = exp.get("category").and_then(|v| v.as_str());
            let description = exp.get("description").and_then(|v| v.as_str());
            let amount = exp.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let date = exp.get("date").and_then(|v| v.as_str()).unwrap_or("");
            if date.is_empty() { continue; }
            let receipt_path = exp.get("receipt_path").and_then(|v| v.as_str());
            let invoiced = exp.get("invoiced").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
            conn.execute(
                "INSERT INTO expenses (project_id, client_id, category, description, amount, date, receipt_path, invoiced) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
                params![project_id, client_id, category, description, amount, date, receipt_path, invoiced],
            ).ok();
        }
    }

    // Import timeline_events
    if let Some(events) = data.get("timeline_events").and_then(|v| v.as_array()) {
        for evt in events {
            let project_id = evt.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let exe_name = evt.get("exe_name").and_then(|v| v.as_str());
            let exe_path = evt.get("exe_path").and_then(|v| v.as_str());
            let window_title = evt.get("window_title").and_then(|v| v.as_str());
            let started_at = evt.get("started_at").and_then(|v| v.as_str()).unwrap_or("");
            if started_at.is_empty() { continue; }
            let ended_at = evt.get("ended_at").and_then(|v| v.as_str());
            let duration = evt.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
            conn.execute(
                "INSERT INTO timeline_events (project_id, exe_name, exe_path, window_title, started_at, ended_at, duration) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
                params![project_id, exe_name, exe_path, window_title, started_at, ended_at, duration],
            ).ok();
        }
    }

    // Import calendar_sources
    if let Some(sources) = data.get("calendar_sources").and_then(|v| v.as_array()) {
        for src in sources {
            let name = src.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if name.is_empty() { continue; }
            let source_type = src.get("source_type").and_then(|v| v.as_str()).unwrap_or("");
            let url = src.get("url").and_then(|v| v.as_str());
            let last_synced = src.get("last_synced").and_then(|v| v.as_str());
            let sync_interval = src.get("sync_interval").and_then(|v| v.as_i64()).unwrap_or(3600) as i32;
            let enabled = src.get("enabled").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
            conn.execute(
                "INSERT INTO calendar_sources (name, type, url, last_synced, sync_interval, enabled) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                params![name, source_type, url, last_synced, sync_interval, enabled],
            ).ok();
        }
    }

    // Import calendar_events
    if let Some(cal_events) = data.get("calendar_events").and_then(|v| v.as_array()) {
        for evt in cal_events {
            let source_id = evt.get("source_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let uid = evt.get("uid").and_then(|v| v.as_str());
            let summary = evt.get("summary").and_then(|v| v.as_str());
            let start_time = evt.get("start_time").and_then(|v| v.as_str());
            let end_time = evt.get("end_time").and_then(|v| v.as_str());
            let duration = evt.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
            let location = evt.get("location").and_then(|v| v.as_str());
            conn.execute(
                "INSERT INTO calendar_events (source_id, uid, summary, start_time, end_time, duration, location) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
                params![source_id, uid, summary, start_time, end_time, duration, location],
            ).ok();
        }
    }

    // Import invoice_payments
    if let Some(payments) = data.get("invoice_payments").and_then(|v| v.as_array()) {
        for pay in payments {
            let invoice_id = pay.get("invoice_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let amount = pay.get("amount").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let date = pay.get("date").and_then(|v| v.as_str()).unwrap_or("");
            if date.is_empty() { continue; }
            let method = pay.get("method").and_then(|v| v.as_str());
            let notes = pay.get("notes").and_then(|v| v.as_str());
            let created_at = pay.get("created_at").and_then(|v| v.as_str());
            conn.execute(
                "INSERT INTO invoice_payments (invoice_id, amount, date, method, notes, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                params![invoice_id, amount, date, method, notes, created_at],
            ).ok();
        }
    }

    // Import recurring_invoices
    if let Some(rec_invs) = data.get("recurring_invoices").and_then(|v| v.as_array()) {
        for ri in rec_invs {
            let client_id = ri.get("client_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let template_id = ri.get("template_id").and_then(|v| v.as_str());
            let line_items_json = ri.get("line_items_json").and_then(|v| v.as_str()).unwrap_or("[]");
            let tax_rate = ri.get("tax_rate").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let discount = ri.get("discount").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let notes = ri.get("notes").and_then(|v| v.as_str());
            let recurrence_rule = ri.get("recurrence_rule").and_then(|v| v.as_str()).unwrap_or("");
            if recurrence_rule.is_empty() { continue; }
            let next_due_date = ri.get("next_due_date").and_then(|v| v.as_str()).unwrap_or("");
            if next_due_date.is_empty() { continue; }
            let enabled = ri.get("enabled").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
            let created_at = ri.get("created_at").and_then(|v| v.as_str());
            conn.execute(
                "INSERT INTO recurring_invoices (client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
                params![client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled, created_at],
            ).ok();
        }
    }

    // Import settings
    if let Some(settings_list) = data.get("settings").and_then(|v| v.as_array()) {
        for setting in settings_list {
            let key = setting.get("key").and_then(|v| v.as_str()).unwrap_or("");
            if key.is_empty() { continue; }
            let value = setting.get("value").and_then(|v| v.as_str());
            conn.execute(
                "INSERT OR REPLACE INTO settings (key, value) VALUES (?1, ?2)",
                params![key, value],
            ).ok();
        }
    }

    // Import entry_templates
    if let Some(templates) = data.get("entry_templates").and_then(|v| v.as_array()) {
        for tmpl in templates {
            let name = tmpl.get("name").and_then(|v| v.as_str()).unwrap_or("");
            if name.is_empty() { continue; }
            let project_id = tmpl.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let task_id = tmpl.get("task_id").and_then(|v| v.as_i64());
            let description = tmpl.get("description").and_then(|v| v.as_str());
            let duration = tmpl.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
            let billable = tmpl.get("billable").and_then(|v| v.as_i64()).unwrap_or(1) as i32;
            conn.execute(
                "INSERT INTO entry_templates (name, project_id, task_id, description, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
                params![name, project_id, task_id, description, duration, billable],
            ).ok();
        }
    }

    // Import timesheet_rows
    if let Some(rows) = data.get("timesheet_rows").and_then(|v| v.as_array()) {
        for row in rows {
            let week_start = row.get("week_start").and_then(|v| v.as_str()).unwrap_or("");
            if week_start.is_empty() { continue; }
            let project_id = row.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
            let task_id = row.get("task_id").and_then(|v| v.as_i64());
            let sort_order = row.get("sort_order").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
            conn.execute(
                "INSERT INTO timesheet_rows (week_start, project_id, task_id, sort_order) VALUES (?1, ?2, ?3, ?4)",
                params![week_start, project_id, task_id, sort_order],
            ).ok();
        }
    }

    // Import timesheet_locks
    if let Some(locks) = data.get("timesheet_locks").and_then(|v| v.as_array()) {
        for lock in locks {
            let week_start = lock.get("week_start").and_then(|v| v.as_str()).unwrap_or("");
            if week_start.is_empty() { continue; }
            let status = lock.get("status").and_then(|v| v.as_str());
            let locked_at = lock.get("locked_at").and_then(|v| v.as_str());
            conn.execute(
                "INSERT OR IGNORE INTO timesheet_locks (week_start, status, locked_at) VALUES (?1, ?2, ?3)",
                params![week_start, status, locked_at],
            ).ok();
        }
    }

    Ok(counts)
}

// File save command (bypasses fs plugin scope for user-selected paths)
#[tauri::command]
pub fn save_binary_file(path: String, data: Vec<u8>) -> Result<(), String> {
    std::fs::write(&path, &data).map_err(|e| e.to_string())
}

// Mini timer window commands
#[tauri::command]
pub fn open_mini_timer(app: tauri::AppHandle) -> Result<(), String> {
    if let Some(win) = app.get_webview_window("mini-timer") {
        win.show().map_err(|e| e.to_string())?;
        win.set_focus().ok();
    }
    Ok(())
}

#[tauri::command]
pub fn close_mini_timer(app: tauri::AppHandle) -> Result<(), String> {
    if let Some(window) = app.get_webview_window("mini-timer") {
        window.hide().map_err(|e| e.to_string())?;
    }
    if let Some(main) = app.get_webview_window("main") {
        main.show().ok();
        main.set_focus().ok();
    }
    Ok(())
}

// Invoice template types and commands
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InvoiceTemplateColors {
    pub primary: String,
    pub secondary: String,
    pub background: String,
    #[serde(rename = "headerBg")]
    pub header_bg: String,
    #[serde(rename = "headerText")]
    pub header_text: String,
    #[serde(rename = "bodyText")]
    pub body_text: String,
    #[serde(rename = "tableHeaderBg")]
    pub table_header_bg: String,
    #[serde(rename = "tableHeaderText")]
    pub table_header_text: String,
    #[serde(rename = "tableRowAlt")]
    pub table_row_alt: String,
    #[serde(rename = "tableBorder")]
    pub table_border: String,
    #[serde(rename = "totalHighlight")]
    pub total_highlight: String,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InvoiceTemplate {
    pub id: String,
    pub name: String,
    pub layout: String,
    pub category: String,
    pub description: String,
    pub colors: InvoiceTemplateColors,
}

#[tauri::command]
pub fn get_invoice_templates(state: State<AppState>) -> Result<Vec<InvoiceTemplate>, String> {
    let templates_dir = state.data_dir.join("templates");
    let mut templates: Vec<InvoiceTemplate> = Vec::new();

    if let Ok(entries) = std::fs::read_dir(&templates_dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) == Some("json") {
                if let Ok(content) = std::fs::read_to_string(&path) {
                    match serde_json::from_str::<InvoiceTemplate>(&content) {
                        Ok(template) => templates.push(template),
                        Err(e) => eprintln!("Failed to parse template {:?}: {}", path, e),
                    }
                }
            }
        }
    }

    // Sort: essentials first, then creative, warm, premium; alphabetical within category
    let cat_order = |c: &str| -> u8 {
        match c { "essential" => 0, "creative" => 1, "warm" => 2, "premium" => 3, _ => 4 }
    };
    templates.sort_by(|a, b| {
        cat_order(&a.category).cmp(&cat_order(&b.category)).then(a.name.cmp(&b.name))
    });

    Ok(templates)
}

#[tauri::command]
pub fn auto_backup(state: State<AppState>, backup_dir: String) -> Result<String, String> {
    let data = export_data(state)?;
    let today = chrono::Local::now().format("%Y-%m-%d").to_string();
    let filename = format!("zeroclock-backup-{}.json", today);
    let path = std::path::Path::new(&backup_dir).join(&filename);
    let json = serde_json::to_string_pretty(&data).map_err(|e| e.to_string())?;
    std::fs::write(&path, json).map_err(|e| e.to_string())?;
    Ok(path.to_string_lossy().to_string())
}

#[tauri::command]
pub fn list_backup_files(backup_dir: String) -> Result<Vec<serde_json::Value>, String> {
    let dir = std::path::Path::new(&backup_dir);
    if !dir.exists() {
        return Ok(Vec::new());
    }
    let mut files: Vec<serde_json::Value> = std::fs::read_dir(dir)
        .map_err(|e| e.to_string())?
        .flatten()
        .filter(|e| {
            e.path().extension().and_then(|ext| ext.to_str()) == Some("json")
                && e.file_name().to_string_lossy().starts_with("zeroclock-backup-")
        })
        .filter_map(|e| {
            let meta = e.metadata().ok()?;
            let modified = meta.modified().ok()?;
            Some(serde_json::json!({
                "path": e.path().to_string_lossy().to_string(),
                "name": e.file_name().to_string_lossy().to_string(),
                "size": meta.len(),
                "modified": modified.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs(),
            }))
        })
        .collect();
    files.sort_by(|a, b| {
        b.get("modified").and_then(|v| v.as_u64())
            .cmp(&a.get("modified").and_then(|v| v.as_u64()))
    });
    Ok(files)
}

#[tauri::command]
pub fn delete_backup_file(path: String) -> Result<(), String> {
    std::fs::remove_file(&path).map_err(|e| e.to_string())
}

// Get recent unique descriptions for autocomplete
#[tauri::command]
pub fn get_recent_descriptions(state: State<AppState>) -> Result<Vec<String>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT description, COUNT(*) as cnt FROM time_entries
         WHERE description IS NOT NULL AND description != ''
         GROUP BY description ORDER BY cnt DESC LIMIT 50"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map([], |row| {
        row.get::<_, String>(0)
    }).map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

// Check for overlapping time entries
#[tauri::command]
pub fn check_entry_overlap(
    state: State<AppState>,
    start_time: String,
    end_time: String,
    exclude_id: Option<i64>,
) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let query = if let Some(eid) = exclude_id {
        let mut stmt = conn.prepare(
            "SELECT te.id, te.description, te.start_time, te.end_time, p.name as project_name
             FROM time_entries te
             JOIN projects p ON te.project_id = p.id
             WHERE te.end_time IS NOT NULL
               AND te.id != ?3
               AND te.start_time < ?2
               AND te.end_time > ?1
             ORDER BY te.start_time"
        ).map_err(|e| e.to_string())?;
        let rows = stmt.query_map(params![start_time, end_time, eid], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "description": row.get::<_, Option<String>>(1)?,
                "start_time": row.get::<_, String>(2)?,
                "end_time": row.get::<_, Option<String>>(3)?,
                "project_name": row.get::<_, String>(4)?
            }))
        }).map_err(|e| e.to_string())?;
        rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
    } else {
        let mut stmt = conn.prepare(
            "SELECT te.id, te.description, te.start_time, te.end_time, p.name as project_name
             FROM time_entries te
             JOIN projects p ON te.project_id = p.id
             WHERE te.end_time IS NOT NULL
               AND te.start_time < ?2
               AND te.end_time > ?1
             ORDER BY te.start_time"
        ).map_err(|e| e.to_string())?;
        let rows = stmt.query_map(params![start_time, end_time], |row| {
            Ok(serde_json::json!({
                "id": row.get::<_, i64>(0)?,
                "description": row.get::<_, Option<String>>(1)?,
                "start_time": row.get::<_, String>(2)?,
                "end_time": row.get::<_, Option<String>>(3)?,
                "project_name": row.get::<_, String>(4)?
            }))
        }).map_err(|e| e.to_string())?;
        rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())?
    };
    Ok(query)
}

// Get actual hours by task for a project (estimates vs actuals)
#[tauri::command]
pub fn get_task_actuals(state: State<AppState>, project_id: i64) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT t.id, t.name, t.estimated_hours, t.hourly_rate,
                COALESCE(SUM(te.duration), 0) as actual_seconds
         FROM tasks t
         LEFT JOIN time_entries te ON te.task_id = t.id
         WHERE t.project_id = ?1
         GROUP BY t.id
         ORDER BY t.name"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map(params![project_id], |row| {
        let estimated: Option<f64> = row.get(2)?;
        let actual_seconds: i64 = row.get(4)?;
        let actual_hours = actual_seconds as f64 / 3600.0;
        let variance = estimated.map(|est| actual_hours - est);
        let progress = estimated.map(|est| if est > 0.0 { (actual_hours / est) * 100.0 } else { 0.0 });
        Ok(serde_json::json!({
            "task_id": row.get::<_, i64>(0)?,
            "task_name": row.get::<_, String>(1)?,
            "estimated_hours": estimated,
            "hourly_rate": row.get::<_, Option<f64>>(3)?,
            "actual_seconds": actual_seconds,
            "actual_hours": actual_hours,
            "variance": variance,
            "progress": progress
        }))
    }).map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

// Invoice payment struct and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct InvoicePayment {
    pub id: Option<i64>,
    pub invoice_id: i64,
    pub amount: f64,
    pub date: String,
    pub method: Option<String>,
    pub notes: Option<String>,
}

#[tauri::command]
pub fn get_invoice_payments(state: State<AppState>, invoice_id: i64) -> Result<Vec<InvoicePayment>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, invoice_id, amount, date, method, notes FROM invoice_payments WHERE invoice_id = ?1 ORDER BY date"
    ).map_err(|e| e.to_string())?;
    let payments = stmt.query_map(params![invoice_id], |row| {
        Ok(InvoicePayment {
            id: Some(row.get(0)?),
            invoice_id: row.get(1)?,
            amount: row.get(2)?,
            date: row.get(3)?,
            method: row.get(4)?,
            notes: row.get(5)?,
        })
    }).map_err(|e| e.to_string())?;
    payments.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn add_invoice_payment(state: State<AppState>, payment: InvoicePayment) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO invoice_payments (invoice_id, amount, date, method, notes) VALUES (?1, ?2, ?3, ?4, ?5)",
        params![payment.invoice_id, payment.amount, payment.date, payment.method, payment.notes],
    ).map_err(|e| e.to_string())?;

    // Update invoice status based on total paid
    let total_paid: f64 = conn.query_row(
        "SELECT COALESCE(SUM(amount), 0) FROM invoice_payments WHERE invoice_id = ?1",
        params![payment.invoice_id],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    let invoice_total: f64 = conn.query_row(
        "SELECT total FROM invoices WHERE id = ?1",
        params![payment.invoice_id],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    let new_status = if total_paid >= invoice_total { "paid" } else { "partial" };
    conn.execute(
        "UPDATE invoices SET status = ?1 WHERE id = ?2",
        params![new_status, payment.invoice_id],
    ).map_err(|e| e.to_string())?;

    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn delete_invoice_payment(state: State<AppState>, id: i64, invoice_id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM invoice_payments WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;

    // Recalculate invoice status
    let total_paid: f64 = conn.query_row(
        "SELECT COALESCE(SUM(amount), 0) FROM invoice_payments WHERE invoice_id = ?1",
        params![invoice_id],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    let invoice_total: f64 = conn.query_row(
        "SELECT total FROM invoices WHERE id = ?1",
        params![invoice_id],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;

    let new_status = if total_paid >= invoice_total {
        "paid"
    } else if total_paid > 0.0 {
        "partial"
    } else {
        "sent"
    };
    conn.execute(
        "UPDATE invoices SET status = ?1 WHERE id = ?2",
        params![new_status, invoice_id],
    ).map_err(|e| e.to_string())?;

    Ok(())
}

// Recurring invoice struct and commands
#[derive(Debug, Serialize, Deserialize)]
pub struct RecurringInvoice {
    pub id: Option<i64>,
    pub client_id: i64,
    pub template_id: Option<String>,
    pub line_items_json: String,
    pub tax_rate: f64,
    pub discount: f64,
    pub notes: Option<String>,
    pub recurrence_rule: String,
    pub next_due_date: String,
    pub enabled: Option<i64>,
}

#[tauri::command]
pub fn get_recurring_invoices(state: State<AppState>) -> Result<Vec<RecurringInvoice>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled
         FROM recurring_invoices ORDER BY next_due_date"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map([], |row| {
        Ok(RecurringInvoice {
            id: Some(row.get(0)?),
            client_id: row.get(1)?,
            template_id: row.get(2)?,
            line_items_json: row.get(3)?,
            tax_rate: row.get(4)?,
            discount: row.get(5)?,
            notes: row.get(6)?,
            recurrence_rule: row.get(7)?,
            next_due_date: row.get(8)?,
            enabled: row.get(9)?,
        })
    }).map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_recurring_invoice(state: State<AppState>, invoice: RecurringInvoice) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO recurring_invoices (client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date, enabled)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
        params![invoice.client_id, invoice.template_id, invoice.line_items_json, invoice.tax_rate,
                invoice.discount, invoice.notes, invoice.recurrence_rule, invoice.next_due_date, invoice.enabled.unwrap_or(1)],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_recurring_invoice(state: State<AppState>, invoice: RecurringInvoice) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE recurring_invoices SET client_id = ?1, template_id = ?2, line_items_json = ?3,
         tax_rate = ?4, discount = ?5, notes = ?6, recurrence_rule = ?7, next_due_date = ?8, enabled = ?9
         WHERE id = ?10",
        params![invoice.client_id, invoice.template_id, invoice.line_items_json, invoice.tax_rate,
                invoice.discount, invoice.notes, invoice.recurrence_rule, invoice.next_due_date, invoice.enabled, invoice.id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_recurring_invoice(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM recurring_invoices WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

// Check recurring invoices and auto-create drafts when due
#[tauri::command]
pub fn check_recurring_invoices(state: State<AppState>, today: String) -> Result<Vec<i64>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, client_id, template_id, line_items_json, tax_rate, discount, notes, recurrence_rule, next_due_date
         FROM recurring_invoices WHERE enabled = 1 AND date(next_due_date) <= date(?1)"
    ).map_err(|e| e.to_string())?;

    let due: Vec<(i64, i64, Option<String>, String, f64, f64, Option<String>, String, String)> = stmt
        .query_map(params![today], |row| {
            Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?,
                row.get(5)?, row.get(6)?, row.get(7)?, row.get(8)?))
        })
        .map_err(|e| e.to_string())?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())?;

    let mut created_ids: Vec<i64> = Vec::new();
    for (ri_id, client_id, template_id, line_items_json, tax_rate, discount, notes, rule, next_due) in &due {
        // Generate invoice number
        let count: i64 = conn.query_row(
            "SELECT COUNT(*) FROM invoices", [], |row| row.get(0)
        ).map_err(|e| e.to_string())?;
        let inv_number = format!("INV-{:04}", count + 1);

        // Parse line items to calculate totals
        let items: Vec<serde_json::Value> = serde_json::from_str(line_items_json).unwrap_or_default();
        let subtotal: f64 = items.iter().map(|item| {
            let qty = item.get("quantity").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let rate = item.get("unit_price").and_then(|v| v.as_f64()).unwrap_or(0.0);
            qty * rate
        }).sum();
        let tax_amount = subtotal * tax_rate / 100.0;
        let total = subtotal + tax_amount - discount;

        conn.execute(
            "INSERT INTO invoices (client_id, invoice_number, date, due_date, subtotal, tax_rate, tax_amount, discount, total, notes, status, template_id)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, 'draft', ?11)",
            params![client_id, inv_number, next_due, next_due, subtotal, tax_rate, tax_amount, discount, total, notes, template_id],
        ).map_err(|e| e.to_string())?;
        let invoice_id = conn.last_insert_rowid();

        // Insert line items
        for item in &items {
            let desc = item.get("description").and_then(|v| v.as_str()).unwrap_or("");
            let qty = item.get("quantity").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let rate = item.get("unit_price").and_then(|v| v.as_f64()).unwrap_or(0.0);
            let amount = qty * rate;
            conn.execute(
                "INSERT INTO invoice_items (invoice_id, description, quantity, rate, amount) VALUES (?1, ?2, ?3, ?4, ?5)",
                params![invoice_id, desc, qty, rate, amount],
            ).map_err(|e| e.to_string())?;
        }

        created_ids.push(invoice_id);

        // Advance next_due_date based on recurrence rule
        let next: String = match rule.as_str() {
            "weekly" => conn.query_row(
                "SELECT date(?1, '+7 days')", params![next_due], |row| row.get(0)
            ).map_err(|e| e.to_string())?,
            "biweekly" => conn.query_row(
                "SELECT date(?1, '+14 days')", params![next_due], |row| row.get(0)
            ).map_err(|e| e.to_string())?,
            "quarterly" => conn.query_row(
                "SELECT date(?1, '+3 months')", params![next_due], |row| row.get(0)
            ).map_err(|e| e.to_string())?,
            "yearly" => conn.query_row(
                "SELECT date(?1, '+1 year')", params![next_due], |row| row.get(0)
            ).map_err(|e| e.to_string())?,
            _ => conn.query_row(
                "SELECT date(?1, '+1 month')", params![next_due], |row| row.get(0)
            ).map_err(|e| e.to_string())?,
        };
        conn.execute(
            "UPDATE recurring_invoices SET next_due_date = ?1 WHERE id = ?2",
            params![next, ri_id],
        ).map_err(|e| e.to_string())?;
    }

    Ok(created_ids)
}

pub fn seed_default_templates(data_dir: &std::path::Path) {
    let templates_dir = data_dir.join("templates");
    std::fs::create_dir_all(&templates_dir).ok();

    // Only seed if directory is empty (no .json files)
    let has_templates = std::fs::read_dir(&templates_dir)
        .map(|entries| entries.flatten().any(|e| {
            e.path().extension().and_then(|ext| ext.to_str()) == Some("json")
        }))
        .unwrap_or(false);

    if has_templates {
        return;
    }

    let defaults = get_default_templates();
    for template in &defaults {
        let filename = format!("{}.json", template.id);
        let path = templates_dir.join(&filename);
        if let Ok(json) = serde_json::to_string_pretty(template) {
            std::fs::write(&path, json).ok();
        }
    }
}

// Calendar integration structs and commands

#[derive(Debug, Serialize, Deserialize)]
pub struct CalendarSource {
    pub id: Option<i64>,
    pub name: String,
    pub source_type: String,
    pub url: Option<String>,
    pub last_synced: Option<String>,
    pub sync_interval: Option<i64>,
    pub enabled: Option<i64>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CalendarEvent {
    pub id: Option<i64>,
    pub source_id: i64,
    pub uid: Option<String>,
    pub summary: Option<String>,
    pub start_time: Option<String>,
    pub end_time: Option<String>,
    pub duration: Option<i64>,
    pub location: Option<String>,
    pub synced_at: Option<String>,
}

struct ParsedCalendarEvent {
    uid: Option<String>,
    summary: Option<String>,
    start_time: Option<String>,
    end_time: Option<String>,
    location: Option<String>,
    description: Option<String>,
    duration: i64,
}

fn parse_ics_datetime(dt: &str) -> Option<String> {
    if dt.is_empty() {
        return None;
    }
    let dt = dt.trim();
    if dt.len() >= 15 {
        // 20260115T090000Z -> 2026-01-15T09:00:00Z
        Some(format!(
            "{}-{}-{}T{}:{}:{}{}",
            &dt[0..4],
            &dt[4..6],
            &dt[6..8],
            &dt[9..11],
            &dt[11..13],
            &dt[13..15],
            if dt.ends_with('Z') { "Z" } else { "" }
        ))
    } else if dt.len() >= 8 {
        // 20260115 -> 2026-01-15
        Some(format!("{}-{}-{}", &dt[0..4], &dt[4..6], &dt[6..8]))
    } else {
        None
    }
}

fn unfold_ics_lines(content: &str) -> String {
    let mut result = String::new();
    for line in content.lines() {
        let line = line.trim_end_matches('\r');
        if line.starts_with(' ') || line.starts_with('\t') {
            result.push_str(line.trim_start());
        } else {
            if !result.is_empty() {
                result.push('\n');
            }
            result.push_str(line);
        }
    }
    result
}

fn parse_ics_duration(dur: &str) -> Option<i64> {
    let dur = dur.strip_prefix("PT")?;
    let mut seconds: i64 = 0;
    let mut num_buf = String::new();
    for ch in dur.chars() {
        if ch.is_ascii_digit() {
            num_buf.push(ch);
        } else {
            let n: i64 = num_buf.parse().ok()?;
            num_buf.clear();
            match ch {
                'H' => seconds += n * 3600,
                'M' => seconds += n * 60,
                'S' => seconds += n,
                _ => {}
            }
        }
    }
    Some(seconds)
}

fn calc_ics_duration_from_times(start: &str, end: &str) -> i64 {
    let parse_ts = |s: &str| -> Option<i64> {
        let s = s.trim();
        if s.len() >= 15 {
            let year: i64 = s[0..4].parse().ok()?;
            let month: i64 = s[4..6].parse().ok()?;
            let day: i64 = s[6..8].parse().ok()?;
            let hour: i64 = s[9..11].parse().ok()?;
            let min: i64 = s[11..13].parse().ok()?;
            let sec: i64 = s[13..15].parse().ok()?;
            // Approximate seconds since epoch (good enough for duration calc)
            Some(((year * 365 + month * 30 + day) * 86400) + hour * 3600 + min * 60 + sec)
        } else {
            None
        }
    };
    match (parse_ts(start), parse_ts(end)) {
        (Some(s), Some(e)) if e > s => e - s,
        _ => 0,
    }
}

fn parse_ics_content(content: &str) -> Vec<ParsedCalendarEvent> {
    let unfolded = unfold_ics_lines(content);
    let mut events = Vec::new();
    let mut in_event = false;
    let mut uid = String::new();
    let mut summary = String::new();
    let mut dtstart = String::new();
    let mut dtend = String::new();
    let mut location = String::new();
    let mut description = String::new();
    let mut duration_str = String::new();

    for line in unfolded.lines() {
        if line == "BEGIN:VEVENT" {
            in_event = true;
            uid.clear();
            summary.clear();
            dtstart.clear();
            dtend.clear();
            location.clear();
            description.clear();
            duration_str.clear();
        } else if line == "END:VEVENT" {
            if in_event {
                let duration = if !duration_str.is_empty() {
                    parse_ics_duration(&duration_str).unwrap_or(0)
                } else if !dtstart.is_empty() && !dtend.is_empty() {
                    calc_ics_duration_from_times(&dtstart, &dtend)
                } else {
                    0
                };
                events.push(ParsedCalendarEvent {
                    uid: if uid.is_empty() { None } else { Some(uid.clone()) },
                    summary: if summary.is_empty() { None } else { Some(summary.clone()) },
                    start_time: parse_ics_datetime(&dtstart),
                    end_time: parse_ics_datetime(&dtend),
                    location: if location.is_empty() { None } else { Some(location.clone()) },
                    description: if description.is_empty() { None } else { Some(description.clone()) },
                    duration,
                });
            }
            in_event = false;
        } else if in_event {
            if let Some(val) = line.strip_prefix("UID:") {
                uid = val.to_string();
            } else if let Some(val) = line.strip_prefix("SUMMARY:") {
                summary = val.to_string();
            } else if line.starts_with("DTSTART") {
                if let Some(idx) = line.find(':') {
                    dtstart = line[idx + 1..].to_string();
                }
            } else if line.starts_with("DTEND") {
                if let Some(idx) = line.find(':') {
                    dtend = line[idx + 1..].to_string();
                }
            } else if let Some(val) = line.strip_prefix("LOCATION:") {
                location = val.to_string();
            } else if let Some(val) = line.strip_prefix("DESCRIPTION:") {
                description = val.replace("\\n", "\n").replace("\\,", ",");
            } else if line.starts_with("DURATION") {
                if let Some(idx) = line.find(':') {
                    duration_str = line[idx + 1..].to_string();
                }
            }
        }
    }
    events
}

#[tauri::command]
pub fn get_calendar_sources(state: State<AppState>) -> Result<Vec<CalendarSource>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn
        .prepare(
            "SELECT id, name, type, url, last_synced, sync_interval, enabled
             FROM calendar_sources ORDER BY name",
        )
        .map_err(|e| e.to_string())?;
    let sources = stmt
        .query_map([], |row| {
            Ok(CalendarSource {
                id: Some(row.get(0)?),
                name: row.get(1)?,
                source_type: row.get(2)?,
                url: row.get(3)?,
                last_synced: row.get(4)?,
                sync_interval: row.get(5)?,
                enabled: row.get(6)?,
            })
        })
        .map_err(|e| e.to_string())?;
    sources
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())
}

#[tauri::command]
pub fn create_calendar_source(
    state: State<AppState>,
    source: CalendarSource,
) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT INTO calendar_sources (name, type, url, sync_interval, enabled)
         VALUES (?1, ?2, ?3, ?4, ?5)",
        params![
            source.name,
            source.source_type,
            source.url,
            source.sync_interval.unwrap_or(30),
            source.enabled.unwrap_or(1)
        ],
    )
    .map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn update_calendar_source(
    state: State<AppState>,
    source: CalendarSource,
) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE calendar_sources SET name = ?1, type = ?2, url = ?3,
         sync_interval = ?4, enabled = ?5 WHERE id = ?6",
        params![
            source.name,
            source.source_type,
            source.url,
            source.sync_interval,
            source.enabled,
            source.id
        ],
    )
    .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn delete_calendar_source(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM calendar_events WHERE source_id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM calendar_sources WHERE id = ?1", params![id])
        .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn import_ics_file(
    state: State<AppState>,
    source_id: i64,
    content: String,
) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let events = parse_ics_content(&content);
    let mut count: i64 = 0;

    let now = chrono::Local::now().to_rfc3339();

    for event in &events {
        // Upsert by source_id + uid when uid is present
        if let Some(ref uid) = event.uid {
            // Delete existing event with same source_id + uid, then insert
            conn.execute(
                "DELETE FROM calendar_events WHERE source_id = ?1 AND uid = ?2",
                params![source_id, uid],
            )
            .map_err(|e| e.to_string())?;
        }

        conn.execute(
            "INSERT INTO calendar_events (source_id, uid, summary, start_time, end_time, duration, location, description, synced_at)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
            params![
                source_id,
                event.uid,
                event.summary,
                event.start_time,
                event.end_time,
                event.duration,
                event.location,
                event.description,
                now
            ],
        )
        .map_err(|e| e.to_string())?;
        count += 1;
    }

    // Update last_synced on the source
    conn.execute(
        "UPDATE calendar_sources SET last_synced = ?1 WHERE id = ?2",
        params![now, source_id],
    )
    .map_err(|e| e.to_string())?;

    Ok(count)
}

#[tauri::command]
pub fn get_calendar_events(
    state: State<AppState>,
    start_date: String,
    end_date: String,
) -> Result<Vec<CalendarEvent>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn
        .prepare(
            "SELECT ce.id, ce.source_id, ce.uid, ce.summary, ce.start_time, ce.end_time,
                    ce.duration, ce.location, ce.synced_at
             FROM calendar_events ce
             JOIN calendar_sources cs ON ce.source_id = cs.id
             WHERE cs.enabled = 1 AND ce.start_time >= ?1 AND ce.start_time <= ?2
             ORDER BY ce.start_time",
        )
        .map_err(|e| e.to_string())?;
    let events = stmt
        .query_map(params![start_date, end_date], |row| {
            Ok(CalendarEvent {
                id: Some(row.get(0)?),
                source_id: row.get(1)?,
                uid: row.get(2)?,
                summary: row.get(3)?,
                start_time: row.get(4)?,
                end_time: row.get(5)?,
                duration: row.get(6)?,
                location: row.get(7)?,
                synced_at: row.get(8)?,
            })
        })
        .map_err(|e| e.to_string())?;
    events
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TimesheetLock {
    pub id: Option<i64>,
    pub week_start: String,
    pub status: Option<String>,
    pub locked_at: Option<String>,
}

#[tauri::command]
pub fn lock_timesheet_week(state: State<AppState>, week_start: String) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "INSERT OR IGNORE INTO timesheet_locks (week_start, status) VALUES (?1, 'locked')",
        params![week_start],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn unlock_timesheet_week(state: State<AppState>, week_start: String) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "DELETE FROM timesheet_locks WHERE week_start = ?1",
        params![week_start],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn get_timesheet_locks(state: State<AppState>) -> Result<Vec<TimesheetLock>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, week_start, status, locked_at FROM timesheet_locks ORDER BY week_start DESC"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map([], |row| {
        Ok(TimesheetLock {
            id: Some(row.get(0)?),
            week_start: row.get(1)?,
            status: row.get(2)?,
            locked_at: row.get(3)?,
        })
    }).map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn is_week_locked(state: State<AppState>, week_start: String) -> Result<bool, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let count: i64 = conn.query_row(
        "SELECT COUNT(*) FROM timesheet_locks WHERE week_start = ?1",
        params![week_start],
        |row| row.get(0),
    ).map_err(|e| e.to_string())?;
    Ok(count > 0)
}

#[tauri::command]
pub fn update_invoice_status(state: State<AppState>, id: i64, status: String) -> Result<(), String> {
    let valid = ["draft", "sent", "paid", "overdue"];
    if !valid.contains(&status.as_str()) {
        return Err(format!("Invalid status: {}. Must be one of: {:?}", status, valid));
    }
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute(
        "UPDATE invoices SET status = ?1 WHERE id = ?2",
        params![status, id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn check_overdue_invoices(state: State<AppState>, today: String) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let count = conn.execute(
        "UPDATE invoices SET status = 'overdue' WHERE status = 'sent' AND due_date < ?1 AND due_date IS NOT NULL",
        params![today],
    ).map_err(|e| e.to_string())?;
    Ok(count as i64)
}

#[derive(serde::Serialize)]
pub struct PaginatedEntries {
    pub entries: Vec<TimeEntry>,
    pub total: i64,
    pub has_more: bool,
}

#[tauri::command]
pub fn get_time_entries_paginated(
    state: State<AppState>,
    start_date: Option<String>,
    end_date: Option<String>,
    limit: Option<i64>,
    offset: Option<i64>,
) -> Result<PaginatedEntries, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let lim = limit.unwrap_or(50);
    let off = offset.unwrap_or(0);

    let (where_clause, has_dates) = match (&start_date, &end_date) {
        (Some(_), Some(_)) => (
            "WHERE date(start_time) >= date(?1) AND date(start_time) <= date(?2)",
            true,
        ),
        _ => ("", false),
    };

    let count_sql = format!("SELECT COUNT(*) FROM time_entries {}", where_clause);
    let total: i64 = if has_dates {
        conn.query_row(&count_sql, params![start_date.as_ref().unwrap(), end_date.as_ref().unwrap()], |r| r.get(0))
    } else {
        conn.query_row(&count_sql, [], |r| r.get(0))
    }.map_err(|e| e.to_string())?;

    let query_sql = if has_dates {
        format!(
            "SELECT id, project_id, task_id, description, start_time, end_time, duration, billable FROM time_entries {} ORDER BY start_time DESC LIMIT ?3 OFFSET ?4",
            where_clause,
        )
    } else {
        "SELECT id, project_id, task_id, description, start_time, end_time, duration, billable FROM time_entries ORDER BY start_time DESC LIMIT ?1 OFFSET ?2".to_string()
    };

    let entries = if has_dates {
        let mut stmt = conn.prepare(&query_sql).map_err(|e| e.to_string())?;
        let result = stmt.query_map(params![start_date.as_ref().unwrap(), end_date.as_ref().unwrap(), lim, off], |row| {
            Ok(TimeEntry {
                id: row.get(0)?,
                project_id: row.get(1)?,
                task_id: row.get(2)?,
                description: row.get(3)?,
                start_time: row.get(4)?,
                end_time: row.get(5)?,
                duration: row.get(6)?,
                billable: row.get(7)?,
            })
        }).map_err(|e| e.to_string())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
        result
    } else {
        let mut stmt = conn.prepare(&query_sql).map_err(|e| e.to_string())?;
        let result = stmt.query_map(params![lim, off], |row| {
            Ok(TimeEntry {
                id: row.get(0)?,
                project_id: row.get(1)?,
                task_id: row.get(2)?,
                description: row.get(3)?,
                start_time: row.get(4)?,
                end_time: row.get(5)?,
                duration: row.get(6)?,
                billable: row.get(7)?,
            })
        }).map_err(|e| e.to_string())?.filter_map(|r| r.ok()).collect::<Vec<_>>();
        result
    };

    Ok(PaginatedEntries {
        has_more: (off + lim) < total,
        entries,
        total,
    })
}

#[tauri::command]
pub fn search_entries(state: State<AppState>, query: String, limit: Option<i64>) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let limit = limit.unwrap_or(10);
    let pattern = format!("%{}%", query);
    let mut stmt = conn.prepare(
        "SELECT te.id, te.project_id, te.description, te.start_time, te.duration, p.name as project_name, p.color as project_color
         FROM time_entries te
         LEFT JOIN projects p ON te.project_id = p.id
         WHERE te.description LIKE ?1
         ORDER BY te.start_time DESC
         LIMIT ?2"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map(params![pattern, limit], |row| {
        Ok(serde_json::json!({
            "id": row.get::<_, i64>(0)?,
            "project_id": row.get::<_, i64>(1)?,
            "description": row.get::<_, Option<String>>(2)?,
            "start_time": row.get::<_, String>(3)?,
            "duration": row.get::<_, i64>(4)?,
            "project_name": row.get::<_, Option<String>>(5)?,
            "project_color": row.get::<_, Option<String>>(6)?,
        }))
    }).map_err(|e| e.to_string())?;
    rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
}

#[tauri::command]
pub fn bulk_delete_entries(state: State<AppState>, ids: Vec<i64>) -> Result<(), String> {
    if ids.is_empty() { return Ok(()); }
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute_batch("BEGIN TRANSACTION").map_err(|e| e.to_string())?;

    let result = (|| -> Result<(), rusqlite::Error> {
        for id in &ids {
            conn.execute("DELETE FROM invoice_items WHERE time_entry_id = ?1", params![id])?;
            conn.execute("DELETE FROM entry_tags WHERE entry_id = ?1", params![id])?;
            conn.execute("DELETE FROM time_entries WHERE id = ?1", params![id])?;
        }
        Ok(())
    })();

    match result {
        Ok(()) => { conn.execute_batch("COMMIT").map_err(|e| e.to_string())?; Ok(()) }
        Err(e) => { conn.execute_batch("ROLLBACK").ok(); Err(e.to_string()) }
    }
}

#[tauri::command]
pub fn bulk_update_entries_project(state: State<AppState>, ids: Vec<i64>, project_id: i64) -> Result<(), String> {
    if ids.is_empty() { return Ok(()); }
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute_batch("BEGIN TRANSACTION").map_err(|e| e.to_string())?;
    let result = (|| -> Result<(), rusqlite::Error> {
        for id in &ids {
            conn.execute("UPDATE time_entries SET project_id = ?1 WHERE id = ?2", params![project_id, id])?;
        }
        Ok(())
    })();
    match result {
        Ok(()) => { conn.execute_batch("COMMIT").map_err(|e| e.to_string())?; Ok(()) }
        Err(e) => { conn.execute_batch("ROLLBACK").ok(); Err(e.to_string()) }
    }
}

#[tauri::command]
pub fn bulk_update_entries_billable(state: State<AppState>, ids: Vec<i64>, billable: i32) -> Result<(), String> {
    if ids.is_empty() { return Ok(()); }
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute_batch("BEGIN TRANSACTION").map_err(|e| e.to_string())?;
    let result = (|| -> Result<(), rusqlite::Error> {
        for id in &ids {
            conn.execute("UPDATE time_entries SET billable = ?1 WHERE id = ?2", params![billable, id])?;
        }
        Ok(())
    })();
    match result {
        Ok(()) => { conn.execute_batch("COMMIT").map_err(|e| e.to_string())?; Ok(()) }
        Err(e) => { conn.execute_batch("ROLLBACK").ok(); Err(e.to_string()) }
    }
}

#[tauri::command]
pub fn upsert_timesheet_entry(
    state: State<AppState>,
    project_id: i64,
    task_id: Option<i64>,
    date: String,
    duration_seconds: i64,
) -> Result<i64, String> {
    if duration_seconds < 0 || duration_seconds > 86400 {
        return Err("Duration must be between 0 and 86400 seconds".to_string());
    }
    let conn = state.db.lock().map_err(|e| e.to_string())?;

    let existing: Option<i64> = conn.query_row(
        "SELECT id FROM time_entries WHERE project_id = ?1 AND (task_id = ?2 OR (task_id IS NULL AND ?2 IS NULL)) AND date(start_time) = date(?3) ORDER BY id ASC LIMIT 1",
        params![project_id, task_id, date],
        |row| row.get(0),
    ).ok();

    if let Some(id) = existing {
        let end_time = format!("{}T{}", date, format_seconds_as_time(duration_seconds));
        conn.execute(
            "UPDATE time_entries SET duration = ?1, end_time = ?2 WHERE id = ?3",
            params![duration_seconds, end_time, id],
        ).map_err(|e| e.to_string())?;
        Ok(id)
    } else {
        let start_time = format!("{}T09:00:00", date);
        let end_secs = 9 * 3600 + duration_seconds;
        let end_h = end_secs / 3600;
        let end_m = (end_secs % 3600) / 60;
        let end_s = end_secs % 60;
        let end_time = format!("{}T{:02}:{:02}:{:02}", date, end_h, end_m, end_s);

        conn.execute(
            "INSERT INTO time_entries (project_id, task_id, description, start_time, end_time, duration, billable) VALUES (?1, ?2, NULL, ?3, ?4, ?5, 1)",
            params![project_id, task_id, start_time, end_time, duration_seconds],
        ).map_err(|e| e.to_string())?;
        Ok(conn.last_insert_rowid())
    }
}

// Entry template commands

#[tauri::command]
pub fn get_entry_templates(state: State<AppState>) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, name, project_id, task_id, description, duration, billable, created_at FROM entry_templates ORDER BY name"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map([], |row| {
        Ok(serde_json::json!({
            "id": row.get::<_, i64>(0)?,
            "name": row.get::<_, String>(1)?,
            "project_id": row.get::<_, i64>(2)?,
            "task_id": row.get::<_, Option<i64>>(3)?,
            "description": row.get::<_, Option<String>>(4)?,
            "duration": row.get::<_, i64>(5)?,
            "billable": row.get::<_, i64>(6)?,
            "created_at": row.get::<_, String>(7)?,
        }))
    }).map_err(|e| e.to_string())?;
    Ok(rows.filter_map(|r| r.ok()).collect())
}

#[tauri::command]
pub fn create_entry_template(state: State<AppState>, template: serde_json::Value) -> Result<i64, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let name = template.get("name").and_then(|v| v.as_str()).unwrap_or("Untitled");
    let project_id = template.get("project_id").and_then(|v| v.as_i64()).ok_or("project_id required")?;
    let task_id = template.get("task_id").and_then(|v| v.as_i64());
    let description = template.get("description").and_then(|v| v.as_str());
    let duration = template.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
    let billable = template.get("billable").and_then(|v| v.as_i64()).unwrap_or(1);
    conn.execute(
        "INSERT INTO entry_templates (name, project_id, task_id, description, duration, billable) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
        params![name, project_id, task_id, description, duration, billable],
    ).map_err(|e| e.to_string())?;
    Ok(conn.last_insert_rowid())
}

#[tauri::command]
pub fn delete_entry_template(state: State<AppState>, id: i64) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("DELETE FROM entry_templates WHERE id = ?1", params![id]).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn update_entry_template(state: State<AppState>, template: serde_json::Value) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let id = template.get("id").and_then(|v| v.as_i64()).ok_or("id required")?;
    let name = template.get("name").and_then(|v| v.as_str()).unwrap_or("Untitled");
    let project_id = template.get("project_id").and_then(|v| v.as_i64()).ok_or("project_id required")?;
    let task_id = template.get("task_id").and_then(|v| v.as_i64());
    let description = template.get("description").and_then(|v| v.as_str());
    let duration = template.get("duration").and_then(|v| v.as_i64()).unwrap_or(0);
    let billable = template.get("billable").and_then(|v| v.as_i64()).unwrap_or(1);
    conn.execute(
        "UPDATE entry_templates SET name=?1, project_id=?2, task_id=?3, description=?4, duration=?5, billable=?6 WHERE id=?7",
        params![name, project_id, task_id, description, duration, billable, id],
    ).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn get_timesheet_rows(state: State<AppState>, week_start: String) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let mut stmt = conn.prepare(
        "SELECT id, week_start, project_id, task_id, sort_order FROM timesheet_rows WHERE week_start = ?1 ORDER BY sort_order"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map(params![week_start], |row| {
        Ok(serde_json::json!({
            "id": row.get::<_, i64>(0)?,
            "week_start": row.get::<_, String>(1)?,
            "project_id": row.get::<_, i64>(2)?,
            "task_id": row.get::<_, Option<i64>>(3)?,
            "sort_order": row.get::<_, i64>(4)?,
        }))
    }).map_err(|e| e.to_string())?;
    Ok(rows.filter_map(|r| r.ok()).collect())
}

#[tauri::command]
pub fn save_timesheet_rows(state: State<AppState>, week_start: String, rows: Vec<serde_json::Value>) -> Result<(), String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    conn.execute("BEGIN TRANSACTION", []).map_err(|e| e.to_string())?;

    if let Err(e) = conn.execute("DELETE FROM timesheet_rows WHERE week_start = ?1", params![week_start]) {
        let _ = conn.execute("ROLLBACK", []);
        return Err(e.to_string());
    }

    for (i, row) in rows.iter().enumerate() {
        let project_id = row.get("project_id").and_then(|v| v.as_i64()).unwrap_or(0);
        let task_id = row.get("task_id").and_then(|v| v.as_i64());
        if let Err(e) = conn.execute(
            "INSERT INTO timesheet_rows (week_start, project_id, task_id, sort_order) VALUES (?1, ?2, ?3, ?4)",
            params![week_start, project_id, task_id, i as i64],
        ) {
            let _ = conn.execute("ROLLBACK", []);
            return Err(e.to_string());
        }
    }

    conn.execute("COMMIT", []).map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn get_previous_week_structure(state: State<AppState>, current_week_start: String) -> Result<Vec<serde_json::Value>, String> {
    let conn = state.db.lock().map_err(|e| e.to_string())?;
    let current = chrono::NaiveDate::parse_from_str(&current_week_start, "%Y-%m-%d")
        .map_err(|e| e.to_string())?;
    let prev = current - chrono::Duration::days(7);
    let prev_str = prev.format("%Y-%m-%d").to_string();

    let mut stmt = conn.prepare(
        "SELECT id, week_start, project_id, task_id, sort_order FROM timesheet_rows WHERE week_start = ?1 ORDER BY sort_order"
    ).map_err(|e| e.to_string())?;
    let rows = stmt.query_map(params![prev_str], |row| {
        Ok(serde_json::json!({
            "id": row.get::<_, i64>(0)?,
            "week_start": row.get::<_, String>(1)?,
            "project_id": row.get::<_, i64>(2)?,
            "task_id": row.get::<_, Option<i64>>(3)?,
            "sort_order": row.get::<_, i64>(4)?,
        }))
    }).map_err(|e| e.to_string())?;
    Ok(rows.filter_map(|r| r.ok()).collect())
}

fn format_seconds_as_time(secs: i64) -> String {
    let h = secs / 3600;
    let m = (secs % 3600) / 60;
    let s = secs % 60;
    format!("{:02}:{:02}:{:02}", h, m, s)
}

fn get_default_templates() -> Vec<InvoiceTemplate> {
    vec![
        InvoiceTemplate {
            id: "clean".into(), name: "Clean".into(), layout: "clean".into(),
            category: "essential".into(), description: "Swiss minimalism with a single blue accent".into(),
            colors: InvoiceTemplateColors {
                primary: "#1e293b".into(), secondary: "#3b82f6".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#1e293b".into(), body_text: "#374151".into(),
                table_header_bg: "#f8fafc".into(), table_header_text: "#374151".into(),
                table_row_alt: "#f8fafc".into(), table_border: "#e5e7eb".into(), total_highlight: "#3b82f6".into(),
            },
        },
        InvoiceTemplate {
            id: "professional".into(), name: "Professional".into(), layout: "professional".into(),
            category: "essential".into(), description: "Navy header band with corporate polish".into(),
            colors: InvoiceTemplateColors {
                primary: "#1e3a5f".into(), secondary: "#2563eb".into(), background: "#ffffff".into(),
                header_bg: "#1e3a5f".into(), header_text: "#ffffff".into(), body_text: "#374151".into(),
                table_header_bg: "#1e3a5f".into(), table_header_text: "#ffffff".into(),
                table_row_alt: "#f3f4f6".into(), table_border: "#d1d5db".into(), total_highlight: "#1e3a5f".into(),
            },
        },
        InvoiceTemplate {
            id: "bold".into(), name: "Bold".into(), layout: "bold".into(),
            category: "essential".into(), description: "Large indigo block with oversized typography".into(),
            colors: InvoiceTemplateColors {
                primary: "#4f46e5".into(), secondary: "#a5b4fc".into(), background: "#ffffff".into(),
                header_bg: "#4f46e5".into(), header_text: "#ffffff".into(), body_text: "#1f2937".into(),
                table_header_bg: "#4f46e5".into(), table_header_text: "#ffffff".into(),
                table_row_alt: "#f5f3ff".into(), table_border: "#e0e7ff".into(), total_highlight: "#4f46e5".into(),
            },
        },
        InvoiceTemplate {
            id: "minimal".into(), name: "Minimal".into(), layout: "minimal".into(),
            category: "essential".into(), description: "Pure monochrome centered layout".into(),
            colors: InvoiceTemplateColors {
                primary: "#18181b".into(), secondary: "#18181b".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#18181b".into(), body_text: "#3f3f46".into(),
                table_header_bg: "#ffffff".into(), table_header_text: "#18181b".into(),
                table_row_alt: "#ffffff".into(), table_border: "#e4e4e7".into(), total_highlight: "#18181b".into(),
            },
        },
        InvoiceTemplate {
            id: "classic".into(), name: "Classic".into(), layout: "classic".into(),
            category: "essential".into(), description: "Traditional layout with burgundy accents".into(),
            colors: InvoiceTemplateColors {
                primary: "#7f1d1d".into(), secondary: "#991b1b".into(), background: "#ffffff".into(),
                header_bg: "#7f1d1d".into(), header_text: "#ffffff".into(), body_text: "#374151".into(),
                table_header_bg: "#7f1d1d".into(), table_header_text: "#ffffff".into(),
                table_row_alt: "#f5f5f4".into(), table_border: "#d6d3d1".into(), total_highlight: "#7f1d1d".into(),
            },
        },
        InvoiceTemplate {
            id: "modern".into(), name: "Modern".into(), layout: "modern".into(),
            category: "creative".into(), description: "Asymmetric header with teal accents".into(),
            colors: InvoiceTemplateColors {
                primary: "#0d9488".into(), secondary: "#14b8a6".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#0f172a".into(), body_text: "#334155".into(),
                table_header_bg: "#ffffff".into(), table_header_text: "#0d9488".into(),
                table_row_alt: "#f0fdfa".into(), table_border: "#99f6e4".into(), total_highlight: "#0d9488".into(),
            },
        },
        InvoiceTemplate {
            id: "elegant".into(), name: "Elegant".into(), layout: "elegant".into(),
            category: "creative".into(), description: "Gold double-rule accents on centered layout".into(),
            colors: InvoiceTemplateColors {
                primary: "#a16207".into(), secondary: "#ca8a04".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#422006".into(), body_text: "#57534e".into(),
                table_header_bg: "#ffffff".into(), table_header_text: "#422006".into(),
                table_row_alt: "#fefce8".into(), table_border: "#a16207".into(), total_highlight: "#a16207".into(),
            },
        },
        InvoiceTemplate {
            id: "creative".into(), name: "Creative".into(), layout: "creative".into(),
            category: "creative".into(), description: "Purple sidebar with card-style rows".into(),
            colors: InvoiceTemplateColors {
                primary: "#7c3aed".into(), secondary: "#a78bfa".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#1f2937".into(), body_text: "#374151".into(),
                table_header_bg: "#faf5ff".into(), table_header_text: "#7c3aed".into(),
                table_row_alt: "#faf5ff".into(), table_border: "#e9d5ff".into(), total_highlight: "#7c3aed".into(),
            },
        },
        InvoiceTemplate {
            id: "compact".into(), name: "Compact".into(), layout: "compact".into(),
            category: "creative".into(), description: "Data-dense layout with tight spacing".into(),
            colors: InvoiceTemplateColors {
                primary: "#475569".into(), secondary: "#64748b".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#0f172a".into(), body_text: "#334155".into(),
                table_header_bg: "#f1f5f9".into(), table_header_text: "#334155".into(),
                table_row_alt: "#f8fafc".into(), table_border: "#e2e8f0".into(), total_highlight: "#475569".into(),
            },
        },
        InvoiceTemplate {
            id: "dark".into(), name: "Dark".into(), layout: "dark".into(),
            category: "warm".into(), description: "Near-black background with cyan highlights".into(),
            colors: InvoiceTemplateColors {
                primary: "#06b6d4".into(), secondary: "#22d3ee".into(), background: "#0f172a".into(),
                header_bg: "#020617".into(), header_text: "#e2e8f0".into(), body_text: "#cbd5e1".into(),
                table_header_bg: "#020617".into(), table_header_text: "#06b6d4".into(),
                table_row_alt: "#1e293b".into(), table_border: "#334155".into(), total_highlight: "#06b6d4".into(),
            },
        },
        InvoiceTemplate {
            id: "vibrant".into(), name: "Vibrant".into(), layout: "vibrant".into(),
            category: "warm".into(), description: "Coral-to-orange gradient header band".into(),
            colors: InvoiceTemplateColors {
                primary: "#ea580c".into(), secondary: "#f97316".into(), background: "#ffffff".into(),
                header_bg: "#ea580c".into(), header_text: "#ffffff".into(), body_text: "#1f2937".into(),
                table_header_bg: "#fff7ed".into(), table_header_text: "#9a3412".into(),
                table_row_alt: "#fff7ed".into(), table_border: "#fed7aa".into(), total_highlight: "#ea580c".into(),
            },
        },
        InvoiceTemplate {
            id: "corporate".into(), name: "Corporate".into(), layout: "corporate".into(),
            category: "warm".into(), description: "Deep blue header with info bar below".into(),
            colors: InvoiceTemplateColors {
                primary: "#1e40af".into(), secondary: "#3b82f6".into(), background: "#ffffff".into(),
                header_bg: "#1e40af".into(), header_text: "#ffffff".into(), body_text: "#1f2937".into(),
                table_header_bg: "#1e40af".into(), table_header_text: "#ffffff".into(),
                table_row_alt: "#eff6ff".into(), table_border: "#bfdbfe".into(), total_highlight: "#1e40af".into(),
            },
        },
        InvoiceTemplate {
            id: "fresh".into(), name: "Fresh".into(), layout: "fresh".into(),
            category: "premium".into(), description: "Oversized watermark invoice number".into(),
            colors: InvoiceTemplateColors {
                primary: "#0284c7".into(), secondary: "#38bdf8".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#0c4a6e".into(), body_text: "#334155".into(),
                table_header_bg: "#0284c7".into(), table_header_text: "#ffffff".into(),
                table_row_alt: "#f0f9ff".into(), table_border: "#bae6fd".into(), total_highlight: "#0284c7".into(),
            },
        },
        InvoiceTemplate {
            id: "natural".into(), name: "Natural".into(), layout: "natural".into(),
            category: "premium".into(), description: "Warm beige background with terracotta accents".into(),
            colors: InvoiceTemplateColors {
                primary: "#c2703e".into(), secondary: "#d97706".into(), background: "#fdf6ec".into(),
                header_bg: "#fdf6ec".into(), header_text: "#78350f".into(), body_text: "#57534e".into(),
                table_header_bg: "#c2703e".into(), table_header_text: "#ffffff".into(),
                table_row_alt: "#fef3c7".into(), table_border: "#d6d3d1".into(), total_highlight: "#c2703e".into(),
            },
        },
        InvoiceTemplate {
            id: "statement".into(), name: "Statement".into(), layout: "statement".into(),
            category: "premium".into(), description: "Total-forward design with hero amount".into(),
            colors: InvoiceTemplateColors {
                primary: "#18181b".into(), secondary: "#be123c".into(), background: "#ffffff".into(),
                header_bg: "#ffffff".into(), header_text: "#18181b".into(), body_text: "#3f3f46".into(),
                table_header_bg: "#ffffff".into(), table_header_text: "#18181b".into(),
                table_row_alt: "#ffffff".into(), table_border: "#e4e4e7".into(), total_highlight: "#be123c".into(),
            },
        },
    ]
}

