#![feature(slice_group_by)]

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

use thiserror::Error;
use anyhow::{Context, Result, anyhow};
use chrono::prelude::{NaiveDate, Datelike};
use strum::Display;
use serde::{Serialize, Deserialize};
use url::Url;
use fully_pub::fully_pub;

/// 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_year_id: u64,
}

// 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)
}

// TODO: find a better way to have the logic implemented
async fn get_auth_client_from_cache(
    user_cache: &mut UserCache,
    ha_client: &mut helloasso::Client,
    login_payload: helloasso::LoginPayload
) -> Result<helloasso::AuthentifiedClient> {

    async fn login(
        user_cache: &mut UserCache,
        ha_client: &mut helloasso::Client,
        login_payload: helloasso::LoginPayload
    ) -> Result<helloasso::AuthentifiedClient> {
        let auth_client = ha_client.login(
            login_payload
        ).await.context("Failed to login")?;
        user_cache.helloasso_session = Some(auth_client.session.clone());
        write_user_cache(&user_cache).expect("unable to write user cache");
        println!("Logged in and wrote token to cache");
        Ok(auth_client)
    }

    match &user_cache.helloasso_session {
        Some(cached_session) => {
            let auth_client = ha_client.authentified_client(cached_session.clone());
            
            if !auth_client.verify_auth().await? {
                println!("Need to relog, token invalid");
                return login(user_cache, ha_client, login_payload).await
            }
            println!("Used anterior token");
            Ok(auth_client)
        },
        None => {
            println!("First time login");
            login(user_cache, ha_client, login_payload).await
        }
    }
}

fn parse_normalize_phone(phone_number_opt: Option<String>) -> Option<String> {
    let number_raw = phone_number_opt?;

    let parsed = match phonenumber::parse(Some(phonenumber::country::Id::FR), number_raw) {
        Ok(r) => {
            r
        },
        Err(_e) => {
            return None;
        }
    };

    Some(parsed.to_string())
}


fn normalize_str(subject: String) -> String {
    subject.trim().replace("\n", ";").to_string()
}

/// remove year precision to comply with GDPR eu rules
fn parse_and_get_birthday_year(raw_date: String) -> Option<u32> {
    let d_res = NaiveDate::parse_from_str(raw_date.trim(), "%d/%m/%Y");
    let d = d_res.ok()?;
    d.year().try_into().ok()
}

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() -> Result<()> {
    dotenvy::dotenv()?;

    let config: Config = envy::from_env().context("Failed to load env vars")?;

    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?;

    sync_helloasso::sync_helloasso(&paheko_client, &config, &mut user_cache).await?;
    // sync_csv::sync(&paheko_client, &config, &mut user_cache).await?;

    Ok(())
}

#[tokio::main]
async fn main() {
    // TODO: add argument parser to have handle config file
    let res = launch_adapter().await;
    match res {
        Err(err) => {
            eprintln!("Program failed, details bellow");
            eprintln!("{:?}", err);
        },
        Ok(()) => {
            eprintln!("Program done");
        }
    }
}