mod utils;
mod paheko;
mod helloasso;

use thiserror::Error;
use anyhow::{Context, Result};
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
use strum::Display;
use serde::{Serialize, Deserialize};
use std::collections::HashSet;
use phonenumber;
use utils::generate_id;

/// permanent config to store long-term config
/// used to ingest env settings
#[derive(Deserialize, Serialize, Debug)]
struct Config {
    helloasso_email: String,
    helloasso_password: String
}

static APP_USER_AGENT: &str = concat!(
    env!("CARGO_PKG_NAME"),
    "/",
    env!("CARGO_PKG_VERSION"),
);

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

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

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


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:
//  - make pagination working
//  - create paheko client
//  - get current paheko membership
//  - function to convert participants to paheko members
//  - clean up names and things
//  - map custom fields with the right thing
//  - handle linked users

fn get_paheko_membership_from_ha_answers() {
    
}

// 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 Ok(login(user_cache, ha_client, login_payload).await?)
            }
            println!("Used anterior token");
            return Ok(auth_client);
        },
        None => {
            println!("First time login");
            return Ok(login(user_cache, ha_client, login_payload).await?);
        }
    };
}


/// rust how to access inner enum value
#[derive(Debug, PartialEq, Clone, Copy)]
enum HelloassoCustomFieldType {
    Email,
    Address,
    PostalCode,
    City,
    Phone,
    Job,
    Skills,
    Birthday,
    LinkedUserFirstName
}

impl TryFrom<&str> for HelloassoCustomFieldType {
    type Error = ();

    fn try_from(subject: &str) -> Result<Self, Self::Error> {
        match subject {
            "Prénom conjoint" => Ok(HelloassoCustomFieldType::LinkedUserFirstName),
            "ADRESSE" => Ok(HelloassoCustomFieldType::Address),
            "CODE POSTAL" => Ok(HelloassoCustomFieldType::PostalCode),
            "VILLE" => Ok(HelloassoCustomFieldType::City),
            "EMAIL" => Ok(HelloassoCustomFieldType::Email),
            "PROFESSION" => Ok(HelloassoCustomFieldType::Job),
            "TÉLÉPHONE" => Ok(HelloassoCustomFieldType::Phone),
            "DATE DE NAISSANCE" => Ok(HelloassoCustomFieldType::Birthday),
            "CENTRE D'INTÉRÊTS / COMPÉTENCES" => Ok(HelloassoCustomFieldType::Skills),
            _ => Err(())
        }
    }
}

fn read_custom_field(form_answer: &helloasso::FormAnswer, custom_field: HelloassoCustomFieldType) -> Option<String> {
    // FIXME: compute the type directly at deserialization with serde
    form_answer.custom_fields.iter()
        .find(|f| HelloassoCustomFieldType::try_from(f.name.as_str()) == Ok(custom_field))
        .and_then(|cf| Some(cf.answer.clone()))
}

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


/// 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()?;
    Some(d.year().try_into().ok()?)
}

fn helloasso_to_paheko_membership(helloasso_membership: &helloasso::MembershipMode) -> paheko::MembershipMode {
    match helloasso_membership {
        helloasso::MembershipMode::Couple => paheko::MembershipMode::Couple,
        helloasso::MembershipMode::Individual => paheko::MembershipMode::Individual,
        helloasso::MembershipMode::BenefactorCouple => paheko::MembershipMode::BenefactorCouple,
        helloasso::MembershipMode::BenefactorIndividual => paheko::MembershipMode::BenefactorIndividual
    }
}

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")?;
    let mut ha_client: helloasso::Client = Default::default();

    let login_payload = helloasso::LoginPayload {
            email: config.helloasso_email,
            password: config.helloasso_password
    };
    let auth_client: helloasso::AuthentifiedClient = get_auth_client_from_cache(&mut user_cache, &mut ha_client, login_payload).await?;

    // dbg!(auth_client.get_user_details().await?);

    let slug = "l-etoile-de-bethleem-association-des-amis-de-la-chapelle-de-bethleem-d-aubevoye";

    let org: helloasso::Organization = auth_client.organization(slug);
    // dbg!(org.get_details().await?);
    let answers = org.get_form_answers("2023-2024".to_string()).await?;
    
    // dbg!(&answers);
    println!("Got {} answers to the membership form. Processing...", &answers.len());

    // first, request the current list of membership in paheko that were created with helloasso
    // get the list of payments associated

    // first step: output a list of PahekoUser with PahekoMembership
    let pk_memberships: Vec<paheko::Membership> = vec![];
    let mut pk_users: Vec<paheko::User> = vec![];
    let mut pk_memberships: Vec<paheko::Membership> = vec![];

    let mut count: u64 = 0;
    let mut names: HashSet<String> = HashSet::new();

    // read_custom_field(&answer, HelloAssoCustomFieldType::Email).or(Some(answer.payer_user.email.clone())),
    use email_address::*;
    fn choose_email(answer: &helloasso::FormAnswer) -> Option<String> {
        read_custom_field(&answer, HelloassoCustomFieldType::Email)
            .and_then(|x| {
                if !EmailAddress::is_valid(&x) {
                    None
                } else {
                    Some(x)
                }
            })
            .or(Some(answer.payer_user.email.clone()))
    } 

    for answer in answers {
        // TODO: parse birthday
        dbg!(&answer);
        for custom_field in answer.custom_fields.iter() {
            names.insert(custom_field.name.clone());
            count += 1;
        }
        let paheko_user = paheko::User {
            id: generate_id(),
            first_name: answer.user.first_name.clone(),
            last_name: answer.user.last_name.clone(),
            email: choose_email(&answer),
            phone: parse_normalize_phone(read_custom_field(&answer, HelloassoCustomFieldType::Phone)),
            skills: read_custom_field(&answer, HelloassoCustomFieldType::Skills),
            address: read_custom_field(&answer, HelloassoCustomFieldType::Address).expect("to have address"),
            postal_code: read_custom_field(&answer, HelloassoCustomFieldType::PostalCode).expect("to have postal code"),
            city: read_custom_field(&answer, HelloassoCustomFieldType::City).expect("to have city"),
            country: answer.payer_user.country.clone(),
            job: read_custom_field(&answer, HelloassoCustomFieldType::Job),
            birthday: read_custom_field(&answer, HelloassoCustomFieldType::Birthday).and_then(parse_and_get_birthday_year),
            // FIXME: the reference will be in the data of the paheko activity, and will only
            // reference the answer id
        };
        let mut pk_membership = paheko::Membership {
            id: generate_id(),
            campaign: "".to_string(),
            inception_datum: Utc::now(),
            mode: helloasso_to_paheko_membership(&answer.mode),
            users: vec![paheko_user.id.clone()],
            external_references: paheko::ExternalReferences {
                helloasso_ref: paheko::HelloassoReferences {
                    answer_id: answer.id,
                    order_id: answer.order.id
                }
            }
        };
        dbg!(&pk_membership.users);
        // then create optional linked user
        
        if answer.mode == helloasso::MembershipMode::Couple {
            let mut second_pk_user = paheko_user.clone();
            second_pk_user.id = generate_id();
            second_pk_user.email = None;
            second_pk_user.phone = None;
            second_pk_user.skills = None;
            second_pk_user.job = None;
            second_pk_user.birthday = None;

            // add first_name
            match read_custom_field(&answer, HelloassoCustomFieldType::LinkedUserFirstName) {
                Some(name) => {
                    second_pk_user.first_name = name;
                },
                None => {
                    second_pk_user.first_name = "Conjoint".to_string();
                    eprintln!("Got a user with Couple mode but no additional name given!")
                }
            }

            pk_membership.users.push(second_pk_user.id.clone());
            pk_users.push(second_pk_user);
        }
        pk_users.push(paheko_user);
        pk_memberships.push(pk_membership);
    }
    dbg!(&pk_users);
    dbg!(&pk_memberships);
    dbg!(&pk_users.len());
    dbg!(&pk_memberships.len());

    // println!("{:?}", &pk_users.iter().map(|user| format!("{:?}", user.email)).collect::<Vec<String>>());

    for u in pk_users.iter() {
        println!("{:?}", (&u.first_name, &u.last_name, &u.email, &u.phone, &u.birthday, &u.country));
    }
    for u in pk_users.iter() {
        let email = u.email.clone();
        if email.is_none() { continue; }
        println!("{:?},{:?}", email.unwrap(), format!("{} {}", &u.first_name, &u.last_name));
    }

    // then, request the current list of users
    // match with the email address
    // we consider the email address as the id for a helloasso user
    // then, upload the PahekoMembership
    // in paheko, there is a custom field "external extensions data" which can be used to put an
    // id,
    // for each uses we extracted
    //  we check if there is an existing user by checking for the ha forn answer id

    Ok(())
}

#[tokio::main]
async fn main() {
    let res = launch_adapter().await;
    dbg!(res);
}