use std::{path::PathBuf, sync::Arc};

use cache::AppCache;
use dashmap::DashMap;
use db::database::AppDatabase;
use model::repos::RepoModel;
use sqlx::query_as;
use uuid::Uuid;

use crate::bare::GitBare;

const STORAGE_PATH_CACHE_KEY_PREFIX: &str = "git:rpc:repo:storage_path";

#[derive(Clone)]
pub struct RepoRegistry {
    repos: DashMap<Uuid, GitBare>,
    db: AppDatabase,
    cache: AppCache,
}

impl RepoRegistry {
    pub fn new(db: AppDatabase, cache: AppCache) -> Self {
        Self {
            repos: DashMap::new(),
            db,
            cache,
        }
    }

    pub fn register(&self, repo_id: Uuid, bare_dir: PathBuf) {
        let bare = GitBare { bare_dir };
        self.repos.insert(repo_id, bare);
    }

    pub fn unregister(&self, repo_id: &Uuid) {
        self.repos.remove(repo_id);
    }

    pub async fn get(
        &self,
        repo_id_str: &str,
    ) -> Result<GitBare, tonic::Status> {
        let repo_id = repo_id_str.parse::<Uuid>().map_err(|e| {
            tonic::Status::invalid_argument(format!(
                "invalid repo_id UUID: {e}"
            ))
        })?;

        if let Some(bare) = self.repos.get(&repo_id) {
            return Ok(bare.value().clone());
        }

        let storage_path = self.lookup_storage_path(repo_id).await?;
        let bare = GitBare {
            bare_dir: PathBuf::from(storage_path),
        };
        self.repos.insert(repo_id, bare.clone());
        Ok(bare)
    }

    async fn lookup_storage_path(
        &self,
        repo_id: Uuid,
    ) -> Result<String, tonic::Status> {
        let cache_key = format!("{STORAGE_PATH_CACHE_KEY_PREFIX}:{repo_id}");

        if let Ok(Some(path)) = self.cache.get::<String>(&cache_key).await {
            return Ok(path);
        }

        let model: RepoModel = query_as(
            "SELECT id, wk, name, description, default_branch, visibility, size_bytes, is_archived, is_template, is_mirror, created_by, storage_path, created_at, updated_at, deleted_at FROM repo WHERE id = $1 AND deleted_at IS NULL",
        )
            .bind(repo_id)
            .fetch_one(self.db.reader())
            .await
            .map_err(|e| tonic::Status::internal(format!("database error: {e}")))?;

        let path = model.storage_path;
        let _ = self.cache.set(&cache_key, &path).await;

        Ok(path)
    }

    pub fn list(&self) -> Vec<Uuid> {
        self.repos.iter().map(|r| *r.key()).collect()
    }
}

pub fn shared_registry(db: AppDatabase, cache: AppCache) -> Arc<RepoRegistry> {
    Arc::new(RepoRegistry::new(db, cache))
}
