//! # ContentDirectory Handler - Gestionnaire du service ContentDirectory
//!
//! Ce module implémente la logique métier du service ContentDirectory en intégrant
//! les sources musicales enregistrées dans le registre.
//!
//! ## Fonctionnalités
//!
//! - **Navigation multi-sources** : Combine toutes les sources dans une hiérarchie
//! - **Browse** : Parcours des containers et items
//! - **Search** : Recherche dans les sources qui le supportent
//! - **Update ID** : Suivi des changements pour les notifications UPnP

use pmodidl::{Container, DIDLLite};
use pmosource::api::{get_source as get_source_from_registry, list_all_sources};
use pmosource::{BrowseResult, MediaSearchType, MusicSource, MusicSourceError, SearchQuery, SearchScope};
use pmodidl::ToXmlElement;
use std::collections::HashSet;
use std::sync::Arc;

/// Convertit des containers et items en XML DIDL-Lite
fn to_didl_lite(containers: &[Container], items: &[pmodidl::Item]) -> Result<String, String> {
    let didl = DIDLLite {
        xmlns: "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/".to_string(),
        xmlns_upnp: Some("urn:schemas-upnp-org:metadata-1-0/upnp/".to_string()),
        xmlns_dc: Some("http://purl.org/dc/elements/1.1/".to_string()),
        xmlns_dlna: Some("urn:schemas-dlna-org:metadata-1-0/".to_string()),
        xmlns_pv: None,
        xmlns_sec: None,
        containers: containers.to_vec(),
        items: items.to_vec(),
    };

    // Retourne uniquement le corps DIDL, sans préfixer une seconde déclaration XML.
    let body = didl.to_xml();

    // Log le DIDL généré pour déboguer (limité aux 500 premiers caractères)
    if !items.is_empty() {
        tracing::debug!(
            "Generated DIDL-Lite with {} items: {}",
            items.len(),
            &body[..body.len().min(800)]
        );
    }

    Ok(body)
}

/// Extrait le texte de recherche depuis un critère UPnP SearchCriteria.
///
/// Exemples supportés :
/// - `dc:title contains "Pink Floyd"` → `"Pink Floyd"`
/// - `upnp:artist contains "Miles"` → `"Miles"`
/// - `Pink Floyd` (texte nu) → `"Pink Floyd"`
/// - `*` → `""`
fn extract_search_text(criteria: &str) -> &str {
    let trimmed = criteria.trim();
    if trimmed == "*" || trimmed.is_empty() {
        return "";
    }
    // Pattern : <property> contains "<query>"  (UPnP CDS search syntax)
    if let Some(pos) = trimmed.find("contains") {
        let after = trimmed[pos + "contains".len()..].trim();
        if after.starts_with('"') && after.ends_with('"') && after.len() >= 2 {
            return &after[1..after.len() - 1];
        }
    }
    trimmed
}

/// Détermine le scope et le type de média depuis le ContainerID UPnP.
fn container_to_search_context(container_id: &str) -> (SearchScope, MediaSearchType) {
    // Containers virtuels de résultats de recherche : qobuz:search:{scope}:{type}:{query}
    // Le CP peut appeler Search sur ces containers — on préserve leur scope+type.
    let search_parts: Vec<&str> = container_id.splitn(5, ':').collect();
    if search_parts.len() >= 4 && search_parts[0] == "qobuz" && search_parts[1] == "search" {
        let scope = if search_parts[2] == "favorites" {
            SearchScope::UserLibrary
        } else {
            SearchScope::Catalog
        };
        let media_type = match search_parts.get(3).copied().unwrap_or("") {
            "albums" => MediaSearchType::Albums,
            "tracks" => MediaSearchType::Tracks,
            "artists" => MediaSearchType::Artists,
            "playlists" => MediaSearchType::Playlists,
            _ => MediaSearchType::All,
        };
        return (scope, media_type);
    }

    // Scope UserLibrary : tout ce qui est sous qobuz:favorites
    if container_id.starts_with("qobuz:favorites") {
        let media_type = match container_id {
            "qobuz:favorites:albums" => MediaSearchType::Albums,
            "qobuz:favorites:tracks" => MediaSearchType::Tracks,
            "qobuz:favorites:artists" => MediaSearchType::Artists,
            "qobuz:favorites:playlists" => MediaSearchType::Playlists,
            _ => MediaSearchType::All,
        };
        return (SearchScope::UserLibrary, media_type);
    }

    // Un artiste spécifique → rechercher ses albums dans le catalog
    if container_id.starts_with("qobuz:artist:") {
        return (SearchScope::Catalog, MediaSearchType::Albums);
    }

    // Un album spécifique → rechercher ses pistes
    if container_id.starts_with("qobuz:album:") {
        return (SearchScope::Catalog, MediaSearchType::Tracks);
    }

    // Scope Catalog : reste de la hiérarchie
    let media_type = if container_id.starts_with("qobuz:discover:artists") {
        MediaSearchType::Artists
    } else if container_id.starts_with("qobuz:discover:albums") {
        MediaSearchType::Albums
    } else {
        MediaSearchType::All
    };
    (SearchScope::Catalog, media_type)
}

/// Handler pour le service ContentDirectory
///
/// Ce handler gère toutes les opérations du ContentDirectory en utilisant
/// les sources musicales enregistrées dans le registre global.
pub struct ContentHandler;

impl ContentHandler {
    /// Crée un nouveau ContentHandler
    pub fn new() -> Self {
        Self
    }

    /// Browse un container ou récupère les métadonnées d'un objet
    ///
    /// # Arguments
    ///
    /// * `object_id` - L'ID de l'objet à parcourir ("0" pour la racine)
    /// * `browse_flag` - "BrowseMetadata" ou "BrowseDirectChildren"
    /// * `starting_index` - Index de départ pour la pagination
    /// * `requested_count` - Nombre d'éléments demandés (0 = tous)
    ///
    /// # Returns
    ///
    /// Un tuple contenant:
    /// - Le résultat DIDL-Lite XML
    /// - Le nombre d'éléments retournés
    /// - Le nombre total d'éléments
    /// - L'update ID
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let handler = ContentHandler::new();
    /// let (didl, returned, total, update_id) =
    ///     handler.browse("0", "BrowseDirectChildren", 0, 0).await?;
    /// ```
    pub async fn browse(
        &self,
        object_id: &str,
        browse_flag: &str,
        starting_index: u32,
        requested_count: u32,
    ) -> Result<(String, u32, u32, u32), String> {
        tracing::debug!(
            object_id = %object_id,
            browse_flag = %browse_flag,
            starting_index = %starting_index,
            requested_count = %requested_count,
            "ContentDirectory::Browse"
        );

        // Log rapide sur la branche flatten vs agrégée
        if object_id == "0" {
            let sources = list_all_sources().await;
            tracing::info!(
                "Browse root: sources.len() = {}, flatten = {}",
                sources.len(),
                sources.len() == 1
            );
        }

        match browse_flag {
            "BrowseMetadata" => self.browse_metadata(object_id).await,
            "BrowseDirectChildren" => {
                self.browse_direct_children(object_id, starting_index, requested_count)
                    .await
            }
            _ => Err(format!("Invalid BrowseFlag: {}", browse_flag)),
        }
    }

    /// Browse les métadonnées d'un objet spécifique
    async fn browse_metadata(&self, object_id: &str) -> Result<(String, u32, u32, u32), String> {
        if object_id == "0" {
            // Si un seul enfant, publier ce fils comme racine
            let sources = list_all_sources().await;
            if sources.len() == 1 {
                let source = sources.into_iter().next().unwrap();
                let mut container = source
                    .root_container()
                    .await
                    .map_err(|e| format!("Failed to get root container: {}", e))?;
                // Le présenter comme la racine (id=0, parent=-1)
                container.id = "0".to_string();
                container.parent_id = "-1".to_string();
                container.child_count = None; // compatibilité CP
                let didl = to_didl_lite(&[container], &[])?;
                let update_id = source.update_id().await.max(1);
                tracing::debug!("BrowseMetadata root (flatten) didl_len={}B", didl.len());
                return Ok((didl, 1, 1, update_id));
            }

            // Sinon retourner le container racine agrégé
            let root = self.build_root_container().await;
            let didl = to_didl_lite(&[root], &[])?;
            tracing::debug!("BrowseMetadata root (aggregate) didl_len={}B", didl.len());
            Ok((didl, 1, 1, 1))
        } else {
            // Essayer de trouver l'objet dans les sources
            // Vérifier si c'est un container racine d'une source
            if let Some(source) = get_source_from_registry(object_id).await {
                let container = source
                    .root_container()
                    .await
                    .map_err(|e| format!("Failed to get root container: {}", e))?;
                let didl = to_didl_lite(&[container], &[])?;
                let update_id = source.update_id().await.max(1);
                tracing::debug!(
                    "BrowseMetadata source_root id={} didl_len={}B",
                    object_id,
                    didl.len()
                );
                return Ok((didl, 1, 1, update_id));
            }

            // Try to get item metadata first (for leaf items)
            for source in list_all_sources().await {
                match source.get_item(object_id).await {
                    Ok(item) => {
                        let didl = to_didl_lite(&[], &[item])?;
                        let update_id = source.update_id().await.max(1);
                        tracing::debug!(
                            "BrowseMetadata item id={} didl_len={}B",
                            object_id,
                            didl.len()
                        );
                        return Ok((didl, 1, 1, update_id));
                    }
                    Err(MusicSourceError::ObjectNotFound(_))
                    | Err(MusicSourceError::NotSupported(_)) => continue,
                    Err(e) => {
                        tracing::debug!("get_item failed for {}: {}", object_id, e);
                        continue;
                    }
                }
            }

            // Fallback to browse for containers
            let mut non_not_found_error: Option<String> = None;
            for source in list_all_sources().await {
                match source.browse(object_id).await {
                    Ok(result) => {
                        // L'objet a été trouvé, retourner ses métadonnées
                        match result {
                            BrowseResult::Containers(containers) => {
                                if let Some(container) = containers.first() {
                                    let didl = to_didl_lite(&[container.clone()], &[])?;
                                    let update_id = source.update_id().await.max(1);
                                    return Ok((didl, 1, 1, update_id));
                                }
                            }
                            BrowseResult::Items(items) => {
                                // object_id is a container (album, playlist…) whose
                                // browse() returns its children as Items.  BrowseMetadata
                                // must return the container itself, not the first child.
                                let title = items
                                    .first()
                                    .and_then(|i| i.album.as_deref())
                                    .unwrap_or(object_id)
                                    .to_string();
                                let album_art =
                                    items.first().and_then(|i| i.album_art.clone());
                                let container = Container {
                                    id: object_id.to_string(),
                                    parent_id: "0".to_string(),
                                    restricted: Some("1".to_string()),
                                    child_count: Some(items.len().to_string()),
                                    searchable: Some("0".to_string()),
                                    title,
                                    class: "object.container".to_string(),
                                    artist: None,
                                    album_art,
                                    containers: vec![],
                                    items: vec![],
                                };
                                let didl = to_didl_lite(&[container], &[])?;
                                let update_id = source.update_id().await.max(1);
                                return Ok((didl, 1, 1, update_id));
                            }
                            BrowseResult::Mixed { containers, items } => {
                                if let Some(container) = containers.first() {
                                    let didl = to_didl_lite(&[container.clone()], &[])?;
                                    let update_id = source.update_id().await.max(1);
                                    return Ok((didl, 1, 1, update_id));
                                } else if let Some(item) = items.first() {
                                    let didl = to_didl_lite(&[], &[item.clone()])?;
                                    let update_id = source.update_id().await.max(1);
                                    return Ok((didl, 1, 1, update_id));
                                }
                            }
                        }
                    }
                    Err(MusicSourceError::ObjectNotFound(_)) => continue,
                    Err(e) => {
                        non_not_found_error = Some(e.to_string());
                        break;
                    }
                }
            }

            if let Some(err) = non_not_found_error {
                Err(format!("Browse failed: {}", err))
            } else {
                Err(format!("Object not found: {}", object_id))
            }
        }
    }

    /// Browse les enfants directs d'un container
    async fn browse_direct_children(
        &self,
        object_id: &str,
        starting_index: u32,
        requested_count: u32,
    ) -> Result<(String, u32, u32, u32), String> {
        if object_id == "0" {
            // Si un seul enfant, publier directement ses enfants comme racine
            let sources = list_all_sources().await;
            if sources.len() == 1 {
                let source = sources.into_iter().next().unwrap();
                let source_id = source.id().to_string();

                // Récupérer les enfants du container racine de la source
                let mut result = source
                    .browse(&source_id)
                    .await
                    .map_err(|e| format!("Browse failed: {}", e))?;

                // Re-mapper parentID sur "0" pour éviter des parents inexistants côté CP
                match &mut result {
                    BrowseResult::Containers(c) => {
                        for cont in c.iter_mut() {
                            if cont.parent_id == source_id {
                                cont.parent_id = "0".to_string();
                            }
                        }
                    }
                    BrowseResult::Items(i) => {
                        for item in i.iter_mut() {
                            if item.parent_id == source_id {
                                item.parent_id = "0".to_string();
                            }
                        }
                    }
                    BrowseResult::Mixed { containers, items } => {
                        for cont in containers.iter_mut() {
                            if cont.parent_id == source_id {
                                cont.parent_id = "0".to_string();
                            }
                        }
                        for item in items.iter_mut() {
                            if item.parent_id == source_id {
                                item.parent_id = "0".to_string();
                            }
                        }
                    }
                }

                return self
                    .browse_result_to_didl("0", result, source, starting_index, requested_count)
                    .await;
            }

            // Retourner toutes les sources comme enfants de la racine
            return self.browse_root(starting_index, requested_count).await;
        }

        // Vérifier si c'est le container racine d'une source
        if let Some(source) = get_source_from_registry(object_id).await {
            return self
                .browse_source_root(source, starting_index, requested_count)
                .await;
        }

        // Sinon, chercher dans les sources
        let mut non_not_found_error: Option<String> = None;
        for source in list_all_sources().await {
            match source.browse(object_id).await {
                Ok(result) => {
                    return self
                        .browse_result_to_didl(
                            object_id,
                            result,
                            source,
                            starting_index,
                            requested_count,
                        )
                        .await;
                }
                Err(MusicSourceError::ObjectNotFound(_)) => continue,
                Err(e) => {
                    non_not_found_error = Some(e.to_string());
                    break;
                }
            }
        }

        if let Some(err) = non_not_found_error {
            Err(format!("Browse failed: {}", err))
        } else {
            Err(format!("Container not found: {}", object_id))
        }
    }

    /// Browse la racine (liste toutes les sources)
    async fn browse_root(
        &self,
        starting_index: u32,
        requested_count: u32,
    ) -> Result<(String, u32, u32, u32), String> {
        let sources = list_all_sources().await;

        let mut containers = Vec::new();
        for source in sources.iter() {
            let container = source
                .root_container()
                .await
                .map_err(|e| format!("Failed to get root container: {}", e))?;
            let mut container = container;
            container.searchable = container.searchable.or_else(|| Some("1".to_string()));
            containers.push(container);
        }

        // Appliquer la pagination
        let total = containers.len();
        let start = starting_index as usize;
        let count = if requested_count == 0 {
            total - start
        } else {
            requested_count as usize
        };

        let paginated: Vec<Container> = containers.into_iter().skip(start).take(count).collect();

        let returned = paginated.len();
        let didl = to_didl_lite(&paginated, &[])?;

        // Aggregate update IDs across all sources
        let mut combined_id = 0u32;
        for source in list_all_sources().await {
            combined_id = combined_id.wrapping_add(source.update_id().await);
        }
        let update_id = combined_id.max(1);

        Ok((didl, returned as u32, total as u32, update_id))
    }

    /// Browse le container racine d'une source spécifique
    async fn browse_source_root(
        &self,
        source: Arc<dyn MusicSource>,
        starting_index: u32,
        requested_count: u32,
    ) -> Result<(String, u32, u32, u32), String> {
        let source_id = source.id().to_string();
        let result = source
            .browse(&source_id)
            .await
            .map_err(|e| format!("Browse failed: {}", e))?;

        self.browse_result_to_didl(&source_id, result, source, starting_index, requested_count)
            .await
    }

    /// Convertit un BrowseResult en DIDL-Lite XML avec pagination
    async fn browse_result_to_didl(
        &self,
        object_id: &str,
        result: BrowseResult,
        source: Arc<dyn MusicSource>,
        starting_index: u32,
        requested_count: u32,
    ) -> Result<(String, u32, u32, u32), String> {
        let (containers, items) = match result {
            BrowseResult::Containers(c) => (c, vec![]),
            BrowseResult::Items(i) => (vec![], i),
            BrowseResult::Mixed { containers, items } => (containers, items),
        };

        // Filter out any container that matches the object_id being browsed
        // (to avoid containers appearing as children of themselves)
        let mut containers: Vec<Container> = containers
            .into_iter()
            .filter(|c| c.id != object_id)
            .collect();
        let mut items = items;

        // Log avant déduplication
        tracing::debug!(
            "BrowseResult before dedup: containers={}, items={}",
            containers.len(),
            items.len()
        );

        // Deduplicate containers/items by id to avoid doubles in the response
        let mut seen_containers = HashSet::new();
        containers.retain(|c| seen_containers.insert(c.id.clone()));

        let mut seen_items = HashSet::new();
        items.retain(|i| seen_items.insert(i.id.clone()));

        // Log après déduplication
        tracing::debug!(
            "BrowseResult after dedup: containers={}, items={}",
            containers.len(),
            items.len()
        );

        // Trier les containers par titre pour ordre déterministe (alphabétique, insensible à la casse)
        // Les HashMap/HashSet utilisés pour la déduplication ne garantissent pas l'ordre,
        // et les APIs externes peuvent renvoyer dans un ordre non-déterministe
        // On NE trie PAS les items (pistes) car l'ordre original (numéro de piste) est important
        containers.sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase()));

        // Calculer le total avant pagination
        let total = (containers.len() + items.len()) as u32;

        // Appliquer la pagination
        let start = starting_index as usize;
        let count = if requested_count == 0 {
            total as usize - start
        } else {
            requested_count as usize
        };

        // Pagination sur les containers d'abord, puis les items
        let total_containers = containers.len();
        if start < total_containers {
            // On commence dans les containers
            containers = containers.into_iter().skip(start).collect();
            let remaining = count.saturating_sub(containers.len());
            containers.truncate(count);

            if remaining > 0 && !items.is_empty() {
                items.truncate(remaining);
            } else {
                items.clear();
            }
        } else {
            // On commence dans les items
            containers.clear();
            let item_start = start - total_containers;
            items = items.into_iter().skip(item_start).take(count).collect();
        }

        let returned = (containers.len() + items.len()) as u32;
        let didl = to_didl_lite(&containers, &items)?;
        let update_id = source.update_id().await.max(1);

        Ok((didl, returned, total, update_id))
    }

    /// Construit le container racine du MediaServer
    async fn build_root_container(&self) -> Container {
        let sources = list_all_sources().await;
        let _child_count = sources.len();

        Container {
            id: "0".to_string(),
            parent_id: "-1".to_string(),
            restricted: Some("1".to_string()),
            // Laisser childCount absent sur la racine pour maximiser la compatibilité (BubbleUPnP)
            child_count: None,
            searchable: Some("1".to_string()),
            title: "PMOMusic".to_string(),
            class: "object.container".to_string(),
            artist: None,
            album_art: None,
            containers: vec![],
            items: vec![],
        }
    }

    /// Recherche dans toutes les sources qui supportent la recherche
    ///
    /// # Arguments
    ///
    /// * `container_id` - ID du container dans lequel rechercher ("0" = partout)
    /// * `search_criteria` - Critères de recherche UPnP
    ///
    /// # Returns
    ///
    /// Les mêmes informations que browse()
    pub async fn search(
        &self,
        container_id: &str,
        search_criteria: &str,
    ) -> Result<(String, u32, u32, u32), String> {
        tracing::debug!(
            container_id = %container_id,
            search_criteria = %search_criteria,
            "ContentDirectory::Search"
        );

        let text = extract_search_text(search_criteria);
        let (scope, media_type) = container_to_search_context(container_id);
        tracing::debug!(
            container_id,
            search_criteria,
            extracted_text = text,
            scope = ?scope,
            media_type = ?media_type,
            "ContentHandler::search resolved"
        );
        let query = SearchQuery {
            text: text.to_string(),
            media_type,
            scope,
            limit: 200,
            offset: 0,
        };

        let mut all_containers = Vec::new();
        let mut all_items = Vec::new();

        // Si le texte ressemble à une URL, seules les sources qui gèrent les URLs
        // sont interrogées — les autres (Qobuz, etc.) interpréteraient l'URL comme
        // du texte libre et renverraient des résultats parasites.
        let is_url_query = text.starts_with("http://") || text.starts_with("https://");

        for source in list_all_sources().await {
            let caps = source.capabilities();
            if !caps.supports_search {
                continue;
            }
            if is_url_query && !caps.handles_url_input {
                continue;
            }
            if let Ok(result) = source.search(&query).await {
                match result {
                    BrowseResult::Containers(c) => all_containers.extend(c),
                    BrowseResult::Items(i) => all_items.extend(i),
                    BrowseResult::Mixed { containers, items } => {
                        all_containers.extend(containers);
                        all_items.extend(items);
                    }
                }
            }
        }

        let total = (all_containers.len() + all_items.len()) as u32;
        let didl = to_didl_lite(&all_containers, &all_items)?;

        let update_id = if total > 0 {
            let sources = list_all_sources().await;
            let mut combined_id = 0u32;
            for source in sources {
                combined_id = combined_id.wrapping_add(source.update_id().await);
            }
            combined_id.max(1)
        } else {
            1
        };

        Ok((didl, total, total, update_id))
    }

    /// Retourne les capacités de recherche
    pub async fn get_search_capabilities(&self) -> String {
        // Capacités de recherche de base UPnP
        "dc:title,dc:creator,upnp:artist,upnp:album,upnp:genre".to_string()
    }

    /// Retourne les capacités de tri
    pub async fn get_sort_capabilities(&self) -> String {
        // Capacités de tri de base UPnP
        "dc:title,dc:date,upnp:artist,upnp:album".to_string()
    }

    /// Retourne le system update ID global
    pub async fn get_system_update_id(&self) -> u32 {
        let sources = list_all_sources().await;

        // Combiner les update IDs de toutes les sources
        let mut combined_id = 0u32;
        for source in sources {
            combined_id = combined_id.wrapping_add(source.update_id().await);
        }

        combined_id.max(1)
    }
}

impl Default for ContentHandler {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_content_handler_creation() {
        let handler = ContentHandler::new();
        let capabilities = handler.get_search_capabilities().await;
        assert!(capabilities.contains("dc:title"));
    }

    #[tokio::test]
    async fn test_browse_root_empty() {
        let handler = ContentHandler::new();
        let result = handler.browse("0", "BrowseDirectChildren", 0, 0).await;
        assert!(result.is_ok());

        let (didl, returned, total, update_id) = result.unwrap();
        assert_eq!(returned, 0);
        assert_eq!(total, 0);
        assert_eq!(update_id, 1);
        assert!(didl.contains("DIDL-Lite"));
    }

    #[tokio::test]
    async fn test_get_system_update_id() {
        let handler = ContentHandler::new();
        let update_id = handler.get_system_update_id().await;
        assert_eq!(update_id, 1); // No sources registered -> minimum 1
    }
}
