use crate::error::GitError;
use crate::hook::pool::types::{HookTask, TaskType};
use anyhow::Context;
use base64::Engine;
use config::AppConfig;
use db::cache::AppCache;
use db::database::AppDatabase;
use deadpool_redis::cluster::Pool as RedisPool;
use models::users::{user, user_token};
use russh::keys::PrivateKey;
use russh::server::Server;
use russh::{MethodKind, MethodSet, SshId, server::Config};
use sea_orm::prelude::*;
use sha2::{Digest, Sha256};
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;

pub mod authz;
pub mod handle;
pub mod server;

#[derive(Clone)]
pub struct SSHHandle {
    pub db: AppDatabase,
    pub app: AppConfig,
    pub cache: AppCache,
    pub redis_pool: RedisPool,
}

impl SSHHandle {
    pub async fn run(&self) {
        let this = self.clone();
        tokio::spawn(async move {
            if let Err(e) = this.run_ssh().await {
                tracing::error!("SSH server error: {}", e);
            }
        });
    }
    pub fn new(
        db: AppDatabase,
        app: AppConfig,
        cache: AppCache,
        redis_pool: RedisPool,
    ) -> Self {
        SSHHandle {
            db,
            app,
            cache,
            redis_pool,
        }
    }
    pub async fn run_ssh(&self) -> anyhow::Result<()> {
        tracing::info!("SSH server starting");
        let private_key_content = self.app.ssh_server_private_key()?;
        if private_key_content.is_empty() {
            return Err(anyhow::anyhow!("SSH server private key is not configured"));
        }

        let preview = if private_key_content.len() > 100 {
            format!("{}...", &private_key_content[..100])
        } else {
            private_key_content.clone()
        };
        tracing::info!(
            "Loading SSH private key (hex, {} bytes)",
            private_key_content.len()
        );

        let private_key_bytes = hex::decode(&private_key_content).with_context(|| {
            format!(
                "Failed to decode hex-encoded SSH private key. Preview: {}",
                preview
            )
        })?;

        tracing::info!(
            "Hex decoded to {} bytes",
            private_key_bytes.len()
        );

        let private_key_pem = std::str::from_utf8(&private_key_bytes)
            .with_context(|| "Decoded SSH private key is not valid UTF-8")?;

        if let Some(first_line) = private_key_pem.lines().next() {
            tracing::info!("PEM format starts with: {}", first_line);
        }

        tracing::info!(
            "Complete private key content:\n{}",
            private_key_pem
        );

        let private_key = {
            match ssh_key::PrivateKey::from_openssh(private_key_pem) {
                Ok(ssh_key) => {
                    tracing::info!("Successfully parsed with ssh-key crate");
                    let openssh_pem = ssh_key
                        .to_openssh(ssh_key::LineEnding::LF)
                        .with_context(|| "Failed to serialize to OpenSSH format")?;

                    PrivateKey::from_str(&openssh_pem)
                        .with_context(|| "Failed to parse with russh after ssh-key conversion")?
                }
                Err(e) => {
                    tracing::info!(
                        "ssh-key from_openssh failed: {}, trying direct russh parse", e
                    );
                    PrivateKey::from_str(private_key_pem).with_context(|| {
                        format!("Failed to parse SSH private key with both methods")
                    })?
                }
            }
        };
        tracing::info!("SSH private key loaded");
        let mut config = Config::default();
        config.keys = vec![private_key];
        let version = format!("SSH-2.0-GitdataAI {}", env!("CARGO_PKG_VERSION"));
        config.server_id = SshId::Standard(version);
        let mut method = MethodSet::empty();
        method.push(MethodKind::PublicKey);
        method.push(MethodKind::Password);
        config.methods = method;
        config.inactivity_timeout = Some(Duration::from_secs(300));
        config.keepalive_interval = Some(Duration::from_secs(60));
        config.keepalive_max = 3;

        tracing::info!(
            "SSH server configured with methods: {:?}", config.methods
        );
        let token_service = SshTokenService::new(self.db.clone());
        let mut server = server::SSHServer::new(
            self.db.clone(),
            self.cache.clone(),
            self.redis_pool.clone(),
            token_service,
        );

        // Start the rate limiter cleanup background task so the HashMap
        // doesn't grow unbounded over time.
        let ssh_port = self.app.ssh_port()?;
        let bind_addr = format!("0.0.0.0:{}", ssh_port);
        let public_host = self.app.ssh_domain()?;
        let msg = if ssh_port == 22 {
            format!(
                "SSH server listening on port 22. Please use port {} for SSH connections.",
                ssh_port
            )
        } else {
            format!(
                "SSH server listening on port {} (public: {}). Please use port {} for SSH connections.",
                ssh_port, public_host, ssh_port
            )
        };
        tracing::info!("{}", msg);
        server.run_on_address(Arc::new(config), bind_addr).await?;
        Ok(())
    }
}

/// Enqueues a sync task to the Redis-backed hook queue.
/// The background worker picks it up and processes it with per-repo locking.
#[derive(Clone)]
pub struct ReceiveSyncService {
    pool: deadpool_redis::cluster::Pool,
    redis_prefix: String,
}

impl ReceiveSyncService {
    pub fn new(pool: deadpool_redis::cluster::Pool) -> Self {
        Self {
            pool,
            redis_prefix: "{hook}".to_string(),
        }
    }

    /// Enqueue a sync task. Fire-and-forget — logs errors but does not block.
    pub async fn send(&self, task: RepoReceiveSyncTask) {
        let hook_task = HookTask {
            id: uuid::Uuid::new_v4().to_string(),
            repo_id: task.repo_uid.to_string(),
            task_type: TaskType::Sync,
            payload: serde_json::Value::Null,
            created_at: chrono::Utc::now(),
            retry_count: 0,
        };

        let task_json = match serde_json::to_string(&hook_task) {
            Ok(j) => j,
            Err(e) => {
                tracing::error!("failed to serialize hook task: {}", e);
                return;
            }
        };

        let queue_key = format!("{}:sync", self.redis_prefix);

        let redis = match self.pool.get().await {
            Ok(c) => c,
            Err(e) => {
                tracing::error!("failed to get Redis connection: {}", e);
                return;
            }
        };

        let mut conn: deadpool_redis::cluster::Connection = redis;
        if let Err(e) = redis::cmd("LPUSH")
            .arg(&queue_key)
            .arg(&task_json)
            .query_async::<()>(&mut conn)
            .await
        {
            tracing::error!("failed to enqueue sync task repo_id={} error={}",
                task.repo_uid, e);
        }
    }
}

#[derive(Clone)]
pub struct RepoReceiveSyncTask {
    pub repo_uid: uuid::Uuid,
}

/// SSH token authentication service.
/// Uses the same token hash algorithm as user access keys (SHA256 + base64).
#[derive(Clone)]
pub struct SshTokenService {
    db: AppDatabase,
}

impl SshTokenService {
    pub fn new(db: AppDatabase) -> Self {
        Self { db }
    }

    fn hash_token(token: &str) -> String {
        let mut hasher = Sha256::new();
        hasher.update(token.as_bytes());
        base64::prelude::BASE64_STANDARD.encode(hasher.finalize())
    }

    pub async fn find_user_by_token(&self, token: &str) -> Result<Option<user::Model>, GitError> {
        let token_hash = Self::hash_token(token);

        let token_model = user_token::Entity::find()
            .filter(user_token::Column::TokenHash.eq(&token_hash))
            .filter(user_token::Column::IsRevoked.eq(false))
            .one(self.db.reader())
            .await
            .map_err(|e| GitError::Internal(e.to_string()))?;

        let token_model = match token_model {
            Some(t) => t,
            None => return Ok(None),
        };

        // Check expiry
        if let Some(expires_at) = token_model.expires_at {
            if expires_at < chrono::Utc::now() {
                return Ok(None);
            }
        }

        let user_model = user::Entity::find()
            .filter(user::Column::Uid.eq(token_model.user))
            .one(self.db.reader())
            .await
            .map_err(|e| GitError::Internal(e.to_string()))?;

        Ok(user_model)
    }
}

pub async fn run_ssh(config: AppConfig) -> anyhow::Result<()> {
    tracing::info!("SSH server initializing");
    let db = AppDatabase::init(&config).await?;
    let cache = AppCache::init(&config).await?;
    let redis_pool = cache.redis_pool().clone();

    // Start the hook worker (Redis queue consumer)
    let hook = crate::hook::HookService::new(
        db.clone(),
        cache.clone(),
        redis_pool.clone(),
        config.clone(),
    );
    let _worker_cancel = hook.start_worker();
    tracing::info!("hook worker started");

    SSHHandle::new(db, config.clone(), cache, redis_pool)
        .run_ssh()
        .await?;
    Ok(())
}
