use url::Url;
use serde::{Serialize, Deserialize};
use anyhow::{Context, Result, anyhow};
use chrono::prelude::{NaiveDate, DateTime, Utc};
use strum::{Display };

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

#[derive(Serialize, Debug)]
struct LoginPayload {
    email: String,
    password: String
}

use thiserror::Error;

#[derive(Error, Debug)]
enum APIClientError {
    #[error("Received non-normal status code from API")]
    InvalidStatusCode
}

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<HelloassoSession>
}

#[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)
}

#[derive(Clone, Serialize, Deserialize, Debug)]
struct HelloassoSession {
    jwt: String
}

enum LoginError {
    TransportFailure(reqwest::Error)
}

#[derive(Debug)]
struct HelloassoClient {
    client: reqwest::Client,
    base_url: Url,
}

impl Default for HelloassoClient {
    fn default() -> Self {
        HelloassoClient {
            client: HelloassoClient::get_base_client_builder()
                .build()
                .expect("reqwest client to be built"),
            base_url: Url::parse("https://api.helloasso.com/v5/")
                .expect("Valid helloasso API base URL")
        }
    }
}

impl HelloassoClient {

    fn get_base_client_builder() -> reqwest::ClientBuilder {
        let mut default_headers = reqwest::header::HeaderMap::new();
        default_headers.insert("Accept", "application/json".parse().unwrap());

        let proxy = reqwest::Proxy::https("https://localhost:8999").unwrap();
        reqwest::Client::builder()
                .proxy(proxy)
                .default_headers(default_headers)
    }


    async fn login(&mut self, payload: LoginPayload) -> Result<AuthentifiedHelloassoClient> {
        let mut login_commons_headers = reqwest::header::HeaderMap::new();
        login_commons_headers.insert(
            "Origin",
            "https://auth.helloasso.com".parse().expect("Header value to be OK")
        );

        let res = self.client.get(self.base_url.join("auth/antiforgerytoken")?)
            .headers(login_commons_headers.clone())
            .send().await?;
        let antiforgerytoken: String = res.json().await?;

        let res = self.client.post(self.base_url.join("auth/login")?)
            .json(&payload)
            .headers(login_commons_headers.clone())
            .header("x-csrf-token", antiforgerytoken)
            .send()
            .await?;

        if res.status() != 200 {
            return Err(anyhow!("Unexpected status code from login"));
        }

        fn get_jwt_from_cookies_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
            for (name_opt, value_raw) in headers {
                let name = String::from(name_opt.as_str());
                if name.to_lowercase() != "set-cookie" {
                    continue
                }
                let value = String::from(value_raw.to_str().unwrap());
                if value.starts_with("tm5-HelloAsso") {
                    let jwt = value.split("tm5-HelloAsso=").nth(1)?.split(";").nth(0)?.trim().to_string();
                    return Some(jwt);
                }
            }
            None
        }

        let jwt = get_jwt_from_cookies_headers(&res.headers())
            .context("Failed to find or parse JWT from login response")?;

        let session = HelloassoSession { jwt };

        Ok(self.authentified_client(session))
    }

    fn authentified_client(&self, session: HelloassoSession) -> AuthentifiedHelloassoClient {
        AuthentifiedHelloassoClient::new(self.base_url.clone(), session)
    }
}

#[derive(Debug, Clone)]
struct AuthentifiedHelloassoClient {
    session: HelloassoSession,
    client: reqwest::Client,
    base_url: Url
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct PaginationMeta {
    continuation_token: String,
    page_index: u64,
    page_size: u64,
    total_count: u64,
    total_pages: u64
}

#[derive(Debug, Serialize, Deserialize)]
struct PaginationCapsule {
    data: serde_json::Value,
    pagination: PaginationMeta
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CustomFieldAnswer {
    answer: String,
    id: u64,
    name: String
    // missing type, it's probably always TextInput, if not, serde will fail to parse
}


#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UserDetails {
    country: String,
    email: String,
    first_name: String,
    last_name: String
}


// #[derive(Debug, Serialize, Deserialize)]
// #[serde(rename_all = "camelCase")]
// struct OrderDetails {
//     date: 
//     form_
// }

impl AuthentifiedHelloassoClient {
    /// each time we need to change the token, we will need to rebuild the client
    fn new(base_url: Url, session: HelloassoSession) -> Self {
        let mut auth_headers = reqwest::header::HeaderMap::new();
        auth_headers.insert("Authorization", format!("Bearer {}", session.jwt).parse().unwrap());
        
        AuthentifiedHelloassoClient {
            base_url,
            session,
            client: HelloassoClient::get_base_client_builder()
                .default_headers(auth_headers)
                .build()
                .expect("reqwest client to be built")
        }
    }

    async fn verify_auth(&self) -> Result<bool> {
        let res = self.client
            .get(self.base_url.join("agg/user")?)
            .send().await?;
        return Ok(res.status() == 200);
    }

    async fn get_user_details(&self) -> Result<()> {
        let res = self.client
            .get(self.base_url.join("agg/user")?)
            .send().await?;
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        let user_details: serde_json::Value = res.json().await?;

        dbg!(user_details);

        Ok(())
    }

    async fn fetch(&self, path: String) -> Result<serde_json::Value> {
        let res = self.client
            .get(self.base_url.join(path.as_str())?)
            .send().await?;
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        let details: serde_json::Value = res.json().await?;

        // handle pagination
        // merge into "data", "pagination" is the key that hold details

        Ok(details)
    }

    async fn fetch_with_pagination(&self, path: String) -> Result<Vec<serde_json::Value>> {
        let mut data: Vec<serde_json::Value> = vec![];
        let mut continuation_token: Option<String> = None;

        loop {
            let mut url = self.base_url.join(path.as_str())?;
            if let Some(token) = &continuation_token {
                url.query_pairs_mut().append_pair("continuationToken", token);
            }
            let res = self.client
                .get(url)
                .send().await?;
            if res.status() != 200 {
                return Err(APIClientError::InvalidStatusCode.into());
            }
            let capsule: PaginationCapsule = res.json().await?;

            // handle pagination
            // merge into "data", "pagination" is the key that hold details

            let page_items = match capsule.data {
                serde_json::Value::Array(inner) => inner,
                _ => {
                    return Err(anyhow!("Unexpected json value in data bundle"));
                }
            };
            if page_items.len() == 0 {
                return Ok(data);
            }
            data.extend(page_items);
            if capsule.pagination.page_index == capsule.pagination.total_pages {
                return Ok(data);
            }
            continuation_token = Some(capsule.pagination.continuation_token);
        }
    }

    fn organization(&self, slug: &str) -> Organization {
        Organization { client: self.clone(), slug: slug.to_string() }
    }
}

#[derive(Debug, Clone)]
struct Organization {
    client: AuthentifiedHelloassoClient,
    slug: String
}

#[derive(Debug, Serialize, Deserialize)]
enum MembershipMode {
    #[serde(rename = "Individuel")]
    Individual,
    #[serde(rename = "Couple")]
    Couple,
    #[serde(rename = "Individuel bienfaiteur")]
    BenefactorIndividual,
    #[serde(rename = "Couple bienfaiteur")]
    BenefactorCouple,
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct FormAnswer {
    amount: u64,
    #[serde(rename = "name")]
    mode: MembershipMode,
    #[serde(rename = "payer")]
    user: UserDetails,
    id: u64,
    custom_fields: Vec<CustomFieldAnswer>
}

impl Organization {
    async fn get_details(&self) -> Result<serde_json::Value> {
        let details = self.client.fetch(format!("organizations/{}", self.slug)).await?;
        Ok(details)
    }

    async fn get_form_answers(&self, form_slug: String) -> Result<Vec<FormAnswer>> {
        let data = self.client.fetch_with_pagination(
            format!("organizations/{}/forms/Membership/{}/participants?withDetails=true", self.slug, form_slug)
        ).await?;
        let mut answers: Vec<FormAnswer> = vec![];
        for entry in data {
            answers.push(serde_json::from_value(entry).context("Cannot parse FormAnswer")?)
        }
        Ok(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 HelloassoClient, login_payload: LoginPayload) -> Result<AuthentifiedHelloassoClient> {

    async fn login(user_cache: &mut UserCache, ha_client: &mut HelloassoClient, login_payload: LoginPayload) -> Result<AuthentifiedHelloassoClient> {
        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?);
        }
    };
}

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

/// for now we include the custom fields into the paheko user
/// we don't have time to implement user settings to change the custom fields mapping
/// for now, manual mapping
#[derive(Debug, Serialize, Deserialize)]
struct PahekoUser {
    first_name: String,
    last_name: String,
    email: String,

    phone: Option<String>,
    address: String,
    city: String,
    postal_code: String,
    skills: Option<String>,
    job: Option<String>,
    birthday: Option<NaiveDate> // we will need to validate some data before
}

#[derive(Debug, Serialize, Deserialize)]
struct PahekoMembership {
    author: PahekoUser,
    linked_users: Vec<PahekoUser>,
    campaign: String,
    mode: String,
    inception_datum: DateTime<Utc>
}

struct CustomFieldsMapping {
    helloasso_id: u64,
    paheko_slug: String,
    label: String
    // address: u64,
    // postal_code: u64,
    // city: u64,
    // phone: u64,
    // skills: u64,
    // birthday: u64,
}

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

impl Into<u64> for HelloAssoCustomFields {
    fn into(self) -> u64 {
        match self {
            HelloAssoCustomFields::Address => 12958695,
            HelloAssoCustomFields::PostalCode => 12958717,
            HelloAssoCustomFields::City => 12958722,
            HelloAssoCustomFields::Phone => 13279172,
            HelloAssoCustomFields::Job => 13279172,
            HelloAssoCustomFields::Skills => 11231129,
            HelloAssoCustomFields::Birthday => 12944367
        }
    }
}

fn read_custom_field(form_answer: &FormAnswer, custom_field_id: HelloAssoCustomFields) -> Option<String> {
    let int_repr: u64 = custom_field_id.into();
    form_answer.custom_fields.iter()
        .find(|f| f.id == int_repr)
        .and_then(|cf| Some(cf.answer.clone()))
}

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: HelloassoClient = Default::default();

    let login_payload = LoginPayload {
            email: config.helloasso_email,
            password: config.helloasso_password
    };
    let auth_client: AuthentifiedHelloassoClient = 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: 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 step: output a list of PahekoUser with PahekoMembership
    let pk_memberships: Vec<PahekoMembership> = vec![];
    let mut pk_users: Vec<PahekoUser> = vec![];

    for answer in answers {
        // TODO: parse birthday
        // NaiveDate::parse_from_str
        dbg!(&answer);
        pk_users.push(PahekoUser {
            first_name: answer.user.first_name.clone(),
            last_name: answer.user.last_name.clone(),
            email: answer.user.email.clone(),
            phone: read_custom_field(&answer, HelloAssoCustomFields::Phone),
            skills: read_custom_field(&answer, HelloAssoCustomFields::Skills),
            address: read_custom_field(&answer, HelloAssoCustomFields::Address).expect("to have address"),
            postal_code: read_custom_field(&answer, HelloAssoCustomFields::PostalCode).expect("to have address"),
            city: read_custom_field(&answer, HelloAssoCustomFields::City).expect("to have address"),
            job: read_custom_field(&answer, HelloAssoCustomFields::Job),
            birthday: None
        });
    }
    dbg!(pk_users);

    // then, request the current list of users
    // then, upload the PahekoMembership

    Ok(())
}

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