use crate::error::GitError;
use base64::{Engine as _, engine::general_purpose};
use db::database::AppDatabase;
use models::projects::MemberRole;
use models::projects::{project, project_history_name, project_members};
use models::repos::{repo, repo_history_name};
use models::users::{user, user_ssh_key};
use sea_orm::sqlx::types::chrono;
use sea_orm::*;
use sha2::{Digest, Sha256};
use slog::{Logger, error, info, warn};

/// SSH authentication service optimized for performance
pub struct SshAuthService {
    db: AppDatabase,
    logger: Logger,
}

impl SshAuthService {
    pub fn new(db: AppDatabase, logger: Logger) -> Self {
        Self { db, logger }
    }

    pub async fn find_repo(
        &self,
        namespace: &str,
        repo_name: &str,
    ) -> Result<repo::Model, GitError> {
        let namespace = self.find_namespace(namespace).await?;
        self.find_repository_by_name_and_project(repo_name, namespace.id)
            .await
    }

    async fn find_namespace(&self, namespace: &str) -> Result<project::Model, GitError> {
        if let Some(project) = project::Entity::find()
            .filter(project::Column::Name.eq(namespace))
            .one(self.db.reader())
            .await
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            return Ok(project);
        }

        if let Some(history) = project_history_name::Entity::find()
            .filter(project_history_name::Column::HistoryName.eq(namespace))
            .one(self.db.reader())
            .await
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            if let Some(project) = project::Entity::find()
                .filter(project::Column::Id.eq(history.project_uid))
                .one(self.db.reader())
                .await
                .map_err(|e| GitError::Internal(e.to_string()))?
            {
                return Ok(project);
            }
        }

        Err(GitError::NotFound("Project not found".to_string()))
    }

    async fn find_repository_by_name_and_project(
        &self,
        repo_name: &str,
        project_id: uuid::Uuid,
    ) -> Result<repo::Model, GitError> {
        if let Some(repo) = repo::Entity::find()
            .filter(repo::Column::RepoName.eq(repo_name))
            .filter(repo::Column::Project.eq(project_id))
            .one(self.db.reader())
            .await
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            return Ok(repo);
        }

        if let Some(history) = repo_history_name::Entity::find()
            .filter(repo_history_name::Column::Name.eq(repo_name))
            .filter(repo_history_name::Column::Project.eq(project_id))
            .one(self.db.reader())
            .await
            .map_err(|e| GitError::Internal(e.to_string()))?
        {
            if let Some(repo) = repo::Entity::find()
                .filter(repo::Column::Id.eq(history.repo))
                .filter(repo::Column::Project.eq(project_id))
                .one(self.db.reader())
                .await
                .map_err(|e| GitError::Internal(e.to_string()))?
            {
                return Ok(repo);
            }
        }

        Err(GitError::NotFound("Repository not found".to_string()))
    }

    pub async fn find_user_by_public_key(
        &self,
        public_key_str: &str,
    ) -> Result<Option<user::Model>, DbErr> {
        let fingerprint = match self.generate_fingerprint_from_public_key(public_key_str) {
            Ok(fp) => fp,
            Err(e) => {
                error!(self.logger, "{}", format!("Failed to generate fingerprint error={}", e));
                return Ok(None);
            }
        };

        let fingerprint_preview = if fingerprint.len() > 16 {
            format!("{}...", &fingerprint[..16])
        } else {
            fingerprint.clone()
        };
        info!(self.logger, "{}", format!("Looking up user with SSH key fingerprint={}", fingerprint_preview));

        let ssh_key = user_ssh_key::Entity::find()
            .filter(user_ssh_key::Column::Fingerprint.eq(&fingerprint))
            .filter(user_ssh_key::Column::IsRevoked.eq(false))
            .one(self.db.reader())
            .await?;

        let ssh_key = match ssh_key {
            Some(key) => key,
            None => {
                warn!(self.logger, "{}", format!("No SSH key found fingerprint={}", fingerprint));
                return Ok(None);
            }
        };

        if self.is_key_expired(&ssh_key) {
            warn!(self.logger, "{}", format!("SSH key expired key_id={} expires_at={:?}", ssh_key.id, ssh_key.expires_at));
            return Ok(None);
        }

        let user_model = user::Entity::find()
            .filter(user::Column::Uid.eq(ssh_key.user))
            .one(self.db.reader())
            .await?;

        if let Some(ref user) = user_model {
            info!(self.logger, "{}", format!("User authenticated user={} key={}", user.username, ssh_key.title));
            self.update_key_last_used_async(ssh_key.id);
        }

        Ok(user_model)
    }

    fn is_key_expired(&self, ssh_key: &user_ssh_key::Model) -> bool {
        if let Some(expires_at) = ssh_key.expires_at {
            let now = chrono::Utc::now();
            now >= expires_at
        } else {
            false
        }
    }

    fn update_key_last_used_async(&self, key_id: i64) {
        let db_clone = self.db.clone();
        let logger = self.logger.clone();
        tokio::spawn(async move {
            if let Err(e) = Self::update_key_last_used_sync(db_clone, &logger, key_id).await {
                warn!(&logger, "{}", format!("Failed to update key last_used key_id={} error={}", key_id, e));
            }
        });
    }

    async fn update_key_last_used_sync(
        db: AppDatabase,
        logger: &Logger,
        key_id: i64,
    ) -> Result<(), DbErr> {
        let key = user_ssh_key::Entity::find_by_id(key_id)
            .one(db.reader())
            .await?;

        if let Some(key) = key {
            let now = chrono::Utc::now();
            let mut active_key: user_ssh_key::ActiveModel = key.into();
            active_key.last_used_at = Set(Some(now));
            active_key.updated_at = Set(now);

            active_key.update(db.writer()).await?;
            info!(logger, "{}", format!("Updated key last_used key_id={}", key_id));
        }

        Ok(())
    }

    pub async fn check_repo_permission(
        &self,
        user: &user::Model,
        repo: &repo::Model,
        is_write: bool,
    ) -> bool {
        if repo.created_by == user.uid {
            info!(self.logger, "{}", format!("User is repo owner user={} repo={}", user.username, repo.repo_name));
            return true;
        }

        if !is_write && !repo.is_private {
            info!(self.logger, "{}", format!("Public repo allows read repo={}", repo.repo_name));
            return true;
        }

        if self
            .check_collaborator_permission(user, repo, is_write)
            .await
            .unwrap_or(false)
        {
            info!(self.logger, "{}", format!("User has collaborator access user={} repo={}", user.username, repo.repo_name));
            return true;
        }

        let project_id = repo.project;
        if self
            .check_project_member_permission(user, project_id, is_write)
            .await
            .unwrap_or(false)
        {
            info!(self.logger, "{}", format!("User has project member access user={} repo={}", user.username, repo.repo_name));
            return true;
        }

        warn!(self.logger, "{}", format!("Access denied user={} repo={} write={}", user.username, repo.repo_name, is_write));
        false
    }

    async fn check_collaborator_permission(
        &self,
        user: &user::Model,
        repo: &repo::Model,
        is_write: bool,
    ) -> Result<bool, DbErr> {
        use models::repos::repo_collaborator;

        let collaborator = repo_collaborator::Entity::find()
            .filter(repo_collaborator::Column::Repo.eq(repo.id))
            .filter(repo_collaborator::Column::User.eq(user.uid))
            .one(self.db.reader())
            .await?;

        if let Some(collab) = collaborator {
            let roles: Vec<&str> = collab.scope.split_whitespace().collect();
            if roles.contains(&"admin") || roles.contains(&"write") {
                return Ok(true);
            }

            if roles.contains(&"read") && !is_write {
                return Ok(true);
            }

            warn!(self.logger, "{}", format!("Collaborator has no valid roles scope={}", collab.scope));
            Ok(false)
        } else {
            Ok(false)
        }
    }

    async fn check_project_member_permission(
        &self,
        user: &user::Model,
        project_id: uuid::Uuid,
        is_write: bool,
    ) -> Result<bool, DbErr> {
        let member = project_members::Entity::find()
            .filter(project_members::Column::Project.eq(project_id))
            .filter(project_members::Column::User.eq(user.uid))
            .one(self.db.reader())
            .await?;

        if let Some(member) = member {
            match member.scope_role() {
                Ok(MemberRole::Admin) | Ok(MemberRole::Owner) => Ok(true),
                Ok(MemberRole::Member) => Ok(!is_write),
                Err(_) => Ok(false),
            }
        } else {
            Ok(false)
        }
    }

    fn generate_fingerprint_from_public_key(&self, public_key_str: &str) -> Result<String, String> {
        // Performance: avoid allocating Vec, use split_once for efficiency
        let key_data_base64 = public_key_str
            .split_whitespace()
            .nth(1)
            .ok_or("Invalid SSH key format")?;

        let key_data = general_purpose::STANDARD
            .decode(key_data_base64)
            .map_err(|e| format!("Base64 decode error: {}", e))?;

        // Performance: SHA256 is already optimized, compute hash directly
        let mut hasher = Sha256::new();
        hasher.update(&key_data);
        let hash = hasher.finalize();

        // Performance: pre-allocate string capacity to avoid reallocation
        let mut fingerprint = String::with_capacity(51); // "SHA256:" (7) + base64 (44)
        fingerprint.push_str("SHA256:");
        fingerprint.push_str(&general_purpose::STANDARD_NO_PAD.encode(&hash));

        Ok(fingerprint)
    }
}
