use std::sync::Arc;

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

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

pub struct CommitServiceImpl {
    pub registry: Arc<RepoRegistry>,
    pub cache: AppCache,
    pub sync: ReceiveSyncService,
}

type CommitHistoryStream = ReceiverStream<Result<p::CommitMeta, Status>>;

#[tonic::async_trait]
impl p::commit_service_server::CommitService for CommitServiceImpl {
    async fn commit_info(
        &self,
        req: Request<p::CommitInfoRequest>,
    ) -> Result<Response<p::CommitInfoResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let oid = inner.oid.unwrap_or_default().into();
        let result = tokio::task::spawn_blocking(move || bare.commit_info(oid))
            .await
            .map_err(spawn_blocking_error)?
            .map_err(to_status)?;
        Ok(Response::new(p::CommitInfoResponse {
            commit: Some(result.into()),
        }))
    }

    async fn commit_history(
        &self,
        req: Request<p::CommitHistoryRequest>,
    ) -> Result<Response<p::CommitHistoryResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let cache_key = format!(
            "git:rpc:cache:commit:history:{}:{}:{}:{}:{}",
            repo_id,
            inner.limit,
            inner.skip,
            inner.sort,
            inner.branch.as_deref().unwrap_or("")
        );

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

        let bare = self.registry.get(&repo_id).await?;
        let params = crate::cmd::commit::CommitWalkParams {
            start_oids: vec![],
            hide_oids: vec![],
            limit: if inner.limit > 0 {
                Some(inner.limit as usize)
            } else {
                None
            },
            skip: inner.skip as usize,
            first_parent: false,
            sort: inner.sort.into(),
            branch: inner.branch.clone(),
        };
        let result =
            tokio::task::spawn_blocking(move || bare.commit_history(params))
                .await
                .map_err(spawn_blocking_error)?
                .map_err(to_status)?;
        let resp = p::CommitHistoryResponse {
            commits: result.into_iter().map(Into::into).collect(),
        };
        let _ = self.cache.set(&cache_key, &resp).await;
        Ok(Response::new(resp))
    }

    type CommitHistoryStreamStream = CommitHistoryStream;

    async fn commit_history_stream(
        &self,
        req: Request<p::CommitHistoryRequest>,
    ) -> Result<Response<CommitHistoryStream>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let params = crate::cmd::commit::CommitWalkParams {
            start_oids: vec![],
            hide_oids: vec![],
            limit: if inner.limit > 0 {
                Some(inner.limit as usize)
            } else {
                None
            },
            skip: inner.skip as usize,
            first_parent: false,
            sort: inner.sort.into(),
            branch: inner.branch.clone(),
        };
        let (tx, rx) = tokio::sync::mpsc::channel(128);
        tokio::task::spawn_blocking(move || {
            let result = bare.commit_history(params);
            match result {
                Ok(commits) => {
                    for c in commits {
                        if tx.blocking_send(Ok(c.into())).is_err() {
                            break;
                        }
                    }
                }
                Err(e) => {
                    let _ = tx.blocking_send(Err(to_status(e)));
                }
            }
        });
        Ok(Response::new(ReceiverStream::new(rx)))
    }

    async fn commit_summary(
        &self,
        req: Request<p::CommitSummaryRequest>,
    ) -> Result<Response<p::CommitSummaryResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let cache_key = format!("git:rpc:cache:commit:summary:{}", repo_id);

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

        let bare = self.registry.get(&repo_id).await?;
        let result = tokio::task::spawn_blocking(move || bare.commit_summary())
            .await
            .map_err(spawn_blocking_error)?
            .map_err(to_status)?;
        let resp = p::CommitSummaryResponse {
            summary: Some(result.into()),
        };
        let _ = self.cache.set(&cache_key, &resp).await;
        Ok(Response::new(resp))
    }

    async fn commit_walk(
        &self,
        req: Request<p::CommitWalkRequest>,
    ) -> Result<Response<p::CommitWalkResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let params = inner.params.unwrap_or_default().into();
        let result =
            tokio::task::spawn_blocking(move || bare.commit_walk(params))
                .await
                .map_err(spawn_blocking_error)?
                .map_err(to_status)?;
        Ok(Response::new(p::CommitWalkResponse {
            commits: result.into_iter().map(Into::into).collect(),
        }))
    }

    async fn commit_refs(
        &self,
        req: Request<p::CommitRefsRequest>,
    ) -> Result<Response<p::CommitRefsResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let result = tokio::task::spawn_blocking(move || {
            let repo = bare.gix_repo()?;
            let head_id = repo.head_id()?.detach();
            let oid =
                crate::cmd::oid::ObjectId::new(head_id.to_hex().to_string());
            bare.commit_refs(oid)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        Ok(Response::new(p::CommitRefsResponse {
            refs: result.into_iter().map(Into::into).collect(),
        }))
    }

    async fn commit_prefix(
        &self,
        req: Request<p::CommitPrefixRequest>,
    ) -> Result<Response<p::CommitPrefixResponse>, Status> {
        let inner = req.into_inner();
        let bare = self.registry.get(&inner.repo_id).await?;
        let prefix = inner.prefix.clone();
        let result = tokio::task::spawn_blocking(move || {
            bare.commit_oid_from_prefix(&prefix)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;
        Ok(Response::new(p::CommitPrefixResponse {
            oid: Some(result.into()),
        }))
    }

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

    async fn cherry_pick(
        &self,
        req: Request<p::CherryPickRequest>,
    ) -> Result<Response<p::CherryPickResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let bare = self.registry.get(&inner.repo_id).await?;
        let params = inner.params.unwrap_or_default().into();
        let result =
            tokio::task::spawn_blocking(move || bare.commit_pick(params))
                .await
                .map_err(spawn_blocking_error)?
                .map_err(to_status)?;

        if let Ok(repo_uid) = Uuid::parse_str(&repo_id) {
            self.sync
                .send(crate::sync::RepoReceiveSyncTask { repo_uid })
                .await;
        }

        Ok(Response::new(p::CherryPickResponse {
            oid: Some(result.into()),
        }))
    }

    async fn cherry_pick_sequence(
        &self,
        req: Request<p::CherryPickSequenceRequest>,
    ) -> Result<Response<p::CherryPickSequenceResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let bare = self.registry.get(&inner.repo_id).await?;
        let params = inner.params.unwrap_or_default().into();
        let result = tokio::task::spawn_blocking(move || {
            bare.commit_cherry_pick_sequence(params)
        })
        .await
        .map_err(spawn_blocking_error)?
        .map_err(to_status)?;

        if let Ok(repo_uid) = Uuid::parse_str(&repo_id) {
            self.sync
                .send(crate::sync::RepoReceiveSyncTask { repo_uid })
                .await;
        }

        Ok(Response::new(p::CherryPickSequenceResponse {
            oid: Some(result.into()),
        }))
    }

    async fn create_commit(
        &self,
        req: Request<p::CreateCommitRequest>,
    ) -> Result<Response<p::CreateCommitResponse>, Status> {
        let inner = req.into_inner();
        let repo_id = inner.repo_id.clone();
        let bare = self.registry.get(&inner.repo_id).await?;
        let params = crate::cmd::commit::CreateCommitParams {
            branch: inner.branch.clone(),
            message: inner.message.clone(),
            author_name: inner.author_name.clone(),
            author_email: inner.author_email.clone(),
            committer_name: inner.committer_name.clone(),
            committer_email: inner.committer_email.clone(),
            files: inner
                .files
                .into_iter()
                .map(|f| crate::cmd::commit::FileChange {
                    path: f.path,
                    content: f.content,
                })
                .collect(),
        };
        let result =
            tokio::task::spawn_blocking(move || bare.commit_create(params))
                .await
                .map_err(spawn_blocking_error)?
                .map_err(to_status)?;

        // Trigger sync after write
        if let Ok(repo_uid) = Uuid::parse_str(&repo_id) {
            self.sync
                .send(crate::sync::RepoReceiveSyncTask { repo_uid })
                .await;
        }

        Ok(Response::new(p::CreateCommitResponse {
            oid: Some(result.into()),
        }))
    }
}
