//! Approved-origins page — lists every entry from the agent's
//! allowlist with a Revoke button. Empty state swaps in via a
//! Gtk.Stack inside the .blp.

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

mod imp {
    use super::*;

    #[derive(Debug, Default, CompositeTemplate)]
    #[template(resource = "/com/printplatformpro/BrowserAgent/ui/origins_page.ui")]
    pub struct ZbplOriginsPage {
        #[template_child]
        pub origins_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        pub empty_group: TemplateChild<adw::PreferencesGroup>,
        #[template_child]
        pub add_origin_row: TemplateChild<adw::EntryRow>,
        #[template_child]
        pub add_origin_button: TemplateChild<gtk::Button>,

        pub agent: OnceCell<Rc<AgentClient>>,
        // Track rows we add ourselves so refresh() can remove only
        // those — walking first_child returns PreferencesGroup's
        // internal GtkBox, which `remove()` rejects, and the result
        // was an infinite loop firing the warning at >1M/sec.
        pub rows: RefCell<Vec<adw::ActionRow>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for ZbplOriginsPage {
        const NAME: &'static str = "ZbplOriginsPage";
        type Type = super::ZbplOriginsPage;
        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 ZbplOriginsPage {}
    impl WidgetImpl for ZbplOriginsPage {}
    impl BinImpl for ZbplOriginsPage {}
}

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

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

        // Wire the manual-add Approve button. Same flow whether the
        // user clicks Approve or hits Enter in the entry: validate
        // the origin shape, call Approve over D-Bus, refresh.
        let this = self.clone();
        imp.add_origin_button
            .connect_clicked(move |_btn| this.submit_add());
        let this = self.clone();
        imp.add_origin_row
            .connect_entry_activated(move |_row| this.submit_add());

        self.refresh();
    }

    /// Validate + persist the manual-add field. On success: clears
    /// the entry, refreshes the list, surfaces a toast. On
    /// validation failure: toast + leaves the entry as-is so the
    /// user can correct it.
    fn submit_add(&self) {
        let imp = self.imp();
        let Some(agent) = imp.agent.get() else { return };
        let raw = imp.add_origin_row.text().to_string();
        let toast_target = self.root().and_then(|r| r.downcast::<ZbplWindow>().ok());

        let origin = match normalise_origin(&raw) {
            Some(o) => o,
            None => {
                if let Some(w) = &toast_target {
                    w.toast("Enter a full origin like https://example.com (no path or trailing slash).");
                }
                return;
            }
        };
        match agent.approve(&origin) {
            Ok(()) => {
                imp.add_origin_row.set_text("");
                if let Some(w) = &toast_target {
                    w.toast(&format!("Approved {origin}"));
                }
                self.refresh();
            }
            Err(e) => {
                tracing::error!("approve({origin}): {e:?}");
                if let Some(w) = &toast_target {
                    w.toast(&format!("Could not approve {origin}: {e}"));
                }
            }
        }
    }

    pub fn refresh(&self) {
        let imp = self.imp();
        let Some(agent) = imp.agent.get() else { return };

        let entries = match agent.list_origins() {
            Ok(v) => v,
            Err(e) => {
                tracing::warn!("list_origins: {e:?}");
                imp.empty_group.set_visible(true);
                imp.origins_group.set_visible(false);
                return;
            }
        };

        // Drop the rows we added on the previous refresh.
        let mut rows = imp.rows.borrow_mut();
        for r in rows.drain(..) {
            imp.origins_group.remove(&r);
        }

        if entries.is_empty() {
            imp.empty_group.set_visible(true);
            imp.origins_group.set_visible(false);
            return;
        }

        imp.empty_group.set_visible(false);
        imp.origins_group.set_visible(true);
        for entry in entries {
            let row = build_origin_row(&entry, agent.clone(), self);
            imp.origins_group.add(&row);
            rows.push(row);
        }
    }
}

/// normalise_origin trims, lowercases scheme + host, and rejects
/// anything that isn't a `<scheme>://<host>[:port]` shape. Mirrors
/// the agent's own `approval.normalise()` (Go side) so the UI
/// fails fast on bad input rather than relying on the agent to
/// reject it via D-Bus error.
fn normalise_origin(raw: &str) -> Option<String> {
    let s = raw.trim();
    if s.is_empty() {
        return None;
    }
    // Require explicit scheme — "example.com" without https:// is
    // ambiguous; force the user to be explicit.
    let (scheme, rest) = s.split_once("://")?;
    let scheme = scheme.to_ascii_lowercase();
    if scheme != "http" && scheme != "https" {
        return None;
    }
    // Drop anything from the first slash onward (paths shouldn't
    // be in an origin) and lowercase the host.
    let host_port = rest.split('/').next().unwrap_or("");
    if host_port.is_empty() {
        return None;
    }
    Some(format!("{scheme}://{}", host_port.to_ascii_lowercase()))
}

/// Renders one origin as an AdwActionRow with a Revoke button.
/// Source pill ('prompt' / 'cli' / 'env') in the prefix area gives
/// a quick visual cue of how each entry got onto the list.
fn build_origin_row(
    entry: &OriginEntry,
    agent: Rc<AgentClient>,
    page: &ZbplOriginsPage,
) -> adw::ActionRow {
    let row = adw::ActionRow::new();
    row.set_title(&entry.origin);
    row.set_subtitle(&format!(
        "Approved {} · source: {}",
        format_unix(entry.approved_at_unix),
        entry.source
    ));

    // Revoke button (suffix). Loading state during the round-trip
    // so a slow D-Bus call doesn't look frozen.
    let revoke = gtk::Button::from_icon_name("user-trash-symbolic");
    revoke.set_tooltip_text(Some("Revoke this origin"));
    revoke.add_css_class("flat");
    revoke.set_valign(gtk::Align::Center);

    let origin = entry.origin.clone();
    let page = page.clone();
    revoke.connect_clicked(move |_btn| {
        // Look up the parent window for toasts. `.root()` returns
        // the top-level widget any time the page is mapped — which
        // is when the user can click anything anyway.
        let toast_target = page
            .root()
            .and_then(|r| r.downcast::<ZbplWindow>().ok());
        match agent.revoke(&origin) {
            Ok(()) => {
                if let Some(w) = &toast_target {
                    w.toast(&format!("Revoked {origin}"));
                }
            }
            Err(e) => {
                tracing::error!("revoke({origin}): {e:?}");
                if let Some(w) = &toast_target {
                    w.toast(&format!("Could not revoke {origin}: {e}"));
                }
            }
        }
        page.refresh();
    });

    row.add_suffix(&revoke);
    row
}

fn format_unix(secs: u64) -> String {
    // Lean on glib::DateTime so we get the user's locale + timezone
    // for free, instead of pulling in chrono.
    glib::DateTime::from_unix_local(secs as i64)
        .and_then(|d| d.format("%Y-%m-%d %H:%M"))
        .map(|s| s.to_string())
        .unwrap_or_else(|_| format!("@{secs}"))
}
