use merlin_env_helper::{EnvKey, get_env_value};
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};

use crate::util::validate_url;

pub static CONFIGURATION: OnceCell<Configuration> = OnceCell::new();

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct BaseConfiguration {
    pub image_store_directory: String,
    pub image_resource_store_directory: String,
    pub base_url: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct DatabaseConfiguration {
    pub url: String,
    pub port: u16,
    pub database: String,
    pub user: Option<String>,
    pub password: Option<String>,
    pub max_connections: u32,
}

impl DatabaseConfiguration {
    pub fn connection_string(&self) -> String {
        format!(
            "postgres://{}:{}@{}:{}/{}",
            self.user.as_ref().unwrap(),
            self.password.as_ref().unwrap(),
            self.url,
            self.port,
            self.database
        )
    }
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ServerConfiguration {
    pub host: String,
    pub port: u16,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Configuration {
    pub main: BaseConfiguration,
    pub server: ServerConfiguration,
    pub db: DatabaseConfiguration,
}

impl Default for BaseConfiguration {
    fn default() -> Self {
        BaseConfiguration {
            image_store_directory: "./images".into(),
            image_resource_store_directory: "./images/resources".into(),
            base_url: "http://localhost".into(),
        }
    }
}

impl Default for ServerConfiguration {
    fn default() -> Self {
        ServerConfiguration {
            host: "localhost".into(),
            port: 8080,
        }
    }
}

impl Default for DatabaseConfiguration {
    fn default() -> Self {
        DatabaseConfiguration {
            url: "localhost".into(),
            port: 5432,
            user: None,
            password: None,
            max_connections: 5,
            database: "no_man_sky".into(),
        }
    }
}

impl Default for Configuration {
    fn default() -> Self {
        Configuration {
            main: BaseConfiguration::default(),
            server: ServerConfiguration::default(),
            db: DatabaseConfiguration::default(),
        }
    }
}

impl Configuration {
    pub async fn new() -> Self {
        let mut config = &mut Configuration::default();

        if Path::new("no_man_sky.toml").exists() {
            // If the config file exists, load it.
            let config_content = fs::read_to_string("no_man_sky.toml")
                .unwrap_or_else(|e| panic!("Failed to read config file: {}", e));

            let config_to_merge: Configuration =
                toml::from_str(&config_content).unwrap_or_else(|e| {
                    panic!("Failed to parse config file: {}", e);
                });

            merge(&mut config, &config_to_merge);
        }

        let image_store_directory = get_env_value(&EnvKey::key_with_fallback(
            "IMAGE_STORE_DIRECTORY",
            &config.main.image_store_directory,
        ))
        .expect("Failed to get IMAGE_STORE_DIRECTORY from environment");

        let image_resource_store_directory = get_env_value(&EnvKey::key_with_fallback(
            "IMAGE_RESOURCE_STORE_DIRECTORY",
            &config.main.image_resource_store_directory,
        ))
        .expect("Failed to get IMAGE_STORE_DIRECTORY from environment");

        validate_configuration(Configuration {
            main: {
                BaseConfiguration {
                    image_store_directory: image_store_directory,
                    image_resource_store_directory: image_resource_store_directory,
                    base_url: get_env_value(&EnvKey::key_with_fallback(
                        "MAIN_BASE_URL",
                        &config.main.base_url,
                    ))
                    .expect("Failed to get MAIN_BASE_URL from environment"),
                }
            },
            server: {
                ServerConfiguration {
                    host: get_env_value(&EnvKey::key_with_fallback(
                        "SERVER_HOST",
                        &config.server.host,
                    ))
                    .expect("Failed to get SERVER_HOST from environment"),
                    port: get_env_value(&EnvKey::key_with_fallback(
                        "SERVER_PORT",
                        &config.server.port.to_string(),
                    ))
                    .expect("Failed to get SERVER_PORT from environment")
                    .parse::<u16>()
                    .expect("Failed to parse SERVER_PORT"),
                }
            },
            db: {
                DatabaseConfiguration {
                    url: get_env_value(&EnvKey::key_with_fallback("DB_URL", &config.db.url))
                        .expect("Failed to ger DB_URL from environment."),
                    port: get_env_value(&EnvKey::key_with_fallback(
                        "DB_PORT",
                        &config.db.port.to_string(),
                    ))
                    .expect("Failed to get DB_URL from environment")
                    .parse::<u16>()
                    .expect("Failed to parse DB_PORT"),
                    database: get_env_value(&EnvKey::key_with_fallback(
                        "DB_DATABASE",
                        &config.db.database,
                    ))
                    .expect("Failed to ger DB_DATABASE from environment."),
                    user: Some(
                        get_env_value(&EnvKey::secure_with_fallback(
                            "DB_USER",
                            "DB_USER_FILE",
                            &config.db.user.as_ref().unwrap(),
                        ))
                        .expect("Failed to ger DB_URL from environment."),
                    ),
                    password: Some(
                        get_env_value(&EnvKey::secure_with_fallback(
                            "DB_PASSWORD",
                            "DB_PASSWORD_FILE",
                            &config.db.password.as_ref().unwrap(),
                        ))
                        .expect("Failed to ger DB_URL from environment."),
                    ),
                    max_connections: get_env_value(&EnvKey::key_with_fallback(
                        "DB_MAX_CONNECTIONS",
                        &config.db.max_connections.to_string(),
                    ))
                    .expect("Failed to get DB_MAX_CONNECTIONS from environment")
                    .parse::<u32>()
                    .expect("Failed to parse DB_MAX_CONNECTIONS"),
                }
            },
        })
        .await
    }
}

fn merge(config: &mut Configuration, config_content: &Configuration) {
    config.main.image_store_directory = config_content.main.image_store_directory.clone();
    config.main.base_url = config_content.main.base_url.clone();
    config.server.host = config_content.server.host.clone();
    config.server.port = config_content.server.port;
    config.db.url = config_content.db.url.clone();
    config.db.port = config_content.db.port;
    config.db.max_connections = config_content.db.max_connections;
    config.db.database = config_content.db.database.clone();

    if config_content.db.user.is_some() {
        config.db.user = config_content.db.user.clone();
    }
    if config_content.db.password.is_some() {
        config.db.password = config_content.db.password.clone();
    }
}

async fn validate_configuration(configuration: Configuration) -> Configuration {
    if !validate_url(&configuration.main.base_url).await {
        panic!("Invalid base URL: {}", configuration.main.base_url);
    }

    if !validate_directory(&configuration.main.image_store_directory) {
        panic!(
            "Image store directory does not exist or is not a directory: {}",
            configuration.main.image_store_directory
        );
    }
    if !validate_directory(&configuration.main.image_resource_store_directory) {
        panic!(
            "Image resource store directory does not exist or is not a directory: {}",
            configuration.main.image_resource_store_directory
        );
    }

    if !validate_directory(&configuration.main.image_store_directory) {
        panic!(
            "Image store directory does not exist or is not a directory: {}",
            configuration.main.image_store_directory
        );
    }

    if configuration.db.url.is_empty() {
        panic!("Database URL cannot be empty.");
    }

    if configuration.db.port == 0 {
        panic!("Database port cannot be zero.");
    }

    if configuration.db.user.is_none()
        || configuration.db.user.as_ref().unwrap().trim().is_empty()
        || configuration.db.password.as_ref().unwrap().trim() == "set by env"
    {
        panic!("Database user cannot be empty or must be set by environment.");
    }

    if configuration.db.password.is_none()
        || configuration
            .db
            .password
            .as_ref()
            .unwrap()
            .trim()
            .is_empty()
        || configuration.db.password.as_ref().unwrap().trim() == "set by env"
    {
        panic!("Database password cannot be empty or must be set by environment.");
    }

    configuration
}

fn validate_directory(dir: &str) -> bool {
    let dir = Path::new(dir);
    dir.exists() && dir.is_dir()
}
