use std::collections::HashMap;

use anyhow::Context;
use itertools::Itertools;

use crate::language::Language;
use crate::prelude::{Page, Sort};
use crate::task::entity::{TaskParams, TaskPayload};
use crate::tvshow::entity::{
    ListEpisodeFileParams, ListTvShowEpisodeResponse, ListTvShowFilter, ListTvShowParams,
    ListTvShowResponse, ListTvShowSeasonFilter, ListTvShowSeasonParams, SynchronizeSeasonParams,
    TvShowTask,
};

pub mod entity;
pub mod prelude;

#[derive(Clone, Debug, bon::Builder)]
pub struct TvShowService<TP, TR, TER, TEFR, TSR, TSubR, TaskPub> {
    tvshow_provider: TP,
    tvshow_repository: TR,
    tvshow_episode_repository: TER,
    tvshow_episode_file_repository: TEFR,
    tvshow_season_repository: TSR,
    tvshow_subscription_repository: TSubR,
    task_publisher: TaskPub,
}

impl<TP, TR, TER, TEFR, TSR, TSubR, TaskPub> prelude::TvShowService
    for TvShowService<TP, TR, TER, TEFR, TSR, TSubR, TaskPub>
where
    TP: Clone + prelude::TvShowProvider,
    TR: Clone + prelude::TvShowRepository,
    TER: Clone + prelude::TvShowEpisodeRepository,
    TEFR: Clone + prelude::TvShowEpisodeFileRepository,
    TSR: Clone + prelude::TvShowSeasonRepository,
    TSubR: Clone + prelude::TvShowSubscriptionRepository,
    TaskPub: Clone + crate::task::prelude::TaskPublisher,
{
    #[tracing::instrument(skip(self), err(Debug))]
    async fn find_tvshow_by_id(
        &self,
        user_id: Option<u64>,
        tvshow_id: u64,
        language: Language,
    ) -> anyhow::Result<Option<entity::FindTvShowResponse>> {
        let tvshow = self
            .tvshow_repository
            .find_by_id(tvshow_id, language)
            .await?;
        let Some(tvshow) = tvshow else {
            return Ok(None);
        };
        let mut res = entity::FindTvShowResponse {
            tvshow,
            subscription: None,
        };

        if let Some(user_id) = user_id {
            let list = self
                .tvshow_subscription_repository
                .list_by_ids(user_id, &[tvshow_id])
                .await?;
            res.subscription = list.into_iter().find(|item| item.tvshow_id == tvshow_id);
        }

        Ok(Some(res))
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn find_tvshow_episode_by_id(
        &self,
        user_id: Option<u64>,
        episode_id: u64,
        language: Language,
    ) -> anyhow::Result<Option<entity::FindTvShowEpisodeResponse>> {
        let episode = self
            .tvshow_episode_repository
            .find_by_id(episode_id, language)
            .await?;
        let Some(episode) = episode else {
            return Ok(None);
        };

        let season = self
            .tvshow_season_repository
            .find_by_id(episode.tvshow_season_id, language)
            .await?;
        let Some(season) = season else {
            return Ok(None);
        };
        let tvshow = self
            .tvshow_repository
            .find_by_id(season.tvshow_id, language)
            .await?;
        let Some(tvshow) = tvshow else {
            return Ok(None);
        };
        let tvshow_id = season.tvshow_id;

        let files = self
            .tvshow_episode_file_repository
            .list(ListEpisodeFileParams {
                episode_ids: &[episode_id],
                content_type: None,
            })
            .await?;

        let mut res = entity::FindTvShowEpisodeResponse {
            tvshow,
            season,
            episode,
            files,
            progress: None,
            subscription: None,
        };

        if let Some(user_id) = user_id {
            let progress = self
                .tvshow_episode_repository
                .list_progressions(user_id, &[episode_id])
                .await?;
            res.progress = progress
                .into_iter()
                .find(|progress| progress.tvshow_episode_id == episode_id);

            let list = self
                .tvshow_subscription_repository
                .list_by_ids(user_id, &[tvshow_id])
                .await?;
            res.subscription = list.into_iter().find(|item| item.tvshow_id == tvshow_id);
        }

        Ok(Some(res))
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn list_tvshow_episodes<'a>(
        &self,
        params: entity::ListTvShowEpisodeParams<'a>,
    ) -> anyhow::Result<entity::ListTvShowEpisodeResponse> {
        let episodes = self.tvshow_episode_repository.list(params).await?;
        let episode_ids: Vec<_> = episodes.iter().map(|episode| episode.id).unique().collect();

        let season_ids: Vec<_> = episodes
            .iter()
            .map(|episode| episode.tvshow_season_id)
            .unique()
            .collect();
        let season_count = season_ids.len();
        let seasons = self
            .tvshow_season_repository
            .list(ListTvShowSeasonParams {
                user_id: params.user_id,
                language: params.language,
                filter: ListTvShowSeasonFilter {
                    tvshow_id: None,
                    season_ids: &season_ids,
                },
                sort: Sort {
                    field: entity::TvShowSeasonField::SeasonNumber,
                    order: crate::prelude::SortOrder::Asc,
                },
                page: Page {
                    limit: season_count as u32,
                    offset: 0,
                },
            })
            .await?;
        let tvshow_ids: Vec<_> = seasons
            .iter()
            .map(|season| season.tvshow_id)
            .unique()
            .collect();

        let progresses = self
            .tvshow_episode_repository
            .list_progressions(params.user_id, &episode_ids)
            .await?;

        let subscriptions = self
            .tvshow_subscription_repository
            .list_by_ids(params.user_id, &tvshow_ids)
            .await?;

        let tvshow_count = tvshow_ids.len();
        let tvshows = self
            .tvshow_repository
            .list(ListTvShowParams {
                user_id: params.user_id,
                language: params.language,
                filter: ListTvShowFilter {
                    tvshow_ids: &tvshow_ids,
                    subscribed: None,
                },
                page: Page {
                    limit: tvshow_count as u32,
                    offset: 0,
                },
            })
            .await?;

        let files = self
            .tvshow_episode_file_repository
            .list(ListEpisodeFileParams {
                episode_ids: &episode_ids,
                content_type: None,
            })
            .await?;
        let files = files.into_iter().fold(
            HashMap::<u64, Vec<_>>::with_capacity(episode_ids.len()),
            |mut res, item| {
                res.entry(item.tvshow_episode_id).or_default().push(item);
                res
            },
        );

        Ok(ListTvShowEpisodeResponse {
            episodes,
            seasons: HashMap::from_iter(seasons.into_iter().map(|season| (season.id, season))),
            tvshows: HashMap::from_iter(tvshows.into_iter().map(|tvshow| (tvshow.id, tvshow))),
            files,
            progresses: HashMap::from_iter(
                progresses
                    .into_iter()
                    .map(|prog| (prog.tvshow_episode_id, prog)),
            ),
            subscriptions: HashMap::from_iter(
                subscriptions.into_iter().map(|sub| (sub.tvshow_id, sub)),
            ),
        })
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn find_tvshow_season_by_id(
        &self,
        user_id: Option<u64>,
        season_id: u64,
        language: Language,
    ) -> anyhow::Result<Option<entity::FindTvShowSeasonResponse>> {
        let season = self
            .tvshow_season_repository
            .find_by_id(season_id, language)
            .await?;
        let Some(season) = season else {
            return Ok(None);
        };
        let tvshow = self
            .tvshow_repository
            .find_by_id(season.tvshow_id, language)
            .await?;
        let Some(tvshow) = tvshow else {
            return Ok(None);
        };

        let subscription = if let Some(user_id) = user_id {
            let res = self
                .tvshow_subscription_repository
                .list_by_ids(user_id, &[season.tvshow_id])
                .await?;
            res.into_iter().find(|sub| sub.tvshow_id == tvshow.id)
        } else {
            None
        };

        Ok(Some(entity::FindTvShowSeasonResponse {
            tvshow,
            season,
            subscription,
        }))
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn list_tvshow_seasons<'a>(
        &self,
        params: ListTvShowSeasonParams<'a>,
    ) -> anyhow::Result<entity::ListTvShowSeasonResponse> {
        let seasons = self.tvshow_season_repository.list(params).await?;
        let tvshow_ids: Vec<_> = seasons.iter().map(|s| s.tvshow_id).unique().collect();
        let tvshow_count = tvshow_ids.len();

        let subscriptions = self
            .tvshow_subscription_repository
            .list_by_ids(params.user_id, &tvshow_ids)
            .await?;

        let tvshows = self
            .tvshow_repository
            .list(ListTvShowParams {
                user_id: params.user_id,
                language: params.language,
                filter: ListTvShowFilter {
                    tvshow_ids: &tvshow_ids,
                    subscribed: None,
                },
                page: Page {
                    limit: tvshow_count as u32,
                    offset: 0,
                },
            })
            .await?;

        Ok(entity::ListTvShowSeasonResponse {
            seasons,
            tvshows: HashMap::from_iter(tvshows.into_iter().map(|item| (item.id, item))),
            subscriptions: HashMap::from_iter(
                subscriptions.into_iter().map(|sub| (sub.tvshow_id, sub)),
            ),
        })
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn list_tvshows(
        &self,
        params: entity::ListTvShowParams<'_>,
    ) -> anyhow::Result<ListTvShowResponse> {
        let tvshows = self.tvshow_repository.list(params).await?;
        let tvshow_ids: Vec<_> = tvshows.iter().map(|tvshow| tvshow.id).unique().collect();
        let subscriptions = self
            .tvshow_subscription_repository
            .list_by_ids(params.user_id, &tvshow_ids)
            .await?;
        Ok(ListTvShowResponse {
            tvshows,
            subscriptions: HashMap::from_iter(
                subscriptions.into_iter().map(|sub| (sub.tvshow_id, sub)),
            ),
        })
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn upsert_tvshow_episode_progressions(
        &self,
        input: &[entity::TvShowEpisodeProgressInput],
    ) -> anyhow::Result<Vec<entity::TvShowEpisodeProgress>> {
        self.tvshow_episode_repository
            .upsert_progressions(input)
            .await
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn subscribe(
        &self,
        user_id: u64,
        source: entity::TvShowSource,
        language: Language,
    ) -> Result<(), prelude::SubscribeError> {
        let tvshow_id = if let Some(existing) = self
            .tvshow_repository
            .find_by_source(source, language)
            .await?
        {
            existing.id
        } else {
            let tvshow = self
                .tvshow_provider
                .find_by_source(source, language)
                .await?;
            let tvshow = tvshow.ok_or(prelude::SubscribeError::NotFound)?;
            let created = self.tvshow_repository.upsert(&tvshow).await?;

            self.task_publisher
                .publish(
                    std::iter::once(TaskPayload::TvShow(TvShowTask::SynchronizeLocated {
                        tvshow_id: created.id,
                        language,
                    })),
                    &TaskParams::new(Some(user_id)),
                )
                .await?;

            created.id
        };

        self.tvshow_subscription_repository
            .create(user_id, tvshow_id)
            .await?;
        Ok(())
    }

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

    #[tracing::instrument(skip(self), err(Debug))]
    async fn request_synchronize(
        &self,
        user_id: Option<u64>,
        tvshow_id: u64,
    ) -> anyhow::Result<()> {
        self.task_publisher
            .publish(
                std::iter::once(TaskPayload::TvShow(TvShowTask::Synchronize { tvshow_id })),
                &TaskParams::new(user_id),
            )
            .await?;
        Ok(())
    }

    #[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::TvShow(TvShowTask::SynchronizeAll)),
                &TaskParams::new(user_id),
            )
            .await?;
        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn request_synchronize_located(
        &self,
        user_id: Option<u64>,
        tvshow_id: u64,
        language: Language,
    ) -> anyhow::Result<()> {
        self.task_publisher
            .publish(
                std::iter::once(TaskPayload::TvShow(TvShowTask::SynchronizeLocated {
                    tvshow_id,
                    language,
                })),
                &TaskParams::new(user_id),
            )
            .await?;
        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn synchronize(&self, user_id: Option<u64>, tvshow_id: u64) -> anyhow::Result<()> {
        let params = TaskParams::new(user_id);
        let langs = self.tvshow_repository.list_languages(&[tvshow_id]).await?;
        let tasks = langs.into_iter().map(|(tvshow_id, language)| {
            TaskPayload::TvShow(TvShowTask::SynchronizeLocated {
                tvshow_id,
                language,
            })
        });

        self.task_publisher.publish(tasks, &params).await?;
        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn synchronize_all(&self, user_id: Option<u64>) -> anyhow::Result<()> {
        let params = TaskParams::new(user_id);
        let langs = self.tvshow_repository.list_languages(&[]).await?;
        let tasks = langs.into_iter().map(|(tvshow_id, language)| {
            TaskPayload::TvShow(TvShowTask::SynchronizeLocated {
                tvshow_id,
                language,
            })
        });

        self.task_publisher.publish(tasks, &params).await?;
        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn synchronize_located(
        &self,
        user_id: Option<u64>,
        tvshow_id: u64,
        language: Language,
    ) -> anyhow::Result<()> {
        let tvshow = self
            .tvshow_repository
            .find_by_id(tvshow_id, language)
            .await?;
        let Some(tvshow) = tvshow else {
            tracing::warn!("unable to find tvshow in repository");
            return Ok(());
        };

        let remote = self
            .tvshow_provider
            .find_by_source(tvshow.source, language)
            .await
            .context("couldn't fetch tvshow from provider")?;
        let Some(remote) = remote else {
            tracing::warn!("unable to find tvshow in source");
            return Ok(());
        };

        let updated = self.tvshow_repository.upsert(&remote).await?;

        self.task_publisher
            .publish(
                (1..=remote.number_of_seasons).map(move |season| {
                    TaskPayload::TvShow(TvShowTask::SynchronizeSeasonLocated(
                        SynchronizeSeasonParams {
                            tvshow_id: updated.id,
                            source: tvshow.source,
                            season,
                            language,
                        },
                    ))
                }),
                &TaskParams::new(user_id),
            )
            .await?;

        Ok(())
    }

    #[tracing::instrument(skip(self), err(Debug))]
    async fn synchronize_season_located(
        &self,
        _user_id: Option<u64>,
        params: entity::SynchronizeSeasonParams,
    ) -> anyhow::Result<()> {
        let Some(season) = self
            .tvshow_provider
            .find_season_by_source(params.source, params.language, params.season)
            .await?
        else {
            return Ok(());
        };

        let saved = self
            .tvshow_season_repository
            .upsert(params.tvshow_id, &season)
            .await?;

        self.tvshow_episode_repository
            .upsert(saved.id, &season.episodes)
            .await?;

        Ok(())
    }
}
