use actix_web::cookie::Key;
use cache::{AppCache, AppCacheConfig};
use config::AppConfig;
use db::database::AppDatabase;
use deadpool_redis::{
    PoolConfig, Runtime, Timeouts,
    cluster::{Config, Pool as RedisPool},
};
use email::AppEmail;
use service::AppService;
use session::storage::RedisClusterSessionStore;
use storage::{AppStorage, AppStorageConfig};
use tonic::transport::Channel;

use channel::{ChannelBus, ChannelBusConfig};
use socketio::SocketIo;

pub struct AppContext {
    pub config: AppConfig,
    pub service: AppService,
    pub session_store: RedisClusterSessionStore,
    pub session_key: Key,
    pub channel_bus: ChannelBus,
}

impl AppContext {
    pub async fn init() -> anyhow::Result<Self> {
        let config = AppConfig::load();
        init_tracing(&config)?;

        tracing::info!("initializing database");
        let db = AppDatabase::init(&config).await?;

        tracing::info!("initializing cache");
        let cache_config = AppCacheConfig::try_from(&config)?;
        let cache = AppCache::init(cache_config).await?;

        tracing::info!("initializing storage");
        let storage_config = AppStorageConfig::try_from(&config)?;
        let storage = AppStorage::init(storage_config).await?;

        tracing::info!("initializing email");
        let email = AppEmail::init(&config).await?;

        tracing::info!("connecting to git RPC");
        let rpc_addr = config.git_rpc_addr()?;
        let rpc_port = config.git_rpc_port()?;
        let git_channel =
            Channel::from_shared(format!("http://{}:{}", rpc_addr, rpc_port))
                .expect("invalid gRPC endpoint")
                .connect()
                .await?;

        let service = AppService {
            db,
            cache,
            email,
            storage,
            config: config.clone(),
            git: git_channel,
            redis_pool: init_redis_pool(&config)?,
        };

        tracing::info!("initializing session store");
        let redis_urls = config.redis_urls()?;
        let session_store = RedisClusterSessionStore::new(redis_urls).await?;

        tracing::info!("initializing session key");
        let secret = config.session_secret()?;
        let session_key = Key::from(secret.as_bytes());

        tracing::info!("initializing channel bus");
        let io = SocketIo::new();
        let channel_config = ChannelBusConfig {
            namespace: "/channel".to_owned(),
            signing_secret: Some(secret.clone()),
            ..Default::default()
        };
        let channel_bus = ChannelBus::new(
            service.db.clone(),
            service.cache.clone(),
            io,
            channel_config,
        );
        channel_bus.attach().await?;

        Ok(Self {
            config,
            service,
            session_store,
            session_key,
            channel_bus,
        })
    }
}

fn init_tracing(config: &AppConfig) -> anyhow::Result<()> {
    let level = config.log_level()?;
    let filter = tracing_subscriber::EnvFilter::try_new(&level)?;
    tracing_subscriber::fmt()
        .with_env_filter(filter)
        .with_target(false)
        .init();
    Ok(())
}

fn init_redis_pool(config: &AppConfig) -> anyhow::Result<RedisPool> {
    let redis_urls = config.redis_urls()?;
    let pool_size = config.redis_pool_size()?;
    let connect_timeout = config.redis_connect_timeout()?;
    let acquire_timeout = config.redis_acquire_timeout()?;

    let mut pool_config = PoolConfig::new(pool_size as usize);
    pool_config.timeouts = Timeouts {
        wait: Some(std::time::Duration::from_secs(acquire_timeout)),
        create: Some(std::time::Duration::from_secs(connect_timeout)),
        recycle: Some(std::time::Duration::from_secs(connect_timeout)),
    };

    let cfg = Config {
        urls: Some(redis_urls),
        connections: None,
        pool: Some(pool_config),
        read_from_replicas: false,
    };
    Ok(cfg.create_pool(Some(Runtime::Tokio1))?)
}
