// Copyright 2022 The Casdoor Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pp

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/casdoor/casdoor/util"
)

type GcPaymentProvider struct {
	Xmpch     string
	SecretKey string
	Host      string
}

type GcPayReqInfo struct {
	OrderDate string `json:"orderdate"`
	OrderNo   string `json:"orderno"`
	Amount    string `json:"amount"`
	Xmpch     string `json:"xmpch"`
	Body      string `json:"body"`
	ReturnUrl string `json:"return_url"`
	NotifyUrl string `json:"notify_url"`
	PayerId   string `json:"payerid"`
	PayerName string `json:"payername"`
	Remark1   string `json:"remark1"`
	Remark2   string `json:"remark2"`
}

type GcPayRespInfo struct {
	Jylsh     string `json:"jylsh"`
	Amount    string `json:"amount"`
	PayerId   string `json:"payerid"`
	PayerName string `json:"payername"`
	PayUrl    string `json:"payurl"`
}

type GcNotifyRespInfo struct {
	Xmpch      string  `json:"xmpch"`
	OrderDate  string  `json:"orderdate"`
	OrderNo    string  `json:"orderno"`
	Amount     float64 `json:"amount"`
	Jylsh      string  `json:"jylsh"`
	TradeNo    string  `json:"tradeno"`
	PayMethod  string  `json:"paymethod"`
	OrderState string  `json:"orderstate"`
	ReturnType string  `json:"return_type"`
	PayerId    string  `json:"payerid"`
	PayerName  string  `json:"payername"`
}

type GcRequestBody struct {
	Op          string `json:"op"`
	Xmpch       string `json:"xmpch"`
	Version     string `json:"version"`
	Data        string `json:"data"`
	RequestTime string `json:"requesttime"`
	Sign        string `json:"sign"`
}

type GcResponseBody struct {
	Op         string `json:"op"`
	Xmpch      string `json:"xmpch"`
	Version    string `json:"version"`
	ReturnCode string `json:"return_code"`
	ReturnMsg  string `json:"return_msg"`
	Data       string `json:"data"`
	NotifyTime string `json:"notifytime"`
	Sign       string `json:"sign"`
}

type GcInvoiceReqInfo struct {
	BusNo        string `json:"busno"`
	PayerName    string `json:"payername"`
	IdNum        string `json:"idnum"`
	PayerType    string `json:"payertype"`
	InvoiceTitle string `json:"invoicetitle"`
	Tin          string `json:"tin"`
	Phone        string `json:"phone"`
	Email        string `json:"email"`
}

type GcInvoiceRespInfo struct {
	BusNo     string `json:"busno"`
	State     string `json:"state"`
	EbillCode string `json:"ebillcode"`
	EbillNo   string `json:"ebillno"`
	CheckCode string `json:"checkcode"`
	Url       string `json:"url"`
	Content   string `json:"content"`
}

func NewGcPaymentProvider(clientId string, clientSecret string, host string) *GcPaymentProvider {
	pp := &GcPaymentProvider{}

	pp.Xmpch = clientId
	pp.SecretKey = clientSecret
	pp.Host = host
	return pp
}

func (pp *GcPaymentProvider) doPost(postBytes []byte) ([]byte, error) {
	client := &http.Client{}

	var resp *http.Response
	var err error

	contentType := "text/plain;charset=UTF-8"
	body := bytes.NewReader(postBytes)

	req, err := http.NewRequest("POST", pp.Host, body)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", contentType)

	resp, err = client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)

	respBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return respBytes, nil
}

func (pp *GcPaymentProvider) Pay(r *PayReq) (*PayResp, error) {
	payReqInfo := GcPayReqInfo{
		OrderDate: util.GenerateSimpleTimeId(),
		OrderNo:   r.PaymentName,
		Amount:    getPriceString(r.Price),
		Xmpch:     pp.Xmpch,
		Body:      r.ProductDisplayName,
		ReturnUrl: r.ReturnUrl,
		NotifyUrl: r.NotifyUrl,
		Remark1:   r.PayerName,
		Remark2:   r.ProductName,
	}

	b, err := json.Marshal(payReqInfo)
	if err != nil {
		return nil, err
	}

	body := GcRequestBody{
		Op:          "OrderCreate",
		Xmpch:       pp.Xmpch,
		Version:     "1.4",
		Data:        base64.StdEncoding.EncodeToString(b),
		RequestTime: util.GenerateSimpleTimeId(),
	}

	params := fmt.Sprintf("data=%s&op=%s&requesttime=%s&version=%s&xmpch=%s%s", body.Data, body.Op, body.RequestTime, body.Version, body.Xmpch, pp.SecretKey)
	body.Sign = strings.ToUpper(util.GetMd5Hash(params))

	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}

	respBytes, err := pp.doPost(bodyBytes)
	if err != nil {
		return nil, err
	}

	var respBody GcResponseBody
	err = json.Unmarshal(respBytes, &respBody)
	if err != nil {
		return nil, err
	}

	if respBody.ReturnCode != "SUCCESS" {
		return nil, fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
	}

	payRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
	if err != nil {
		return nil, err
	}

	var payRespInfo GcPayRespInfo
	err = json.Unmarshal(payRespInfoBytes, &payRespInfo)
	if err != nil {
		return nil, err
	}
	payResp := &PayResp{
		PayUrl: payRespInfo.PayUrl,
	}
	return payResp, nil
}

func (pp *GcPaymentProvider) Notify(body []byte, orderId string) (*NotifyResult, error) {
	reqBody := GcRequestBody{}
	m, err := url.ParseQuery(string(body))
	if err != nil {
		return nil, err
	}

	reqBody.Op = m["op"][0]
	reqBody.Xmpch = m["xmpch"][0]
	reqBody.Version = m["version"][0]
	reqBody.Data = m["data"][0]
	reqBody.RequestTime = m["requesttime"][0]
	reqBody.Sign = m["sign"][0]

	notifyReqInfoBytes, err := base64.StdEncoding.DecodeString(reqBody.Data)
	if err != nil {
		return nil, err
	}

	var notifyRespInfo GcNotifyRespInfo
	err = json.Unmarshal(notifyReqInfoBytes, &notifyRespInfo)
	if err != nil {
		return nil, err
	}

	providerName := ""
	productName := ""

	productDisplayName := ""
	paymentName := notifyRespInfo.OrderNo
	price := notifyRespInfo.Amount

	if notifyRespInfo.OrderState != "1" {
		return nil, fmt.Errorf("error order state: %s", notifyRespInfo.OrderDate)
	}
	notifyResult := &NotifyResult{
		ProductName:        productName,
		ProductDisplayName: productDisplayName,
		ProviderName:       providerName,
		OrderId:            orderId,
		Price:              price,
		PaymentStatus:      PaymentStatePaid,
		PaymentName:        paymentName,
	}
	return notifyResult, nil
}

func (pp *GcPaymentProvider) GetInvoice(paymentName string, personName string, personIdCard string, personEmail string, personPhone string, invoiceType string, invoiceTitle string, invoiceTaxId string) (string, error) {
	payerType := "0"
	if invoiceType == "Organization" {
		payerType = "1"
	}

	invoiceReqInfo := GcInvoiceReqInfo{
		BusNo:        paymentName,
		PayerName:    personName,
		IdNum:        personIdCard,
		PayerType:    payerType,
		InvoiceTitle: invoiceTitle,
		Tin:          invoiceTaxId,
		Phone:        personPhone,
		Email:        personEmail,
	}

	b, err := json.Marshal(invoiceReqInfo)
	if err != nil {
		return "", err
	}

	body := GcRequestBody{
		Op:          "InvoiceEBillByOrder",
		Xmpch:       pp.Xmpch,
		Version:     "1.4",
		Data:        base64.StdEncoding.EncodeToString(b),
		RequestTime: util.GenerateSimpleTimeId(),
	}

	params := fmt.Sprintf("data=%s&op=%s&requesttime=%s&version=%s&xmpch=%s%s", body.Data, body.Op, body.RequestTime, body.Version, body.Xmpch, pp.SecretKey)
	body.Sign = strings.ToUpper(util.GetMd5Hash(params))

	bodyBytes, err := json.Marshal(body)
	if err != nil {
		return "", err
	}

	respBytes, err := pp.doPost(bodyBytes)
	if err != nil {
		return "", err
	}

	var respBody GcResponseBody
	err = json.Unmarshal(respBytes, &respBody)
	if err != nil {
		return "", err
	}

	if respBody.ReturnCode != "SUCCESS" {
		return "", fmt.Errorf("%s: %s", respBody.ReturnCode, respBody.ReturnMsg)
	}

	invoiceRespInfoBytes, err := base64.StdEncoding.DecodeString(respBody.Data)
	if err != nil {
		return "", err
	}

	var invoiceRespInfo GcInvoiceRespInfo
	err = json.Unmarshal(invoiceRespInfoBytes, &invoiceRespInfo)
	if err != nil {
		return "", err
	}

	if invoiceRespInfo.State == "0" {
		return "", fmt.Errorf("申请成功，开票中")
	}

	if invoiceRespInfo.Url == "" {
		return "", fmt.Errorf("invoice URL is empty")
	}

	return invoiceRespInfo.Url, nil
}

func (pp *GcPaymentProvider) GetResponseError(err error) string {
	if err == nil {
		return "success"
	} else {
		return "fail"
	}
}
