#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::collections::HashMap;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};

use eframe::egui;

const URL: &str = "https://rate.bot.com.tw/xrt/flcsv/0/day";
const TARGETS: &[(&str, &str)] = &[("USD", "美元"), ("JPY", "日幣"), ("EUR", "歐元")];

#[derive(Clone, Debug)]
struct Rates {
    items: Vec<(String, String, f64)>, // code, name, rate
    fetched_at: String,
}

enum Msg {
    Ok(Rates),
    Err(String),
}

fn fetch_rates() -> Result<Rates, String> {
    let agent = ureq::AgentBuilder::new()
        .timeout(Duration::from_secs(10))
        .user_agent("Mozilla/5.0 bot_rates")
        .build();

    let body = agent
        .get(URL)
        .call()
        .map_err(|e| format!("{e}"))?
        .into_string()
        .map_err(|e| format!("{e}"))?;

    let body = body.trim_start_matches('\u{feff}');
    let mut items = Vec::new();
    for (idx, line) in body.lines().enumerate() {
        if idx == 0 {
            continue;
        }
        let cols: Vec<&str> = line.split(',').collect();
        if cols.len() < 14 {
            continue;
        }
        let code = cols[0].trim();
        if let Some((_, name)) = TARGETS.iter().find(|(c, _)| *c == code) {
            if let Ok(v) = cols[13].trim().parse::<f64>() {
                items.push((code.to_string(), (*name).to_string(), v));
            }
        }
    }

    items.sort_by_key(|it| {
        TARGETS
            .iter()
            .position(|(c, _)| *c == it.0)
            .unwrap_or(usize::MAX)
    });

    if items.is_empty() {
        return Err("解析結果為空".into());
    }

    let fetched_at = now_string();
    Ok(Rates { items, fetched_at })
}

fn now_string() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0) as i64
        + 8 * 3600;

    let days = secs / 86_400;
    let sod = secs % 86_400;
    let (h, m, s) = (sod / 3600, (sod % 3600) / 60, sod % 60);
    let (y, mo, d) = civil_from_days(days);
    format!("{y:04}-{mo:02}-{d:02} {h:02}:{m:02}:{s:02}")
}

fn civil_from_days(z: i64) -> (i32, u32, u32) {
    let z = z + 719_468;
    let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
    let doe = (z - era * 146_097) as u64;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let y = yoe as i64 + era * 400;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = if m <= 2 { y + 1 } else { y };
    (y as i32, m as u32, d as u32)
}

fn setup_fonts(ctx: &egui::Context) {
    let mut fonts = egui::FontDefinitions::default();

    let candidates: &[&str] = &[
        r"C:\Windows\Fonts\msjh.ttc",
        r"C:\Windows\Fonts\msjhl.ttc",
        r"C:\Windows\Fonts\msyh.ttc",
        r"C:\Windows\Fonts\simhei.ttf",
        "/System/Library/Fonts/PingFang.ttc",
        "/System/Library/Fonts/Hiragino Sans GB.ttc",
    ];

    for path in candidates {
        if let Ok(bytes) = std::fs::read(path) {
            fonts
                .font_data
                .insert("cjk".to_owned(), egui::FontData::from_owned(bytes));
            fonts
                .families
                .entry(egui::FontFamily::Proportional)
                .or_default()
                .insert(0, "cjk".to_owned());
            fonts
                .families
                .entry(egui::FontFamily::Monospace)
                .or_default()
                .push("cjk".to_owned());
            break;
        }
    }

    ctx.set_fonts(fonts);
}

fn parse_amount(s: &str) -> Option<f64> {
    let cleaned: String = s.chars().filter(|c| *c != ',' && !c.is_whitespace()).collect();
    if cleaned.is_empty() {
        return None;
    }
    cleaned.parse::<f64>().ok()
}

fn format_twd(v: f64) -> String {
    let rounded = (v * 100.0).round() / 100.0;
    let sign = if rounded < 0.0 { "-" } else { "" };
    let abs = rounded.abs();
    let int_part = abs.trunc() as u64;
    let frac = ((abs - int_part as f64) * 100.0).round() as u64;
    let int_str = int_part.to_string();
    let mut with_sep = String::new();
    for (i, ch) in int_str.chars().rev().enumerate() {
        if i > 0 && i % 3 == 0 {
            with_sep.push(',');
        }
        with_sep.push(ch);
    }
    let int_sep: String = with_sep.chars().rev().collect();
    format!("{sign}{int_sep}.{frac:02}")
}

fn format_twd_raw(v: f64) -> String {
    let rounded = (v * 100.0).round() / 100.0;
    format!("{rounded:.2}")
}

struct App {
    rx: Option<mpsc::Receiver<Msg>>,
    rates: Option<Rates>,
    error: Option<String>,
    loading: bool,
    fonts_ready: bool,
    amounts: HashMap<String, String>,
    copied: Option<(String, Instant)>,
}

impl App {
    fn new(_cc: &eframe::CreationContext<'_>) -> Self {
        let mut amounts = HashMap::new();
        for (code, _) in TARGETS {
            amounts.insert((*code).to_string(), "100".to_string());
        }
        let mut app = Self {
            rx: None,
            rates: None,
            error: None,
            loading: false,
            fonts_ready: false,
            amounts,
            copied: None,
        };
        app.start_fetch();
        app
    }

    fn start_fetch(&mut self) {
        if self.loading {
            return;
        }
        self.loading = true;
        self.error = None;
        let (tx, rx) = mpsc::channel();
        thread::spawn(move || {
            let msg = match fetch_rates() {
                Ok(r) => Msg::Ok(r),
                Err(e) => Msg::Err(e),
            };
            let _ = tx.send(msg);
        });
        self.rx = Some(rx);
    }
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        if !self.fonts_ready {
            setup_fonts(ctx);
            self.fonts_ready = true;
        }

        if let Some(rx) = &self.rx {
            if let Ok(msg) = rx.try_recv() {
                match msg {
                    Msg::Ok(r) => {
                        self.rates = Some(r);
                        self.error = None;
                    }
                    Msg::Err(e) => self.error = Some(e),
                }
                self.loading = false;
                self.rx = None;
            } else {
                ctx.request_repaint_after(Duration::from_millis(100));
            }
        }

        let bg = egui::Color32::from_rgb(245, 246, 250);
        let card = egui::Color32::WHITE;
        let text_main = egui::Color32::from_rgb(31, 41, 55);
        let text_sub = egui::Color32::from_rgb(107, 114, 128);
        let rate_color = egui::Color32::from_rgb(15, 118, 110);
        let twd_color = egui::Color32::from_rgb(180, 83, 9);
        let err_color = egui::Color32::from_rgb(185, 28, 28);

        let frame = egui::Frame::none().fill(bg).inner_margin(egui::Margin::same(18.0));
        egui::CentralPanel::default().frame(frame).show(ctx, |ui| {
            ui.label(
                egui::RichText::new("台灣銀行 即期賣出")
                    .size(20.0)
                    .strong()
                    .color(text_main),
            );
            ui.label(
                egui::RichText::new("左欄輸入外幣金額，右欄顯示台幣換算")
                    .size(11.0)
                    .color(text_sub),
            );
            ui.add_space(12.0);

            let items: Vec<(String, String, f64)> = self
                .rates
                .as_ref()
                .map(|r| r.items.clone())
                .unwrap_or_else(|| {
                    TARGETS
                        .iter()
                        .map(|(c, n)| ((*c).to_string(), (*n).to_string(), f64::NAN))
                        .collect()
                });

            egui::Frame::none()
                .fill(card)
                .stroke(egui::Stroke::new(1.0, egui::Color32::from_rgb(229, 231, 235)))
                .rounding(egui::Rounding::same(6.0))
                .inner_margin(egui::Margin::symmetric(14.0, 10.0))
                .show(ui, |ui| {
                    for (i, (code, name, rate)) in items.iter().enumerate() {
                        if i > 0 {
                            ui.add_space(4.0);
                            ui.separator();
                            ui.add_space(4.0);
                        }
                        ui.horizontal(|ui| {
                            ui.vertical(|ui| {
                                ui.set_width(110.0);
                                ui.label(
                                    egui::RichText::new(format!("{name}  {code}"))
                                        .size(14.0)
                                        .strong()
                                        .color(text_main),
                                );
                                let rate_text = if rate.is_nan() {
                                    "--".to_string()
                                } else {
                                    format!("{rate:.4}")
                                };
                                ui.label(
                                    egui::RichText::new(rate_text)
                                        .size(12.0)
                                        .color(rate_color)
                                        .monospace(),
                                );
                            });

                            ui.add_space(4.0);

                            let input = self
                                .amounts
                                .entry(code.clone())
                                .or_insert_with(|| "0".to_string());
                            ui.add_sized(
                                [120.0, 28.0],
                                egui::TextEdit::singleline(input)
                                    .hint_text("金額")
                                    .font(egui::TextStyle::Monospace)
                                    .horizontal_align(egui::Align::Max),
                            );

                            ui.label(
                                egui::RichText::new("→").size(14.0).color(text_sub),
                            );

                            let twd = parse_amount(input)
                                .filter(|_| !rate.is_nan())
                                .map(|a| a * rate);
                            let twd_text = match twd {
                                Some(v) => format!("NT$ {}", format_twd(v)),
                                None => "NT$ --".to_string(),
                            };
                            let just_copied = self
                                .copied
                                .as_ref()
                                .is_some_and(|(c, t)| c == code && t.elapsed() < Duration::from_millis(1200));
                            ui.with_layout(
                                egui::Layout::right_to_left(egui::Align::Center),
                                |ui| {
                                    let btn_text = if just_copied { "已複製" } else { "複製" };
                                    let btn = egui::Button::new(
                                        egui::RichText::new(btn_text).size(11.0),
                                    )
                                    .min_size(egui::vec2(48.0, 24.0));
                                    if ui.add_enabled(twd.is_some(), btn).clicked() {
                                        if let Some(v) = twd {
                                            let raw = format_twd_raw(v);
                                            ui.output_mut(|o| o.copied_text = raw);
                                            self.copied = Some((code.clone(), Instant::now()));
                                        }
                                    }
                                    ui.add_space(4.0);
                                    ui.add(
                                        egui::Label::new(
                                            egui::RichText::new(twd_text)
                                                .size(15.0)
                                                .strong()
                                                .color(twd_color)
                                                .monospace(),
                                        )
                                        .selectable(true),
                                    );
                                },
                            );
                            if just_copied {
                                ctx.request_repaint_after(Duration::from_millis(200));
                            }
                        });
                    }
                });

            ui.add_space(14.0);
            ui.horizontal(|ui| {
                if let Some(err) = &self.error {
                    ui.label(
                        egui::RichText::new(format!("載入失敗：{err}"))
                            .size(11.0)
                            .color(err_color),
                    );
                } else if self.loading {
                    ui.label(egui::RichText::new("載入中…").size(11.0).color(text_sub));
                } else if let Some(r) = &self.rates {
                    ui.label(
                        egui::RichText::new(format!("更新時間：{}", r.fetched_at))
                            .size(11.0)
                            .color(text_sub),
                    );
                }

                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                    let btn = egui::Button::new(
                        egui::RichText::new("重新整理").size(13.0).strong(),
                    );
                    if ui.add_enabled(!self.loading, btn).clicked() {
                        self.start_fetch();
                    }
                });
            });
        });
    }
}

fn main() -> Result<(), eframe::Error> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_inner_size([620.0, 360.0])
            .with_min_inner_size([620.0, 360.0])
            .with_resizable(false)
            .with_title("台灣銀行 即期賣出匯率"),
        ..Default::default()
    };
    eframe::run_native(
        "台灣銀行 即期賣出匯率",
        options,
        Box::new(|cc| Box::new(App::new(cc))),
    )
}
