mod budgets_view;
mod calendar_view;
mod category_combo;
mod charts_view;
mod credit_cards_view;
mod date_picker;
mod edit_dialog;
mod forecast_view;
mod goals_view;
mod history_view;
mod icon_theme;
mod insights_view;
mod log_view;
mod month_nav;
mod numpad;
mod quick_add;
mod recurring_view;
mod settings_view;
mod sparkline;
mod subscriptions_view;
mod tray;
mod wishlist_view;
mod window;

use adw::prelude::*;
use adw::Application;
use gtk::glib;
use outlay_core::db::Database;
use std::rc::Rc;

const APP_ID: &str = "com.outlay.app";

fn main() {
    let app = Application::builder()
        .application_id(APP_ID)
        .build();

    app.connect_activate(build_ui);
    app.run();
}

mod fontconfig_ffi {
    use std::os::raw::c_char;

    #[repr(C)]
    pub struct FcConfig {
        _private: [u8; 0],
    }

    unsafe extern "C" {
        pub fn FcConfigGetCurrent() -> *mut FcConfig;
        pub fn FcConfigAppFontAddFile(config: *mut FcConfig, file: *const c_char) -> i32;
    }
}

fn register_fonts() {
    let exe_path = std::env::current_exe().unwrap_or_default();
    let exe_dir = exe_path.parent().unwrap_or(std::path::Path::new("."));

    let candidates = [
        exe_dir.join("../share/fonts"),
        exe_dir.join("../../outlay-gtk/data/fonts"),
    ];

    for dir in &candidates {
        if dir.exists() {
            for entry in std::fs::read_dir(dir).into_iter().flatten().flatten() {
                let path = entry.path();
                if path.extension().map_or(false, |e| e == "ttf") {
                    let Some(path_str) = path.to_str() else { continue };
                    let Ok(c_path) = std::ffi::CString::new(path_str) else { continue };
                    unsafe {
                        let config = fontconfig_ffi::FcConfigGetCurrent();
                        if !config.is_null() {
                            fontconfig_ffi::FcConfigAppFontAddFile(
                                config,
                                c_path.as_ptr(),
                            );
                        }
                    }
                }
            }
            break;
        }
    }
}

fn load_css() {
    let provider = gtk::CssProvider::new();
    provider.load_from_data(include_str!("style.css"));
    gtk::style_context_add_provider_for_display(
        &gtk::gdk::Display::default().expect("Could not connect to a display."),
        &provider,
        gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
    );
}

fn build_ui(app: &Application) {
    register_fonts();
    load_css();
    icon_theme::setup_themed_icons();

    // Register app icon for the hicolor theme so it works for window + tray
    {
        let display = gtk::gdk::Display::default().expect("Could not get display");
        let theme = gtk::IconTheme::for_display(&display);
        let exe_path = std::env::current_exe().unwrap_or_default();
        let exe_dir = exe_path.parent().unwrap_or(std::path::Path::new("."));
        let candidates = [
            exe_dir.join("../../outlay-gtk/data/icons"),
            exe_dir.join("../share/icons"),
        ];
        for candidate in &candidates {
            if candidate.exists() {
                theme.add_search_path(candidate);
                break;
            }
        }
    }
    gtk::Window::set_default_icon_name("com.outlay.app");

    let data_dir = glib::user_data_dir().join("outlay");
    std::fs::create_dir_all(&data_dir).expect("Failed to create data directory");
    let db_path = data_dir.join("outlay.db");

    let db = Database::open(&db_path).expect("Failed to open database");

    // Auto-resume paused recurring transactions that have passed their resume date
    {
        let today = chrono::Local::now().date_naive();
        let resumed = db.auto_resume_recurring(today).unwrap_or(0);
        if resumed > 0 {
            // Will show a toast after window is created
        }
    }

    // Generate any missed recurring transactions on launch
    let base_currency = db.get_setting("base_currency")
        .ok().flatten()
        .unwrap_or_else(|| "USD".to_string());
    let (recurring_count, recurring_details) = outlay_core::recurring::generate_missed_transactions_detailed(
        &db,
        chrono::Local::now().date_naive(),
        &base_currency,
    ).unwrap_or((0, Vec::new()));

    // Send per-transaction notifications if enabled
    {
        let notify_recurring = db.get_setting("notify_recurring")
            .ok().flatten().unwrap_or_else(|| "1".to_string()) == "1";
        if notify_recurring {
            for info in &recurring_details {
                let body = format!("{}: {:.2} {}", info.description, info.amount, info.currency);
                outlay_core::notifications::send_notification("Recurring Transaction", &body, "normal");
            }
        }
    }

    // Weekly spending digest notification
    {
        let last_digest = db.get_setting("last_digest_date").ok().flatten();
        let today = chrono::Local::now().date_naive();
        let today_str = today.format("%Y-%m-%d").to_string();
        let should_show = match &last_digest {
            Some(date_str) => {
                if let Ok(last) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
                    (today - last).num_days() >= 7
                } else {
                    true
                }
            }
            None => true,
        };
        if should_show {
            use chrono::Datelike;
            use outlay_core::models::TransactionType;
            let expense = db.get_monthly_total(today.year(), today.month(), TransactionType::Expense).unwrap_or(0.0);
            let income = db.get_monthly_total(today.year(), today.month(), TransactionType::Income).unwrap_or(0.0);
            let body = format!(
                "This month so far: spent {:.2} {}, earned {:.2} {}",
                expense, base_currency, income, base_currency
            );
            let notification = gtk::gio::Notification::new("Monthly Summary");
            notification.set_body(Some(&body));
            app.send_notification(Some("monthly-digest"), &notification);
            db.set_setting("last_digest_date", &today_str).ok();
        }
    }

    // Budget threshold notifications
    {
        use chrono::Datelike;
        let today = chrono::Local::now().date_naive();
        let all_progress = db.get_all_budget_progress(today.year(), today.month())
            .unwrap_or_default();
        for (cat_name, spent, budget_amt, _pct, cat_id) in &all_progress {
            let crossed = db.check_budget_thresholds(*cat_id, &format!("{:04}-{:02}", today.year(), today.month()))
                .unwrap_or_default();
            for threshold in crossed {
                let days_left = {
                    let days_in_month = if today.month() == 12 {
                        chrono::NaiveDate::from_ymd_opt(today.year() + 1, 1, 1)
                    } else {
                        chrono::NaiveDate::from_ymd_opt(today.year(), today.month() + 1, 1)
                    }.and_then(|d| d.pred_opt()).map(|d| d.day()).unwrap_or(30);
                    days_in_month - today.day()
                };
                let body = if threshold >= 100 {
                    format!(
                        "Over budget: {} spending ({:.2} {}) exceeded your {:.2} {} limit",
                        cat_name, spent, base_currency, budget_amt, base_currency
                    )
                } else {
                    format!(
                        "Budget alert: You've used {}% of your {} budget ({:.2} of {:.2} {}) with {} days left",
                        threshold, cat_name, spent, budget_amt, base_currency, days_left
                    )
                };
                let notif = gtk::gio::Notification::new("Budget Alert");
                notif.set_body(Some(&body));
                app.send_notification(
                    Some(&format!("budget-{}-{}", cat_id, threshold)),
                    &notif,
                );
                let month_str = format!("{:04}-{:02}", today.year(), today.month());
                db.record_notification(*cat_id, &month_str, threshold).ok();
            }
        }
    }

    // Bill reminder notifications
    {
        let upcoming = db.list_upcoming_bills(14).unwrap_or_default();
        for (rec, next_date) in &upcoming {
            let today = chrono::Local::now().date_naive();
            let days_until = (*next_date - today).num_days();
            if days_until <= rec.reminder_days as i64 {
                let cat_name = db.get_category(rec.category_id)
                    .ok()
                    .map(|c| c.name)
                    .unwrap_or_else(|| "Bill".to_string());
                let body = if days_until == 0 {
                    format!("{} ({:.2} {}) is due today", cat_name, rec.amount, rec.currency)
                } else if days_until == 1 {
                    format!("{} ({:.2} {}) is due tomorrow", cat_name, rec.amount, rec.currency)
                } else {
                    format!("{} ({:.2} {}) is due in {} days", cat_name, rec.amount, rec.currency, days_until)
                };
                let notification = gtk::gio::Notification::new("Bill Reminder");
                notification.set_body(Some(&body));
                app.send_notification(Some(&format!("bill-{}", rec.id)), &notification);
                outlay_core::notifications::send_notification("Bill Reminder", &body, "normal");
            }
        }
    }

    let db = Rc::new(db);

    let main_window = window::MainWindow::new(app, db.clone());

    if recurring_count > 0 {
        let msg = format!("Added {} recurring transaction(s)", recurring_count);
        let toast = adw::Toast::new(&msg);
        main_window.log_view.toast_overlay.add_toast(toast);

        let notification = gtk::gio::Notification::new("Recurring Transactions");
        notification.set_body(Some(&msg));
        app.send_notification(Some("recurring-added"), &notification);
    }

    // Scheduled automatic backup
    {
        if let Some(path) = settings_view::SettingsView::check_and_run_auto_backup(&db) {
            let toast = adw::Toast::new(&format!("Auto-backup saved to {}", path.display()));
            main_window.log_view.toast_overlay.add_toast(toast);
        }
    }

    // 6.3: Backup reminder
    {
        let last_backup = db.get_setting("last_backup_date").ok().flatten();
        let today = chrono::Local::now().date_naive();
        let needs_reminder = match &last_backup {
            Some(date_str) => {
                if let Ok(last) = chrono::NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
                    (today - last).num_days() >= 30
                } else {
                    true
                }
            }
            None => true,
        };
        let txn_count = db.get_export_stats().map(|(c, _, _, _)| c).unwrap_or(0);
        if needs_reminder && txn_count > 50 {
            let toast = adw::Toast::new("It's been a while since your last backup - visit Settings to create one");
            toast.set_timeout(10);
            main_window.log_view.toast_overlay.add_toast(toast);
        }
    }

    // Achievement check on startup
    {
        let today = chrono::Local::now().date_naive();
        let newly_earned = db.check_and_award_achievements(today).unwrap_or_default();
        for name in &newly_earned {
            let toast = adw::Toast::new(&format!("Achievement unlocked: {}", name));
            main_window.log_view.toast_overlay.add_toast(toast);
        }
    }

    // Anomaly toast on startup
    {
        use chrono::Datelike;
        let today = chrono::Local::now().date_naive();
        let anomalies = db.detect_anomalies(today.year(), today.month());
        if !anomalies.is_empty() {
            let toast = adw::Toast::new(&format!("{} spending insight(s) this month", anomalies.len()));
            toast.set_timeout(5);
            main_window.log_view.toast_overlay.add_toast(toast);
        }
    }

    // Keep the app alive when the window is hidden.
    // The guard must be moved into the timeout closure so it lives
    // as long as the GTK main loop runs.
    let hold_guard = app.hold();

    // Spawn system tray
    let (tray_tx, tray_rx) = std::sync::mpsc::channel::<tray::TrayCommand>();
    tray::spawn_tray(tray_tx);

    // Poll tray commands from the GTK main loop
    let main_window = Rc::new(main_window);

    // Wire donut chart click -> navigate to filtered History
    {
        let mw = main_window.clone();
        main_window.charts_view.set_on_navigate_category(Rc::new(move |cat_id| {
            mw.switch_to_history_filtered(cat_id);
        }));
    }

    // Wire insights anomaly click -> navigate to filtered History
    {
        let mw = main_window.clone();
        main_window.insights_view.set_on_navigate_category(Rc::new(move |cat_id| {
            mw.switch_to_history_filtered(cat_id);
        }));
    }

    // Wire history anomaly banner -> navigate to Insights
    {
        let mw = main_window.clone();
        main_window.history_view.set_on_navigate_insights(Rc::new(move || {
            mw.switch_to_insights();
        }));
    }

    let app_ref = app.clone();
    {
        let mw = main_window.clone();
        let db_ref = db;
        let _hold = hold_guard;
        glib::timeout_add_local(std::time::Duration::from_millis(100), move || {
            let _ = &_hold;
            while let Ok(cmd) = tray_rx.try_recv() {
                match cmd {
                    tray::TrayCommand::Show => {
                        mw.show();
                    }
                    tray::TrayCommand::QuickAdd => {
                        quick_add::show_quick_add_popup(&db_ref, &app_ref);
                    }
                    tray::TrayCommand::LogExpense => {
                        mw.switch_to_log(false);
                        mw.show();
                    }
                    tray::TrayCommand::LogIncome => {
                        mw.switch_to_log(true);
                        mw.show();
                    }
                    tray::TrayCommand::Quit => {
                        mw.save_window_state(&db_ref);
                        app_ref.quit();
                    }
                }
            }
            glib::ControlFlow::Continue
        });
    }

    main_window.window.present();
}
