Documentation
use br_reqwest::Client;
use base64::Engine;
use crate::{PayMode, PayNotify, RefundNotify, RefundStatus, TradeState, TradeType, Types};
use json::{array, object, JsonValue};
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::rsa::Padding;
use openssl::sign::Signer;
use rand::distr::Alphanumeric;
use rand::Rng;

/// 通联支付
/// 文档: https://prodoc.allinpay.com/project/38/
/// 统一下单: https://prodoc.allinpay.com/project/17/
#[derive(Clone, Debug)]
pub struct Allinpay {
    pub debug: bool,
    /// 应用appid
    pub appid: String,
    /// 服务商商家号
    pub sp_mchid: String,
    /// 通知地址
    pub notify_url: String,
    /// 微信小程序appid
    pub appid_mini: String,
    /// RSA私钥
    pub rsa_private: String,
}
impl Allinpay {
    pub fn https(&mut self, url: &str, mut data: JsonValue) -> Result<JsonValue, String> {
        let randomstr: String = rand::rng().sample_iter(&Alphanumeric).take(16).map(char::from).collect();
        data["randomstr"] = randomstr.into();
        data["signtype"] = "RSA".into();

        let http_url = if self.debug {
            format!("https://syb-test.allinpay.com{}", url)
        } else {
            format!("https://vsp.allinpay.com{}", url)
        };
        let mut http = Client::new();

        let mut keys = vec![];

        for (key, value) in data.entries() {
            if !value.is_empty() && key != "sign" {
                keys.push(key);
            }
        }
        keys.sort();

        let mut txt = vec![];
        for key in keys {
            txt.push(format!("{}={}", key, data[key]));
        }
        let txt = txt.join("&");
        let sig_b64 = match sign_sha1withrsa_base64(self.rsa_private.as_str(), txt.as_bytes()) {
            Ok(e) => e,
            Err(e) => {
                return Err(e.to_string())
            }
        };
        data["sign"] = sig_b64.into();
        let res = match http.post(&http_url).form_urlencoded(data).send() {
            Ok(e) => e,
            Err(e) => return Err(e.to_string())
        };
        let res = res.json()?;
        if res["retcode"].eq("FAIL") {
            return Err(res["retmsg"].to_string());
        }
        println!("{:#}", res);
        Ok(res)
    }
}
impl PayMode for Allinpay {
    fn check(&mut self) -> Result<bool, String> {
        let data = object! {
            appid:self.appid.clone(),
            orgid:self.sp_mchid.clone(),
            cusid:"",
            reqsn:"",
        };
        match self.https("/apiweb/tranx/query", data) {
            Ok(e) => e,
            Err(e) => {
                if e.contains("auth_code不存在") {
                    return Ok(true);
                }
                return Err(e);
            }
        };
        Ok(true)
    }

    fn get_sub_mchid(&mut self, sub_mchid: &str) -> Result<JsonValue, String> {
        let res = self.https("alipay.open.agent.signstatus.query", object! {
            "biz_content":{
                "pid":sub_mchid,
                "product_codes":array!["QUICK_WAP_WAY"]
            }
        })?;
        if !res["code"].eq("10000") {
            return Err(res["msg"].to_string());
        }
        for item in res["sign_status_list"].members() {
            if item["status"].eq("none") {
                return Err(format!("{} 未开通", res["product_name"]));
            }
        }
        Ok(true.into())
    }


    fn config(&mut self) -> JsonValue {
        todo!()
    }


    fn pay(&mut self, channel: &str, types: Types, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, sp_openid: &str) -> Result<JsonValue, String> {
        let total = format!("{:.0}", total_fee * 100.0);

        let mut order = object! {
            appid:self.appid.clone(),
            orgid:self.sp_mchid.clone(),
            cusid:sub_mchid,
            trxamt:total,
            reqsn:out_trade_no,
            paytype:"",
            body:description,
            acct:"",
            notify_url:self.notify_url.clone()
        };
        match (types.clone(), channel) {
            (Types::MiniJsapi, "wechat") => {
                order["paytype"] = "W06".into();
                order["sub_appid"] = self.appid_mini.clone().into();
                order["acct"] = sp_openid.into();
            }
            (Types::MiniJsapi, "alipay") => {
                order["paytype"] = "A02".into();
                order["sub_appid"] = self.appid_mini.clone().into();
                order["acct"] = sp_openid.into();
            }
            (Types::Jsapi, "wechat") => {
                order["paytype"] = "W02".into();
            }
            (Types::Jsapi, "alipay") => {
                order["paytype"] = "A02".into();
            }
            (Types::H5, "wechat") => {
                order["paytype"] = "W02".into();
            }
            (Types::H5, "alipay") => {
                order["paytype"] = "A02".into();
            }
            (Types::Native, "wechat") => {
                order["paytype"] = "W01".into();
            }
            (Types::Native, "alipay") => {
                order["paytype"] = "A01".into();
            }
            _ => {
                order["paytype"] = "".into();
            }
        };
        match self.https("/apiweb/unitorder/pay", order) {
            Ok(e) => {
                match types {
                    Types::Native => {}
                    Types::Jsapi | Types::H5 => {
                        return Ok(object! {url:e});
                    }
                    Types::MiniJsapi => {
                        return Ok(e);
                    }
                    Types::App => {}
                    Types::Micropay => {}
                }
                Ok(e)
            }
            Err(e) => {
                if e == "ACCESS_FORBIDDEN" {
                    return Err("请商户授权JSAPI 绑定 服务商小程序APPID".to_string());
                }
                println!("Err: {e:#}");
                Err(e)
            }
        }
    }

    fn micropay(&mut self, _channel: &str, auth_code: &str, sub_mchid: &str, out_trade_no: &str, description: &str, total_fee: f64, org_openid: &str, _ip: &str) -> Result<JsonValue, String> {
        let total = format!("{:.0}", total_fee * 100.0);

        let order = object! {
            appid:self.appid.clone(),
            orgid:self.sp_mchid.clone(),
            cusid:sub_mchid,
            trxamt:total,
            reqsn:out_trade_no,
            body:description,
            authcode:auth_code,
            operatorid:org_openid,
        };
        match self.https("/apiweb/unitorder/scanqrpay", order) {
            Ok(e) => {
                if e["code"].ne("10000") {
                    return Err(e["msg"].to_string());
                }
                if e["msg"].eq("FAIL") {
                    if e["err_code_des"].ne("需要用户输入支付密码") {
                        return Err(e["err_code_des"].to_string());
                    }
                    let res = PayNotify {
                        trade_type: TradeType::MICROPAY,
                        out_trade_no: out_trade_no.to_string(),
                        sp_mchid: self.sp_mchid.clone(),
                        sub_mchid: sub_mchid.to_string(),
                        sp_appid: self.appid.to_string(),
                        transaction_id: "".to_string(),
                        success_time: 0,
                        sp_openid: "".to_string(),
                        sub_openid: "".to_string(),
                        total: total_fee,
                        payer_total: total_fee,
                        currency: "CNY".to_string(),
                        payer_currency: "CNY".to_string(),
                        trade_state: TradeState::NOTPAY,
                    };
                    return Ok(res.json());
                }
                let res = PayNotify {
                    trade_type: TradeType::MICROPAY,
                    out_trade_no: out_trade_no.to_string(),
                    sp_mchid: self.sp_mchid.clone(),
                    sub_mchid: sub_mchid.to_string(),
                    sp_appid: self.appid.to_string(),
                    transaction_id: e["trade_no"].to_string(),
                    success_time: PayNotify::datetime_to_timestamp(e["gmt_payment"].as_str().unwrap(), "%Y-%m-%d %H:%M:%S"),
                    sp_openid: e["buyer_open_id"].to_string(),
                    sub_openid: e["buyer_open_id"].to_string(),
                    total: total_fee,
                    payer_total: total_fee,
                    currency: "CNY".to_string(),
                    payer_currency: "CNY".to_string(),
                    trade_state: TradeState::SUCCESS,
                };
                Ok(res.json())
            }
            Err(e) => Err(e)
        }
    }


    fn close(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
        let order = object! {
            appid:self.appid.clone(),
            orgid:self.sp_mchid.clone(),
            cusid:sub_mchid,
            oldreqsn:out_trade_no,
        };
        match self.https("/apiweb/tranx/close", order) {
            Ok(_) => Ok(true.into()),
            Err(e) => {
                if e.contains("交易不存在") {
                    return Ok(true.into());
                }
                Err(e)
            }
        }
    }

    fn pay_query(&mut self, out_trade_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
        let order = object! {
            appid:self.appid.clone(),
            orgid:self.sp_mchid.clone(),
            cusid:sub_mchid,
            reqsn:out_trade_no,
        };
        match self.https("/apiweb/tranx/query", order) {
            Ok(e) => {
                if e.has_key("code") && e["code"].as_str().unwrap() != "10000" {
                    return Err(e["msg"].to_string());
                }
                let buyer_open_id = if e.has_key("buyer_open_id") {
                    e["buyer_open_id"].to_string()
                } else {
                    e["buyer_user_id"].to_string()
                };
                let send_pay_date = e["send_pay_date"].as_str().unwrap_or("");
                let success_time = if !send_pay_date.is_empty() {
                    PayNotify::datetime_to_timestamp(send_pay_date, "%Y-%m-%d %H:%M:%S")
                } else {
                    0
                };
                let res = PayNotify {
                    trade_type: TradeType::None,
                    out_trade_no: e["out_trade_no"].to_string(),
                    sp_mchid: "".to_string(),
                    sub_mchid: sub_mchid.to_string(),
                    sp_appid: "".to_string(),
                    transaction_id: e["trade_no"].to_string(),
                    success_time,
                    sp_openid: buyer_open_id.clone(),
                    sub_openid: buyer_open_id.clone(),
                    total: (e["total_amount"].to_string().parse::<f64>().unwrap_or(0.0)),
                    payer_total: (e["total_amount"].to_string().parse::<f64>().unwrap_or(0.0)),
                    currency: "CNY".to_string(),
                    payer_currency: "CNY".to_string(),
                    trade_state: TradeState::from(e["trade_status"].as_str().unwrap_or("")),
                };
                Ok(res.json())
            }
            Err(e) => Err(e)
        }
    }

    fn pay_micropay_query(&mut self, out_trade_no: &str, sub_mchid: &str, _channel: &str) -> Result<JsonValue, String> {
        self.pay_query(out_trade_no, sub_mchid)
    }

    fn pay_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
        todo!()
    }

    fn refund(&mut self, sub_mchid: &str, out_trade_no: &str, _transaction_id: &str, out_refund_no: &str, amount: f64, _total: f64, _currency: &str) -> Result<JsonValue, String> {
        let total = format!("{:.0}", amount * 100.0);

        let body = object! {
            appid:self.appid.clone(),
            orgid:self.sp_mchid.clone(),
            cusid:sub_mchid,
            trxamt:total.clone(),
            reqsn:out_refund_no,
            oldreqsn:out_trade_no,
            notify_url:self.notify_url.clone(),

        };
        match self.https("/apiweb/tranx/refund", body.clone()) {
            Ok(e) => {
                if e.has_key("code") && e["code"].as_str().unwrap() != "10000" {
                    return Err(e["msg"].to_string());
                }
                let res = RefundNotify {
                    out_trade_no: e["out_trade_no"].to_string(),
                    refund_no: out_refund_no.to_string(),
                    sp_mchid: "".to_string(),
                    sub_mchid: sub_mchid.to_string(),
                    transaction_id: e["trade_no"].to_string(),
                    refund_id: out_refund_no.to_string(),
                    success_time: PayNotify::datetime_to_timestamp(e["gmt_refund_pay"].as_str().unwrap(), "%Y-%m-%d %H:%M:%S"),
                    total: total.to_string().parse::<f64>().unwrap_or(0.0),
                    refund: e["refund_fee"].to_string().parse::<f64>().unwrap(),
                    payer_total: e["refund_fee"].to_string().parse::<f64>().unwrap(),
                    payer_refund: e["send_back_fee"].to_string().parse::<f64>().unwrap(),
                    status: RefundStatus::from(e["fund_change"].as_str().unwrap()),
                };
                Ok(res.json())
            }
            Err(e) => Err(e)
        }
    }

    fn micropay_refund(&mut self, _sub_mchid: &str, _out_trade_no: &str, _transaction_id: &str, _out_refund_no: &str, _amount: f64, _total: f64, _currency: &str, _refund_text: &str) -> Result<JsonValue, String> {
        todo!()
    }

    fn refund_notify(&mut self, _nonce: &str, _ciphertext: &str, _associated_data: &str) -> Result<JsonValue, String> {
        todo!()
    }

    fn refund_query(&mut self, trade_no: &str, _out_refund_no: &str, sub_mchid: &str) -> Result<JsonValue, String> {
        self.pay_query(trade_no, sub_mchid)
    }

    fn incoming(&mut self, _business_code: &str, _contact_info: JsonValue, _subject_info: JsonValue, _business_info: JsonValue, _settlement_info: JsonValue, _bank_account_info: JsonValue) -> Result<JsonValue, String> {
        todo!()
    }
}


fn sign_sha1withrsa_base64(private_pem: &str, data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
    let private_pem = pkcs1_private_pem_from_base64(private_pem);
    let pkey = PKey::private_key_from_pem(private_pem.as_bytes())?;
    let mut signer = Signer::new(MessageDigest::sha1(), &pkey)?;
    signer.set_rsa_padding(Padding::PKCS1)?;
    signer.update(data)?;
    let sig = signer.sign_to_vec()?;
    Ok(base64::engine::general_purpose::STANDARD.encode(sig))
}

fn pkcs1_private_pem_from_base64(body: &str) -> String {
    fn wrap64(s: &str) -> String {
        s.as_bytes().chunks(64).map(|c| std::str::from_utf8(c).unwrap()).collect::<Vec<&str>>().join("\n")
    }
    format!(
        "-----BEGIN RSA PRIVATE KEY-----\n{}\n-----END RSA PRIVATE KEY-----\n",
        wrap64(body.trim().replace(['\r', '\n', ' '], "").as_str())
    )
}