use anyhow::{Context, Result, anyhow};
use url::Url;
use serde::{Serialize, Deserialize};
use fully_pub::fully_pub;
use crate::utils::Id;
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
use thiserror::Error;

#[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub]
struct HelloassoReferences {
    answer_id: u64,
    order_id: u64
    // payment_id: u64,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub]
struct ExternalReferences {
    helloasso_ref: HelloassoReferences
}

/// 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, Clone)]
#[fully_pub]
struct User {
    id: Id,
    first_name: Option<String>,
    last_name: String,
    email: Option<String>,

    phone: Option<String>,
    address: String,
    city: String,
    postal_code: String,
    country: String,
    skills: Option<String>,
    job: Option<String>,
    birth_year: Option<u32>,

    register_time: DateTime<Utc>
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub]
struct UserSummary {
    id: Id,
    first_name: Option<String>,
    last_name: String,
    email: Option<String>
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[fully_pub]
enum MembershipMode {
    Individual,
    Couple,
    BenefactorIndividual,
    BenefactorCouple,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[fully_pub]
struct Membership {
    id: Id,
    users: Vec<Id>,
    campaign: String,
    mode: MembershipMode,
    inception_time: DateTime<Utc>,
    external_references: ExternalReferences
}




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


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

#[derive(Debug, Clone)]
#[fully_pub]
struct Credentials {
    client_id: String,
    client_secret: String
}


impl Default for Client {
    fn default() -> Self {
        Client {
            client: Client::get_base_client_builder()
                .build()
                .expect("Expected reqwest client to be built"),
            base_url: Url::parse("https://paheko.etoiledebethleem.fr/api/") // the traling slash is important
                .expect("Expected valid paheko API base URL")
        }
    }
}

use base64_light::base64_encode;

fn build_auth_headers(credentials: &Credentials) -> reqwest::header::HeaderMap {
    let mut login_headers = reqwest::header::HeaderMap::new();
    login_headers.insert(
        "Authorization",
        format!("Basic {}", &base64_encode(
            &format!("{}:{}", &credentials.client_id, &credentials.client_secret)
        )).parse().expect("Header value to be OK")
    );
    login_headers
}

impl Client {
    pub fn new(base_url: String) -> Client {
        Client {
            client: Client::get_base_client_builder()
                .build()
                .expect("Expected reqwest client to be built"),
            base_url: Url::parse(&base_url) // the traling slash is important
                .expect("Expected valid paheko API base URL")
        }
    }

    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::http("http://localhost:8998").unwrap();
        reqwest::Client::builder()
                .proxy(proxy)
                .default_headers(default_headers)
    }

    pub async fn login(&mut self, credentials: Credentials) -> Result<AuthentifiedClient> {
        let hypothetic_client = self.authentified_client(credentials);
        let query: String = r#"
            SELECT key,value FROM config WHERE key="org_name"
        "#.to_string();

        match hypothetic_client.sql_query(query).await {
            Ok(_value) => {
                Ok(hypothetic_client)
            },
            Err(err) => {
                Err(anyhow!("Failed to authenticate: Credentials provided are invalids, {:?}", err))
            }
        }
    }

    pub fn authentified_client(&self, credentials: Credentials) -> AuthentifiedClient {
        AuthentifiedClient::new(self.base_url.clone(), credentials)
    }
}


#[derive(Debug, Clone)]
pub struct AuthentifiedClient {
    credentials: Credentials,
    client: reqwest::Client,
    base_url: Url
}

// SELECT id,nom AS first_name,last_name,email,external_custom_data FROM users LIMIT 5;
#[derive(Debug, Deserialize)]
#[fully_pub]
struct SimpleUser {
    id: u32,
    first_name: String,
    last_name: String,
    email: Option<String>,
    external_custom_data: Option<String>
}

#[derive(Debug, Deserialize)]
#[fully_pub]
struct SqlQueryOutput {
    count: u64,
    results: serde_json::Value
}

#[derive(Debug, Deserialize)]
#[fully_pub]
struct TransactionSummary {
    id: u64,
    reference: String
}

impl AuthentifiedClient {
    pub fn new(base_url: Url, credentials: Credentials) -> Self {
        AuthentifiedClient {
            client: Client::get_base_client_builder()
                .default_headers(build_auth_headers(&credentials))
                .build()
                .expect("Expect client to be built"),
            credentials,
            base_url
        }
    }

    pub async fn sql_query(&self, query: String) -> Result<SqlQueryOutput> {
        #[derive(Serialize)]
        struct Payload {
            sql: String
        }
        let payload = Payload { sql: query };
        let path = self.base_url.join("sql")?;
        let res = self.client
            .post(path)
            .json(&payload)
            .send().await?;
        if res.status() != 200 {
            dbg!(res);
            return Err(APIClientError::InvalidStatusCode.into());
        }
        Ok(res.json().await.context("Sql query")?)
    }

    pub async fn get_users(&self) -> Result<Vec<UserSummary>> {
        let query: String = r#"
            SELECT id,nom AS first_name,last_name,email FROM users;
        "#.to_string();

        let users_val = self.sql_query(query).await.context("Fetching users")?;
        
        Ok(serde_json::from_value(users_val.results)?)
    }

    pub async fn get_user_next_id(&self) -> Result<u64> {
        let query: String = r#"
            SELECT id FROM users ORDER BY id DESC LIMIT 1
        "#.to_string();

        let users_id_val = self.sql_query(query).await.context("Fetching users")?;

        #[derive(Deserialize)]
        struct UserIdEntry {
            id: u64
        }
        
        let users_ids: Vec<UserIdEntry> = serde_json::from_value(users_id_val.results)?;

        Ok(users_ids.iter().nth(0).map(|x| x.id).unwrap_or(1)+1)
    }

    pub async fn get_transactions(&self, id_year: u32)
        -> Result<Vec<TransactionSummary>>
    {
        let query: String = format!(r#"
            SELECT id,reference FROM acc_transactions WHERE id_year={} AND reference LIKE 'HA/%';
        "#, id_year).to_string();

        let val = self.sql_query(query).await.context("Fetching transactions")?;
        
        Ok(serde_json::from_value(val.results)?)
    }

    pub async fn create_user(&self, user: &User, next_id: u64)
        -> Result<UserSummary>
    {
        // single-user import
        // create virtual file
        let u = user.clone();

        let mut csv_content: String = String::new();
        csv_content.push_str("numero,nom,last_name,adresse,code_postal,ville,pays,telephone,email,annee_naissance,profession,interets,lettre_infos,date_inscription\n");
        csv_content.push_str(
            format!("{},{:?},{:?},{:?},{:?},{:?},{:?},{:?},{:?},{},{:?},{:?},{},{}\n",
                "".to_string(),
                u.first_name.clone().unwrap_or("".to_string()),
                u.last_name.clone(),
                u.address,
                u.postal_code,
                u.city,
                u.country,
                u.phone.unwrap_or("".to_string()),
                u.email.clone().unwrap_or("".to_string()),
                u.birth_year.map(|x| format!("{}", x)).unwrap_or("".to_string()),
                u.job.unwrap_or("".to_string()),
                u.skills.unwrap_or("".to_string()),
                1,
                user.register_time.format("%d/%m/%Y")
            ).as_str());

        use reqwest::multipart::Form;
        use reqwest::multipart::Part;
        
        let part = Part::text(csv_content).file_name("file");

        let form = Form::new()
            .part("file", part);

        let res = self.client
            .post(self.base_url.join("user/import/")?)
            .multipart(form)
            .send().await?;
        
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        Ok(
            UserSummary {
                id: Id(next_id),
                first_name: u.first_name,
                last_name: u.last_name,
                email: u.email
            }
        )
    }

    pub async fn register_user_to_service(&self, user: &UserSummary)
        -> Result<()>
    {
        // single-user import
        // create virtual file
        let u = user.clone();

        let mut csv_content: String = String::new();
        csv_content.push_str(
            r#""Numéro de membre","Activité","Tarif","Date d'inscription","Date d'expiration","Montant à régler","Payé ?""#);
        csv_content.push_str("\n");
        csv_content.push_str(
            format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
                u.id,
                "Cotisation 2023-2024",
                "Physique Individuelle",
                "10/10/2023",
                "10/10/2025",
                "10",
                "Oui"
            ).as_str());

        use reqwest::multipart::Form;
        use reqwest::multipart::Part;
        
        let part = Part::text(csv_content).file_name("file");

        let form = Form::new()
            .part("file", part);

        let res = self.client
            .post(self.base_url.join("services/subscriptions/import")?)
            .multipart(form)
            .send().await?;
        
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        Ok(())
    }
}