use db::sqlx;
use git::rpc::{proto as p, proto::blob_service_client::BlobServiceClient};
use session::Session;

use crate::{AppService, error::AppError, git::rpc_err};

#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
pub struct ContentResponse {
    pub path: String,
    pub name: String,
    #[serde(rename = "type")]
    pub content_type: String,
    pub size: i64,
    pub encoding: Option<String>,
    pub content: Option<String>,
}

#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct CreateContent {
    pub message: String,
    pub content: String,
    pub branch: Option<String>,
}

#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
pub struct UpdateContent {
    pub message: String,
    pub content: String,
    pub sha: String,
    pub branch: Option<String>,
}

impl AppService {
    pub async fn git_contents_get_by_name(
        &self,
        ctx: &Session,
        wk: &str,
        repo: &str,
        path: &str,
        ref_name: Option<&str>,
    ) -> Result<ContentResponse, AppError> {
        let _ = self.git_require_member(ctx, wk, repo).await?;
        self.git_contents_get(ctx, wk, repo, path, ref_name).await
    }
    pub async fn git_contents_create_by_name(
        &self,
        ctx: &Session,
        wk: &str,
        repo: &str,
        path: &str,
        params: CreateContent,
    ) -> Result<ContentResponse, AppError> {
        let _ = self.git_require_member(ctx, wk, repo).await?;
        self.git_contents_create(ctx, wk, repo, path, params).await
    }
    pub async fn git_contents_update_by_name(
        &self,
        ctx: &Session,
        wk: &str,
        repo: &str,
        path: &str,
        params: UpdateContent,
    ) -> Result<ContentResponse, AppError> {
        let _ = self.git_require_member(ctx, wk, repo).await?;
        self.git_contents_update(ctx, wk, repo, path, params).await
    }
    pub async fn git_contents_delete_by_name(
        &self,
        ctx: &Session,
        wk: &str,
        repo: &str,
        path: &str,
        msg: &str,
        sha: &str,
        branch: Option<&str>,
    ) -> Result<(), AppError> {
        let _ = self.git_require_member(ctx, wk, repo).await?;
        self.git_contents_delete(ctx, wk, repo, path, msg, sha, branch)
            .await
    }
}

impl AppService {
    pub async fn git_contents_get(
        &self,
        ctx: &Session,
        wk_name: &str,
        repo_name: &str,
        path: &str,
        _ref_name: Option<&str>,
    ) -> Result<ContentResponse, AppError> {
        let repo = self.git_require_member(ctx, wk_name, repo_name).await?;
        let mut blob_client = BlobServiceClient::new(self.git.clone());

        let empty_oid = p::ObjectId {
            value: String::new(),
        };

        let resp = blob_client
            .blob_load(tonic::Request::new(p::BlobLoadRequest {
                repo_id: repo.id.to_string(),
                id: Some(empty_oid.clone()),
                path: path.to_string(),
            }))
            .await
            .map_err(rpc_err)?
            .into_inner();

        let is_binary = blob_client
            .blob_is_binary(tonic::Request::new(p::BlobIsBinaryRequest {
                repo_id: repo.id.to_string(),
                id: Some(empty_oid.clone()),
            }))
            .await
            .map(|r| r.into_inner().is_binary)
            .unwrap_or(false);

        let size_resp = blob_client
            .blob_size(tonic::Request::new(p::BlobSizeRequest {
                repo_id: repo.id.to_string(),
                id: Some(empty_oid),
                path: String::new(),
            }))
            .await
            .map_err(rpc_err)?
            .into_inner();

        let blob_data = resp.blob;
        let size = size_resp.size as i64;

        let (encoding, content) = if is_binary {
            (Some("base64".to_string()), Some(base64_encode(&blob_data)))
        } else {
            (None, Some(String::from_utf8_lossy(&blob_data).to_string()))
        };

        Ok(ContentResponse {
            path: path.to_string(),
            name: path.rsplit('/').next().unwrap_or(path).to_string(),
            content_type: "file".to_string(),
            size,
            encoding,
            content,
        })
    }

    pub async fn git_contents_create(
        &self,
        ctx: &Session,
        wk_name: &str,
        repo_name: &str,
        path: &str,
        params: CreateContent,
    ) -> Result<ContentResponse, AppError> {
        let repo = self.git_require_member(ctx, wk_name, repo_name).await?;
        let user = crate::session_user(ctx)?;

        let user_model = sqlx::query_as::<_, model::users::UserModel>(
            "SELECT id, username, display_name, avatar_url, website_url, allow_use, can_search, \
             last_sign_in_at, created_at, updated_at FROM \"user\" WHERE id = $1",
        )
        .bind(user)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::UserNotFound)?;

        let display = user_model.display_name.clone();
        let username = user_model.username.clone();
        let author_name = if display.is_empty() {
            username.clone()
        } else {
            display
        };

        let file_size = params.content.len() as i64;
        let content_bytes = params.content.clone().into_bytes();
        let mut client = p::commit_service_client::CommitServiceClient::new(
            self.git.clone(),
        );
        let resp = client
            .create_commit(tonic::Request::new(p::CreateCommitRequest {
                repo_id: repo.id.to_string(),
                branch: params
                    .branch
                    .unwrap_or_else(|| repo.default_branch.clone()),
                message: params.message,
                author_name: author_name.clone(),
                author_email: format!("{username}@gitdata.ai"),
                committer_name: "redpanda".to_string(),
                committer_email: "redpanda@gitdata.ai".to_string(),
                files: vec![p::FileChange {
                    path: path.to_string(),
                    content: content_bytes,
                }],
            }))
            .await
            .map_err(rpc_err)?
            .into_inner();

        let _oid = resp.oid.map(|o| o.value).unwrap_or_default();
        Ok(ContentResponse {
            path: path.to_string(),
            name: path.rsplit('/').next().unwrap_or(path).to_string(),
            content_type: "file".to_string(),
            size: file_size,
            encoding: None,
            content: Some(params.content),
        })
    }

    pub async fn git_contents_update(
        &self,
        _ctx: &Session,
        _wk: &str,
        _repo: &str,
        _path: &str,
        _params: UpdateContent,
    ) -> Result<ContentResponse, AppError> {
        Err(AppError::InternalServerError(
            "contents update not yet implemented".to_string(),
        ))
    }

    pub async fn git_contents_delete(
        &self,
        _ctx: &Session,
        _wk: &str,
        _repo: &str,
        _path: &str,
        _message: &str,
        _sha: &str,
        _branch: Option<&str>,
    ) -> Result<(), AppError> {
        Err(AppError::InternalServerError(
            "contents delete not yet implemented".to_string(),
        ))
    }
}

fn base64_encode(data: &[u8]) -> String {
    use base64::Engine as _;
    base64::engine::general_purpose::STANDARD.encode(data)
}
