🏡 index : ~doyle/shalom.git

use std::{
    borrow::Cow,
    collections::{BTreeMap, BTreeSet, HashMap},
    str::FromStr,
    sync::{Arc, Mutex},
    time::Duration,
};

use iced::futures::{future, Stream, StreamExt};
use internment::Intern;
use itertools::Itertools;
use tokio::sync::{broadcast, broadcast::error::RecvError};
use tokio_stream::wrappers::BroadcastStream;
use url::Url;

use crate::{
    hass_client::{
        responses::{
            Area, AreaRegistryList, ColorMode, DeviceRegistryList, Entity,
            EntityRegistryList, StateAttributes, StateLightAttributes, StateMediaPlayerAttributes,
            StateWeatherAttributes, StatesList, WeatherCondition,
        }, CallServiceRequestData, CallServiceRequestLight,
        CallServiceRequestLightTurnOn, Event, HassRequestKind,
    },
    widgets::colour_picker::clamp_to_u8,
};

#[allow(dead_code)]
#[derive(Debug)]
pub struct Oracle {
    client: crate::hass_client::Client,
    rooms: BTreeMap<&'static str, Room>,
    weather: Mutex<Weather>,
    media_players: Mutex<BTreeMap<&'static str, MediaPlayer>>,
    lights: Mutex<BTreeMap<&'static str, Light>>,
    entity_updates: broadcast::Sender<Arc<str>>,
}

impl Oracle {
    pub async fn new(hass_client: crate::hass_client::Client) -> Arc<Self> {
        let (rooms, devices, entities, states) = tokio::join!(
            hass_client.request::<AreaRegistryList<'_>>(HassRequestKind::AreaRegistry),
            hass_client.request::<DeviceRegistryList<'_>>(HassRequestKind::DeviceRegistry),
            hass_client.request::<EntityRegistryList<'_>>(HassRequestKind::EntityRegistry),
            hass_client.request::<StatesList<'_>>(HassRequestKind::GetStates),
        );

        let rooms = &rooms.get().0;
        let states = states.get();
        let devices = &devices.get().0;
        let entities = &entities.get().0;

        let all_entities = entities
            .iter()
            .filter_map(|v| v.device_id.as_deref().zip(Some(v)))
            .into_group_map();

        let room_devices = devices
            .iter()
            .filter_map(|v| v.area_id.as_deref().zip(all_entities.get(v.id.as_ref())))
            .into_group_map();

        let rooms = rooms
            .iter()
            .map(|room| build_room(&room_devices, room))
            .collect();

        eprintln!("{rooms:#?}");

        let mut media_players = BTreeMap::new();
        let mut lights = BTreeMap::new();

        for state in &states.0 {
            match &state.attributes {
                StateAttributes::MediaPlayer(attr) => {
                    media_players.insert(
                        Intern::<str>::from(state.entity_id.as_ref()).as_ref(),
                        MediaPlayer::new(attr, &hass_client.base),
                    );
                }
                StateAttributes::Light(attr) => {
                    lights.insert(
                        Intern::<str>::from(state.entity_id.as_ref()).as_ref(),
                        Light::from((attr.clone(), state.state.as_ref())),
                    );
                }
                _ => {}
            }
        }

        let (entity_updates, _) = broadcast::channel(10);

        let this = Arc::new(Self {
            client: hass_client,
            rooms,
            weather: Mutex::new(Weather::parse_from_states(states)),
            media_players: Mutex::new(media_players),
            lights: Mutex::new(lights),
            entity_updates: entity_updates.clone(),
        });

        this.clone().spawn_worker();

        this
    }

    pub fn rooms(&self) -> impl Iterator<Item = (&'static str, &'_ Room)> + '_ {
        self.rooms.iter().map(|(k, v)| (*k, v))
    }

    pub fn room(&self, id: &str) -> &Room {
        self.rooms.get(id).unwrap()
    }

    pub fn current_weather(&self) -> Weather {
        *self.weather.lock().unwrap()
    }

    pub fn subscribe_weather(&self) -> impl Stream<Item = ()> {
        BroadcastStream::new(self.entity_updates.subscribe())
            .filter_map(|v| future::ready(v.ok()))
            .filter(|v| future::ready(v.starts_with("weather.")))
            .map(|_| ())
    }

    pub fn subscribe_id(&self, id: &'static str) -> impl Stream<Item = ()> {
        BroadcastStream::new(self.entity_updates.subscribe())
            .filter_map(|v| future::ready(v.ok()))
            .filter(move |v| future::ready(&**v == id))
            .map(|_| ())
    }

    pub fn fetch_light(&self, entity_id: &'static str) -> Option<Light> {
        self.lights.lock().unwrap().get(entity_id).cloned()
    }

    pub async fn set_light_state(&self, entity_id: &'static str, on: bool) {
        let _res = self
            .client
            .call_service(
                entity_id,
                CallServiceRequestData::Light(if on {
                    CallServiceRequestLight::TurnOn(CallServiceRequestLightTurnOn {
                        brightness: None,
                        hs_color: None,
                    })
                } else {
                    CallServiceRequestLight::TurnOff
                }),
            )
            .await;
    }

    pub async fn update_light(
        &self,
        entity_id: &'static str,
        hue: f32,
        saturation: f32,
        brightness: f32,
    ) {
        let _res = self
            .client
            .call_service(
                entity_id,
                CallServiceRequestData::Light(CallServiceRequestLight::TurnOn(
                    CallServiceRequestLightTurnOn {
                        hs_color: Some((hue, saturation * 100.)),
                        brightness: Some(clamp_to_u8(brightness)),
                    },
                )),
            )
            .await;
    }

    pub fn spawn_worker(self: Arc<Self>) {
        tokio::spawn(async move {
            let mut recv = self.client.subscribe();

            loop {
                let msg = match recv.recv().await {
                    Ok(msg) => msg,
                    Err(RecvError::Lagged(_)) => continue,
                    Err(RecvError::Closed) => break,
                };

                match msg.get() {
                    Event::StateChanged(state_changed) => {
                        match &state_changed.new_state.attributes {
                            StateAttributes::MediaPlayer(attrs) => {
                                self.media_players.lock().unwrap().insert(
                                    Intern::<str>::from(state_changed.entity_id.as_ref()).as_ref(),
                                    MediaPlayer::new(attrs, &self.client.base),
                                );
                            }
                            StateAttributes::Weather(attrs) => {
                                *self.weather.lock().unwrap() =
                                    Weather::parse_from_state_and_attributes(
                                        state_changed.new_state.state.as_ref(),
                                        attrs,
                                    );
                            }
                            StateAttributes::Light(attrs) => {
                                self.lights.lock().unwrap().insert(
                                    Intern::<str>::from(state_changed.entity_id.as_ref()).as_ref(),
                                    Light::from((
                                        attrs.clone(),
                                        state_changed.new_state.state.as_ref(),
                                    )),
                                );
                            }
                            _ => {
                                // TODO
                            }
                        }

                        let _res = self
                            .entity_updates
                            .send(Arc::from(state_changed.entity_id.as_ref()));
                    }
                }
            }
        });
    }
}

fn build_room(
    room_devices: &HashMap<&str, Vec<&Vec<&Entity>>>,
    room: &Area,
) -> (&'static str, Room) {
    let entities = room_devices
        .get(room.area_id.as_ref())
        .iter()
        .flat_map(|v| v.iter())
        .flat_map(|v| v.iter())
        .map(|v| Intern::from(v.entity_id.as_ref()))
        .collect::<Vec<Intern<str>>>();

    let speaker_id = entities
        .iter()
        .filter(|v| {
            // TODO: support multiple media players in one room
            v.as_ref() != "media_player.lg_webos_smart_tv"
        })
        .find(|v| v.starts_with("media_player."))
        .copied();

    let lights = entities
        .iter()
        .filter(|v| v.starts_with("light."))
        .copied()
        .collect();

    let area = Intern::<str>::from(room.area_id.as_ref()).as_ref();
    let room = Room {
        name: Intern::from(room.name.as_ref()),
        entities,
        speaker_id,
        lights,
    };

    (area, room)
}

#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum MediaPlayer {
    Speaker(MediaPlayerSpeaker),
    Tv(MediaPlayerTv),
}

impl MediaPlayer {
    fn new(attr: &StateMediaPlayerAttributes, base: &Url) -> Self {
        if attr.volume_level.is_some() {
            MediaPlayer::Speaker(MediaPlayerSpeaker {
                volume: attr.volume_level.unwrap(),
                muted: attr.is_volume_muted.unwrap(),
                source: Box::from(attr.source.as_deref().unwrap_or("")),
                media_duration: attr.media_duration.map(Duration::from_secs),
                media_position: attr.media_position.map(Duration::from_secs),
                media_title: attr.media_title.as_deref().map(Box::from),
                media_artist: attr.media_artist.as_deref().map(Box::from),
                media_album_name: attr.media_album_name.as_deref().map(Box::from),
                shuffle: attr.shuffle.unwrap_or(false),
                repeat: Box::from(attr.repeat.as_deref().unwrap_or("")),
                entity_picture: attr
                    .entity_picture
                    .as_deref()
                    .map(|path| base.join(path).unwrap()),
            })
        } else {
            MediaPlayer::Tv(MediaPlayerTv {})
        }
    }
}

#[derive(Debug, Clone)]
pub struct Light {
    pub on: Option<bool>,
    pub min_color_temp_kelvin: Option<u16>,
    pub max_color_temp_kelvin: Option<u16>,
    pub min_mireds: Option<u16>,
    pub max_mireds: Option<u16>,
    pub supported_color_modes: Vec<ColorMode>,
    pub mode: Option<Box<str>>,
    pub dynamics: Option<Box<str>>,
    pub friendly_name: Box<str>,
    pub color_mode: Option<ColorMode>,
    pub brightness: Option<f32>,
    pub color_temp_kelvin: Option<u16>,
    pub color_temp: Option<u16>,
    pub hs_color: Option<(f32, f32)>,
}

impl From<(StateLightAttributes<'_>, &str)> for Light {
    fn from((value, state): (StateLightAttributes<'_>, &str)) -> Self {
        let on = match state {
            "on" => Some(true),
            "off" => Some(false),
            "unavailable" => None,
            v => panic!("unknown light state: {v}"),
        };

        Self {
            on,
            min_color_temp_kelvin: value.min_color_temp_kelvin,
            max_color_temp_kelvin: value.max_color_temp_kelvin,
            min_mireds: value.min_mireds,
            max_mireds: value.max_mireds,
            supported_color_modes: value.supported_color_modes.clone(),
            mode: value.mode.map(Cow::into_owned).map(Box::from),
            dynamics: value.dynamics.map(Cow::into_owned).map(Box::from),
            friendly_name: Box::from(value.friendly_name.as_ref()),
            color_mode: value.color_mode,
            brightness: value.brightness,
            color_temp_kelvin: value.color_temp_kelvin,
            color_temp: value.color_temp,
            hs_color: value.hs_color,
        }
    }
}

#[derive(Debug, Clone)]
pub struct MediaPlayerSpeaker {
    pub volume: f32,
    pub muted: bool,
    pub source: Box<str>,
    pub media_duration: Option<Duration>,
    pub media_position: Option<Duration>,
    pub media_title: Option<Box<str>>,
    pub media_artist: Option<Box<str>>,
    pub media_album_name: Option<Box<str>>,
    pub shuffle: bool,
    pub repeat: Box<str>,
    pub entity_picture: Option<Url>,
}

#[derive(Debug, Clone)]
pub struct MediaPlayerTv {}

#[derive(Debug, Clone)]
pub struct Room {
    pub name: Intern<str>,
    pub entities: Vec<Intern<str>>,
    pub speaker_id: Option<Intern<str>>,
    pub lights: BTreeSet<Intern<str>>,
}

impl Room {
    pub fn speaker(&self, oracle: &Oracle) -> Option<MediaPlayerSpeaker> {
        match self.speaker_id.and_then(|v| {
            oracle
                .media_players
                .lock()
                .unwrap()
                .get(v.as_ref())
                .cloned()
        })? {
            MediaPlayer::Speaker(v) => Some(v),
            MediaPlayer::Tv(_) => None,
        }
    }

    pub fn lights(&self, oracle: &Oracle) -> BTreeMap<&'static str, Light> {
        let lights = oracle.lights.lock().unwrap();

        self.lights
            .iter()
            .filter_map(|v| Some((*v).as_ref()).zip(lights.get(v.as_ref()).cloned()))
            .collect()
    }
}

#[derive(Debug, Copy, Clone)]
pub struct Weather {
    pub temperature: i16,
    pub high: i16,
    pub low: i16,
    pub condition: WeatherCondition,
}

impl Weather {
    #[allow(clippy::cast_possible_truncation)]
    fn parse_from_state_and_attributes(state: &str, attributes: &StateWeatherAttributes) -> Self {
        let condition = WeatherCondition::from_str(state).unwrap_or_default();

        let (high, low) =
            attributes
                .forecast
                .iter()
                .fold((i16::MIN, i16::MAX), |(high, low), curr| {
                    let temp = curr.temperature.round() as i16;

                    (high.max(temp), low.min(temp))
                });

        Self {
            temperature: attributes.temperature.round() as i16,
            condition,
            high,
            low,
        }
    }

    fn parse_from_states(states: &StatesList) -> Self {
        let (state, attrs) = states
            .0
            .iter()
            .find_map(|v| match &v.attributes {
                StateAttributes::Weather(attr) => Some((&v.state, attr)),
                _ => None,
            })
            .unwrap();

        Self::parse_from_state_and_attributes(state.as_ref(), attrs)
    }
}