use super::ContactResolverBackend;
use dbus::arg::{RefArg, Variant};
use dbus::blocking::Connection;
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;

const LOOKUP_TIMEOUT: Duration = Duration::from_secs(2);

#[derive(Clone)]
pub struct EDSContactResolverBackend;

// Cache the UID of the default local address book so we do not have to scan
// all sources over and over again. Discovering the address book requires a
// D-Bus round-trip that we would rather avoid on every lookup.
static ADDRESS_BOOK_SOURCE_UID: OnceCell<String> = OnceCell::new();

/// Holds a D-Bus connection and the identifiers needed to create an address-book proxy.
struct AddressBookHandle {
    connection: Connection,
    object_path: String,
    bus_name: String,
}

impl AddressBookHandle {
    fn new() -> anyhow::Result<Self> {
        let connection = new_session_connection()?;
        let source_uid = ensure_address_book_uid(&connection)?;
        let (object_path, bus_name) = open_address_book(&connection, &source_uid)?;

        Ok(Self {
            connection,
            object_path,
            bus_name,
        })
    }
}

/// Obtain the global address-book handle, initialising it on the first call.
static ADDRESS_BOOK_HANDLE: OnceCell<Mutex<AddressBookHandle>> = OnceCell::new();

/// Check whether a given well-known name currently has an owner on the bus.
fn name_has_owner(conn: &Connection, name: &str) -> bool {
    let proxy = conn.with_proxy(
        "org.freedesktop.DBus",
        "/org/freedesktop/DBus",
        Duration::from_secs(2),
    );
    let result: Result<(bool,), _> =
        proxy.method_call("org.freedesktop.DBus", "NameHasOwner", (name.to_string(),));
    result.map(|(b,)| b).unwrap_or(false)
}

/// Returns a fresh handle, ensuring the cached one is still valid. If the backend owning the
/// address-book disappeared, the cache is cleared and we try to create a new handle.
fn obtain_handle() -> Option<std::sync::MutexGuard<'static, AddressBookHandle>> {
    // Initialize cell if necessary.
    let cell = ADDRESS_BOOK_HANDLE
        .get_or_try_init(|| AddressBookHandle::new().map(Mutex::new))
        .ok()?;

    // Validate existing handle.
    {
        let mut guard = cell.lock().ok()?;
        if !name_has_owner(&guard.connection, &guard.bus_name) {
            // Try to refresh the handle in-place.
            match AddressBookHandle::new() {
                Ok(new_h) => {
                    *guard = new_h;
                }
                Err(e) => {
                    log::debug!("EDS resolver: failed to refresh address book handle: {}", e);
                    // keep the stale handle but report failure
                    return None;
                }
            }
        }
        // Return guard after ensuring validity.
        return Some(guard);
    }
}

/// Helper that returns a blocking D-Bus session connection. Creating the
/// connection is cheap (<1 ms) but we still keep it around because the
/// underlying socket is re-used by the dbus crate.
fn new_session_connection() -> Result<Connection, dbus::Error> {
    Connection::new_session()
}

/// Scan Evolution-Data-Server sources to find a suitable address-book source
/// UID. The implementation mirrors what `gdbus introspect` reveals for the
/// EDS interfaces. We search all `org.gnome.evolution.dataserver.Source`
/// objects and pick the first one that advertises the `[Address Book]` section
/// with a `BackendName=` entry in its INI-style `Data` property.
fn ensure_address_book_uid(conn: &Connection) -> anyhow::Result<String> {
    if let Some(uid) = ADDRESS_BOOK_SOURCE_UID.get() {
        return Ok(uid.clone());
    }

    let source_manager_proxy = conn.with_proxy(
        "org.gnome.evolution.dataserver.Sources5",
        "/org/gnome/evolution/dataserver/SourceManager",
        Duration::from_secs(5),
    );

    // The GetManagedObjects reply is the usual ObjectManager map.
    let (managed_objects,): (
        HashMap<dbus::Path<'static>, HashMap<String, HashMap<String, Variant<Box<dyn RefArg>>>>>,
    ) = source_manager_proxy.method_call(
        "org.freedesktop.DBus.ObjectManager",
        "GetManagedObjects",
        (),
    )?;

    let uid = managed_objects
        .values()
        .filter_map(|ifaces| ifaces.get("org.gnome.evolution.dataserver.Source"))
        .filter_map(|props| {
            let uid = props.get("UID")?.as_str()?;
            if uid == "system-address-book" {
                // Decoy.
                return None;
            }

            let data = props.get("Data")?.as_str()?;
            if data_contains_address_book_backend(data) {
                Some(uid.to_owned())
            } else {
                None
            }
        })
        .next()
        .ok_or_else(|| anyhow::anyhow!("No address book source found"))?;

    // Remember for future look-ups.
    log::debug!("EDS resolver: found address book source UID: {}", uid);
    let _ = ADDRESS_BOOK_SOURCE_UID.set(uid.clone());
    Ok(uid)
}

fn data_contains_address_book_backend(data: &str) -> bool {
    let mut in_address_book_section = false;
    for line in data.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('[') && trimmed.ends_with(']') {
            in_address_book_section = trimmed == "[Address Book]";
            continue;
        }
        if in_address_book_section && trimmed.starts_with("BackendName=") {
            return true;
        }
    }
    false
}

/// Open the Evolution address book referenced by `source_uid` and return the
/// pair `(object_path, bus_name)` that identifies the newly created D-Bus
/// proxy.
fn open_address_book(conn: &Connection, source_uid: &str) -> anyhow::Result<(String, String)> {
    let factory_proxy = conn.with_proxy(
        "org.gnome.evolution.dataserver.AddressBook10",
        "/org/gnome/evolution/dataserver/AddressBookFactory",
        Duration::from_secs(60),
    );

    let (object_path, bus_name): (String, String) = factory_proxy.method_call(
        "org.gnome.evolution.dataserver.AddressBookFactory",
        "OpenAddressBook",
        (source_uid.to_owned(),),
    )?;

    Ok((object_path, bus_name))
}

/// Ensure that the backend for the given address-book proxy is opened.
/// Evolution-Data-Server returns "Backend is not opened yet" until someone
/// calls the `Open` method once per process.  We ignore any error here
/// because the backend might already be open.
fn ensure_address_book_open(proxy: &dbus::blocking::Proxy<&Connection>) {
    let _: Result<(), _> =
        proxy.method_call("org.gnome.evolution.dataserver.AddressBook", "Open", ());
}

impl ContactResolverBackend for EDSContactResolverBackend {
    type ContactID = String;

    fn resolve_contact_id(&self, address: &str) -> Option<Self::ContactID> {
        let handle = match obtain_handle() {
            Some(h) => h,
            None => return None,
        };

        let address_book_proxy =
            handle
                .connection
                .with_proxy(&handle.bus_name, &handle.object_path, LOOKUP_TIMEOUT);

        ensure_address_book_open(&address_book_proxy);

        let filter = if address.contains('@') {
            format!("(is \"email\" \"{}\")", address)
        } else {
            let mut filters: Vec<String> = Vec::new();
            filters.push(format!("(is \"phone\" \"{}\")", address));

            let normalized_address = address
                .chars()
                .filter(|c| c.is_numeric())
                .collect::<String>();

            filters.push(format!("(is \"phone\" \"{}\")", normalized_address));

            let local_address = address
                .replace('+', "")
                .chars()
                .skip_while(|c| c.is_numeric() || *c == '(' || *c == ')')
                .collect::<String>()
                .chars()
                .filter(|c| c.is_numeric())
                .collect::<String>();

            if !local_address.is_empty() {
                filters.push(format!("(is \"phone\" \"{}\")", local_address));
            }

            format!("(or {})", filters.join(" "))
        };

        log::trace!(
            "EDS resolver: GetContactListUids filter: {}, address: {}",
            filter,
            address
        );

        let uids_result: Result<(Vec<String>,), _> = address_book_proxy.method_call(
            "org.gnome.evolution.dataserver.AddressBook",
            "GetContactListUids",
            (filter,),
        );

        let (uids,) = match uids_result {
            Ok(v) => v,
            Err(e) => {
                log::debug!("EDS resolver: GetContactListUids failed: {}", e);
                return None;
            }
        };

        uids.into_iter().next()
    }

    fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option<String> {
        let handle = match obtain_handle() {
            Some(h) => h,
            None => return None,
        };

        let address_book_proxy =
            handle
                .connection
                .with_proxy(&handle.bus_name, &handle.object_path, LOOKUP_TIMEOUT);

        ensure_address_book_open(&address_book_proxy);

        let vcard_result: Result<(String,), _> = address_book_proxy.method_call(
            "org.gnome.evolution.dataserver.AddressBook",
            "GetContact",
            (contact_id.clone(),),
        );

        let (vcard,) = match vcard_result {
            Ok(v) => v,
            Err(e) => {
                log::debug!("EDS resolver: GetContact failed: {}", e);
                return None;
            }
        };

        for line in vcard.lines() {
            if let Some(rest) = line.strip_prefix("FN:") {
                return Some(rest.to_string());
            }
        }

        None
    }
}

impl Default for EDSContactResolverBackend {
    fn default() -> Self {
        Self
    }
}
