use std::{fmt::Debug, sync::Arc};

use tokio::sync::OnceCell;

use super::{Environment, config::AppConfig};
use crate::{
    auth::AuthService,
    cache::CacheService,
    crypto::CryptoService,
    database::DatabaseService,
    errors::RecorderResult,
    extract::mikan::MikanClient,
    graphql::GraphQLService,
    logger::LoggerService,
    storage::{StorageService, StorageServiceTrait},
    tasks::TaskService,
};

pub trait AppContextTrait: Send + Sync + Debug {
    fn logger(&self) -> &LoggerService;
    fn db(&self) -> &DatabaseService;
    fn config(&self) -> &AppConfig;
    fn cache(&self) -> &CacheService;
    fn mikan(&self) -> &MikanClient;
    fn auth(&self) -> &AuthService;
    fn graphql(&self) -> &GraphQLService;
    fn storage(&self) -> &dyn StorageServiceTrait;
    fn working_dir(&self) -> &String;
    fn environment(&self) -> &Environment;
    fn crypto(&self) -> &CryptoService;
    fn task(&self) -> &TaskService;
}

pub struct AppContext {
    logger: LoggerService,
    db: DatabaseService,
    config: AppConfig,
    cache: CacheService,
    mikan: MikanClient,
    auth: AuthService,
    graphql: GraphQLService,
    storage: StorageService,
    crypto: CryptoService,
    working_dir: String,
    environment: Environment,
    task: OnceCell<TaskService>,
}

impl AppContext {
    pub async fn new(
        environment: Environment,
        config: AppConfig,
        working_dir: impl ToString,
    ) -> RecorderResult<Arc<Self>> {
        let config_cloned = config.clone();

        let logger = LoggerService::from_config(config.logger).await?;
        let cache = CacheService::from_config(config.cache).await?;
        let db = DatabaseService::from_config(config.database).await?;
        let storage = StorageService::from_config(config.storage).await?;
        let auth = AuthService::from_conf(config.auth).await?;
        let mikan = MikanClient::from_config(config.mikan).await?;
        let crypto = CryptoService::from_config(config.crypto).await?;
        let graphql = GraphQLService::from_config_and_database(config.graphql, db.clone()).await?;

        let ctx = Arc::new(AppContext {
            config: config_cloned,
            environment,
            logger,
            auth,
            cache,
            db,
            storage,
            mikan,
            working_dir: working_dir.to_string(),
            graphql,
            crypto,
            task: OnceCell::new(),
        });

        ctx.task
            .get_or_try_init(async || {
                TaskService::from_config_and_ctx(config.tasks, ctx.clone()).await
            })
            .await?;

        Ok(ctx)
    }
}

impl Debug for AppContext {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "AppContext")
    }
}

impl AppContextTrait for AppContext {
    fn logger(&self) -> &LoggerService {
        &self.logger
    }
    fn db(&self) -> &DatabaseService {
        &self.db
    }
    fn config(&self) -> &AppConfig {
        &self.config
    }
    fn cache(&self) -> &CacheService {
        &self.cache
    }
    fn mikan(&self) -> &MikanClient {
        &self.mikan
    }
    fn auth(&self) -> &AuthService {
        &self.auth
    }
    fn graphql(&self) -> &GraphQLService {
        &self.graphql
    }
    fn storage(&self) -> &dyn StorageServiceTrait {
        &self.storage
    }
    fn working_dir(&self) -> &String {
        &self.working_dir
    }
    fn environment(&self) -> &Environment {
        &self.environment
    }
    fn crypto(&self) -> &CryptoService {
        &self.crypto
    }
    fn task(&self) -> &TaskService {
        self.task.get().expect("task should be set")
    }
}
