use std::collections::HashMap;

use itertools::Itertools;

use crate::podcast::entity::{
    ListPodcastFilter, ListPodcastParams, ListPodcastResponse, PodcastTask,
};
use crate::prelude::Page;
use crate::task::entity::{TaskMethod, TaskParams, TaskPayload};

pub mod entity;
pub mod prelude;

#[derive(Clone, Debug, bon::Builder)]
pub struct PodcastService<RFL, PR, PER, PSR, PTR, TP> {
    rss_feed_loader: RFL,
    podcast_repository: PR,
    podcast_episode_repository: PER,
    podcast_subscription_repository: PSR,
    podcast_task_repository: PTR,
    task_publisher: TP,
}

impl<RFL, PR, PER, PSR, PTR, TP> PodcastService<RFL, PR, PER, PSR, PTR, TP>
where
    RFL: Clone + prelude::RssFeedLoader,
    PR: Clone + prelude::PodcastRepository,
    PER: Clone + prelude::PodcastEpisodeRepository,
    PSR: Clone + prelude::PodcastSubscriptionRepository,
    PTR: Clone + prelude::PodcastTaskRepository,
{
    async fn find_or_sync_by_feed_url(
        &self,
        feed_url: &str,
    ) -> anyhow::Result<self::entity::Podcast> {
        if let Some(item) = self.podcast_repository.find_by_feed_url(feed_url).await? {
            return Ok(item);
        }

        let loaded = self.rss_feed_loader.load(feed_url).await?;
        self.podcast_repository.upsert(&loaded).await
    }
}

impl<RFL, PR, PER, PSR, PTR, TP> prelude::PodcastService
    for PodcastService<RFL, PR, PER, PSR, PTR, TP>
where
    RFL: Clone + prelude::RssFeedLoader,
    PR: Clone + prelude::PodcastRepository,
    PER: Clone + prelude::PodcastEpisodeRepository,
    PSR: Clone + prelude::PodcastSubscriptionRepository,
    PTR: Clone + prelude::PodcastTaskRepository,
    TP: Clone + crate::task::prelude::TaskPublisher,
{
    #[tracing::instrument(skip(self), err(Debug))]
    async fn request_synchronize_all(&self, user_id: Option<u64>) -> anyhow::Result<()> {
        self.task_publisher
            .publish(
                std::iter::once(TaskPayload::Podcast(PodcastTask::SynchronizeAll)),
                &TaskParams::new(user_id),
            )
            .await?;
        Ok(())
    }

    #[tracing::instrument(skip(self, podcast_ids), err(Debug))]
    async fn request_synchronize<It>(
        &self,
        user_id: Option<u64>,
        podcast_ids: It,
    ) -> anyhow::Result<()>
    where
        It: Iterator<Item = u64> + Send + 'static,
    {
        self.task_publisher
            .publish(
                podcast_ids.map(|podcast_id| {
                    TaskPayload::Podcast(PodcastTask::Synchronize { podcast_id })
                }),
                &TaskParams::new(user_id),
            )
            .await?;
        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn synchronize_all(&self, user_id: Option<u64>) -> anyhow::Result<()> {
        let podcasts = self
            .podcast_repository
            .list(ListPodcastParams {
                user_id,
                filter: ListPodcastFilter::default(),
                page: Page::default(),
            })
            .await?;

        self.request_synchronize(user_id, podcasts.into_iter().map(|podcast| podcast.id))
            .await
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn synchronize(&self, _user_id: Option<u64>, podcast_id: u64) -> anyhow::Result<()> {
        let Some(podcast) = self.podcast_repository.find_by_id(podcast_id).await? else {
            return Ok(());
        };
        let loaded = self.rss_feed_loader.load(podcast.feed_url.as_str()).await?;
        self.podcast_repository.upsert(&loaded).await?;
        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn find_podcast_by_id(
        &self,
        user_id: u64,
        podcast_id: u64,
    ) -> anyhow::Result<Option<entity::FindPodcastResponse>> {
        let podcast = self.podcast_repository.find_by_id(podcast_id).await?;
        let Some(podcast) = podcast else {
            return Ok(None);
        };

        let subscription = self
            .podcast_subscription_repository
            .find(user_id, podcast_id)
            .await?;

        let synchronization = self
            .podcast_task_repository
            .last(TaskMethod::PodcastSynchronize, &[podcast_id])
            .await?
            .into_iter()
            .find(|item| item.payload.target_id() == Some(podcast_id));

        Ok(Some(entity::FindPodcastResponse {
            podcast,
            subscription,
            synchronization,
        }))
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn list_podcast<'a>(
        &self,
        params: ListPodcastParams<'a>,
    ) -> anyhow::Result<ListPodcastResponse> {
        let podcasts = self.podcast_repository.list(params).await?;
        let podcast_ids: Vec<_> = podcasts.iter().map(|p| p.id).collect();
        let subscriptions = if let Some(user_id) = params.user_id {
            self.podcast_subscription_repository
                .list(user_id, &podcast_ids)
                .await?
        } else {
            Vec::new()
        };
        let synchronizations = self
            .podcast_task_repository
            .last(TaskMethod::PodcastSynchronize, &podcast_ids)
            .await?;
        Ok(ListPodcastResponse {
            podcasts,
            subscriptions: HashMap::from_iter(
                subscriptions.into_iter().map(|sub| (sub.podcast_id, sub)),
            ),
            synchronizations: HashMap::from_iter(
                synchronizations
                    .into_iter()
                    .filter_map(|sync| sync.payload.target_id().map(|id| (id, sync))),
            ),
        })
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn find_podcast_episode_by_id(
        &self,
        user_id: u64,
        episode_id: u64,
    ) -> anyhow::Result<Option<entity::FindPodcastEpisodeResponse>> {
        let episode = self
            .podcast_episode_repository
            .find_by_id(episode_id)
            .await?;
        let Some(episode) = episode else {
            return Ok(None);
        };
        let podcast = self
            .podcast_repository
            .find_by_id(episode.podcast_id)
            .await?;
        let Some(podcast) = podcast else {
            return Ok(None);
        };

        let progress = self
            .podcast_episode_repository
            .list_progressions(user_id, &[episode_id])
            .await?;
        let progress = progress
            .into_iter()
            .find(|item| item.podcast_episode_id == episode_id);
        let subscription = self
            .podcast_subscription_repository
            .list(user_id, &[podcast.id])
            .await?;
        let subscription = subscription
            .into_iter()
            .find(|item| item.podcast_id == episode.podcast_id);
        let synchronization = self
            .podcast_task_repository
            .last(TaskMethod::PodcastSynchronize, &[episode.podcast_id])
            .await?;
        let synchronization = synchronization
            .into_iter()
            .find(|item| item.payload.target_id() == Some(episode.podcast_id));

        Ok(Some(entity::FindPodcastEpisodeResponse {
            episode,
            podcast,
            progress,
            subscription,
            synchronization,
        }))
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn list_podcast_episode<'a>(
        &self,
        params: entity::ListPodcastEpisodeParams<'a>,
    ) -> anyhow::Result<entity::ListPodcastEpisodeResponse> {
        let episodes = self.podcast_episode_repository.list(params).await?;
        let episode_ids: Vec<_> = episodes.iter().map(|ep| ep.id).unique().collect();
        let podcast_ids: Vec<_> = episodes.iter().map(|ep| ep.podcast_id).unique().collect();
        let podcasts = self
            .podcast_repository
            .list(ListPodcastParams {
                user_id: Some(params.user_id),
                filter: ListPodcastFilter {
                    podcast_ids: &podcast_ids,
                    subscribed: None,
                },
                page: Page {
                    limit: podcast_ids.len() as u32,
                    offset: 0,
                },
            })
            .await?;

        let progresses = self
            .podcast_episode_repository
            .list_progressions(params.user_id, &episode_ids)
            .await?;
        let subscriptions = self
            .podcast_subscription_repository
            .list(params.user_id, &podcast_ids)
            .await?;
        let synchronizations = self
            .podcast_task_repository
            .last(TaskMethod::PodcastSynchronize, &podcast_ids)
            .await?;

        Ok(entity::ListPodcastEpisodeResponse {
            episodes,
            podcasts: HashMap::from_iter(podcasts.into_iter().map(|p| (p.id, p))),
            progresses: HashMap::from_iter(
                progresses.into_iter().map(|p| (p.podcast_episode_id, p)),
            ),
            subscriptions: HashMap::from_iter(subscriptions.into_iter().map(|s| (s.podcast_id, s))),
            synchronizations: HashMap::from_iter(
                synchronizations
                    .into_iter()
                    .filter_map(|s| s.payload.target_id().map(|id| (id, s))),
            ),
        })
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn delete_podcast_episode_progressions(
        &self,
        user_id: u64,
        episode_ids: &[u64],
    ) -> anyhow::Result<()> {
        self.podcast_episode_repository
            .delete_progressions(user_id, episode_ids)
            .await
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn upsert_podcast_episode_progressions(
        &self,
        inputs: &[entity::PodcastEpisodeProgressInput],
    ) -> anyhow::Result<Vec<entity::PodcastEpisodeProgress>> {
        self.podcast_episode_repository
            .upsert_progressions(inputs)
            .await
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn find_subscription_by_id(
        &self,
        user_id: u64,
        podcast_id: u64,
    ) -> anyhow::Result<Option<entity::PodcastSubscription>> {
        self.podcast_subscription_repository
            .find(user_id, podcast_id)
            .await
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn subscribe(
        &self,
        user_id: u64,
        feed_url: &str,
    ) -> anyhow::Result<self::entity::Podcast> {
        let subscription = self.find_or_sync_by_feed_url(feed_url).await?;
        self.podcast_subscription_repository
            .create(user_id, subscription.id)
            .await?;
        Ok(subscription)
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn unsubscribe(&self, user_id: u64, podcast_id: u64) -> anyhow::Result<()> {
        self.podcast_subscription_repository
            .delete(user_id, podcast_id)
            .await
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn update_subscription_by_id(
        &self,
        user_id: u64,
        podcast_id: u64,
        body: entity::PodcastSubscriptionUpdate,
    ) -> anyhow::Result<()> {
        self.podcast_subscription_repository
            .update(user_id, podcast_id, body)
            .await
    }
}
