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


#[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_refs: 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)]
#[fully_pub]
struct Membership {
    id: Id,
    users_ids: Vec<Id>,
    service_name: String,
    mode_name: String,
    start_time: DateTime<Utc>,
    end_time: DateTime<Utc>,
    payed_amount: f64
}

#[derive(Debug, Clone)]
#[fully_pub]
enum TransactionKind {
    Expense,
    Revenue
}

impl From<TransactionKind> for String {
    fn from(val: TransactionKind) -> Self {
        match val {
            TransactionKind::Expense => "EXPENSE".to_string(),
            TransactionKind::Revenue => "REVENUE".to_string()
        }
    }
}

#[derive(Debug, Clone)]
#[fully_pub]
struct SimpleTransaction {
    label: String,
    kind: TransactionKind,
    inception_time: DateTime<Utc>,
    amount: f64,
    credit_account_code: String, 
    debit_account_code: String,
    reference: String,
    linked_users: Vec<Id>,
    linked_services: Vec<Id>,
    accounting_year: Id 
}


#[derive(Debug, Clone, Deserialize)]
#[fully_pub]
struct AccountingYear {
    id: Id,
    label: String,
    closed: u32,

    #[serde(deserialize_with = "deserialize_date", rename="start_date")]
    start_date: NaiveDate,
    #[serde(deserialize_with = "deserialize_date", rename="end_date")]
    end_date: NaiveDate
}


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

#[derive(Debug, Clone)]
#[fully_pub]
struct ClientConfig {
    base_url: Url,
    proxy: Option<reqwest::Proxy>,
    user_agent: String
}

impl Default for ClientConfig {
    fn default() -> Self {
        ClientConfig {
            proxy: None,
            base_url: Url::parse("https://paheko.example.org/api/") // the traling slash is important
                .expect("Expected valid paheko API base URL"),
            user_agent: "".to_string()
        }
    }
}

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

impl Default for Client {
    fn default() -> Self {
        let base_config: ClientConfig = Default::default();
        Client {
            client: Client::get_base_client_builder(&base_config)
                .build()
                .expect("Expected reqwest client to be built"),
            config: base_config
        }
    }
}

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(config: ClientConfig) -> Client {
        Client {
            client: Client::get_base_client_builder(&config)
                .build()
                .expect("Expected reqwest client to be built"),
            config
        }
    }

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

        let mut builder = reqwest::Client::builder()
                .default_headers(default_headers);
        if let Some(proxy) = &config.proxy {
            builder = builder.proxy(proxy.clone());
        }
        builder
    }

    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.config.clone(), credentials)
    }
}


#[derive(Debug, Clone)]
pub struct AuthentifiedClient {
    _credentials: Credentials,
    client: reqwest::Client,
    config: ClientConfig
}

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

#[derive(Debug)]
#[fully_pub]
struct UserServiceRegistration {
    id: Id
}

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

    pub async fn sql_query(&self, query: String) -> Result<SqlQueryOutput> {
        #[derive(Serialize)]
        struct Payload {
            sql: String
        }
        let payload = Payload { sql: query };
        let path = self.config.base_url.join("sql")?;
        let res = self.client
            .post(path)
            .json(&payload)
            .send().await?;
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        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_next_id(&self, table_name: &str) -> Result<u64> {
        let query: String = format!(r#"
            SELECT id FROM {} ORDER BY id DESC LIMIT 1
        "#, table_name).to_string();

        let data = self.sql_query(query).await.context("Fetching next id from table")?;

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

        Ok(ids.get(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 get_accounting_years(&self)
        -> Result<Vec<AccountingYear>>
    {
        let path = self.config.base_url.join("accounting/years")?;
        let res = self.client
            .get(path)
            .send().await?;
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        res.json().await.context("Get accounting years")
    }

    /// get a list of membership 
    pub async fn get_service_subscriptions(&self, service_name: &str)
        -> Result<Vec<Membership>>
    {
        let query: String = format!(r#"
            SELECT su.id,su.id_user,su.date,su.expiry_date FROM services_users AS su JOIN services AS s ON su.id_service = s.id WHERE s.label = '{}';
        "#, service_name);

        let val = self.sql_query(query).await.context("Fetching service subscriptions")?;
        
        #[derive(Deserialize)]
        struct Row {
            id: u64,
            id_user: u64,
            #[serde(deserialize_with = "deserialize_date")]
            date: NaiveDate,
            #[serde(deserialize_with = "deserialize_date")]
            expiry_date: NaiveDate
        }
        let intermidiate: Vec<Row> = serde_json::from_value(val.results)?;
        // regroup the row with the same id
        Ok(intermidiate
            .group_by(|a,b| a.id == b.id)
            .map(|rows| {
                let base = rows.first().unwrap();
                
                Membership {
                    id: Id(base.id),
                    mode_name: service_name.to_string(),
                    service_name: "".to_string(),
                    start_time: DateTime::<Utc>::from_naive_utc_and_offset(
                        base.date.and_hms_opt(0, 0, 0).unwrap(),
                        Utc
                    ),
                    end_time: DateTime::<Utc>::from_naive_utc_and_offset(
                        base.expiry_date.and_hms_opt(0, 0, 0).unwrap(),
                        Utc
                    ),
                    users_ids: rows.iter().map(|x| Id(x.id_user)).collect(),
                    payed_amount: 0.0
                }
            }).collect()
        )
    }

    pub async fn create_user(&self, user: &GeneralizedAnswer, 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",
                next_id.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.clone().unwrap_or("".to_string()),
                u.email.clone().unwrap_or("".to_string()),
                u.birth_year.map(|x| format!("{}", x)).unwrap_or("".to_string()),
                u.job.clone().unwrap_or("".to_string()),
                u.skills.clone().unwrap_or("".to_string()),
                1,
                user.inception_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.config.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, user_membership: &Membership, next_id: u64)
        -> Result<UserServiceRegistration>
    {
        // 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('\n');
        csv_content.push_str(
            format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
                u.id,
                user_membership.service_name,
                user_membership.mode_name,
                user_membership.start_time.format("%d/%m/%Y").to_string(),
                user_membership.end_time.format("%d/%m/%Y").to_string(),
                format!("{}", user_membership.payed_amount),
                "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.config.base_url.join("services/subscriptions/import")?)
            .multipart(form)
            .send().await?;
        
        if res.status() != 200 {
            return Err(APIClientError::InvalidStatusCode.into());
        }
        Ok(UserServiceRegistration {
            id: Id(next_id)
        })
    }

    pub async fn register_transaction(&self, transaction: SimpleTransaction)
        -> Result<()>
    {
        use reqwest::multipart::Form;
        
        let mut form = Form::new()
            .text("id_year", transaction.accounting_year.to_string())
            .text("label", transaction.label)
            .text("date", transaction.inception_time.format("%d/%m/%Y").to_string())
            .text("type", Into::<String>::into(transaction.kind))
            .text("amount", format!("{}", transaction.amount))
            .text("debit", transaction.debit_account_code)
            .text("credit", transaction.credit_account_code)
            // "Numéro pièce comptable" enregistré au niveau de la transaction
            .text("reference", transaction.reference) 
        ;

        for linked_id in transaction.linked_users {
            form = form.text("linked_users[]", format!("{}", linked_id.0));
        }
        // only possible with paheko fork
        for linked_id in transaction.linked_services {
            form = form.text("linked_services[]", format!("{}", linked_id.0));
        }

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