use std::sync::Arc;

use cache::AppCache;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};

use crate::rpc::{
    error::{spawn_blocking_error, to_status},
    proto as p,
    registry::RepoRegistry,
};

pub struct DiffServiceImpl {
    pub registry: Arc<RepoRegistry>,
    pub cache: AppCache,
}

type DiffStream = ReceiverStream<Result<p::DiffDelta, Status>>;

#[tonic::async_trait]
impl p::diff_service_server::DiffService for DiffServiceImpl {
    async fn diff_stats(
        &self,
        req: Request<p::DiffStatsRequest>,
    ) -> Result<Response<p::DiffStatsResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let old_str =
            inner.old_oid.clone().map(|o| o.value).unwrap_or_default();
        let new_str =
            inner.new_oid.clone().map(|o| o.value).unwrap_or_default();
        let cache_key = format!(
            "git:rpc:cache:diff:stats:{}:{}:{}",
            repo_id, old_str, new_str
        );

        if let Ok(Some(cached)) =
            self.cache.get::<p::DiffStatsResponse>(&cache_key).await
        {
            return Ok(Response::new(cached));
        }

        let bare = self.registry.get(&repo_id).await?;
        let old: crate::cmd::oid::ObjectId =
            inner.old_oid.unwrap_or_default().into();
        let new: crate::cmd::oid::ObjectId =
            inner.new_oid.unwrap_or_default().into();
        let opts = inner.options.map(Into::into);
        let stats = tokio::task::spawn_blocking(move || {
            bare.diff_stats(old, new, opts)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        let result = crate::cmd::diff::DiffResult {
            stats,
            deltas: vec![],
        };
        let resp = p::DiffStatsResponse {
            result: Some(result.into()),
        };
        let _ = self.cache.set(&cache_key, &resp).await;
        Ok(Response::new(resp))
    }

    async fn diff_patch(
        &self,
        req: Request<p::DiffPatchRequest>,
    ) -> Result<Response<p::DiffPatchResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let old_str =
            inner.old_oid.clone().map(|o| o.value).unwrap_or_default();
        let new_str =
            inner.new_oid.clone().map(|o| o.value).unwrap_or_default();
        let cache_key = format!(
            "git:rpc:cache:diff:patch:{}:{}:{}",
            repo_id, old_str, new_str
        );

        if let Ok(Some(cached)) =
            self.cache.get::<p::DiffPatchResponse>(&cache_key).await
        {
            return Ok(Response::new(cached));
        }

        let bare = self.registry.get(&repo_id).await?;
        let old: crate::cmd::oid::ObjectId =
            inner.old_oid.unwrap_or_default().into();
        let new: crate::cmd::oid::ObjectId =
            inner.new_oid.unwrap_or_default().into();
        let opts = inner.options.map(Into::into);
        let result = tokio::task::spawn_blocking(move || {
            bare.diff_patch(old, new, opts)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        let resp = p::DiffPatchResponse {
            result: Some(result.into()),
        };
        let _ = self.cache.set(&cache_key, &resp).await;
        Ok(Response::new(resp))
    }

    type DiffStreamStream = DiffStream;

    async fn diff_stream(
        &self,
        req: Request<p::DiffPatchRequest>,
    ) -> Result<Response<DiffStream>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let old: crate::cmd::oid::ObjectId =
            inner.old_oid.unwrap_or_default().into();
        let new: crate::cmd::oid::ObjectId =
            inner.new_oid.unwrap_or_default().into();
        let opts = inner.options.map(Into::into);
        let (tx, rx) = tokio::sync::mpsc::channel(128);
        tokio::task::spawn_blocking(move || {
            let result = bare.diff_patch(old, new, opts);
            match result {
                Ok(diff_result) => {
                    for delta in diff_result.deltas {
                        if tx.blocking_send(Ok(delta.into())).is_err() {
                            break;
                        }
                    }
                }
                Err(e) => {
                    let _ = tx.blocking_send(Err(to_status(e)));
                }
            }
        });
        Ok(Response::new(ReceiverStream::new(rx)))
    }

    async fn diff_patch_side_by_side(
        &self,
        req: Request<p::DiffPatchSideBySideRequest>,
    ) -> Result<Response<p::DiffPatchSideBySideResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let old: crate::cmd::oid::ObjectId =
            inner.old_oid.unwrap_or_default().into();
        let new: crate::cmd::oid::ObjectId =
            inner.new_oid.unwrap_or_default().into();
        let opts = inner.options.map(Into::into);
        let result = tokio::task::spawn_blocking(move || {
            bare.diff_patch_side_by_side(old, new, opts)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        Ok(Response::new(p::DiffPatchSideBySideResponse {
            result: Some(result.into()),
        }))
    }

    async fn diff_tree_to_tree(
        &self,
        req: Request<p::DiffTreeToTreeRequest>,
    ) -> Result<Response<p::DiffTreeToTreeResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let old_tree: crate::cmd::oid::ObjectId =
            inner.old_tree.unwrap_or_default().into();
        let new_tree: crate::cmd::oid::ObjectId =
            inner.new_tree.unwrap_or_default().into();
        let options = inner.options.map(Into::into);
        let result = tokio::task::spawn_blocking(move || {
            bare.diff_tree_to_tree(old_tree, new_tree, options)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        Ok(Response::new(p::DiffTreeToTreeResponse {
            result: Some(result.into()),
        }))
    }

    async fn diff_index_to_tree(
        &self,
        req: Request<p::DiffIndexToTreeRequest>,
    ) -> Result<Response<p::DiffIndexToTreeResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let tree_oid: crate::cmd::oid::ObjectId =
            inner.tree_oid.unwrap_or_default().into();
        let options = inner.options.map(Into::into);
        let result = tokio::task::spawn_blocking(move || {
            bare.diff_index_to_tree(tree_oid, options)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        Ok(Response::new(p::DiffIndexToTreeResponse {
            result: Some(result.into()),
        }))
    }
}
