#![feature(slice_group_by)]

mod utils;
mod paheko;
mod helloasso;
mod sync_helloasso;
mod sync_csv;
mod sync_paheko;

#[cfg(test)]
mod test_utils;

use thiserror::Error;
use anyhow::{Context, Result, anyhow};
use strum::Display;
use serde::{Serialize, Deserialize};
use url::Url;
use fully_pub::fully_pub;
use argh::FromArgs;

/// permanent config to store long-term config
/// used to ingest env settings
/// config loaded from env variables
#[derive(Deserialize, Serialize, Debug)]
#[fully_pub]
struct Config {
    helloasso_proxy: Option<String>,
    helloasso_email: String,
    helloasso_password: String,

    helloasso_organization_slug: String,
    helloasso_form_name: String,

    paheko_proxy: Option<String>,
    paheko_base_url: String,
    paheko_client_id: String,
    paheko_client_secret: String,

    paheko_target_activity_name: String,
    paheko_accounting_years_ids: Vec<u32>,
}

// start user cache management
use std::fs;

#[derive(Serialize, Deserialize, Debug)]
#[fully_pub]
struct UserCache {
    helloasso_session: Option<helloasso::WebSession>
}

#[derive(Display, Debug, Error)]
#[strum(serialize_all = "snake_case")]
enum LoadError {
    XDG,
    Fs,
    FailedToParse,
    FailedToEncode,
    FailedToCreate,
    FailedToWrite
}

const APP_USER_AGENT: &str = "helloasso_paheko_adapter";

fn write_user_cache(cache: &UserCache) -> Result<(), LoadError> {
    let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
        .map_err(|_e| { LoadError::XDG })?;
    let user_cache_path = xdg_dirs.place_cache_file("session.json").map_err(|_e| { LoadError::FailedToCreate })?;
    let encoded_cache = serde_json::to_string(&cache).map_err(|_e| { LoadError::FailedToEncode })?;
    fs::write(user_cache_path, encoded_cache.as_str()).map_err(|_e| { LoadError::FailedToWrite })?;
    Ok(())
}

fn load_user_cache() -> Result<UserCache, LoadError> {
    let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
        .map_err(|_e| { LoadError::XDG })?;
    let user_cache_path = xdg_dirs.get_cache_file("session.json");

    if !user_cache_path.exists() {
        let default_cache = UserCache {
            helloasso_session: None
        };
        write_user_cache(&default_cache)?;
    }

    let session_content = fs::read_to_string(user_cache_path).map_err(|_e| { LoadError::Fs })?;
    let cache: UserCache =  serde_json::from_str(&session_content).map_err(|_e| { LoadError::FailedToParse })?;

    Ok(cache)
}

fn get_proxy_from_url(proxy_url: &Option<String>) -> Result<Option<reqwest::Proxy>> {
    Ok(match proxy_url {
        Some(p) => Some(reqwest::Proxy::all(p)
            .context("Expected to build Proxy from paheko_proxy config value")?),
        None => None
    })
}

async fn launch_adapter(source: SourceType, config: &Config) -> Result<()> {

    let mut user_cache = load_user_cache().context("Failed to load user cache")?;

    if !&config.paheko_base_url.ends_with('/') {
        return Err(anyhow!("Invalid paheko base_url, it must end with a slash"))
    }
    let mut paheko_client: paheko::Client = paheko::Client::new(paheko::ClientConfig {
        base_url: Url::parse(&config.paheko_base_url).expect("Expected paheko base url to be a valid URL"),
        proxy: get_proxy_from_url(&config.paheko_proxy)?,
        user_agent: APP_USER_AGENT.to_string()
    });

    let paheko_credentials = paheko::Credentials {
        client_id: config.paheko_client_id.clone(),
        client_secret: config.paheko_client_secret.clone()
    };
    let paheko_client: paheko::AuthentifiedClient = paheko_client.login(paheko_credentials).await?;
    
    match source {
        SourceType::Csv => sync_csv::sync_csv(&paheko_client, config, &mut user_cache).await?,
        SourceType::Helloasso => sync_helloasso::sync_helloasso(&paheko_client, config, &mut user_cache).await?
    }

    Ok(())
}


#[derive(FromArgs)]
/// Members and Membership sync adaper for paheko (support Hellosso and CSV)
struct App {
    /// the source of sync (CSV or helloasso)
    #[argh(option, short = 'm')]
    source: Option<String>,

    /// output debug info
    #[argh(switch, short = 'i')]
    info: bool
}

enum SourceType {
    Helloasso,
    Csv
}

#[tokio::main]
async fn main() {
    let app: App = argh::from_env();
    dotenvy::dotenv().expect("Could not load dot env file");
    let config: Config = envy::from_env().expect("Failed to load env vars");

    if app.info {
        dbg!(config);
        return;
    }

    let source = match app.source.unwrap().as_ref() {
        "helloasso" => SourceType::Helloasso,
        "csv" => SourceType::Csv,
        _ => {
            eprintln!("Must provide a valid source argument.");
            return;
        }
    };
    let res = launch_adapter(source, &config).await;
    match res {
        Err(err) => {
            eprintln!("Program failed, details bellow");
            eprintln!("{:?}", err);
        },
        Ok(()) => {
            eprintln!("Program done");
        }
    }
}