//! About page — version + project links.
//!
//! Inline in the main window's tab stack rather than a modal
//! AdwAboutWindow because the window already has a 4-tab
//! AdwViewSwitcher; mixing modal and tab navigation for the same
//! information makes for a worse UX.

use crate::dbus::{AgentClient, LicenseState};
use crate::window::ZbplWindow;
use adw::prelude::*;
use adw::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate};
use std::cell::OnceCell;
use std::rc::Rc;

const DOWNLOADS_URL: &str = "https://printplatformpro.com/downloads/browser-print-linux";
const PROTOCOL_URL: &str = "https://printplatformpro.com/docs/browser-print-agent";

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(resource = "/com/printplatformpro/BrowserAgent/ui/about_page.ui")]
    pub struct ZbplAboutPage {
        #[template_child]
        pub agent_version_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub ui_version_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub downloads_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub protocol_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub download_logs_button: TemplateChild<gtk::Button>,

        // Licensing group widgets — bound to about_page.blp.
        #[template_child]
        pub license_status_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub license_status_pill: TemplateChild<gtk::Label>,
        #[template_child]
        pub license_key_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub copy_key_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub license_period_row: TemplateChild<adw::ActionRow>,
        #[template_child]
        pub refresh_license_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub enter_license_button: TemplateChild<gtk::Button>,
        #[template_child]
        pub subscribe_button: TemplateChild<gtk::Button>,

        pub agent: OnceCell<Rc<AgentClient>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for ZbplAboutPage {
        const NAME: &'static str = "ZbplAboutPage";
        type Type = super::ZbplAboutPage;
        type ParentType = adw::Bin;

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
        }
        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    impl ObjectImpl for ZbplAboutPage {}
    impl WidgetImpl for ZbplAboutPage {}
    impl BinImpl for ZbplAboutPage {}
}

glib::wrapper! {
    pub struct ZbplAboutPage(ObjectSubclass<imp::ZbplAboutPage>)
        @extends gtk::Widget, adw::Bin,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
}

impl ZbplAboutPage {
    pub fn bind_agent(&self, agent: Rc<AgentClient>) {
        self.imp().agent.set(agent).ok();

        // Each link row opens its URL in the user's default browser
        // via xdg-open. We set `activatable: true` in the .blp so the
        // whole row is clickable, not just the trailing icon.
        let imp = self.imp();
        wire_link(&imp.downloads_row, DOWNLOADS_URL);
        wire_link(&imp.protocol_row, PROTOCOL_URL);

        // Download logs — opens a Save dialog and copies the agent's
        // ZPL history file to the chosen path. Useful when sharing
        // a debug trace with a web app integrator.
        let this = self.clone();
        imp.download_logs_button.connect_clicked(move |_| {
            this.download_zpl_log();
        });

        // Licensing — three actions: Refresh re-validates against the
        // server, Enter key opens a prompt + activates, Subscribe opens
        // the buy URL in the browser.
        let this = self.clone();
        imp.refresh_license_button.connect_clicked(move |_| {
            this.refresh_license();
        });
        let this = self.clone();
        imp.enter_license_button.connect_clicked(move |_| {
            this.prompt_for_license_key();
        });
        let this = self.clone();
        imp.subscribe_button.connect_clicked(move |_| {
            this.open_manage_or_buy_page();
        });
        let this = self.clone();
        imp.copy_key_button.connect_clicked(move |_| {
            this.copy_license_key();
        });

        self.refresh();
    }

    /// Pull the current license state and update the row labels +
    /// pill colour. Errors fall through to a neutral "Unknown" pill.
    pub fn refresh_license_state(&self) {
        let Some(agent) = self.imp().agent.get() else { return };
        let imp = self.imp();
        let st = match agent.get_license_state() {
            Ok(st) => st,
            Err(e) => {
                tracing::warn!("license state: {e:?}");
                imp.license_status_row.set_subtitle("Agent unreachable");
                imp.license_status_pill.set_label("?");
                return;
            }
        };
        apply_license_to_ui(imp, &st);
    }

    fn refresh_license(&self) {
        let Some(agent) = self.imp().agent.get().cloned() else { return };
        let toast_target = self.root().and_then(|r| r.downcast::<ZbplWindow>().ok());
        // Refresh is a network round-trip (~200ms). Could push it onto
        // a worker thread, but the About tab is already a low-traffic
        // surface and the button is rarely clicked — synchronous keeps
        // the code straightforward.
        match agent.refresh_license() {
            Ok(status) => {
                if let Some(w) = toast_target.as_ref() {
                    w.toast(&format!("License refreshed — status: {status}"));
                }
            }
            Err(e) => {
                if let Some(w) = toast_target.as_ref() {
                    w.toast(&format!("Refresh failed: {e}"));
                }
            }
        }
        self.refresh_license_state();
    }

    /// Open the right web page for the current license state:
    ///   - `active` or `grace`     → /account/licenses (manage subscription, view receipts, deactivate devices, hit Stripe Portal)
    ///   - everything else (trial / expired / revoked / no key) → /buy/<product> (subscribe)
    ///
    /// We compute the URL client-side rather than asking the agent so a
    /// stale agent build doesn't lock us into the buy URL forever once a
    /// subscription is active.
    fn open_manage_or_buy_page(&self) {
        let Some(agent) = self.imp().agent.get() else { return };
        let is_active = agent
            .get_license_state()
            .ok()
            .map(|s| s.status == "active" || s.status == "grace")
            .unwrap_or(false);
        let url = if is_active {
            "https://printplatformpro.com/account/licenses".to_string()
        } else {
            agent
                .get_buy_url()
                .unwrap_or_else(|_| "https://printplatformpro.com/buy/pppro-browser-linux".to_string())
        };
        if let Err(e) = open_in_browser(&url) {
            tracing::warn!("xdg-open {url}: {e:?}");
        }
    }

    /// Copy the license key to the clipboard. No-op if no key is loaded
    /// yet (button is disabled in that state anyway).
    fn copy_license_key(&self) {
        let Some(agent) = self.imp().agent.get() else { return };
        let toast_target = self.root().and_then(|r| r.downcast::<ZbplWindow>().ok());
        let key = match agent.get_license_state() {
            Ok(s) if !s.key.is_empty() => s.key,
            _ => {
                if let Some(w) = toast_target.as_ref() {
                    w.toast("No license key to copy yet.");
                }
                return;
            }
        };
        if let Some(d) = gtk::gdk::Display::default() {
            d.clipboard().set_text(&key);
            if let Some(w) = toast_target.as_ref() {
                w.toast("License key copied to clipboard.");
            }
        }
    }

    /// Pop a small AdwAlertDialog with a single Entry to collect the
    /// license key, then push it to the agent. Toasts the result.
    fn prompt_for_license_key(&self) {
        let Some(agent) = self.imp().agent.get().cloned() else { return };
        let parent_window = self.root().and_then(|r| r.downcast::<gtk::Window>().ok());

        let entry = gtk::Entry::builder()
            .placeholder_text("PPPRO-XXXXX-XXXXX-XXXXX-XXXXX")
            .activates_default(true)
            .build();

        let dialog = adw::AlertDialog::builder()
            .heading("Enter license key")
            .body("Paste the key from your purchase email. The agent will validate it with the license server and unlock printing on this machine.")
            .extra_child(&entry)
            .default_response("apply")
            .close_response("cancel")
            .build();
        dialog.add_response("cancel", "Cancel");
        dialog.add_response("apply", "Apply");
        dialog.set_response_appearance("apply", adw::ResponseAppearance::Suggested);

        let this = self.clone();
        let entry_clone = entry.clone();
        dialog.connect_response(None, move |dlg, response| {
            if response != "apply" {
                dlg.close();
                return;
            }
            let key = entry_clone.text().trim().to_string();
            let toast_target = this.root().and_then(|r| r.downcast::<ZbplWindow>().ok());
            if key.is_empty() {
                if let Some(w) = toast_target.as_ref() {
                    w.toast("License key cannot be empty.");
                }
                return;
            }
            match agent.apply_license_key(&key) {
                Ok(status) => {
                    if let Some(w) = toast_target.as_ref() {
                        w.toast(&format!("License applied — status: {status}"));
                    }
                    dlg.close();
                    this.refresh_license_state();
                }
                Err(e) => {
                    if let Some(w) = toast_target.as_ref() {
                        w.toast(&format!("Activation failed: {e}"));
                    }
                }
            }
        });
        dialog.present(parent_window.as_ref());
    }

    /// Open a Save dialog defaulting to the user's Downloads folder
    /// and copy the agent's ZPL history JSONL file there. Toasts on
    /// success / failure. We resolve the source path via D-Bus so a
    /// build skew between agent and UI doesn't end up pointing at a
    /// stale path.
    fn download_zpl_log(&self) {
        let Some(agent) = self.imp().agent.get().cloned() else { return };
        let toast_target = self.root().and_then(|r| r.downcast::<ZbplWindow>().ok());

        let src_path = match agent.get_zpl_log_path() {
            Ok(p) if !p.is_empty() => p,
            _ => {
                if let Some(w) = toast_target.as_ref() {
                    w.toast("Agent doesn't expose a log path (older agent?).");
                }
                return;
            }
        };
        if !std::path::Path::new(&src_path).exists() {
            if let Some(w) = toast_target.as_ref() {
                w.toast("No ZPL printed yet — nothing to download.");
            }
            return;
        }

        let parent_window = self.root().and_then(|r| r.downcast::<gtk::Window>().ok());
        let dialog = gtk::FileDialog::builder()
            .title("Save ZPL history")
            .modal(true)
            .build();
        // Default filename includes a timestamp so consecutive
        // downloads don't trample each other in ~/Downloads.
        let ts = chrono_like_stamp();
        dialog.set_initial_name(Some(&format!("zbpl-zpl-history-{ts}.jsonl")));

        let src_path_clone = src_path.clone();
        let this = self.clone();
        dialog.save(
            parent_window.as_ref(),
            None::<&gio::Cancellable>,
            move |result| {
                let toast_target = this.root().and_then(|r| r.downcast::<ZbplWindow>().ok());
                let dest = match result {
                    Ok(f) => f,
                    Err(e) => {
                        // GTK uses dismissal as an error result; only
                        // toast actual IO / permission failures.
                        if !e.matches(gtk::DialogError::Dismissed) {
                            tracing::warn!("FileDialog.save: {e:?}");
                            if let Some(w) = toast_target.as_ref() {
                                w.toast(&format!("Save cancelled: {e}"));
                            }
                        }
                        return;
                    }
                };
                let Some(dest_path) = dest.path() else {
                    if let Some(w) = toast_target.as_ref() {
                        w.toast("Couldn't resolve destination path.");
                    }
                    return;
                };
                match std::fs::copy(&src_path_clone, &dest_path) {
                    Ok(bytes) => {
                        if let Some(w) = toast_target.as_ref() {
                            w.toast(&format!(
                                "Saved {} bytes to {}",
                                bytes,
                                dest_path.display()
                            ));
                        }
                    }
                    Err(e) => {
                        tracing::warn!("copy zpl-history: {e}");
                        if let Some(w) = toast_target.as_ref() {
                            w.toast(&format!("Could not save log: {e}"));
                        }
                    }
                }
            },
        );
    }

    pub fn refresh(&self) {
        let imp = self.imp();
        // UI version is baked in at compile time; agent version comes
        // from D-Bus so a build skew between the two is visible.
        imp.ui_version_row
            .set_subtitle(env!("CARGO_PKG_VERSION"));
        if let Some(agent) = imp.agent.get() {
            match agent.get_version() {
                Ok(v) => imp.agent_version_row.set_subtitle(&v),
                Err(_) => imp.agent_version_row.set_subtitle("(agent not running)"),
            }
        }
        self.refresh_license_state();
    }
}

/// Map a [`LicenseState`] onto the About-tab Licensing widgets in one
/// place so the refresh path and the apply-key callback don't drift.
fn apply_license_to_ui(imp: &imp::ZbplAboutPage, st: &LicenseState) {
    let (pill_label, pill_class, subtitle): (&str, &str, String) = match st.status.as_str() {
        "active" => (
            "Active",
            "success",
            if st.cancel_at_period_end {
                format!("Subscription active. Will cancel after {}.", date_part(&st.current_period_end))
            } else {
                "Subscription active.".to_string()
            },
        ),
        "trial" => (
            "Trial",
            "accent",
            if st.days_remaining > 1 {
                format!("Trial — {} days remaining.", st.days_remaining)
            } else if st.days_remaining == 1 {
                "Trial — 1 day remaining.".to_string()
            } else {
                "Trial ends today.".to_string()
            },
        ),
        "grace" => (
            "Grace",
            "warning",
            "Payment failed but printing still allowed during the grace window. Please update your card.".to_string(),
        ),
        "expired" => (
            "Expired",
            "error",
            "Trial or subscription has expired. Printing is paused.".to_string(),
        ),
        "revoked" => (
            "Revoked",
            "error",
            "Subscription cancelled or revoked. Printing is paused.".to_string(),
        ),
        other => (
            "Unknown",
            "dim-label",
            format!("Status: {other}"),
        ),
    };

    imp.license_status_row.set_subtitle(&subtitle);
    imp.license_status_pill.set_label(pill_label);
    // Clear previous status classes before applying the new one.
    for cls in ["success", "accent", "warning", "error", "dim-label"] {
        imp.license_status_pill.remove_css_class(cls);
    }
    imp.license_status_pill.add_css_class(pill_class);
    imp.license_status_pill.add_css_class("caption");
    imp.license_status_pill.add_css_class("pill");

    if st.key.is_empty() {
        imp.license_key_row.set_subtitle("Not yet issued");
    } else {
        imp.license_key_row.set_subtitle(&st.key);
    }
    imp.license_period_row
        .set_subtitle(if st.current_period_end.is_empty() {
            "—".to_string()
        } else {
            date_part(&st.current_period_end)
        }.as_str());

    // Subscribe button morphs based on state:
    //   - active           → "Manage subscription"     (opens /account/licenses)
    //   - grace            → "Update payment details"  (opens /account/licenses, same URL — the page surfaces the past_due banner)
    //   - everything else  → "Subscribe — $10/month"  (opens /buy/<product>)
    match st.status.as_str() {
        "active" => imp.subscribe_button.set_label("Manage subscription"),
        "grace" => imp.subscribe_button.set_label("Update payment details"),
        _ => imp.subscribe_button.set_label("Subscribe — $10/month"),
    }

    // Copy-key button is only useful once a key actually exists.
    imp.copy_key_button.set_sensitive(!st.key.is_empty());
}

/// Trim an RFC3339 timestamp to its date portion (`2026-06-10T...Z` →
/// `2026-06-10`). Cheap; avoids dragging chrono in for a one-liner.
fn date_part(rfc3339: &str) -> String {
    rfc3339.split('T').next().unwrap_or(rfc3339).to_string()
}

fn wire_link(row: &adw::ActionRow, url: &'static str) {
    row.connect_activated(move |_| {
        if let Err(e) = open_in_browser(url) {
            tracing::warn!("xdg-open {url}: {e:?}");
        }
    });
}

fn open_in_browser(url: &str) -> std::io::Result<()> {
    // gio's `gtk::show_uri` would also work, but xdg-open is one
    // process spawn and doesn't need a parent window context — fine
    // for a static link list.
    std::process::Command::new("xdg-open").arg(url).spawn()?;
    Ok(())
}

/// "YYYYMMDD-HHMM" UTC stamp using only stdlib — we already have a
/// similar helper in demos.rs but duplicating one ten-line function
/// is cheaper than carving out a shared time-formatting module.
fn chrono_like_stamp() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let secs = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0);
    let days = secs / 86_400;
    let hours = (secs % 86_400) / 3600;
    let mins = (secs % 3600) / 60;
    let (y, m, d) = days_to_ymd(days as i64);
    format!("{y:04}{m:02}{d:02}-{hours:02}{mins:02}")
}

fn days_to_ymd(z: i64) -> (i32, u32, u32) {
    let z = z + 719_468;
    let era = if z >= 0 { z / 146_097 } 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) as u32;
    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
    let y = if m <= 2 { y + 1 } else { y } as i32;
    (y, m, d)
}
