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 utils::generate_id;

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

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


/// 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))
        .map(|cf| 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())
}


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

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 paheko_client: paheko::Client = paheko::Client::new(config.paheko_base_url);

    let paheko_credentials = paheko::Credentials {
        client_id: config.paheko_client_id,
        client_secret: config.paheko_client_secret
    };
    let paheko_client: paheko::AuthentifiedClient = paheko_client.login(paheko_credentials).await?;


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

    // FIXME: make it configurable
    let ha_org_slug = "l-etoile-de-bethleem-association-des-amis-de-la-chapelle-de-bethleem-d-aubevoye";
    // FIXME: make it configurable
    let pk_target_campaign_name = "Cotisation 2023-2024";
    let ha_form_name = "2023-2024";

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

    let mut pk_memberships: Vec<paheko::Membership> = vec![];

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

    // 1. get summary of existing paheko users
    let mut existing_users = paheko_client.get_users().await.context("Get users")?;
    // 2. get summary of transactions for that year
    let existing_transactions = paheko_client.get_transactions(1).await.context("Get transactions")?;

    // query paheko to get top ids
    // IMPORTANT: this mean that while the script is running, there must be NO mutations to the
    // users and services_users table on the paheko side
    let mut pk_next_user_id = paheko_client.get_next_id("users")
        .await.context("Get paheko users next id")?;
    let mut pk_next_user_service_id = paheko_client.get_next_id("services_users")
        .await.context("Get paheko services_users next id")?;

    for answer in answers {
        eprintln!("Processing answer:");
        let email = choose_email(&answer);
        eprintln!(" email: {:?}", email);

        // list of users involved in this answer
        let mut pk_users_summaries: Vec<paheko::UserSummary> = vec![];
        let mut pk_user_service_registrations: Vec<paheko::UserServiceRegistration> = vec![];

        let mut pk_user = paheko::User {
            id: utils::Id(0),
            first_name: Some(normalize_str(answer.user.first_name.clone())),
            last_name: normalize_str(answer.user.last_name.clone()),
            email,
            phone: parse_normalize_phone(read_custom_field(&answer, HelloassoCustomFieldType::Phone)),
            skills: read_custom_field(&answer, HelloassoCustomFieldType::Skills).map(normalize_str),
            address: read_custom_field(&answer, HelloassoCustomFieldType::Address)
                .map(normalize_str)
                .expect("Expected ha answer to have address"),
            postal_code: read_custom_field(&answer, HelloassoCustomFieldType::PostalCode)
                .expect("Expected ha answer to have postalcode"),
            city: read_custom_field(&answer, HelloassoCustomFieldType::City)
                .map(normalize_str)
                .expect("Expected ha answer to have city"),
            country: answer.payer_user.country.clone().trim()[..=1].to_string(), // we expect country code ISO 3166-1 alpha-2
            job: read_custom_field(&answer, HelloassoCustomFieldType::Job).map(normalize_str),
            birth_year: read_custom_field(&answer, HelloassoCustomFieldType::Birthday).and_then(parse_and_get_birthday_year),
            register_time: answer.order.inception_time,
        };

        // apply custom user override
        // this particular answer had duplicate phone and email from another answer
        if answer.id == 64756582 {
            pk_user.email = None;
            pk_user.phone = None;
        }

        // check for existing transactions
        if existing_transactions.iter().any(
            |summary| summary.reference == format!("HA/{}", answer.id)
        ) {
            eprintln!(" skipped: existing transaction found");
            continue;
        }

        let existing_user_opt = existing_users.iter().find(|user| user.email == pk_user.email).cloned();

        // check for existing paheko user, or create paheko user
        let pk_user_summary = match existing_user_opt.clone() {
            Some(user) => user,
            None => {
                let c = paheko_client.create_user(
                    &pk_user, pk_next_user_id
                ).await.context("Expected to create paheko user")?;
                eprintln!(" Created paheko user");
                pk_next_user_id += 1;
                existing_users.push(c.clone());
                c
            }
        };
        pk_users_summaries.push(pk_user_summary);

        let mut pk_membership = paheko::Membership {
            id: generate_id(),
            campaign_name: pk_target_campaign_name.to_string(),
            // FIXME: handle errors
            mode_name: serde_json::to_value(answer.mode.clone())
                .unwrap().as_str().unwrap().to_string(),
            start_time: answer.order.inception_time,
            end_time: 
                DateTime::<Utc>::from_naive_utc_and_offset(
                    NaiveDate::from_ymd_opt(2024, 12, 31).unwrap().and_hms_opt(23, 59, 59).unwrap(),
                    Utc
                ),
            payed_amount: f64::from(answer.amount)/100.0,
            users: vec![pk_user.id.clone()],
            external_references: paheko::ExternalReferences {
                helloasso_refs: paheko::HelloassoReferences {
                    answer_id: answer.id,
                    order_id: answer.order.id
                }
            }
        };

        // add activity for first member
        let user_registration = paheko_client.register_user_to_service(
            pk_users_summaries.get(0).unwrap(),
            &pk_membership,
            pk_next_user_service_id
        ).await.context("Expected to register user activity to paheko")?;
        pk_user_service_registrations.push(user_registration);
        pk_next_user_service_id += 1;
        eprintln!(" Created paheko activity registration");

        // then create optional linked user
        if answer.mode == helloasso::MembershipMode::Couple {
            let mut second_pk_user = pk_user.clone();
            second_pk_user.id = utils::Id(0);
            second_pk_user.email = None;
            second_pk_user.phone = None;
            second_pk_user.skills = None;
            second_pk_user.job = None;
            second_pk_user.birth_year = None;

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

            if existing_user_opt.is_none() {
                let second_pk_user_summary = paheko_client.create_user(&second_pk_user, pk_next_user_id)
                    .await.context("Expected to create second paheko user")?;
                eprintln!(" Created conjoint paheko user");
                pk_users_summaries.push(second_pk_user_summary);
                pk_next_user_id += 1;
                
                // create activity of second user
                let user_registration = paheko_client.register_user_to_service(
                    pk_users_summaries.get(1).unwrap(),
                    &pk_membership,
                    pk_next_user_service_id
                ).await.context("Registering service to second paheko server")?;
                pk_user_service_registrations.push(user_registration);
                pk_next_user_service_id += 1;
                eprintln!(" Created paheko activity registration for conjoint user");
            }
            // TODO: get existing linked user from previous year

            pk_membership.users.push(second_pk_user.id.clone());
        }

        // add transaction
        let transaction = paheko::SimpleTransaction {
            // TODO: make the label template configurable
            label: format!("Adhésion {:?} via HelloAsso", pk_membership.mode_name),
            amount: pk_membership.payed_amount,
            reference: format!("HA/{}", pk_membership.external_references.helloasso_refs.answer_id),
            // TODO: make these field configurable
            credit_account_code: "756".to_string(), // cotisations account
            debit_account_code: "512HA".to_string(), // helloasso account
            inception_time: answer.order.inception_time,
            kind: paheko::TransactionKind::Revenue,
            linked_users: pk_users_summaries.iter().map(|x| x.id.clone()).collect(),
            // this depend on a patch to paheko API code to work
            linked_services: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect()
        };
        let _ = paheko_client.register_transaction(transaction)
            .await.context("Expected to create new paheko transaction");
        eprintln!(" Created paheko transaction");

        pk_memberships.push(pk_membership);
    }

    eprintln!();
    eprintln!("Done.");

    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");
        }
    }
}