//! Backend-agnostic music renderer façade for PMOMusic.
//!
//! `MusicRenderer` wraps every supported backend (UPnP AV/DLNA, OpenHome,
//! LinkPlay HTTP, Arylic TCP, Chromecast, and the hybrid UPnP + Arylic pairing) behind a
//! single control surface. Higher layers in PMOMusic must only interact with
//! renderers through this type so that transport, volume, and state queries
//! stay backend-neutral.

use std::sync::{Arc, OnceLock, RwLock};

use crate::capabilities::{PlaybackPositionInfo, PlaybackStatus};
use crate::control_point::RendererRuntimeStateMut;
use crate::control_point::music_queue::MusicQueue;
use crate::control_point::openhome_queue::didl_id_from_metadata;
use crate::media_server::ServerId;
use crate::model::{RendererId, RendererInfo, RendererProtocol};
use crate::openhome_client::parse_track_metadata_from_didl;
use crate::openhome_playlist::OpenHomePlaylistSnapshot;
use crate::queue_backend::PlaybackItem;
use crate::{
    ArylicTcpRenderer, ChromecastRenderer, DeviceRegistry, LinkPlayRenderer, OpenHomeRenderer,
    PlaybackPosition, PlaybackState, TransportControl, UpnpRenderer, VolumeControl,
};
use anyhow::{Result, anyhow};
use tracing::warn;

/// Backend-agnostic façade exposing transport, volume, and status contracts.
#[derive(Clone, Debug)]
pub enum MusicRenderer {
    /// Classic UPnP AV / DLNA renderer (AVTransport + RenderingControl).
    Upnp(UpnpRenderer),
    /// Renderer powered by OpenHome services.
    OpenHome(OpenHomeRenderer),
    /// Renderer controlled via the LinkPlay HTTP API.
    LinkPlay(LinkPlayRenderer),
    /// Renderer reachable through the Arylic TCP control protocol (port 8899).
    ArylicTcp(ArylicTcpRenderer),
    /// Renderer controlled via the Google Cast protocol (Chromecast).
    Chromecast(ChromecastRenderer),
    /// Combined backend using UPnP for transport + volume writes and Arylic TCP
    /// to read detailed playback information as well as live volume/mute state.
    HybridUpnpArylic {
        upnp: UpnpRenderer,
        arylic: ArylicTcpRenderer,
    },
}

/// Build a standardized error when an operation is not supported by a backend.
pub(crate) fn op_not_supported(op: &str, backend: &str) -> anyhow::Error {
    anyhow!(
        "MusicRenderer operation '{}' is not supported by backend '{}'",
        op,
        backend
    )
}

#[derive(Clone, Debug)]
pub struct RendererRuntimeState {
    pub queue: MusicQueue,
}

pub trait OpenHomeQueueProvider: Send + Sync + 'static {
    fn renderer_state(&self, renderer_id: &RendererId) -> Result<RendererRuntimeState>;
    fn renderer_state_mut<'a>(
        &'a self,
        renderer_id: &RendererId,
    ) -> Result<RendererRuntimeStateMut<'a>>;
    fn invalidate_openhome_cache(&self, renderer_id: &RendererId) -> Result<()>;
}

static OPENHOME_QUEUE_PROVIDER: OnceLock<Arc<dyn OpenHomeQueueProvider>> = OnceLock::new();

pub fn set_openhome_queue_provider(provider: Arc<dyn OpenHomeQueueProvider>) {
    let _ = OPENHOME_QUEUE_PROVIDER.set(provider);
}

impl MusicRenderer {
    /// Renderer identifier (stable within the registry).
    pub fn id(&self) -> &RendererId {
        match self {
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.id(),
            MusicRenderer::OpenHome(r) => r.id(),
            MusicRenderer::Upnp(r) => r.id(),
            MusicRenderer::LinkPlay(r) => r.id(),
            MusicRenderer::ArylicTcp(r) => r.id(),
            MusicRenderer::Chromecast(r) => r.id(),
        }
    }

    /// Human-friendly name reported by the device.
    pub fn friendly_name(&self) -> &str {
        match self {
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.friendly_name(),
            MusicRenderer::OpenHome(r) => r.friendly_name(),
            MusicRenderer::Upnp(r) => r.friendly_name(),
            MusicRenderer::LinkPlay(r) => r.friendly_name(),
            MusicRenderer::ArylicTcp(r) => r.friendly_name(),
            MusicRenderer::Chromecast(r) => r.friendly_name(),
        }
    }

    /// Protocol classification (UPnP AV only, OpenHome only, hybrid).
    pub fn protocol(&self) -> &RendererProtocol {
        &self.info().protocol
    }

    /// Full static info as stored in the registry.
    pub fn info(&self) -> &RendererInfo {
        match self {
            MusicRenderer::HybridUpnpArylic { arylic, .. } => &arylic.info,
            MusicRenderer::OpenHome(r) => &r.info,
            MusicRenderer::Upnp(r) => &r.info,
            MusicRenderer::LinkPlay(r) => &r.info,
            MusicRenderer::ArylicTcp(r) => &r.info,
            MusicRenderer::Chromecast(r) => &r.info,
        }
    }

    /// Return a reference to the underlying UPnP backend, if any.
    pub fn as_upnp(&self) -> Option<&UpnpRenderer> {
        match self {
            MusicRenderer::Upnp(r) => Some(r),
            _ => None,
        }
    }

    /// Construct a music renderer from a [`RendererInfo`] and the registry.
    ///
    /// Returns `None` when no supported backend can be built for this renderer.
    /// UPnP AV / hybrid renderers map either to [`MusicRenderer::LinkPlay`] (when supported)
    /// or [`MusicRenderer::Upnp`].
    pub fn from_registry_info(
        info: RendererInfo,
        registry: &Arc<RwLock<DeviceRegistry>>,
    ) -> Option<Self> {
        if matches!(
            info.protocol,
            RendererProtocol::OpenHomeOnly | RendererProtocol::Hybrid
        ) {
            if let Some(renderer) = {
                let renderer = OpenHomeRenderer::new(info.clone());
                renderer.has_any_openhome_service().then_some(renderer)
            } {
                return Some(MusicRenderer::OpenHome(renderer));
            }

            if matches!(info.protocol, RendererProtocol::OpenHomeOnly) {
                warn!(
                    renderer = info.friendly_name.as_str(),
                    "Renderer advertises OpenHome only but exposes no usable services"
                );
                return None;
            }
        }

        if matches!(info.protocol, RendererProtocol::ChromecastOnly) {
            if let Ok(renderer) = ChromecastRenderer::from_renderer_info(info.clone()) {
                return Some(MusicRenderer::Chromecast(renderer));
            }
            warn!(
                renderer = info.friendly_name.as_str(),
                "Failed to build Chromecast renderer"
            );
            return None;
        }

        match info.protocol {
            RendererProtocol::UpnpAvOnly | RendererProtocol::Hybrid => {
                let has_arylic = info.capabilities.has_arylic_tcp;
                let has_avtransport = info.capabilities.has_avtransport;

                if has_arylic && has_avtransport {
                    // Construire UpnpRenderer
                    let upnp = UpnpRenderer::from_registry(info.clone(), registry);

                    // Construire ArylicTcpRenderer
                    match ArylicTcpRenderer::from_renderer_info(info.clone()) {
                        Ok(arylic) => {
                            return Some(MusicRenderer::HybridUpnpArylic { upnp, arylic });
                        }
                        Err(err) => {
                            warn!(
                                "Failed to build Arylic TCP backend for {}: {}. Falling back to UPnP only.",
                                info.friendly_name, err
                            );
                            return Some(MusicRenderer::Upnp(upnp));
                        }
                    }
                }

                // Pas d’Arylic : logique existante
                if info.capabilities.has_linkplay_http {
                    if let Ok(lp) = LinkPlayRenderer::from_renderer_info(info.clone()) {
                        return Some(MusicRenderer::LinkPlay(lp));
                    }
                }

                Some(MusicRenderer::Upnp(UpnpRenderer::from_registry(
                    info, registry,
                )))
            }
            RendererProtocol::OpenHomeOnly => None,
            RendererProtocol::ChromecastOnly => None,
        }
    }

    pub fn openhome_playlist_snapshot(&self) -> Result<OpenHomePlaylistSnapshot> {
        self.fetch_openhome_playlist_snapshot()
    }

    pub(crate) fn fetch_openhome_playlist_snapshot(&self) -> Result<OpenHomePlaylistSnapshot> {
        match self {
            MusicRenderer::OpenHome(renderer) => renderer.snapshot_openhome_playlist(),
            _ => Err(op_not_supported(
                "openhome_playlist_snapshot",
                self.unsupported_backend_name(),
            )),
        }
    }

    pub fn openhome_playlist_len(&self) -> Result<usize> {
        match self {
            MusicRenderer::OpenHome(renderer) => renderer.openhome_playlist_len(),
            _ => Err(op_not_supported(
                "openhome_playlist_len",
                self.unsupported_backend_name(),
            )),
        }
    }

    pub fn openhome_playlist_ids(&self) -> Result<Vec<u32>> {
        match self {
            MusicRenderer::OpenHome(renderer) => renderer.openhome_playlist_ids(),
            _ => Err(op_not_supported(
                "openhome_playlist_ids",
                self.unsupported_backend_name(),
            )),
        }
    }

    pub fn openhome_playlist_clear(&self) -> Result<()> {
        if let Some(provider) = OPENHOME_QUEUE_PROVIDER.get() {
            let result = {
                let mut state = provider.renderer_state_mut(self.id())?;
                match &mut *state.queue {
                    MusicQueue::OpenHome(queue) => queue.clear(),
                    _ => Err(op_not_supported(
                        "openhome_playlist_clear",
                        self.unsupported_backend_name(),
                    )),
                }
            };
            if result.is_ok() {
                provider.invalidate_openhome_cache(self.id())?;
            }
            result
        } else {
            self.fetch_openhome_playlist_clear()
        }
    }

    pub(crate) fn fetch_openhome_playlist_clear(&self) -> Result<()> {
        match self {
            MusicRenderer::OpenHome(renderer) => renderer.clear_openhome_playlist(),
            _ => Err(op_not_supported(
                "openhome_playlist_clear",
                self.unsupported_backend_name(),
            )),
        }
    }

    pub fn openhome_playlist_add_track(
        &self,
        uri: &str,
        metadata: &str,
        after_id: Option<u32>,
        play: bool,
    ) -> Result<u32> {
        if let Some(provider) = OPENHOME_QUEUE_PROVIDER.get() {
            let result = {
                let mut state = provider.renderer_state_mut(self.id())?;
                match &mut *state.queue {
                    MusicQueue::OpenHome(queue) => {
                        let playback_item =
                            Self::playback_item_from_params(self.id(), uri, metadata)?;
                        queue.add_playback_item(playback_item, after_id, play)
                    }
                    _ => Err(op_not_supported(
                        "openhome_playlist_add_track",
                        self.unsupported_backend_name(),
                    )),
                }
            };
            if result.is_ok() {
                provider.invalidate_openhome_cache(self.id())?;
            }
            result
        } else {
            self.fetch_openhome_playlist_add_track(uri, metadata, after_id, play)
        }
    }

    pub fn openhome_playlist_play_id(&self, id: u32) -> Result<()> {
        if let Some(provider) = OPENHOME_QUEUE_PROVIDER.get() {
            let result = {
                let mut state = provider.renderer_state_mut(self.id())?;
                match &mut *state.queue {
                    MusicQueue::OpenHome(queue) => queue.select_track_id(id),
                    _ => Err(op_not_supported(
                        "openhome_playlist_play_id",
                        self.unsupported_backend_name(),
                    )),
                }
            };
            if result.is_ok() {
                provider.invalidate_openhome_cache(self.id())?;
            }
            result
        } else {
            self.fetch_openhome_playlist_play_id(id)
        }
    }

    fn unsupported_backend_name(&self) -> &'static str {
        match self {
            MusicRenderer::Upnp(_) => "UPnP",
            MusicRenderer::OpenHome(_) => "OpenHome",
            MusicRenderer::LinkPlay(_) => "LinkPlay",
            MusicRenderer::ArylicTcp(_) => "ArylicTcp",
            MusicRenderer::Chromecast(_) => "Chromecast",
            MusicRenderer::HybridUpnpArylic { .. } => "HybridUpnpArylic",
        }
    }

    fn fetch_openhome_playlist_add_track(
        &self,
        uri: &str,
        metadata: &str,
        after_id: Option<u32>,
        play: bool,
    ) -> Result<u32> {
        match self {
            MusicRenderer::OpenHome(renderer) => {
                renderer.add_track_openhome(uri, metadata, after_id, play)
            }
            _ => Err(op_not_supported(
                "openhome_playlist_add_track",
                self.unsupported_backend_name(),
            )),
        }
    }

    fn fetch_openhome_playlist_play_id(&self, id: u32) -> Result<()> {
        match self {
            MusicRenderer::OpenHome(renderer) => renderer.play_openhome_track_id(id),
            _ => Err(op_not_supported(
                "openhome_playlist_play_id",
                self.unsupported_backend_name(),
            )),
        }
    }

    fn playback_item_from_params(
        renderer_id: &RendererId,
        uri: &str,
        metadata_xml: &str,
    ) -> Result<PlaybackItem> {
        let metadata = parse_track_metadata_from_didl(metadata_xml);
        let didl_id = didl_id_from_metadata(metadata_xml)
            .unwrap_or_else(|| format!("openhome:{}", renderer_id.0));
        Ok(PlaybackItem {
            media_server_id: ServerId(format!("openhome:{}", renderer_id.0)),
            didl_id,
            uri: uri.to_string(),
            // OpenHome tracks don't provide protocolInfo, use generic default
            protocol_info: "http-get:*:audio/*:*".to_string(),
            metadata,
        })
    }
}

/// Transport control façade that dispatches to whichever backend can fulfill
/// the request, returning a standardized error if the backend lacks support.
impl TransportControl for MusicRenderer {
    fn play_uri(&self, uri: &str, meta: &str) -> Result<()> {
        match self {
            MusicRenderer::Upnp(upnp) => upnp.play_uri(uri, meta),
            MusicRenderer::OpenHome(oh) => oh.play_uri(uri, meta),
            MusicRenderer::LinkPlay(lp) => lp.play_uri(uri, meta),
            MusicRenderer::ArylicTcp(_) => Err(op_not_supported("play_uri", "ArylicTcp")),
            MusicRenderer::Chromecast(cc) => cc.play_uri(uri, meta),
            MusicRenderer::HybridUpnpArylic { upnp, .. } => upnp.play_uri(uri, meta),
        }
    }

    fn play(&self) -> Result<()> {
        match self {
            MusicRenderer::Upnp(upnp) => upnp.play(),
            MusicRenderer::OpenHome(oh) => oh.play(),
            MusicRenderer::LinkPlay(lp) => lp.play(),
            MusicRenderer::ArylicTcp(ary) => ary.play(),
            MusicRenderer::Chromecast(cc) => cc.play(),
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.play(),
        }
    }

    fn pause(&self) -> Result<()> {
        match self {
            MusicRenderer::Upnp(upnp) => upnp.pause(),
            MusicRenderer::OpenHome(oh) => oh.pause(),
            MusicRenderer::LinkPlay(lp) => lp.pause(),
            MusicRenderer::ArylicTcp(ary) => ary.pause(),
            MusicRenderer::Chromecast(cc) => cc.pause(),
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.pause(),
        }
    }

    fn stop(&self) -> Result<()> {
        match self {
            MusicRenderer::Upnp(upnp) => upnp.stop(),
            MusicRenderer::OpenHome(oh) => oh.stop(),
            MusicRenderer::LinkPlay(lp) => lp.stop(),
            MusicRenderer::ArylicTcp(ary) => ary.stop(),
            MusicRenderer::Chromecast(cc) => cc.stop(),
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.stop(),
        }
    }

    fn seek_rel_time(&self, hhmmss: &str) -> Result<()> {
        match self {
            MusicRenderer::Upnp(upnp) => upnp.seek_rel_time(hhmmss),
            MusicRenderer::OpenHome(oh) => oh.seek_rel_time(hhmmss),
            MusicRenderer::LinkPlay(lp) => lp.seek_rel_time(hhmmss),
            MusicRenderer::ArylicTcp(_) => Err(op_not_supported("seek_rel_time", "ArylicTcp")),
            MusicRenderer::Chromecast(cc) => cc.seek_rel_time(hhmmss),
            MusicRenderer::HybridUpnpArylic { upnp, .. } => upnp.seek_rel_time(hhmmss),
        }
    }
}

/// Volume and mute controls exposed via the façade.
///
/// Hybrid backends may read via Arylic TCP and write via UPnP, but callers
/// always depend on a single [`VolumeControl`] entry point.
impl VolumeControl for MusicRenderer {
    fn volume(&self) -> Result<u16> {
        match self {
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.volume(),
            MusicRenderer::ArylicTcp(ary) => ary.volume(),
            MusicRenderer::OpenHome(oh) => oh.volume(),
            MusicRenderer::Upnp(upnp) => upnp.volume(),
            MusicRenderer::LinkPlay(lp) => lp.volume(),
            MusicRenderer::Chromecast(cc) => cc.volume(),
        }
    }

    fn set_volume(&self, vol: u16) -> Result<()> {
        match self {
            MusicRenderer::HybridUpnpArylic { upnp, .. } => upnp.set_volume(vol),
            MusicRenderer::ArylicTcp(ary) => ary.set_volume(vol),
            MusicRenderer::OpenHome(oh) => oh.set_volume(vol),
            MusicRenderer::Upnp(upnp) => upnp.set_volume(vol),
            MusicRenderer::LinkPlay(lp) => lp.set_volume(vol),
            MusicRenderer::Chromecast(cc) => cc.set_volume(vol),
        }
    }

    fn mute(&self) -> Result<bool> {
        match self {
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.mute(),
            MusicRenderer::OpenHome(r) => r.mute(),
            MusicRenderer::Upnp(r) => r.get_master_mute(),
            MusicRenderer::LinkPlay(r) => r.mute(),
            MusicRenderer::ArylicTcp(r) => r.mute(),
            MusicRenderer::Chromecast(cc) => cc.mute(),
        }
    }

    fn set_mute(&self, m: bool) -> Result<()> {
        match self {
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.set_mute(m),
            MusicRenderer::OpenHome(r) => r.set_mute(m),
            MusicRenderer::Upnp(r) => r.set_master_mute(m),
            MusicRenderer::LinkPlay(r) => r.set_mute(m),
            MusicRenderer::ArylicTcp(r) => r.set_mute(m),
            MusicRenderer::Chromecast(cc) => cc.set_mute(m),
        }
    }
}

/// Playback-state queries sourced from the backend best suited for the job.
///
/// Each backend reports into [`PlaybackState`], ensuring consumers never have
/// to reason about protocol-specific state machines.
impl PlaybackStatus for MusicRenderer {
    fn playback_state(&self) -> Result<PlaybackState> {
        match self {
            MusicRenderer::Upnp(r) => PlaybackStatus::playback_state(r),
            MusicRenderer::OpenHome(r) => PlaybackStatus::playback_state(r),
            MusicRenderer::LinkPlay(r) => r.playback_state(),
            MusicRenderer::ArylicTcp(r) => r.playback_state(),
            MusicRenderer::Chromecast(cc) => cc.playback_state(),
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.playback_state(),
        }
    }
}

/// Playback-position queries that always yield a [`PlaybackPositionInfo`]
/// regardless of the backend providing the raw transport data.
impl PlaybackPosition for MusicRenderer {
    fn playback_position(&self) -> Result<PlaybackPositionInfo> {
        match self {
            MusicRenderer::Upnp(r) => r.playback_position(),
            MusicRenderer::OpenHome(r) => r.playback_position(),
            MusicRenderer::LinkPlay(r) => r.playback_position(),
            MusicRenderer::ArylicTcp(r) => r.playback_position(),
            MusicRenderer::Chromecast(cc) => cc.playback_position(),
            MusicRenderer::HybridUpnpArylic { arylic, .. } => arylic.playback_position(),
        }
    }
}
