use std::collections::hash_map::Entry;
use std::fmt::Debug;
use std::hash::Hash;
use std::sync::Arc;

use ahash::AHashMap;

use super::instance::{Action, DeviceInstance, Event, Property, Service};
use super::translation::Translation;

/// Extracts the short name (keyword) from a type URN.
/// Example: "urn:miot-spec-v2:property:brightness:0000000D:philips-bulb:1" -> "brightness"
pub fn extract_urn_name(urn: &str) -> Option<&str> {
    urn.split(':').nth(3)
}

/// A combined property info containing the original property and its service.
#[derive(Debug, Clone)]
pub struct PropertyInfo<'a> {
    pub service: &'a Service,
    pub property: &'a Property,
    pub name: &'a str,
    pub description: &'a str,
}

#[derive(Debug, Clone)]
pub struct ActionInfo<'a> {
    pub service: &'a Service,
    pub action: &'a Action,
    pub name: &'a str,
    pub description: &'a str,
}

#[derive(Debug, Clone)]
pub struct EventInfo<'a> {
    pub service: &'a Service,
    pub event: &'a Event,
    pub name: &'a str,
    pub description: &'a str,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct IndexedMember {
    service_index: usize,
    member_index: usize,
    name: String,
}

impl IndexedMember {
    fn new(service_index: usize, member_index: usize, name: String) -> Self {
        Self {
            service_index,
            member_index,
            name,
        }
    }
}

fn insert_or_warn<K, V>(map: &mut AHashMap<K, V>, key: K, value: V, index_name: &str)
where
    K: Eq + Hash + Debug,
    V: PartialEq + Debug,
{
    match map.entry(key) {
        Entry::Vacant(entry) => {
            entry.insert(value);
        }
        Entry::Occupied(entry) => {
            if entry.get() != &value {
                eprintln!(
                    "warning: conflicting duplicate in {index_name} for key {:?}, keep existing {:?}, ignore new {:?}",
                    entry.key(),
                    entry.get(),
                    value
                );
            }
        }
    }
}

/// An index mapping for fast read-only queries of device specifications.
#[derive(Debug, Clone)]
pub struct DeviceSpec {
    instance: DeviceInstance,
    translation: Option<Translation>,

    // Maps (siid, piid) to indexed property location + cached short name.
    properties_by_id: AHashMap<(u32, u32), IndexedMember>,
    // Maps service_name::property_name to indexed property location + cached short name.
    properties_by_name: AHashMap<String, IndexedMember>,

    actions_by_id: AHashMap<(u32, u32), IndexedMember>,
    actions_by_name: AHashMap<String, IndexedMember>,

    events_by_id: AHashMap<(u32, u32), IndexedMember>,
    events_by_name: AHashMap<String, IndexedMember>,
}

impl DeviceSpec {
    pub fn new(instance: DeviceInstance, translation: Option<Translation>) -> Self {
        let total_properties = instance.services.iter().map(|s| s.properties.len()).sum();
        let total_actions = instance.services.iter().map(|s| s.actions.len()).sum();
        let total_events = instance.services.iter().map(|s| s.events.len()).sum();

        let mut properties_by_id = AHashMap::with_capacity(total_properties);
        let mut properties_by_name = AHashMap::with_capacity(total_properties);
        let mut actions_by_id = AHashMap::with_capacity(total_actions);
        let mut actions_by_name = AHashMap::with_capacity(total_actions);
        let mut events_by_id = AHashMap::with_capacity(total_events);
        let mut events_by_name = AHashMap::with_capacity(total_events);

        for (s_idx, service) in instance.services.iter().enumerate() {
            let service_name = extract_urn_name(&service.urn).unwrap_or("");

            for (p_idx, property) in service.properties.iter().enumerate() {
                let property_name = extract_urn_name(&property.urn).unwrap_or("").to_string();
                let indexed = IndexedMember::new(s_idx, p_idx, property_name.clone());

                insert_or_warn(
                    &mut properties_by_id,
                    (service.iid, property.iid),
                    indexed.clone(),
                    "properties_by_id",
                );

                if !property_name.is_empty() {
                    insert_or_warn(
                        &mut properties_by_name,
                        format!("{}::{}", service_name, property_name),
                        indexed,
                        "properties_by_name",
                    );
                }
            }

            for (a_idx, action) in service.actions.iter().enumerate() {
                let action_name = extract_urn_name(&action.urn).unwrap_or("").to_string();
                let indexed = IndexedMember::new(s_idx, a_idx, action_name.clone());

                insert_or_warn(
                    &mut actions_by_id,
                    (service.iid, action.iid),
                    indexed.clone(),
                    "actions_by_id",
                );

                if !action_name.is_empty() {
                    insert_or_warn(
                        &mut actions_by_name,
                        format!("{}::{}", service_name, action_name),
                        indexed,
                        "actions_by_name",
                    );
                }
            }

            for (e_idx, event) in service.events.iter().enumerate() {
                let event_name = extract_urn_name(&event.urn).unwrap_or("").to_string();
                let indexed = IndexedMember::new(s_idx, e_idx, event_name.clone());

                insert_or_warn(
                    &mut events_by_id,
                    (service.iid, event.iid),
                    indexed.clone(),
                    "events_by_id",
                );

                if !event_name.is_empty() {
                    insert_or_warn(
                        &mut events_by_name,
                        format!("{}::{}", service_name, event_name),
                        indexed,
                        "events_by_name",
                    );
                }
            }
        }

        Self {
            instance,
            translation,
            properties_by_id,
            properties_by_name,
            actions_by_id,
            actions_by_name,
            events_by_id,
            events_by_name,
        }
    }

    pub fn instance(&self) -> &DeviceInstance {
        &self.instance
    }

    pub fn translation(&self) -> Option<&Translation> {
        self.translation.as_ref()
    }

    fn make_property_info<'a>(&'a self, indexed: &'a IndexedMember) -> PropertyInfo<'a> {
        let service = &self.instance.services[indexed.service_index];
        let property = &service.properties[indexed.member_index];

        PropertyInfo {
            service,
            property,
            name: indexed.name.as_str(),
            description: property.description.as_str(),
        }
    }

    fn make_action_info<'a>(&'a self, indexed: &'a IndexedMember) -> ActionInfo<'a> {
        let service = &self.instance.services[indexed.service_index];
        let action = &service.actions[indexed.member_index];

        ActionInfo {
            service,
            action,
            name: indexed.name.as_str(),
            description: action.description.as_str(),
        }
    }

    fn make_event_info<'a>(&'a self, indexed: &'a IndexedMember) -> EventInfo<'a> {
        let service = &self.instance.services[indexed.service_index];
        let event = &service.events[indexed.member_index];

        EventInfo {
            service,
            event,
            name: indexed.name.as_str(),
            description: event.description.as_str(),
        }
    }

    pub fn get_property(&self, siid: u32, piid: u32) -> Option<PropertyInfo<'_>> {
        self.properties_by_id
            .get(&(siid, piid))
            .map(|indexed| self.make_property_info(indexed))
    }

    pub fn get_property_by_name(&self, s_name: &str, p_name: &str) -> Option<PropertyInfo<'_>> {
        let key = format!("{}::{}", s_name, p_name);
        self.properties_by_name
            .get(&key)
            .map(|indexed| self.make_property_info(indexed))
    }

    pub fn get_action(&self, siid: u32, aiid: u32) -> Option<ActionInfo<'_>> {
        self.actions_by_id
            .get(&(siid, aiid))
            .map(|indexed| self.make_action_info(indexed))
    }

    pub fn get_action_by_name(&self, s_name: &str, a_name: &str) -> Option<ActionInfo<'_>> {
        let key = format!("{}::{}", s_name, a_name);
        self.actions_by_name
            .get(&key)
            .map(|indexed| self.make_action_info(indexed))
    }

    pub fn get_event(&self, siid: u32, eiid: u32) -> Option<EventInfo<'_>> {
        self.events_by_id
            .get(&(siid, eiid))
            .map(|indexed| self.make_event_info(indexed))
    }

    pub fn get_event_by_name(&self, s_name: &str, e_name: &str) -> Option<EventInfo<'_>> {
        let key = format!("{}::{}", s_name, e_name);
        self.events_by_name
            .get(&key)
            .map(|indexed| self.make_event_info(indexed))
    }

    /// Thread-safe shared access for async environments.
    pub fn into_shared(self) -> Arc<Self> {
        Arc::new(self)
    }
}

#[cfg(test)]
mod tests {
    use std::collections::BTreeMap;

    use super::*;

    fn make_urn(kind: &str, name: &str) -> String {
        format!("urn:miot-spec-v2:{kind}:{name}:00000001:test-device:1")
    }

    fn make_property(iid: u32, name: &str, description: &str) -> Property {
        Property {
            iid,
            urn: make_urn("property", name),
            description: description.to_string(),
            format: "bool".to_string(),
            access: vec!["read".to_string(), "write".to_string()],
            unit: None,
            value_range: None,
            value_list: None,
        }
    }

    fn make_action(iid: u32, name: &str, description: &str) -> Action {
        Action {
            iid,
            urn: make_urn("action", name),
            description: description.to_string(),
            in_args: vec![],
            out_args: vec![],
        }
    }

    fn make_event(iid: u32, name: &str, description: &str) -> Event {
        Event {
            iid,
            urn: make_urn("event", name),
            description: description.to_string(),
            arguments: vec![],
        }
    }

    fn make_service(iid: u32, name: &str) -> Service {
        Service {
            iid,
            urn: make_urn("service", name),
            description: "Service description".to_string(),
            properties: vec![],
            actions: vec![],
            events: vec![],
        }
    }

    #[test]
    fn query_uses_original_description_without_auto_translation() {
        let mut service = make_service(2, "light");
        service
            .properties
            .push(make_property(1, "power", "Original property description"));
        service
            .actions
            .push(make_action(1, "toggle", "Original action description"));
        service
            .events
            .push(make_event(1, "fault", "Original event description"));

        let instance = DeviceInstance {
            urn: make_urn("device", "lamp"),
            description: "Device description".to_string(),
            services: vec![service],
        };

        let mut en = BTreeMap::new();
        en.insert(
            "service:002:property:001".to_string(),
            "Translated property description".to_string(),
        );
        let mut data = BTreeMap::new();
        data.insert("en".to_string(), en);

        let translation = Translation {
            data,
            timestamp: "1".to_string(),
        };

        let index = DeviceSpec::new(instance, Some(translation));

        let property = index
            .get_property(2, 1)
            .expect("property should be found by id");
        assert_eq!(property.description, "Original property description");

        let action = index
            .get_action(2, 1)
            .expect("action should be found by id");
        assert_eq!(action.description, "Original action description");

        let event = index
            .get_event(2, 1)
            .expect("event should be found by id");
        assert_eq!(event.description, "Original event description");

        assert!(
            index.translation().and_then(|t| t.locale("en")).is_some(),
            "all language entries should remain available via translation()"
        );
    }

    #[test]
    fn duplicate_property_id_keeps_first_entry() {
        let mut service = make_service(2, "light");
        service
            .properties
            .push(make_property(1, "brightness", "First description"));
        service
            .properties
            .push(make_property(1, "power", "Second description"));

        let instance = DeviceInstance {
            urn: make_urn("device", "lamp"),
            description: "Device description".to_string(),
            services: vec![service],
        };

        let index = DeviceSpec::new(instance, None);
        let property = index
            .get_property(2, 1)
            .expect("property should be found by duplicated id key");

        assert_eq!(property.name, "brightness");
        assert_eq!(property.description, "First description");
    }

    #[test]
    fn duplicate_property_name_keeps_first_entry() {
        let mut service = make_service(2, "light");
        service
            .properties
            .push(make_property(1, "brightness", "First description"));
        service
            .properties
            .push(make_property(2, "brightness", "Second description"));

        let instance = DeviceInstance {
            urn: make_urn("device", "lamp"),
            description: "Device description".to_string(),
            services: vec![service],
        };

        let index = DeviceSpec::new(instance, None);
        let property = index
            .get_property_by_name("light", "brightness")
            .expect("property should be found by duplicated name key");

        assert_eq!(property.property.iid, 1);
        assert_eq!(property.description, "First description");
    }
}
