use adw::prelude::*;
use chrono::{Datelike, Local};
use gtk::{cairo, gio, glib};
use outlay_core::db::Database;
use outlay_core::models::TransactionType;
use std::cell::{Cell, RefCell};
use std::f64::consts::PI;
use std::rc::Rc;

// Donut chart palette (fallback when category has no color)
const PALETTE: &[(f64, f64, f64)] = &[
    (0.906, 0.298, 0.235), // #e74c3c
    (0.204, 0.596, 0.859), // #3498db
    (0.180, 0.800, 0.443), // #2ecc71
    (0.608, 0.349, 0.714), // #9b59b6
    (0.945, 0.769, 0.059), // #f1c40f
    (0.902, 0.494, 0.133), // #e67e22
    (0.102, 0.737, 0.612), // #1abc9c
    (0.914, 0.118, 0.388), // #e91e63
    (0.000, 0.737, 0.831), // #00bcd4
    (0.475, 0.333, 0.282), // #795548
    (0.376, 0.490, 0.545), // #607d8b
    (1.000, 0.341, 0.133), // #ff5722
    (0.247, 0.318, 0.710), // #3f51b5
    (0.000, 0.588, 0.533), // #009688
];

// Inner padding for all charts within their frames
const PAD: f64 = 32.0;

struct ThemeColors {
    income: (f64, f64, f64),
    expense: (f64, f64, f64),
    grid: (f64, f64, f64),
    text: (f64, f64, f64),
    text_secondary: (f64, f64, f64),
    tooltip_bg: (f64, f64, f64, f64), // rgba
    tooltip_text: (f64, f64, f64),
    tooltip_border: (f64, f64, f64, f64),
}

fn theme_colors() -> ThemeColors {
    if adw::StyleManager::default().is_dark() {
        ThemeColors {
            income: (0.341, 0.890, 0.537),
            expense: (0.949, 0.365, 0.392),
            grid: (0.30, 0.30, 0.30),
            text: (0.86, 0.86, 0.86),
            text_secondary: (0.58, 0.58, 0.58),
            tooltip_bg: (0.16, 0.16, 0.16, 0.94),
            tooltip_text: (0.92, 0.92, 0.92),
            tooltip_border: (0.40, 0.40, 0.40, 0.70),
        }
    } else {
        ThemeColors {
            income: (0.180, 0.761, 0.494),
            expense: (0.831, 0.263, 0.306),
            grid: (0.90, 0.90, 0.90),
            text: (0.20, 0.20, 0.20),
            text_secondary: (0.50, 0.50, 0.50),
            tooltip_bg: (1.0, 1.0, 1.0, 0.96),
            tooltip_text: (0.13, 0.13, 0.13),
            tooltip_border: (0.78, 0.78, 0.78, 0.80),
        }
    }
}

fn hex_to_rgb(hex: &str) -> Option<(f64, f64, f64)> {
    let hex = hex.trim_start_matches('#');
    if hex.len() != 6 { return None; }
    let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
    let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
    let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
    Some((r as f64 / 255.0, g as f64 / 255.0, b as f64 / 255.0))
}

fn currency_symbol(code: &str) -> &'static str {
    match code {
        "USD" | "AUD" | "CAD" | "NZD" | "SGD" | "HKD" | "MXN" | "ARS"
            | "CLP" | "COP" | "TWD" => "$",
        "EUR" => "\u{20ac}",
        "GBP" => "\u{00a3}",
        "JPY" | "CNY" => "\u{00a5}",
        "KRW" => "\u{20a9}",
        "INR" | "PKR" => "\u{20b9}",
        "RUB" => "\u{20bd}",
        "BRL" => "R$",
        "CHF" => "Fr",
        "SEK" | "NOK" | "DKK" | "ISK" => "kr",
        "PLN" => "z\u{0142}",
        "CZK" => "K\u{010d}",
        "HUF" => "Ft",
        "TRY" => "\u{20ba}",
        "ZAR" => "R",
        "THB" => "\u{0e3f}",
        "ILS" => "\u{20aa}",
        "AED" => "Dh",
        "SAR" => "SR",
        "EGP" => "LE",
        "NGN" => "\u{20a6}",
        "KES" => "KSh",
        "PHP" => "\u{20b1}",
        "MYR" => "RM",
        "IDR" => "Rp",
        "VND" => "\u{20ab}",
        "BDT" => "\u{09f3}",
        "UAH" => "\u{20b4}",
        "RON" => "lei",
        "BGN" => "\u{043b}\u{0432}",
        "HRK" => "kn",
        _ => "",
    }
}

fn format_amount_with_sym(val: f64, sym: &str) -> String {
    if val.abs() >= 1_000_000.0 {
        format!("{}{:.1}M", sym, val / 1_000_000.0)
    } else if val.abs() >= 1_000.0 {
        format!("{}{:.1}K", sym, val / 1_000.0)
    } else {
        format!("{}{:.0}", sym, val)
    }
}

fn format_currency_full(val: f64, sym: &str) -> String {
    if val.abs() >= 1_000_000.0 {
        format!("{}{:.2}M", sym, val / 1_000_000.0)
    } else if val.abs() >= 1_000.0 {
        format!("{}{:.2}", sym, val)
    } else {
        format!("{}{:.2}", sym, val)
    }
}

fn short_month(m: u32) -> &'static str {
    match m {
        1 => "Jan", 2 => "Feb", 3 => "Mar", 4 => "Apr",
        5 => "May", 6 => "Jun", 7 => "Jul", 8 => "Aug",
        9 => "Sep", 10 => "Oct", 11 => "Nov", 12 => "Dec",
        _ => "?",
    }
}

// -- Chart data structs --

#[derive(Clone)]
struct DonutSegment {
    category_id: i64,
    label: String,
    value: f64,
    color: (f64, f64, f64),
}

#[derive(Clone)]
struct BarMonth {
    label: String,
    income: f64,
    expense: f64,
}

#[derive(Clone)]
struct LinePoint {
    day: f64,
    value: f64,
}

#[derive(Clone)]
struct NetTrendPoint {
    label: String,
    value: f64,
}

pub struct ChartsView {
    pub container: gtk::Box,
    refresh: Rc<dyn Fn()>,
    on_navigate_category: Rc<RefCell<Option<Rc<dyn Fn(i64)>>>>,
    current_year: Rc<Cell<i32>>,
    current_month: Rc<Cell<u32>>,
    month_label: gtk::Label,
}

impl ChartsView {
    pub fn new(db: Rc<Database>) -> Self {
        let container = gtk::Box::new(gtk::Orientation::Vertical, 0);

        let clamp = adw::Clamp::new();
        clamp.set_maximum_size(750);
        clamp.set_margin_start(16);
        clamp.set_margin_end(16);

        let inner = gtk::Box::new(gtk::Orientation::Vertical, 20);
        inner.set_margin_top(20);
        inner.set_margin_bottom(20);

        let today = Local::now().date_naive();
        let current_year = Rc::new(Cell::new(today.year()));
        let current_month = Rc::new(Cell::new(today.month()));

        // Month navigation
        let nav_box = gtk::Box::new(gtk::Orientation::Horizontal, 12);
        nav_box.set_halign(gtk::Align::Center);
        nav_box.set_margin_top(12);
        nav_box.set_margin_bottom(12);

        let prev_btn = gtk::Button::from_icon_name("go-previous-symbolic");
        prev_btn.add_css_class("flat");
        prev_btn.add_css_class("circular");
        prev_btn.set_tooltip_text(Some("Previous month"));

        let month_label = gtk::Label::new(None);
        month_label.add_css_class("month-nav-label");
        month_label.set_width_chars(18);

        let label_btn = gtk::Button::new();
        label_btn.set_child(Some(&month_label));
        label_btn.add_css_class("flat");
        label_btn.set_tooltip_text(Some("Pick a month"));

        let next_btn = gtk::Button::from_icon_name("go-next-symbolic");
        next_btn.add_css_class("flat");
        next_btn.add_css_class("circular");
        next_btn.set_tooltip_text(Some("Next month"));

        nav_box.append(&prev_btn);
        nav_box.append(&label_btn);
        nav_box.append(&next_btn);

        // Shared data stores
        let donut_data: Rc<RefCell<Vec<DonutSegment>>> = Rc::new(RefCell::new(Vec::new()));
        let bar_data: Rc<RefCell<Vec<BarMonth>>> = Rc::new(RefCell::new(Vec::new()));
        let bar_compare_data: Rc<RefCell<Vec<BarMonth>>> = Rc::new(RefCell::new(Vec::new()));
        let bar_compare: Rc<Cell<bool>> = Rc::new(Cell::new(false));
        let line_data: Rc<RefCell<Vec<LinePoint>>> = Rc::new(RefCell::new(Vec::new()));
        let net_trend_data: Rc<RefCell<Vec<NetTrendPoint>>> = Rc::new(RefCell::new(Vec::new()));
        let cur_sym: Rc<RefCell<String>> = Rc::new(RefCell::new("$".to_string()));

        // Animation progress
        let donut_progress = Rc::new(Cell::new(0.0_f64));
        let bar_progress = Rc::new(Cell::new(0.0_f64));
        let line_progress = Rc::new(Cell::new(0.0_f64));
        let net_trend_progress = Rc::new(Cell::new(0.0_f64));

        // Mouse position for tooltips (x, y)
        let donut_mouse = Rc::new(Cell::new((-1.0_f64, -1.0_f64)));
        let bar_mouse = Rc::new(Cell::new((-1.0_f64, -1.0_f64)));
        let line_mouse = Rc::new(Cell::new((-1.0_f64, -1.0_f64)));
        let net_trend_mouse = Rc::new(Cell::new((-1.0_f64, -1.0_f64)));

        // Donut hover animation state
        let donut_hover_idx: Rc<Cell<i32>> = Rc::new(Cell::new(-1)); // -1 = none
        let donut_hover_progress = Rc::new(Cell::new(0.0_f64));
        let donut_hover_anim: Rc<RefCell<Option<adw::TimedAnimation>>> = Rc::new(RefCell::new(None));

        // -- Month-over-month comparison label (3.3) --
        let mom_label = gtk::Label::new(None);
        mom_label.add_css_class("caption");
        mom_label.set_margin_bottom(4);
        mom_label.set_visible(false);

        // -- Donut type toggle --
        let donut_type: Rc<Cell<TransactionType>> = Rc::new(Cell::new(TransactionType::Expense));

        let donut_toggle_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
        donut_toggle_box.add_css_class("linked");
        donut_toggle_box.set_halign(gtk::Align::Center);
        donut_toggle_box.set_margin_bottom(8);

        let donut_expense_btn = gtk::ToggleButton::with_label("Expenses");
        donut_expense_btn.set_active(true);
        let donut_income_btn = gtk::ToggleButton::with_label("Income");
        donut_income_btn.set_group(Some(&donut_expense_btn));

        donut_toggle_box.append(&donut_expense_btn);
        donut_toggle_box.append(&donut_income_btn);

        // -- Donut chart --
        let donut_area = gtk::DrawingArea::builder()
            .accessible_role(gtk::AccessibleRole::Img)
            .build();
        donut_area.update_property(&[gtk::accessible::Property::Label("Spending breakdown by category")]);
        donut_area.set_content_height(400);
        {
            let data = donut_data.clone();
            let progress = donut_progress.clone();
            let mouse = donut_mouse.clone();
            let sym = cur_sym.clone();
            let hi = donut_hover_idx.clone();
            let hp = donut_hover_progress.clone();
            donut_area.set_draw_func(move |_area, cr, width, height| {
                Self::draw_donut(cr, width, height, &data.borrow(), progress.get(), mouse.get(), &sym.borrow(), hi.get(), hp.get());
            });
        }
        {
            let mouse = donut_mouse.clone();
            let area = donut_area.clone();
            let data = donut_data.clone();
            let hi = donut_hover_idx.clone();
            let hp = donut_hover_progress.clone();
            let ha = donut_hover_anim.clone();
            let motion = gtk::EventControllerMotion::new();
            motion.connect_motion(move |_, x, y| {
                mouse.set((x, y));
                let new_idx = Self::donut_hit_test(x, y, &data.borrow(), area.width() as f64, area.height() as f64);
                let old_idx = hi.get();
                if new_idx != old_idx {
                    hi.set(new_idx);
                    let (from, to) = if new_idx >= 0 { (0.0, 1.0) } else { (hp.get(), 0.0) };
                    let target = adw::CallbackAnimationTarget::new({
                        let p = hp.clone();
                        let a = area.clone();
                        move |v| { p.set(v); a.queue_draw(); }
                    });
                    let anim = adw::TimedAnimation::new(&area, from, to, 150, target);
                    anim.set_easing(adw::Easing::EaseOutCubic);
                    anim.play();
                    *ha.borrow_mut() = Some(anim);
                }
                area.queue_draw();
            });
            let mouse2 = donut_mouse.clone();
            let area2 = donut_area.clone();
            let hi2 = donut_hover_idx.clone();
            let hp2 = donut_hover_progress.clone();
            let ha2 = donut_hover_anim.clone();
            motion.connect_leave(move |_| {
                mouse2.set((-1.0, -1.0));
                if hi2.get() >= 0 {
                    hi2.set(-1);
                    let target = adw::CallbackAnimationTarget::new({
                        let p = hp2.clone();
                        let a = area2.clone();
                        move |v| { p.set(v); a.queue_draw(); }
                    });
                    let anim = adw::TimedAnimation::new(&area2, hp2.get(), 0.0, 150, target);
                    anim.set_easing(adw::Easing::EaseOutCubic);
                    anim.play();
                    *ha2.borrow_mut() = Some(anim);
                }
                area2.queue_draw();
            });
            donut_area.add_controller(motion);
        }

        // Donut click -> navigate to filtered History
        let on_navigate_category: Rc<RefCell<Option<Rc<dyn Fn(i64)>>>> = Rc::new(RefCell::new(None));
        {
            let data = donut_data.clone();
            let area = donut_area.clone();
            let nav_cb = on_navigate_category.clone();
            let click = gtk::GestureClick::new();
            click.connect_released(move |_, _, x, y| {
                let idx = Self::donut_hit_test(x, y, &data.borrow(), area.width() as f64, area.height() as f64);
                if idx >= 0 {
                    let segments = data.borrow();
                    if let Some(seg) = segments.get(idx as usize) {
                        if let Some(ref cb) = *nav_cb.borrow() {
                            cb(seg.category_id);
                        }
                    }
                }
            });
            donut_area.add_controller(click);
        }

        // Donut section with title + export button
        let donut_section = {
            let save_btn = gtk::Button::from_icon_name("tabler-download");
            save_btn.add_css_class("flat");
            save_btn.add_css_class("circular");
            save_btn.set_tooltip_text(Some("Save chart as PNG"));
            let data = donut_data.clone();
            let sym = cur_sym.clone();
            let hi = donut_hover_idx.clone();
            let hp = donut_hover_progress.clone();
            let area = donut_area.clone();
            save_btn.connect_clicked(move |btn| {
                let w = area.width();
                let h = area.height();
                let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
                let cr = cairo::Context::new(&surface).unwrap();
                Self::draw_donut(&cr, w, h, &data.borrow(), 1.0, (-1.0, -1.0), &sym.borrow(), hi.get(), hp.get());
                Self::save_surface_dialog(btn, surface);
            });
            Self::make_chart_section("By Category", &save_btn, &donut_area)
        };

        // -- Bar compare toggle --
        let bar_compare_btn = gtk::ToggleButton::with_label("Compare Previous Year");
        bar_compare_btn.add_css_class("pill");
        bar_compare_btn.set_halign(gtk::Align::Center);
        bar_compare_btn.set_margin_top(8);

        // -- Bar chart --
        let bar_area = gtk::DrawingArea::builder()
            .accessible_role(gtk::AccessibleRole::Img)
            .build();
        bar_area.update_property(&[gtk::accessible::Property::Label("Monthly income vs expenses bar chart")]);
        bar_area.set_content_height(360);
        {
            let data = bar_data.clone();
            let cmp_data = bar_compare_data.clone();
            let cmp_on = bar_compare.clone();
            let progress = bar_progress.clone();
            let mouse = bar_mouse.clone();
            let sym = cur_sym.clone();
            bar_area.set_draw_func(move |_area, cr, width, height| {
                let cmp = if cmp_on.get() { Some(cmp_data.borrow()) } else { None };
                let cmp_slice: Option<&[BarMonth]> = cmp.as_ref().map(|v| v.as_slice());
                Self::draw_bars(cr, width, height, &data.borrow(), cmp_slice, progress.get(), mouse.get(), &sym.borrow());
            });
        }
        {
            let mouse = bar_mouse.clone();
            let area = bar_area.clone();
            let motion = gtk::EventControllerMotion::new();
            motion.connect_motion(move |_, x, y| {
                mouse.set((x, y));
                area.queue_draw();
            });
            let mouse2 = bar_mouse.clone();
            let area2 = bar_area.clone();
            motion.connect_leave(move |_| {
                mouse2.set((-1.0, -1.0));
                area2.queue_draw();
            });
            bar_area.add_controller(motion);
        }
        // Bar section with title + export button
        let bar_section = {
            let save_btn = gtk::Button::from_icon_name("tabler-download");
            save_btn.add_css_class("flat");
            save_btn.add_css_class("circular");
            save_btn.set_tooltip_text(Some("Save chart as PNG"));
            let data = bar_data.clone();
            let cmp_data = bar_compare_data.clone();
            let cmp_on = bar_compare.clone();
            let sym = cur_sym.clone();
            let area = bar_area.clone();
            save_btn.connect_clicked(move |btn| {
                let w = area.width();
                let h = area.height();
                let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
                let cr = cairo::Context::new(&surface).unwrap();
                let cmp = if cmp_on.get() { Some(cmp_data.borrow()) } else { None };
                let cmp_slice: Option<&[BarMonth]> = cmp.as_ref().map(|v| v.as_slice());
                Self::draw_bars(&cr, w, h, &data.borrow(), cmp_slice, 1.0, (-1.0, -1.0), &sym.borrow());
                Self::save_surface_dialog(btn, surface);
            });
            Self::make_chart_section("Monthly Income vs Expenses", &save_btn, &bar_area)
        };

        // -- Line chart --
        let line_area = gtk::DrawingArea::builder()
            .accessible_role(gtk::AccessibleRole::Img)
            .build();
        line_area.update_property(&[gtk::accessible::Property::Label("Daily net spending trend line chart")]);
        line_area.set_content_height(340);
        {
            let data = line_data.clone();
            let progress = line_progress.clone();
            let mouse = line_mouse.clone();
            let sym = cur_sym.clone();
            line_area.set_draw_func(move |_area, cr, width, height| {
                Self::draw_line(cr, width, height, &data.borrow(), progress.get(), mouse.get(), &sym.borrow());
            });
        }
        {
            let mouse = line_mouse.clone();
            let area = line_area.clone();
            let motion = gtk::EventControllerMotion::new();
            motion.connect_motion(move |_, x, y| {
                mouse.set((x, y));
                area.queue_draw();
            });
            let mouse2 = line_mouse.clone();
            let area2 = line_area.clone();
            motion.connect_leave(move |_| {
                mouse2.set((-1.0, -1.0));
                area2.queue_draw();
            });
            line_area.add_controller(motion);
        }
        // Line section with title + export button
        let line_section = {
            let save_btn = gtk::Button::from_icon_name("tabler-download");
            save_btn.add_css_class("flat");
            save_btn.add_css_class("circular");
            save_btn.set_tooltip_text(Some("Save chart as PNG"));
            let data = line_data.clone();
            let sym = cur_sym.clone();
            let area = line_area.clone();
            save_btn.connect_clicked(move |btn| {
                let w = area.width();
                let h = area.height();
                let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
                let cr = cairo::Context::new(&surface).unwrap();
                Self::draw_line(&cr, w, h, &data.borrow(), 1.0, (-1.0, -1.0), &sym.borrow());
                Self::save_surface_dialog(btn, surface);
            });
            Self::make_chart_section("Daily Net Trend", &save_btn, &line_area)
        };

        // -- Net trend chart --
        let net_trend_area = gtk::DrawingArea::builder()
            .accessible_role(gtk::AccessibleRole::Img)
            .build();
        net_trend_area.update_property(&[gtk::accessible::Property::Label("Net worth trend over time")]);
        net_trend_area.set_content_height(340);
        {
            let data = net_trend_data.clone();
            let progress = net_trend_progress.clone();
            let mouse = net_trend_mouse.clone();
            let sym = cur_sym.clone();
            net_trend_area.set_draw_func(move |_area, cr, width, height| {
                Self::draw_net_trend(cr, width, height, &data.borrow(), progress.get(), mouse.get(), &sym.borrow());
            });
        }
        {
            let mouse = net_trend_mouse.clone();
            let area = net_trend_area.clone();
            let motion = gtk::EventControllerMotion::new();
            motion.connect_motion(move |_, x, y| {
                mouse.set((x, y));
                area.queue_draw();
            });
            let mouse2 = net_trend_mouse.clone();
            let area2 = net_trend_area.clone();
            motion.connect_leave(move |_| {
                mouse2.set((-1.0, -1.0));
                area2.queue_draw();
            });
            net_trend_area.add_controller(motion);
        }
        // Net trend section with title + export button
        let net_trend_section = {
            let save_btn = gtk::Button::from_icon_name("tabler-download");
            save_btn.add_css_class("flat");
            save_btn.add_css_class("circular");
            save_btn.set_tooltip_text(Some("Save chart as PNG"));
            let data = net_trend_data.clone();
            let sym = cur_sym.clone();
            let area = net_trend_area.clone();
            save_btn.connect_clicked(move |btn| {
                let w = area.width();
                let h = area.height();
                let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h).unwrap();
                let cr = cairo::Context::new(&surface).unwrap();
                Self::draw_net_trend(&cr, w, h, &data.borrow(), 1.0, (-1.0, -1.0), &sym.borrow());
                Self::save_surface_dialog(btn, surface);
            });
            Self::make_chart_section("Net Trend (12 Months)", &save_btn, &net_trend_area)
        };

        // Animation storage
        let donut_anim: Rc<RefCell<Option<adw::TimedAnimation>>> = Rc::new(RefCell::new(None));
        let bar_anim: Rc<RefCell<Option<adw::TimedAnimation>>> = Rc::new(RefCell::new(None));
        let line_anim: Rc<RefCell<Option<adw::TimedAnimation>>> = Rc::new(RefCell::new(None));
        let net_trend_anim: Rc<RefCell<Option<adw::TimedAnimation>>> = Rc::new(RefCell::new(None));

        // Refresh closure: fetch data + animate
        let refresh_inner = {
            let db = db.clone();
            let donut_data = donut_data.clone();
            let bar_data = bar_data.clone();
            let bar_compare_data = bar_compare_data.clone();
            let line_data = line_data.clone();
            let cur_sym = cur_sym.clone();
            let donut_progress = donut_progress.clone();
            let bar_progress = bar_progress.clone();
            let line_progress = line_progress.clone();
            let donut_area = donut_area.clone();
            let bar_area = bar_area.clone();
            let line_area = line_area.clone();
            let donut_anim = donut_anim.clone();
            let bar_anim = bar_anim.clone();
            let line_anim = line_anim.clone();
            let net_trend_data = net_trend_data.clone();
            let net_trend_progress = net_trend_progress.clone();
            let net_trend_area = net_trend_area.clone();
            let net_trend_anim = net_trend_anim.clone();
            let donut_type = donut_type.clone();
            let mom_label = mom_label.clone();

            move |year: i32, month: u32| {
                // Currency symbol
                {
                    let code = db.get_setting("base_currency")
                        .ok().flatten()
                        .unwrap_or_else(|| "USD".to_string());
                    *cur_sym.borrow_mut() = currency_symbol(&code).to_string();
                }

                // Month-over-month comparison (3.3)
                {
                    let this_exp = db.get_monthly_total(year, month, TransactionType::Expense).unwrap_or(0.0);
                    let (prev_y, prev_m) = if month == 1 { (year - 1, 12) } else { (year, month - 1) };
                    let prev_exp = db.get_monthly_total(prev_y, prev_m, TransactionType::Expense).unwrap_or(0.0);
                    if prev_exp > 0.0 && this_exp > 0.0 {
                        let change_pct = ((this_exp - prev_exp) / prev_exp * 100.0).abs();
                        if this_exp > prev_exp {
                            mom_label.set_label(&format!("{:.0}% more than last month", change_pct));
                            mom_label.remove_css_class("amount-income");
                            mom_label.add_css_class("amount-expense");
                        } else {
                            mom_label.set_label(&format!("{:.0}% less than last month", change_pct));
                            mom_label.remove_css_class("amount-expense");
                            mom_label.add_css_class("amount-income");
                        }
                        mom_label.set_visible(true);
                    } else {
                        mom_label.set_visible(false);
                    }
                }

                // Donut data
                {
                    let cats = db
                        .get_monthly_totals_by_category(year, month, donut_type.get())
                        .unwrap_or_default();
                    let mut segments = Vec::new();
                    for (i, (cat, amount)) in cats.iter().enumerate() {
                        let color = cat.color.as_deref()
                            .and_then(hex_to_rgb)
                            .unwrap_or(PALETTE[i % PALETTE.len()]);
                        segments.push(DonutSegment {
                            category_id: cat.id,
                            label: cat.name.clone(),
                            value: *amount,
                            color,
                        });
                    }
                    *donut_data.borrow_mut() = segments;
                }

                // Bar data (last 6 months)
                {
                    let mut months = Vec::new();
                    let mut y = year;
                    let mut m = month;
                    for _ in 0..6 {
                        let income = db.get_monthly_total(y, m, TransactionType::Income).unwrap_or(0.0);
                        let expense = db.get_monthly_total(y, m, TransactionType::Expense).unwrap_or(0.0);
                        months.push(BarMonth {
                            label: short_month(m).to_string(),
                            income,
                            expense,
                        });
                        if m == 1 { m = 12; y -= 1; } else { m -= 1; }
                    }
                    months.reverse();
                    *bar_data.borrow_mut() = months;
                }

                // Bar compare data (same months, previous year)
                {
                    let mut months = Vec::new();
                    let mut y = year - 1;
                    let mut m = month;
                    for _ in 0..6 {
                        let income = db.get_monthly_total(y, m, TransactionType::Income).unwrap_or(0.0);
                        let expense = db.get_monthly_total(y, m, TransactionType::Expense).unwrap_or(0.0);
                        months.push(BarMonth {
                            label: short_month(m).to_string(),
                            income,
                            expense,
                        });
                        if m == 1 { m = 12; y -= 1; } else { m -= 1; }
                    }
                    months.reverse();
                    *bar_compare_data.borrow_mut() = months;
                }

                // Line data
                {
                    let daily = db.get_daily_totals(year, month).unwrap_or_default();
                    let mut sorted = daily;
                    sorted.sort_by_key(|(d, _, _)| *d);
                    let mut points = Vec::new();
                    let mut running = 0.0_f64;
                    for (date, income, expense) in &sorted {
                        running += income - expense;
                        points.push(LinePoint {
                            day: date.day() as f64,
                            value: running,
                        });
                    }
                    *line_data.borrow_mut() = points;
                }

                // Animate donut (600ms)
                donut_progress.set(0.0);
                {
                    let target = adw::CallbackAnimationTarget::new({
                        let p = donut_progress.clone();
                        let a = donut_area.clone();
                        move |v| { p.set(v); a.queue_draw(); }
                    });
                    let anim = adw::TimedAnimation::new(&donut_area, 0.0, 1.0, 600, target);
                    anim.set_easing(adw::Easing::EaseOutCubic);
                    anim.play();
                    *donut_anim.borrow_mut() = Some(anim);
                }

                // Animate bars (500ms)
                bar_progress.set(0.0);
                {
                    let target = adw::CallbackAnimationTarget::new({
                        let p = bar_progress.clone();
                        let a = bar_area.clone();
                        move |v| { p.set(v); a.queue_draw(); }
                    });
                    let anim = adw::TimedAnimation::new(&bar_area, 0.0, 1.0, 500, target);
                    anim.set_easing(adw::Easing::EaseOutCubic);
                    anim.play();
                    *bar_anim.borrow_mut() = Some(anim);
                }

                // Animate line (700ms)
                line_progress.set(0.0);
                {
                    let target = adw::CallbackAnimationTarget::new({
                        let p = line_progress.clone();
                        let a = line_area.clone();
                        move |v| { p.set(v); a.queue_draw(); }
                    });
                    let anim = adw::TimedAnimation::new(&line_area, 0.0, 1.0, 700, target);
                    anim.set_easing(adw::Easing::EaseOutCubic);
                    anim.play();
                    *line_anim.borrow_mut() = Some(anim);
                }

                // Net trend data (last 12 months)
                {
                    let mut points = Vec::new();
                    let mut y = year;
                    let mut m = month;
                    for _ in 0..12 {
                        let income = db.get_monthly_total(y, m, TransactionType::Income).unwrap_or(0.0);
                        let expense = db.get_monthly_total(y, m, TransactionType::Expense).unwrap_or(0.0);
                        points.push(NetTrendPoint {
                            label: format!("{} {}", short_month(m), y % 100),
                            value: income - expense,
                        });
                        if m == 1 { m = 12; y -= 1; } else { m -= 1; }
                    }
                    points.reverse();
                    *net_trend_data.borrow_mut() = points;
                }

                // Animate net trend (600ms)
                net_trend_progress.set(0.0);
                {
                    let target = adw::CallbackAnimationTarget::new({
                        let p = net_trend_progress.clone();
                        let a = net_trend_area.clone();
                        move |v| { p.set(v); a.queue_draw(); }
                    });
                    let anim = adw::TimedAnimation::new(&net_trend_area, 0.0, 1.0, 600, target);
                    anim.set_easing(adw::Easing::EaseOutCubic);
                    anim.play();
                    *net_trend_anim.borrow_mut() = Some(anim);
                }
            }
        };

        // Initial render
        Self::update_month_label(&month_label, current_year.get(), current_month.get());
        refresh_inner(current_year.get(), current_month.get());

        // Previous month
        {
            let yr = current_year.clone();
            let mo = current_month.clone();
            let lbl = month_label.clone();
            let r = refresh_inner.clone();
            prev_btn.connect_clicked(move |_| {
                let mut y = yr.get();
                let mut m = mo.get();
                if m == 1 { m = 12; y -= 1; } else { m -= 1; }
                yr.set(y); mo.set(m);
                Self::update_month_label(&lbl, y, m);
                r(y, m);
            });
        }

        // Next month
        {
            let yr = current_year.clone();
            let mo = current_month.clone();
            let lbl = month_label.clone();
            let r = refresh_inner.clone();
            next_btn.connect_clicked(move |_| {
                let mut y = yr.get();
                let mut m = mo.get();
                if m == 12 { m = 1; y += 1; } else { m += 1; }
                yr.set(y); mo.set(m);
                Self::update_month_label(&lbl, y, m);
                r(y, m);
            });
        }

        // Theme change
        {
            let yr = current_year.clone();
            let mo = current_month.clone();
            let r = refresh_inner.clone();
            adw::StyleManager::default().connect_dark_notify(move |_| {
                r(yr.get(), mo.get());
            });
        }

        // Month picker popover
        let popover = gtk::Popover::new();
        popover.set_parent(&label_btn);

        let picker_box = gtk::Box::new(gtk::Orientation::Vertical, 8);
        picker_box.set_margin_start(12);
        picker_box.set_margin_end(12);
        picker_box.set_margin_top(12);
        picker_box.set_margin_bottom(12);

        let year_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
        year_row.set_halign(gtk::Align::Center);

        let picker_year = Rc::new(Cell::new(current_year.get()));

        let year_label_pick = gtk::Label::new(Some(&current_year.get().to_string()));
        year_label_pick.add_css_class("heading");
        year_label_pick.set_width_chars(6);

        let py_btn = gtk::Button::from_icon_name("go-previous-symbolic");
        py_btn.add_css_class("flat");
        py_btn.add_css_class("circular");
        let ny_btn = gtk::Button::from_icon_name("go-next-symbolic");
        ny_btn.add_css_class("flat");
        ny_btn.add_css_class("circular");

        year_row.append(&py_btn);
        year_row.append(&year_label_pick);
        year_row.append(&ny_btn);
        picker_box.append(&year_row);

        {
            let py = picker_year.clone();
            let yl = year_label_pick.clone();
            py_btn.connect_clicked(move |_| {
                py.set(py.get() - 1);
                yl.set_label(&py.get().to_string());
            });
        }
        {
            let py = picker_year.clone();
            let yl = year_label_pick.clone();
            ny_btn.connect_clicked(move |_| {
                py.set(py.get() + 1);
                yl.set_label(&py.get().to_string());
            });
        }

        let month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
                           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
        let month_grid = gtk::Grid::new();
        month_grid.set_row_spacing(4);
        month_grid.set_column_spacing(4);

        for i in 0..12u32 {
            let btn = gtk::Button::with_label(month_names[i as usize]);
            btn.add_css_class("flat");
            btn.set_hexpand(true);

            let y = current_year.clone();
            let m = current_month.clone();
            let py = picker_year.clone();
            let lbl = month_label.clone();
            let pop = popover.clone();
            let r = refresh_inner.clone();
            btn.connect_clicked(move |_| {
                let new_month = i + 1;
                let new_year = py.get();
                y.set(new_year);
                m.set(new_month);
                Self::update_month_label(&lbl, new_year, new_month);
                r(new_year, new_month);
                pop.popdown();
            });

            month_grid.attach(&btn, (i % 4) as i32, (i / 4) as i32, 1, 1);
        }

        picker_box.append(&month_grid);

        let today_btn = gtk::Button::with_label("Today");
        today_btn.add_css_class("flat");
        {
            let y = current_year.clone();
            let m = current_month.clone();
            let lbl = month_label.clone();
            let pop = popover.clone();
            let r = refresh_inner.clone();
            today_btn.connect_clicked(move |_| {
                let now = Local::now().date_naive();
                y.set(now.year());
                m.set(now.month());
                Self::update_month_label(&lbl, now.year(), now.month());
                r(now.year(), now.month());
                pop.popdown();
            });
        }
        picker_box.append(&today_btn);

        popover.set_child(Some(&picker_box));

        {
            let py = picker_year;
            let y = current_year.clone();
            let yl = year_label_pick;
            popover.connect_show(move |_| {
                py.set(y.get());
                yl.set_label(&y.get().to_string());
            });
        }

        label_btn.connect_clicked(move |_| {
            popover.popup();
        });

        // Wire donut toggle
        {
            let yr = current_year.clone();
            let mo = current_month.clone();
            let r = refresh_inner.clone();
            let dt = donut_type.clone();
            donut_expense_btn.connect_toggled(move |btn| {
                if btn.is_active() {
                    dt.set(TransactionType::Expense);
                    r(yr.get(), mo.get());
                }
            });
        }
        {
            let yr = current_year.clone();
            let mo = current_month.clone();
            let r = refresh_inner.clone();
            let dt = donut_type.clone();
            donut_income_btn.connect_toggled(move |btn| {
                if btn.is_active() {
                    dt.set(TransactionType::Income);
                    r(yr.get(), mo.get());
                }
            });
        }

        // Wire bar compare toggle
        {
            let bc = bar_compare.clone();
            let ba = bar_area.clone();
            bar_compare_btn.connect_toggled(move |btn| {
                bc.set(btn.is_active());
                ba.queue_draw();
            });
        }

        // Public refresh closure for tab switching
        let refresh: Rc<dyn Fn()> = {
            let yr = current_year.clone();
            let mo = current_month.clone();
            let r = refresh_inner.clone();
            Rc::new(move || { r(yr.get(), mo.get()); })
        };

        // YoY comparison section
        let yoy_group = adw::PreferencesGroup::builder()
            .title("YEAR-OVER-YEAR")
            .build();
        {
            let y = current_year.get();
            let m = current_month.get();
            let years: Vec<i32> = ((y - 2)..=y).collect();
            let comparison = db.get_yearly_comparison(m, &years);
            for (year, income, expense) in &comparison {
                if *income <= 0.0 && *expense <= 0.0 { continue; }
                let row = adw::ActionRow::builder()
                    .title(&format!("{}", year))
                    .subtitle(&format!("Income: {:.0} - Expenses: {:.0}", income, expense))
                    .build();

                let net = income - expense;
                let net_str = if net >= 0.0 {
                    format!("+{:.0}", net)
                } else {
                    format!("{:.0}", net)
                };
                let net_label = gtk::Label::new(Some(&net_str));
                net_label.add_css_class("amount-display");
                if net >= 0.0 {
                    net_label.add_css_class("amount-income");
                } else {
                    net_label.add_css_class("amount-expense");
                }
                row.add_suffix(&net_label);

                yoy_group.add(&row);
            }
        }

        // Calendar heatmap section
        let heatmap_label = gtk::Label::new(Some("SPENDING HEATMAP"));
        heatmap_label.add_css_class("heading");
        heatmap_label.set_halign(gtk::Align::Start);
        heatmap_label.set_margin_start(4);
        let heatmap = crate::calendar_view::calendar_heatmap(&db, current_year.get(), current_month.get());

        inner.append(&nav_box);
        inner.append(&mom_label);
        inner.append(&donut_toggle_box);
        inner.append(&donut_section);
        inner.append(&bar_compare_btn);
        inner.append(&bar_section);
        inner.append(&line_section);
        inner.append(&net_trend_section);
        inner.append(&yoy_group);
        inner.append(&heatmap_label);
        inner.append(&heatmap);

        // Sankey money flow diagram
        let sankey_label = gtk::Label::new(Some("MONEY FLOW"));
        sankey_label.add_css_class("heading");
        sankey_label.set_halign(gtk::Align::Start);
        sankey_label.set_margin_start(4);
        sankey_label.set_margin_top(12);

        let sankey_area = gtk::DrawingArea::builder()
            .accessible_role(gtk::AccessibleRole::Img)
            .build();
        sankey_area.update_property(&[gtk::accessible::Property::Label("Money flow diagram showing income sources and expense categories")]);
        sankey_area.set_content_width(600);
        sankey_area.set_content_height(400);
        sankey_area.set_margin_top(8);
        sankey_area.set_margin_bottom(12);

        {
            let db_ref = db.clone();
            let y = current_year.get();
            let m = current_month.get();
            Self::draw_sankey(&sankey_area, &db_ref, y, m);
        }

        inner.append(&sankey_label);
        inner.append(&sankey_area);

        clamp.set_child(Some(&inner));

        let scroll = gtk::ScrolledWindow::builder()
            .hscrollbar_policy(gtk::PolicyType::Never)
            .vexpand(true)
            .child(&clamp)
            .build();

        container.append(&scroll);

        ChartsView {
            container, refresh, on_navigate_category,
            current_year, current_month, month_label,
        }
    }

    pub fn refresh(&self) {
        (self.refresh)();
    }

    pub fn get_month(&self) -> (i32, u32) {
        (self.current_year.get(), self.current_month.get())
    }

    pub fn set_month(&self, year: i32, month: u32) {
        self.current_year.set(year);
        self.current_month.set(month);
        Self::update_month_label(&self.month_label, year, month);
        (self.refresh)();
    }

    pub fn set_on_navigate_category(&self, cb: Rc<dyn Fn(i64)>) {
        *self.on_navigate_category.borrow_mut() = Some(cb);
    }

    fn save_surface_dialog(widget: &impl IsA<gtk::Widget>, surface: cairo::ImageSurface) {
        let filter = gtk::FileFilter::new();
        filter.add_pattern("*.png");
        filter.set_name(Some("PNG images"));

        let filters = gio::ListStore::new::<gtk::FileFilter>();
        filters.append(&filter);

        let dialog = gtk::FileDialog::builder()
            .title("Save Chart")
            .initial_name("chart.png")
            .default_filter(&filter)
            .filters(&filters)
            .build();

        let window = widget.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        dialog.save(window.as_ref(), gio::Cancellable::NONE, move |result: Result<gio::File, glib::Error>| {
            if let Ok(file) = result {
                if let Some(path) = file.path() {
                    match std::fs::File::create(&path) {
                        Ok(mut f) => {
                            if surface.write_to_png(&mut f).is_ok() {
                                // Toast would be nice but we don't have a reference here
                            }
                        }
                        Err(_) => {}
                    }
                }
            }
        });
    }

    fn make_chart_section(title: &str, save_btn: &gtk::Button, content: &impl IsA<gtk::Widget>) -> gtk::Box {
        let section = gtk::Box::new(gtk::Orientation::Vertical, 0);
        let title_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
        title_row.set_margin_start(12);
        title_row.set_margin_end(12);
        title_row.set_margin_bottom(6);
        let label = gtk::Label::new(Some(title));
        label.add_css_class("chart-title");
        label.set_hexpand(true);
        label.set_halign(gtk::Align::Start);
        save_btn.set_halign(gtk::Align::End);
        title_row.append(&label);
        title_row.append(save_btn);
        section.append(&title_row);
        let frame = gtk::Frame::new(None::<&str>);
        frame.set_child(Some(content));
        section.append(&frame);
        section
    }

    fn update_month_label(label: &gtk::Label, year: i32, month: u32) {
        let date = chrono::NaiveDate::from_ymd_opt(year, month, 1).unwrap();
        label.set_label(&date.format("%B %Y").to_string());
    }

    // =====================================================
    // Tooltip drawing helper
    // =====================================================
    fn draw_tooltip(cr: &cairo::Context, mx: f64, my: f64, lines: &[&str], canvas_w: f64, canvas_h: f64) {
        let tc = theme_colors();
        cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
        cr.set_font_size(13.0);

        let line_height = 20.0;
        let pad_x = 12.0;
        let pad_y = 8.0;

        let mut max_w: f64 = 0.0;
        for line in lines {
            let ext = cr.text_extents(line).unwrap();
            max_w = max_w.max(ext.width());
        }

        let box_w = max_w + pad_x * 2.0;
        let box_h = lines.len() as f64 * line_height + pad_y * 2.0;
        let r = 6.0;

        // Position: prefer right-below cursor, flip if near edge
        let mut bx = mx + 14.0;
        let mut by = my + 14.0;
        if bx + box_w > canvas_w - 4.0 { bx = mx - box_w - 14.0; }
        if by + box_h > canvas_h - 4.0 { by = my - box_h - 14.0; }
        bx = bx.max(4.0);
        by = by.max(4.0);

        Self::rounded_rect(cr, bx + 2.0, by + 3.0, box_w, box_h, r);
        cr.set_source_rgba(0.0, 0.0, 0.0, 0.18);
        cr.fill().ok();

        Self::rounded_rect(cr, bx, by, box_w, box_h, r);
        cr.set_source_rgba(tc.tooltip_bg.0, tc.tooltip_bg.1, tc.tooltip_bg.2, tc.tooltip_bg.3);
        cr.fill_preserve().ok();

        cr.set_source_rgba(tc.tooltip_border.0, tc.tooltip_border.1, tc.tooltip_border.2, tc.tooltip_border.3);
        cr.set_line_width(1.0);
        cr.stroke().ok();

        cr.set_source_rgb(tc.tooltip_text.0, tc.tooltip_text.1, tc.tooltip_text.2);
        for (i, line) in lines.iter().enumerate() {
            cr.move_to(bx + pad_x, by + pad_y + (i as f64 + 1.0) * line_height - 4.0);
            cr.show_text(line).ok();
        }
    }

    fn rounded_rect(cr: &cairo::Context, x: f64, y: f64, w: f64, h: f64, r: f64) {
        cr.new_path();
        cr.arc(x + w - r, y + r, r, -PI / 2.0, 0.0);
        cr.arc(x + w - r, y + h - r, r, 0.0, PI / 2.0);
        cr.arc(x + r, y + h - r, r, PI / 2.0, PI);
        cr.arc(x + r, y + r, r, PI, 1.5 * PI);
        cr.close_path();
    }

    // =====================================================
    // Donut hit-test (used by motion handler)
    // =====================================================
    fn donut_hit_test(mx: f64, my: f64, data: &[DonutSegment], w: f64, h: f64) -> i32 {
        if data.is_empty() { return -1; }
        let total: f64 = data.iter().map(|s| s.value).sum();
        if total <= 0.0 { return -1; }

        let legend_rows = (data.len() + 1) / 2;
        let legend_height = legend_rows as f64 * 28.0 + 20.0;
        let donut_h = h - PAD * 2.0 - legend_height;
        let cx = w / 2.0;
        let cy = PAD + donut_h / 2.0;
        let outer_r = ((w / 2.0 - PAD - 20.0).min(donut_h / 2.0 - 10.0)).max(40.0);
        let inner_r = outer_r * 0.58;

        let dx = mx - cx;
        let dy = my - cy;
        let dist = (dx * dx + dy * dy).sqrt();
        if dist < inner_r || dist > outer_r { return -1; }

        let gap_angle = 1.5_f64.to_radians();
        let mut start = -PI / 2.0;
        for (idx, seg) in data.iter().enumerate() {
            let frac = seg.value / total;
            let sweep = frac * 2.0 * PI;
            let seg_start = start + gap_angle / 2.0;
            let seg_end = start + sweep - gap_angle / 2.0;
            if seg_end > seg_start {
                let mut angle = dy.atan2(dx);
                if angle < -PI / 2.0 { angle += 2.0 * PI; }
                if angle >= seg_start && angle <= seg_end {
                    return idx as i32;
                }
            }
            start += sweep;
        }
        -1
    }

    // =====================================================
    // Donut chart
    // =====================================================
    fn draw_donut(
        cr: &cairo::Context,
        width: i32, height: i32,
        data: &[DonutSegment],
        progress: f64,
        mouse: (f64, f64),
        sym: &str,
        hover_idx: i32,
        hover_progress: f64,
    ) {
        let tc = theme_colors();
        let w = width as f64;
        let h = height as f64;

        if data.is_empty() {
            cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
            cr.set_font_size(16.0);
            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            let msg = "No expense data this month";
            let ext = cr.text_extents(msg).unwrap();
            cr.move_to((w - ext.width()) / 2.0, h / 2.0);
            cr.show_text(msg).ok();
            return;
        }

        let total: f64 = data.iter().map(|s| s.value).sum();
        if total <= 0.0 { return; }

        let legend_rows = (data.len() + 1) / 2;
        let legend_height = legend_rows as f64 * 28.0 + 20.0;

        let donut_h = h - PAD * 2.0 - legend_height;
        let cx = w / 2.0;
        let cy = PAD + donut_h / 2.0;
        let outer_r = ((w / 2.0 - PAD - 20.0).min(donut_h / 2.0 - 10.0)).max(40.0);
        let inner_r = outer_r * 0.58;

        let gap_angle = 1.5_f64.to_radians();

        let mut start = -PI / 2.0;
        for (idx, seg) in data.iter().enumerate() {
            let frac = seg.value / total;
            let sweep = frac * 2.0 * PI * progress;
            if sweep <= 0.0 { start += sweep; continue; }

            let seg_start = start + gap_angle / 2.0;
            let seg_end = start + sweep - gap_angle / 2.0;

            if seg_end > seg_start {
                let is_hovered = hover_idx == idx as i32;
                let r_offset = if is_hovered { 6.0 * hover_progress } else { 0.0 };
                let mid_a = (seg_start + seg_end) / 2.0;
                let offset_x = r_offset * mid_a.cos();
                let offset_y = r_offset * mid_a.sin();

                // Drop shadow for hovered
                if is_hovered {
                    cr.new_path();
                    cr.arc(cx + offset_x + 1.0, cy + offset_y + 2.0, outer_r + 1.0, seg_start, seg_end);
                    cr.arc_negative(cx + offset_x + 1.0, cy + offset_y + 2.0, inner_r - 1.0, seg_end, seg_start);
                    cr.close_path();
                    cr.set_source_rgba(0.0, 0.0, 0.0, 0.20);
                    cr.fill().ok();
                }

                // Gradient on segment (lighter at outer edge)
                cr.new_path();
                cr.arc(cx + offset_x, cy + offset_y, outer_r, seg_start, seg_end);
                cr.arc_negative(cx + offset_x, cy + offset_y, inner_r, seg_end, seg_start);
                cr.close_path();

                let grad = cairo::RadialGradient::new(cx + offset_x, cy + offset_y, inner_r, cx + offset_x, cy + offset_y, outer_r);
                let (cr_r, cr_g, cr_b) = seg.color;
                grad.add_color_stop_rgb(0.0, cr_r * 0.85, cr_g * 0.85, cr_b * 0.85);
                grad.add_color_stop_rgb(1.0, (cr_r * 1.12).min(1.0), (cr_g * 1.12).min(1.0), (cr_b * 1.12).min(1.0));
                cr.set_source(&grad).ok();
                cr.fill().ok();
            }

            start += sweep;
        }

        // Percentage labels on slices > 5% (3.1)
        cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Bold);
        cr.set_font_size(12.0);
        let label_r = (outer_r + inner_r) / 2.0;
        let mut start2 = -PI / 2.0;
        for seg in data.iter() {
            let frac = seg.value / total;
            let sweep2 = frac * 2.0 * PI * progress;
            if frac >= 0.05 && sweep2 > 0.0 {
                let mid_angle = start2 + sweep2 / 2.0;
                let lx = cx + label_r * mid_angle.cos();
                let ly = cy + label_r * mid_angle.sin();
                let pct_text = format!("{:.0}%", frac * 100.0);
                let ext = cr.text_extents(&pct_text).unwrap();
                // Choose text color based on slice luminance
                let lum = seg.color.0 * 0.299 + seg.color.1 * 0.587 + seg.color.2 * 0.114;
                if lum > 0.5 {
                    cr.set_source_rgba(0.0, 0.0, 0.0, 0.85);
                } else {
                    cr.set_source_rgba(1.0, 1.0, 1.0, 0.95);
                }
                cr.move_to(lx - ext.width() / 2.0, ly + ext.height() / 2.0);
                cr.show_text(&pct_text).ok();
            }
            start2 += sweep2;
        }

        cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Bold);
        cr.set_font_size(22.0);
        cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
        let total_str = format_currency_full(total, sym);
        let ext = cr.text_extents(&total_str).unwrap();
        cr.move_to(cx - ext.width() / 2.0, cy + ext.height() / 2.0);
        cr.show_text(&total_str).ok();

        cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
        cr.set_font_size(13.0);

        let legend_top = PAD + donut_h + 12.0;
        let col_w = (w - PAD * 2.0) / 2.0;

        for (i, seg) in data.iter().enumerate() {
            let col = i % 2;
            let row = i / 2;
            let x = PAD + col as f64 * col_w + 4.0;
            let y = legend_top + row as f64 * 28.0 + 16.0;

            cr.arc(x + 6.0, y - 4.0, 6.0, 0.0, 2.0 * PI);
            cr.set_source_rgb(seg.color.0, seg.color.1, seg.color.2);
            cr.fill().ok();

            let pct = seg.value / total * 100.0;
            let label = format!("{} ({:.0}%)", seg.label, pct);
            cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
            cr.move_to(x + 18.0, y);
            cr.show_text(&label).ok();
        }

        if hover_idx >= 0 && (hover_idx as usize) < data.len() && hover_progress > 0.1 {
            let seg = &data[hover_idx as usize];
            let pct = seg.value / total * 100.0;
            let l1 = seg.label.clone();
            let l2 = format!("{} ({:.1}%)", format_currency_full(seg.value, sym), pct);
            Self::draw_tooltip(cr, mouse.0, mouse.1, &[&l1, &l2], w, h);
        }
    }

    // =====================================================
    // Bar chart
    // =====================================================
    fn draw_bars(
        cr: &cairo::Context,
        width: i32, height: i32,
        data: &[BarMonth],
        compare: Option<&[BarMonth]>,
        progress: f64,
        mouse: (f64, f64),
        sym: &str,
    ) {
        let tc = theme_colors();
        let w = width as f64;
        let h = height as f64;

        if data.is_empty() { return; }

        let has_compare = compare.is_some();

        let margin_left = PAD + 50.0;
        let margin_right = PAD;
        let margin_top = PAD + 8.0;
        let margin_bottom = PAD + 44.0;

        let chart_w = w - margin_left - margin_right;
        let chart_h = h - margin_top - margin_bottom;

        // Max value considers both current and compare data
        let mut max_val = data.iter()
            .map(|d| d.income.max(d.expense))
            .fold(0.0_f64, f64::max);
        if let Some(cmp) = compare {
            let cmp_max = cmp.iter()
                .map(|d| d.income.max(d.expense))
                .fold(0.0_f64, f64::max);
            max_val = max_val.max(cmp_max);
        }
        let y_max = if max_val > 0.0 { max_val * 1.15 } else { 100.0 };

        let n = data.len() as f64;
        let group_w = chart_w / n;
        let bar_w = if has_compare {
            (group_w * 0.20).min(24.0)
        } else {
            (group_w * 0.34).min(38.0)
        };
        let gap = 4.0;
        let corner_r = 5.0;

        cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Normal);

        cr.set_font_size(13.0);
        cr.set_line_width(0.5);
        for i in 0..=4 {
            let frac = i as f64 / 4.0;
            let y = margin_top + chart_h * (1.0 - frac);
            let val = y_max * frac;

            cr.set_source_rgb(tc.grid.0, tc.grid.1, tc.grid.2);
            cr.move_to(margin_left, y);
            cr.line_to(w - margin_right, y);
            cr.stroke().ok();

            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            let label = format_amount_with_sym(val, sym);
            let ext = cr.text_extents(&label).unwrap();
            cr.move_to(margin_left - ext.width() - 8.0, y + ext.height() / 2.0);
            cr.show_text(&label).ok();
        }

        // Track hovered bar
        let mut hovered: Option<(usize, bool, bool)> = None; // (month_idx, is_expense, is_compare)
        let (mx, my) = mouse;

        // Draw compare bars first (behind current bars)
        if let Some(cmp) = compare {
            for (i, bar) in cmp.iter().enumerate() {
                let group_x = margin_left + i as f64 * group_w;
                let center_x = group_x + group_w / 2.0;

                // Compare income bar (leftmost)
                let bh_inc = (bar.income / y_max * chart_h * progress).max(0.0);
                let bx_inc = center_x - bar_w * 2.0 - gap * 0.5;
                let by_inc = margin_top + chart_h - bh_inc;

                if mx >= bx_inc && mx <= bx_inc + bar_w && my >= by_inc && my <= margin_top + chart_h {
                    hovered = Some((i, false, true));
                }

                Self::rounded_top_rect(cr, bx_inc, by_inc, bar_w, bh_inc, corner_r);
                cr.set_source_rgba(tc.income.0, tc.income.1, tc.income.2, 0.30);
                cr.fill().ok();

                // Compare expense bar
                let bh_exp = (bar.expense / y_max * chart_h * progress).max(0.0);
                let bx_exp = center_x - bar_w + gap * 0.5;
                let by_exp = margin_top + chart_h - bh_exp;

                if mx >= bx_exp && mx <= bx_exp + bar_w && my >= by_exp && my <= margin_top + chart_h {
                    hovered = Some((i, true, true));
                }

                Self::rounded_top_rect(cr, bx_exp, by_exp, bar_w, bh_exp, corner_r);
                cr.set_source_rgba(tc.expense.0, tc.expense.1, tc.expense.2, 0.30);
                cr.fill().ok();
            }
        }

        // Current bars
        for (i, bar) in data.iter().enumerate() {
            let group_x = margin_left + i as f64 * group_w;
            let center_x = group_x + group_w / 2.0;

            let (bx_inc_offset, bx_exp_offset) = if has_compare {
                // Shift current bars to the right
                (center_x - gap * 0.5, center_x + bar_w + gap * 0.5)
            } else {
                (center_x - bar_w - gap / 2.0, center_x + gap / 2.0)
            };

            let bh_inc = (bar.income / y_max * chart_h * progress).max(0.0);
            let bx_inc = bx_inc_offset;
            let by_inc = margin_top + chart_h - bh_inc;

            // Check income hover
            if mx >= bx_inc && mx <= bx_inc + bar_w && my >= by_inc && my <= margin_top + chart_h {
                hovered = Some((i, false, false));
            }

            // Income bar shadow
            Self::rounded_top_rect(cr, bx_inc + 1.5, by_inc + 2.0, bar_w, bh_inc, corner_r);
            cr.set_source_rgba(0.0, 0.0, 0.0, 0.10);
            cr.fill().ok();

            Self::rounded_top_rect(cr, bx_inc, by_inc, bar_w, bh_inc, corner_r);
            if bh_inc > 1.0 {
                let grad = cairo::LinearGradient::new(0.0, by_inc, 0.0, margin_top + chart_h);
                let (ir, ig, ib) = tc.income;
                grad.add_color_stop_rgb(0.0, (ir * 1.15).min(1.0), (ig * 1.15).min(1.0), (ib * 1.15).min(1.0));
                grad.add_color_stop_rgb(1.0, ir * 0.85, ig * 0.85, ib * 0.85);
                cr.set_source(&grad).ok();
            } else {
                cr.set_source_rgb(tc.income.0, tc.income.1, tc.income.2);
            }
            cr.fill().ok();

            let bh_exp = (bar.expense / y_max * chart_h * progress).max(0.0);
            let bx_exp = bx_exp_offset;
            let by_exp = margin_top + chart_h - bh_exp;

            // Check expense hover
            if mx >= bx_exp && mx <= bx_exp + bar_w && my >= by_exp && my <= margin_top + chart_h {
                hovered = Some((i, true, false));
            }

            // Expense bar shadow
            Self::rounded_top_rect(cr, bx_exp + 1.5, by_exp + 2.0, bar_w, bh_exp, corner_r);
            cr.set_source_rgba(0.0, 0.0, 0.0, 0.10);
            cr.fill().ok();

            Self::rounded_top_rect(cr, bx_exp, by_exp, bar_w, bh_exp, corner_r);
            if bh_exp > 1.0 {
                let grad = cairo::LinearGradient::new(0.0, by_exp, 0.0, margin_top + chart_h);
                let (er, eg, eb) = tc.expense;
                grad.add_color_stop_rgb(0.0, (er * 1.15).min(1.0), (eg * 1.15).min(1.0), (eb * 1.15).min(1.0));
                grad.add_color_stop_rgb(1.0, er * 0.85, eg * 0.85, eb * 0.85);
                cr.set_source(&grad).ok();
            } else {
                cr.set_source_rgb(tc.expense.0, tc.expense.1, tc.expense.2);
            }
            cr.fill().ok();

            cr.set_font_size(14.0);
            cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
            let ext = cr.text_extents(&bar.label).unwrap();
            cr.move_to(center_x - ext.width() / 2.0, margin_top + chart_h + 22.0);
            cr.show_text(&bar.label).ok();
        }

        let legend_y = h - PAD - 2.0;
        cr.set_font_size(13.0);
        let mut lx = margin_left;

        cr.arc(lx + 6.0, legend_y - 4.0, 6.0, 0.0, 2.0 * PI);
        cr.set_source_rgb(tc.income.0, tc.income.1, tc.income.2);
        cr.fill().ok();
        cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
        cr.move_to(lx + 18.0, legend_y);
        cr.show_text("Income").ok();
        lx += 18.0 + cr.text_extents("Income").unwrap().width() + 24.0;

        cr.arc(lx + 6.0, legend_y - 4.0, 6.0, 0.0, 2.0 * PI);
        cr.set_source_rgb(tc.expense.0, tc.expense.1, tc.expense.2);
        cr.fill().ok();
        cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
        cr.move_to(lx + 18.0, legend_y);
        cr.show_text("Expenses").ok();

        if has_compare {
            lx += 18.0 + cr.text_extents("Expenses").unwrap().width() + 24.0;
            // Faded square for previous year
            cr.rectangle(lx, legend_y - 10.0, 12.0, 12.0);
            cr.set_source_rgba(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2, 0.35);
            cr.fill().ok();
            cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
            cr.move_to(lx + 18.0, legend_y);
            cr.show_text("Prev Year").ok();
        }

        if let Some((idx, is_expense, is_cmp)) = hovered {
            let src = if is_cmp { compare.unwrap_or(data) } else { data };
            let bar = &src[idx];
            let prefix = if is_cmp { "Prev " } else { "" };
            let l1 = format!("{}{}", prefix, bar.label);
            let l2 = if is_expense {
                format!("Expenses: {}", format_currency_full(bar.expense, sym))
            } else {
                format!("Income: {}", format_currency_full(bar.income, sym))
            };
            Self::draw_tooltip(cr, mx, my, &[&l1, &l2], w, h);
        }
    }

    fn rounded_top_rect(cr: &cairo::Context, x: f64, y: f64, w: f64, h: f64, r: f64) {
        if h < 1.0 { return; }
        let r = r.min(h / 2.0).min(w / 2.0);
        cr.new_path();
        cr.move_to(x, y + h);
        cr.line_to(x, y + r);
        cr.arc(x + r, y + r, r, PI, 1.5 * PI);
        cr.arc(x + w - r, y + r, r, 1.5 * PI, 2.0 * PI);
        cr.line_to(x + w, y + h);
        cr.close_path();
    }

    // =====================================================
    // Line chart
    // =====================================================
    fn draw_line(
        cr: &cairo::Context,
        width: i32, height: i32,
        data: &[LinePoint],
        progress: f64,
        mouse: (f64, f64),
        sym: &str,
    ) {
        let tc = theme_colors();
        let w = width as f64;
        let h = height as f64;

        if data.is_empty() {
            cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
            cr.set_font_size(16.0);
            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            let msg = "No data this month";
            let ext = cr.text_extents(msg).unwrap();
            cr.move_to((w - ext.width()) / 2.0, h / 2.0);
            cr.show_text(msg).ok();
            return;
        }

        let margin_left = PAD + 50.0;
        let margin_right = PAD;
        let margin_top = PAD;
        let margin_bottom = PAD + 16.0;

        let chart_w = w - margin_left - margin_right;
        let chart_h = h - margin_top - margin_bottom;

        let min_val = data.iter().map(|p| p.value).fold(f64::INFINITY, f64::min).min(0.0);
        let max_val = data.iter().map(|p| p.value).fold(f64::NEG_INFINITY, f64::max).max(0.0);
        let range = (max_val - min_val).max(10.0);
        let y_min = min_val - range * 0.1;
        let y_max = max_val + range * 0.1;
        let y_range = y_max - y_min;

        let first_day = data.first().map(|p| p.day).unwrap_or(1.0);
        let last_day = data.last().map(|p| p.day).unwrap_or(30.0);
        let x_range = (last_day - first_day).max(1.0);

        let to_x = |day: f64| -> f64 { margin_left + (day - first_day) / x_range * chart_w };
        let to_y = |val: f64| -> f64 { margin_top + (1.0 - (val - y_min) / y_range) * chart_h };

        cr.select_font_face("JetBrains Mono", cairo::FontSlant::Normal, cairo::FontWeight::Normal);
        cr.set_font_size(13.0);

        cr.set_line_width(0.5);
        for i in 0..=4 {
            let frac = i as f64 / 4.0;
            let val = y_min + y_range * frac;
            let y = to_y(val);

            cr.set_source_rgb(tc.grid.0, tc.grid.1, tc.grid.2);
            cr.move_to(margin_left, y);
            cr.line_to(w - margin_right, y);
            cr.stroke().ok();

            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            let label = format_amount_with_sym(val, sym);
            let ext = cr.text_extents(&label).unwrap();
            cr.move_to(margin_left - ext.width() - 8.0, y + ext.height() / 2.0);
            cr.show_text(&label).ok();
        }

        if y_min < 0.0 && y_max > 0.0 {
            let zy = to_y(0.0);
            cr.set_source_rgba(tc.text.0, tc.text.1, tc.text.2, 0.35);
            cr.set_dash(&[6.0, 4.0], 0.0);
            cr.set_line_width(1.0);
            cr.move_to(margin_left, zy);
            cr.line_to(w - margin_right, zy);
            cr.stroke().ok();
            cr.set_dash(&[], 0.0);
        }

        {
            cr.set_font_size(13.0);
            let step = if x_range > 20.0 { 5.0 } else if x_range > 10.0 { 2.0 } else { 1.0 };
            let mut day = (first_day / step).ceil() * step;
            while day <= last_day {
                let x = to_x(day);
                cr.set_source_rgb(tc.text.0, tc.text.1, tc.text.2);
                let label = format!("{}", day as i32);
                let ext = cr.text_extents(&label).unwrap();
                cr.move_to(x - ext.width() / 2.0, margin_top + chart_h + 20.0);
                cr.show_text(&label).ok();
                day += step;
            }
        }

        if data.len() < 2 {
            let p = &data[0];
            let x = to_x(p.day);
            let y = to_y(p.value);
            let clip_x = margin_left + chart_w * progress;
            if x <= clip_x {
                let accent = tc.income;
                cr.arc(x, y, 4.0, 0.0, 2.0 * PI);
                cr.set_source_rgb(accent.0, accent.1, accent.2);
                cr.fill().ok();
            }
            return;
        }

        let spline_points = Self::catmull_rom_to_bezier(data, &to_x, &to_y);

        // Clip to progress
        let clip_x = margin_left + chart_w * progress;
        cr.save().ok();
        cr.rectangle(margin_left, margin_top - 4.0, clip_x - margin_left, chart_h + 8.0);
        cr.clip();

        let accent = tc.income;
        let zero_y = to_y(0.0).clamp(margin_top, margin_top + chart_h);

        {
            cr.new_path();
            let first_x = to_x(data[0].day);
            let first_y = to_y(data[0].value);
            cr.move_to(first_x, first_y);

            for &(c1x, c1y, c2x, c2y, ex, ey) in &spline_points {
                cr.curve_to(c1x, c1y, c2x, c2y, ex, ey);
            }

            let last_x = to_x(data.last().unwrap().day);
            cr.line_to(last_x, zero_y);
            cr.line_to(first_x, zero_y);
            cr.close_path();

            let grad = cairo::LinearGradient::new(0.0, margin_top, 0.0, margin_top + chart_h);
            grad.add_color_stop_rgba(0.0, accent.0, accent.1, accent.2, 0.32);
            grad.add_color_stop_rgba(1.0, accent.0, accent.1, accent.2, 0.02);
            cr.set_source(&grad).ok();
            cr.fill().ok();
        }

        {
            // Glow/shadow under the line
            cr.new_path();
            cr.move_to(to_x(data[0].day), to_y(data[0].value) + 2.0);
            for &(c1x, c1y, c2x, c2y, ex, ey) in &spline_points {
                cr.curve_to(c1x, c1y + 2.0, c2x, c2y + 2.0, ex, ey + 2.0);
            }
            cr.set_source_rgba(accent.0 * 0.3, accent.1 * 0.3, accent.2 * 0.3, 0.18);
            cr.set_line_width(4.0);
            cr.stroke().ok();

            // Actual line
            cr.new_path();
            cr.move_to(to_x(data[0].day), to_y(data[0].value));
            for &(c1x, c1y, c2x, c2y, ex, ey) in &spline_points {
                cr.curve_to(c1x, c1y, c2x, c2y, ex, ey);
            }
            cr.set_source_rgb(accent.0, accent.1, accent.2);
            cr.set_line_width(2.5);
            cr.stroke().ok();
        }

        let mut hovered_pt: Option<usize> = None;
        let (mx, my) = mouse;
        for (i, p) in data.iter().enumerate() {
            let x = to_x(p.day);
            let y = to_y(p.value);

            let is_hov = mx >= 0.0 && ((mx - x).powi(2) + (my - y).powi(2)).sqrt() < 12.0;
            if is_hov { hovered_pt = Some(i); }

            let radius = if is_hov { 5.5 } else { 3.5 };

            // White/dark ring behind dot
            cr.arc(x, y, radius + 1.5, 0.0, 2.0 * PI);
            if adw::StyleManager::default().is_dark() {
                cr.set_source_rgba(0.14, 0.14, 0.14, 0.9);
            } else {
                cr.set_source_rgba(1.0, 1.0, 1.0, 0.9);
            }
            cr.fill().ok();

            cr.arc(x, y, radius, 0.0, 2.0 * PI);
            cr.set_source_rgb(accent.0, accent.1, accent.2);
            cr.fill().ok();
        }

        cr.restore().ok();

        if let Some(idx) = hovered_pt {
            let p = &data[idx];
            let l1 = format!("Day {}", p.day as i32);
            let l2 = format!("Net: {}", format_currency_full(p.value, sym));
            Self::draw_tooltip(cr, mx, my, &[&l1, &l2], w, h);
        }
    }

    fn catmull_rom_to_bezier(
        data: &[LinePoint],
        to_x: &dyn Fn(f64) -> f64,
        to_y: &dyn Fn(f64) -> f64,
    ) -> Vec<(f64, f64, f64, f64, f64, f64)> {
        let n = data.len();
        if n < 2 { return Vec::new(); }

        let pts: Vec<(f64, f64)> = data.iter().map(|p| (to_x(p.day), to_y(p.value))).collect();
        let mut result = Vec::with_capacity(n - 1);
        let alpha = 0.5;

        for i in 0..n - 1 {
            let p0 = if i == 0 { pts[0] } else { pts[i - 1] };
            let p1 = pts[i];
            let p2 = pts[i + 1];
            let p3 = if i + 2 < n { pts[i + 2] } else { pts[n - 1] };

            let cp1x = p1.0 + (p2.0 - p0.0) / (6.0 / alpha);
            let cp1y = p1.1 + (p2.1 - p0.1) / (6.0 / alpha);
            let cp2x = p2.0 - (p3.0 - p1.0) / (6.0 / alpha);
            let cp2y = p2.1 - (p3.1 - p1.1) / (6.0 / alpha);

            result.push((cp1x, cp1y, cp2x, cp2y, p2.0, p2.1));
        }

        result
    }

    fn monotone_subdivide(pts: &[(f64, f64)], subdivisions: usize) -> Vec<(f64, f64)> {
        crate::charts_view::monotone_subdivide(pts, subdivisions)
    }
}

/// Monotone cubic Hermite interpolation (Fritsch-Carlson).
/// Returns densely subdivided points for smooth line_to drawing.
pub(crate) fn monotone_subdivide(pts: &[(f64, f64)], subdivisions: usize) -> Vec<(f64, f64)> {
        let n = pts.len();
        if n < 2 { return pts.to_vec(); }
        if n == 2 {
            let mut out = Vec::with_capacity(subdivisions + 1);
            for j in 0..=subdivisions {
                let t = j as f64 / subdivisions as f64;
                out.push((
                    pts[0].0 + t * (pts[1].0 - pts[0].0),
                    pts[0].1 + t * (pts[1].1 - pts[0].1),
                ));
            }
            return out;
        }

        // Compute slopes (tangents) using Fritsch-Carlson monotone method
        let mut dx = Vec::with_capacity(n - 1);
        let mut dy = Vec::with_capacity(n - 1);
        let mut slopes = Vec::with_capacity(n - 1);
        for i in 0..n - 1 {
            let dxi = pts[i + 1].0 - pts[i].0;
            let dyi = pts[i + 1].1 - pts[i].1;
            dx.push(dxi);
            dy.push(dyi);
            slopes.push(if dxi.abs() < 1e-12 { 0.0 } else { dyi / dxi });
        }

        let mut tangents = vec![0.0f64; n];
        tangents[0] = slopes[0];
        tangents[n - 1] = slopes[n - 2];
        for i in 1..n - 1 {
            if slopes[i - 1].signum() != slopes[i].signum()
                || slopes[i - 1].abs() < 1e-12
                || slopes[i].abs() < 1e-12
            {
                tangents[i] = 0.0;
            } else {
                tangents[i] = (slopes[i - 1] + slopes[i]) / 2.0;
            }
        }

        // Fritsch-Carlson monotonicity fix
        for i in 0..n - 1 {
            if slopes[i].abs() < 1e-12 {
                tangents[i] = 0.0;
                tangents[i + 1] = 0.0;
            } else {
                let a = tangents[i] / slopes[i];
                let b = tangents[i + 1] / slopes[i];
                let s = a * a + b * b;
                if s > 9.0 {
                    let tau = 3.0 / s.sqrt();
                    tangents[i] = tau * a * slopes[i];
                    tangents[i + 1] = tau * b * slopes[i];
                }
            }
        }

        // Hermite interpolation between each pair of points
        let mut out = Vec::with_capacity((n - 1) * subdivisions + 1);
        for i in 0..n - 1 {
            let h = dx[i];
            let steps = if i == n - 2 { subdivisions + 1 } else { subdivisions };
            for j in 0..steps {
                let t = j as f64 / subdivisions as f64;
                let t2 = t * t;
                let t3 = t2 * t;
                let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
                let h10 = t3 - 2.0 * t2 + t;
                let h01 = -2.0 * t3 + 3.0 * t2;
                let h11 = t3 - t2;

                let x = pts[i].0 + t * h;
                let y = h00 * pts[i].1
                    + h10 * h * tangents[i]
                    + h01 * pts[i + 1].1
                    + h11 * h * tangents[i + 1];
                out.push((x, y));
            }
        }
        out
}

impl ChartsView {
    // =====================================================
    // Net trend chart (12-month net income - expenses)
    // =====================================================
    fn draw_net_trend(
        cr: &cairo::Context,
        width: i32, height: i32,
        data: &[NetTrendPoint],
        progress: f64,
        mouse: (f64, f64),
        sym: &str,
    ) {
        let w = width as f64;
        let h = height as f64;
        let tc = theme_colors();

        if data.is_empty() {
            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            cr.set_font_size(14.0);
            let _ = cr.move_to(w / 2.0 - 40.0, h / 2.0);
            let _ = cr.show_text("No data");
            return;
        }

        let chart_left = PAD + 50.0;
        let chart_right = w - PAD;
        let chart_top = PAD;
        let chart_bottom = h - PAD - 20.0;
        let chart_w = chart_right - chart_left;
        let chart_h = chart_bottom - chart_top;

        // Find min/max values
        let max_val = data.iter().map(|p| p.value).fold(f64::NEG_INFINITY, f64::max);
        let min_val = data.iter().map(|p| p.value).fold(f64::INFINITY, f64::min);
        let range = (max_val - min_val).max(1.0);
        let y_min = min_val - range * 0.1;
        let y_max = max_val + range * 0.1;
        let y_range = y_max - y_min;

        let val_to_y = |v: f64| -> f64 {
            chart_bottom - (v - y_min) / y_range * chart_h
        };

        let n = data.len();
        let step = if n > 1 { chart_w / (n - 1) as f64 } else { chart_w };

        cr.set_source_rgb(tc.grid.0, tc.grid.1, tc.grid.2);
        cr.set_line_width(0.5);
        for i in 0..5 {
            let y = chart_top + chart_h * i as f64 / 4.0;
            let _ = cr.move_to(chart_left, y);
            let _ = cr.line_to(chart_right, y);
            let _ = cr.stroke();

            let val = y_max - (y_max - y_min) * i as f64 / 4.0;
            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            cr.set_font_size(12.0);
            let label = format_amount_with_sym(val, sym);
            let _ = cr.move_to(PAD, y + 4.0);
            let _ = cr.show_text(&label);
            cr.set_source_rgb(tc.grid.0, tc.grid.1, tc.grid.2);
        }

        let zero_y = val_to_y(0.0);
        if zero_y >= chart_top && zero_y <= chart_bottom {
            cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
            cr.set_line_width(1.0);
            let _ = cr.move_to(chart_left, zero_y);
            let _ = cr.line_to(chart_right, zero_y);
            let _ = cr.stroke();
        }

        let points: Vec<(f64, f64)> = data.iter().enumerate().map(|(i, p)| {
            let x = chart_left + i as f64 * step;
            let y = val_to_y(p.value * progress);
            (x, y)
        }).collect();

        if points.len() >= 2 {
            let zy = val_to_y(0.0).clamp(chart_top, chart_bottom);
            let smooth = Self::monotone_subdivide(&points, 16);

            // Overall net determines fill color
            let net_sum: f64 = data.iter().map(|p| p.value).sum();
            if net_sum >= 0.0 {
                cr.set_source_rgba(tc.income.0, tc.income.1, tc.income.2, 0.15);
            } else {
                cr.set_source_rgba(tc.expense.0, tc.expense.1, tc.expense.2, 0.15);
            }

            let _ = cr.move_to(smooth[0].0, zy);
            let _ = cr.line_to(smooth[0].0, smooth[0].1);
            for &(x, y) in &smooth[1..] {
                let _ = cr.line_to(x, y);
            }
            let _ = cr.line_to(smooth.last().unwrap().0, zy);
            let _ = cr.close_path();
            let _ = cr.fill();

            cr.set_line_width(2.5);
            if net_sum >= 0.0 {
                cr.set_source_rgb(tc.income.0, tc.income.1, tc.income.2);
            } else {
                cr.set_source_rgb(tc.expense.0, tc.expense.1, tc.expense.2);
            }
            let _ = cr.move_to(smooth[0].0, smooth[0].1);
            for &(x, y) in &smooth[1..] {
                let _ = cr.line_to(x, y);
            }
            let _ = cr.stroke();

            for (i, &(x, y)) in points.iter().enumerate() {
                let val = data[i].value;
                if val >= 0.0 {
                    cr.set_source_rgb(tc.income.0, tc.income.1, tc.income.2);
                } else {
                    cr.set_source_rgb(tc.expense.0, tc.expense.1, tc.expense.2);
                }
                let _ = cr.arc(x, y, 3.5, 0.0, 2.0 * PI);
                let _ = cr.fill();
            }
        }

        cr.set_font_size(12.0);
        cr.set_source_rgb(tc.text_secondary.0, tc.text_secondary.1, tc.text_secondary.2);
        for (i, p) in data.iter().enumerate() {
            let x = chart_left + i as f64 * step;
            let _ = cr.move_to(x - 12.0, chart_bottom + 14.0);
            let _ = cr.show_text(&p.label);
        }

        if mouse.0 >= chart_left && mouse.0 <= chart_right && mouse.1 >= chart_top && mouse.1 <= chart_bottom {
            let idx = ((mouse.0 - chart_left) / step + 0.5) as usize;
            if idx < data.len() {
                let p = &data[idx];
                let px = chart_left + idx as f64 * step;
                let py = val_to_y(p.value * progress);

                let text = format!("{}: {}{:.2}", p.label, sym, p.value);
                cr.set_font_size(12.0);
                let ext = cr.text_extents(&text).unwrap();
                let tw = ext.width() + 16.0;
                let th = 24.0;
                let tx = (px - tw / 2.0).clamp(chart_left, chart_right - tw);
                let ty = py - th - 10.0;

                Self::rounded_rect(cr, tx, ty, tw, th, 6.0);
                cr.set_source_rgba(tc.tooltip_bg.0, tc.tooltip_bg.1, tc.tooltip_bg.2, tc.tooltip_bg.3);
                let _ = cr.fill_preserve();
                cr.set_source_rgba(tc.tooltip_border.0, tc.tooltip_border.1, tc.tooltip_border.2, tc.tooltip_border.3);
                cr.set_line_width(1.0);
                let _ = cr.stroke();

                cr.set_source_rgb(tc.tooltip_text.0, tc.tooltip_text.1, tc.tooltip_text.2);
                let _ = cr.move_to(tx + 8.0, ty + 16.0);
                let _ = cr.show_text(&text);
            }
        }
    }

    fn parse_hex_color(hex: &str) -> (f64, f64, f64) {
        let hex = hex.trim_start_matches('#');
        if hex.len() >= 6 {
            let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(128) as f64 / 255.0;
            let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(128) as f64 / 255.0;
            let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(128) as f64 / 255.0;
            (r, g, b)
        } else {
            (0.5, 0.5, 0.5)
        }
    }

    fn draw_sankey(area: &gtk::DrawingArea, db: &Database, year: i32, month: u32) {
        use outlay_core::models::TransactionType;

        let income_cats = db.get_monthly_totals_by_category(year, month, TransactionType::Income).unwrap_or_default();
        let expense_cats = db.get_monthly_totals_by_category(year, month, TransactionType::Expense).unwrap_or_default();

        let default_income_colors = [(0.2, 0.7, 0.3), (0.3, 0.8, 0.4), (0.1, 0.6, 0.2)];
        let default_expense_colors = [(0.8, 0.3, 0.3), (0.3, 0.5, 0.8), (0.8, 0.6, 0.2), (0.5, 0.3, 0.7), (0.7, 0.4, 0.5)];

        let income_sources: Vec<(String, f64, (f64, f64, f64))> = income_cats.iter().enumerate().map(|(i, (cat, amt))| {
            let color = cat.color.as_deref()
                .map(|h| Self::parse_hex_color(h))
                .unwrap_or(default_income_colors[i % default_income_colors.len()]);
            (cat.name.clone(), *amt, color)
        }).collect();

        let expense_categories: Vec<(String, f64, (f64, f64, f64))> = expense_cats.iter().enumerate().map(|(i, (cat, amt))| {
            let color = cat.color.as_deref()
                .map(|h| Self::parse_hex_color(h))
                .unwrap_or(default_expense_colors[i % default_expense_colors.len()]);
            (cat.name.clone(), *amt, color)
        }).collect();

        let is_dark = adw::StyleManager::default().is_dark();

        area.set_draw_func(move |_area, cr, width, height| {
            let w = width as f64;
            let h = height as f64;

            if income_sources.is_empty() && expense_categories.is_empty() {
                let tc = if is_dark { (0.8, 0.8, 0.8) } else { (0.4, 0.4, 0.4) };
                cr.set_source_rgb(tc.0, tc.1, tc.2);
                cr.set_font_size(14.0);
                let _ = cr.move_to(w / 2.0 - 60.0, h / 2.0);
                let _ = cr.show_text("No data for this month");
                return;
            }

            let layout = outlay_core::sankey::compute_sankey_layout(
                &income_sources,
                &expense_categories,
                h - 20.0,
            );

            let left_x = 10.0;
            let node_w = 100.0;
            let center_x = w / 2.0 - node_w / 2.0;
            let right_x = w - node_w - 10.0;

            let text_color = if is_dark { (0.9, 0.9, 0.9) } else { (0.15, 0.15, 0.15) };

            // Draw flows: income -> center
            for flow in &layout.flows_in {
                if flow.from_idx >= layout.left_nodes.len() { continue; }
                let node = &layout.left_nodes[flow.from_idx];
                let (r, g, b) = node.color;
                cr.set_source_rgba(r, g, b, 0.25);

                let from_right = left_x + node_w;
                let to_left = center_x;
                let cp = (to_left - from_right) * 0.5;

                let _ = cr.move_to(from_right, flow.from_y + 10.0);
                let _ = cr.curve_to(
                    from_right + cp, flow.from_y + 10.0,
                    to_left - cp, layout.center_y + flow.to_y + 10.0,
                    to_left, layout.center_y + flow.to_y + 10.0,
                );
                let _ = cr.line_to(to_left, layout.center_y + flow.to_y + flow.width + 10.0);
                let _ = cr.curve_to(
                    to_left - cp, layout.center_y + flow.to_y + flow.width + 10.0,
                    from_right + cp, flow.from_y + flow.width + 10.0,
                    from_right, flow.from_y + flow.width + 10.0,
                );
                let _ = cr.close_path();
                let _ = cr.fill();
            }

            // Draw flows: center -> expenses
            for flow in &layout.flows_out {
                if flow.to_idx >= layout.right_nodes.len() { continue; }
                let node = &layout.right_nodes[flow.to_idx];
                let (r, g, b) = node.color;
                cr.set_source_rgba(r, g, b, 0.25);

                let from_right = center_x + node_w;
                let to_left = right_x;
                let cp = (to_left - from_right) * 0.5;

                let _ = cr.move_to(from_right, layout.center_y + flow.from_y + 10.0);
                let _ = cr.curve_to(
                    from_right + cp, layout.center_y + flow.from_y + 10.0,
                    to_left - cp, flow.to_y + 10.0,
                    to_left, flow.to_y + 10.0,
                );
                let _ = cr.line_to(to_left, flow.to_y + flow.width + 10.0);
                let _ = cr.curve_to(
                    to_left - cp, flow.to_y + flow.width + 10.0,
                    from_right + cp, layout.center_y + flow.from_y + flow.width + 10.0,
                    from_right, layout.center_y + flow.from_y + flow.width + 10.0,
                );
                let _ = cr.close_path();
                let _ = cr.fill();
            }

            // Draw left nodes (income)
            cr.set_font_size(12.0);
            for node in &layout.left_nodes {
                let (r, g, b) = node.color;
                cr.set_source_rgb(r, g, b);
                Self::rounded_rect(cr, left_x, node.y + 10.0, node_w, node.height.max(2.0), 4.0);
                let _ = cr.fill();

                cr.set_source_rgb(text_color.0, text_color.1, text_color.2);
                let label = if node.height > 16.0 {
                    format!("{} ({:.0})", node.label, node.value)
                } else {
                    node.label.clone()
                };
                let _ = cr.move_to(left_x + 4.0, node.y + 10.0 + (node.height / 2.0).max(10.0) + 4.0);
                let _ = cr.show_text(&label);
            }

            // Draw center node
            {
                let (r, g, b) = if layout.net >= 0.0 { (0.2, 0.5, 0.8) } else { (0.8, 0.3, 0.3) };
                cr.set_source_rgb(r, g, b);
                Self::rounded_rect(cr, center_x, layout.center_y + 10.0, node_w, layout.center_height.max(2.0), 4.0);
                let _ = cr.fill();

                cr.set_source_rgb(text_color.0, text_color.1, text_color.2);
                let _ = cr.move_to(center_x + 4.0, layout.center_y + 10.0 + layout.center_height / 2.0 + 4.0);
                let _ = cr.show_text(&format!("Net: {:.0}", layout.net));
            }

            // Draw right nodes (expenses)
            for node in &layout.right_nodes {
                let (r, g, b) = node.color;
                cr.set_source_rgb(r, g, b);
                Self::rounded_rect(cr, right_x, node.y + 10.0, node_w, node.height.max(2.0), 4.0);
                let _ = cr.fill();

                cr.set_source_rgb(text_color.0, text_color.1, text_color.2);
                let label = if node.height > 16.0 {
                    format!("{} ({:.0})", node.label, node.value)
                } else {
                    node.label.clone()
                };
                let _ = cr.move_to(right_x + 4.0, node.y + 10.0 + (node.height / 2.0).max(10.0) + 4.0);
                let _ = cr.show_text(&label);
            }
        });
    }

}
