package billing

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"time"

	"github.com/pingcap/dbaas/billing/api/model"
	"github.com/pingcap/dbaas/pkg/auth/auth0"
	"github.com/pingcap/log"
	"go.uber.org/zap"
)

//TODO: unify the error usage, change to "github.com/pingcap/errors"

type HttpClient interface {
	Do(r *http.Request) (*http.Response, error)
}

type HttpAuthenticator interface {
	Auth(r *http.Request) error
}

type Auth0Authenticator struct {
	Auth0Domain       string
	Auth0ClientID     string
	Auth0ClientSecret string
	Auth0Audience     string
}

func (auth *Auth0Authenticator) Auth(req *http.Request) error {
	token, err := auth0.GetClientToken(auth.Auth0Domain, auth.Auth0ClientID, auth.Auth0ClientSecret, auth.Auth0Audience)
	if err != nil {
		return err
	}
	req.Header.Add("Authorization", fmt.Sprintf("bearer %s", token))
	return nil
}

type Service interface {
	GetHttpResultAsString(method string, path string, payload []byte) (string, error)
	SendInvoiceNow(customer string, end int64) (*model.EmptyResponse, error)
	UpdateCustomer(customer string, payload *model.Customer) (*model.Customer, error)
	SetupIntent(customer string) (*model.SetupIntent, error)
	ListPaymentMethods(customer string, pagination Pagination) (*model.ListPaymentMethodResponse, error)
	DeletePaymentMethod(customer, id string) (*model.EmptyResponse, error)
	UpdatePaymentMethod(customer, id string, payload *model.PaymentMethod) (*model.PaymentMethod, error)
	ListInvoices(customer string, pagination Pagination) (*model.ListInvoiceResponse, error)
	GetCustomer(customer string) (*model.Customer, error)
	GetCurrentBillingCycle(customer string, startTime, endTime int64) (*model.CurrentBillingCycle, error)
	GetNodeUsageFee(customer string, startTime, endTime int64) (*model.NodeUsageFee, error)
	GetCloudUsageFee(customer string, startTime, endTime int64) (*model.CloudUsageFee, error)
	GetTenantHeartbeatFee(customer string, startTime, endTime int64) (*model.TenantHeartbeatFee, error)
	GetMonthUsageGroupByProject(customer string, year, month int) (*model.MonthUsageGroupByProject, error)
}

type FakeService struct{}

func (f FakeService) GetHttpResultAsString(method string, path string, payload []byte) (string, error) {
	panic("implement me")
}

func (f FakeService) SendInvoiceNow(customer string, end int64) (*model.EmptyResponse, error) {
	panic("implement me")
}

func (f FakeService) UpdateCustomer(customer string, payload *model.Customer) (*model.Customer, error) {
	panic("implement me")
}

func (f FakeService) SetupIntent(customer string) (*model.SetupIntent, error) {
	panic("implement me")
}

func (f FakeService) ListPaymentMethods(customer string, pagination Pagination) (*model.ListPaymentMethodResponse, error) {
	panic("implement me")
}

func (f FakeService) DeletePaymentMethod(customer, id string) (*model.EmptyResponse, error) {
	panic("implement me")
}

func (f FakeService) UpdatePaymentMethod(customer, id string, payload *model.PaymentMethod) (*model.PaymentMethod, error) {
	panic("implement me")
}

func (f FakeService) ListInvoices(customer string, pagination Pagination) (*model.ListInvoiceResponse, error) {
	panic("implement me")
}

func (f FakeService) GetCustomer(customer string) (*model.Customer, error) {
	panic("implement me")
}

func (f FakeService) GetCurrentBillingCycle(customer string, startTime, endTime int64) (*model.CurrentBillingCycle, error) {
	panic("implement me")
}

func (f FakeService) GetNodeUsageFee(customer string, startTime, endTime int64) (*model.NodeUsageFee, error) {
	panic("implement me")
}

func (f FakeService) GetCloudUsageFee(customer string, startTime, endTime int64) (*model.CloudUsageFee, error) {
	panic("implement me")
}

func (f FakeService) GetTenantHeartbeatFee(customer string, startTime, endTime int64) (*model.TenantHeartbeatFee, error) {
	panic("implement me")
}

func (f FakeService) GetMonthUsageGroupByProject(customer string, year, month int) (*model.MonthUsageGroupByProject, error) {
	panic("implement me")
}

var _ Service = &FakeService{}

func NewService(baseURL string, auth0Domain, auth0ClientID, auth0ClientSecret, auth0Audience string) Service {
	client := client{
		baseURL: baseURL,
		transport: &HttpTransport{
			HttpClient: &http.Client{Timeout: 60 * time.Second},
			Authenticator: &Auth0Authenticator{
				Auth0Domain:       auth0Domain,
				Auth0ClientID:     auth0ClientID,
				Auth0ClientSecret: auth0ClientSecret,
				Auth0Audience:     auth0Audience,
			},
		},
	}
	return &client
}

// client for billing system
type client struct {
	baseURL string

	transport ClientTransport
}

type ClientTransport interface {
	Submit(apiPath, method string, payload io.Reader) ([]byte, error)
}

type HttpTransport struct {
	HttpClient    HttpClient
	Authenticator HttpAuthenticator
}

func (c *client) SendInvoiceNow(customer string, end int64) (*model.EmptyResponse, error) {
	ret := &model.EmptyResponse{}
	return ret, c.callPOST(fmt.Sprintf("%s/invoices?end=%d", c.baseAPIPathForCustomer(customer), end), nil, ret)
}

func (c *client) GetHttpResultAsString(method string, path string, payload []byte) (string, error) {
	data, err := c.transport.Submit(fmt.Sprintf("%s%s", c.baseURL, path), method, bytes.NewReader(payload))
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func (c *client) UpdateCustomer(customer string, payload *model.Customer) (*model.Customer, error) {
	ret := &model.Customer{}
	return ret, c.callPUT(c.baseAPIPathForCustomer(customer), payload, ret)
}

func (c *client) SetupIntent(customer string) (*model.SetupIntent, error) {
	ret := &model.SetupIntent{}
	return ret, c.callPOST(fmt.Sprintf("%s/setup_intent", c.baseAPIPathForCustomer(customer)), &model.EmptyResponse{}, ret)
}

func (c *client) ListPaymentMethods(customer string, pagination Pagination) (*model.ListPaymentMethodResponse, error) {
	ret := &model.ListPaymentMethodResponse{}
	return ret, c.listEntities(fmt.Sprintf("%s/payment_methods", c.baseAPIPathForCustomer(customer)), pagination, ret)
}

func (c *client) DeletePaymentMethod(customer, id string) (*model.EmptyResponse, error) {
	ret := &model.EmptyResponse{}
	return ret, c.callDELETE(fmt.Sprintf("%s/payment_methods/%s", c.baseAPIPathForCustomer(customer), id), ret)
}

func (c *client) UpdatePaymentMethod(customer, id string, payload *model.PaymentMethod) (*model.PaymentMethod, error) {
	ret := &model.PaymentMethod{}
	return ret, c.callPUT(fmt.Sprintf("%s/payment_methods/%s", c.baseAPIPathForCustomer(customer), id), payload, ret)
}

func (c *client) ListInvoices(customer string, pagination Pagination) (*model.ListInvoiceResponse, error) {
	ret := &model.ListInvoiceResponse{}
	return ret, c.listEntities(fmt.Sprintf("%s/invoices", c.baseAPIPathForCustomer(customer)), pagination, ret)
}

func (c *client) GetCustomer(customer string) (*model.Customer, error) {
	ret := &model.Customer{}
	return ret, c.callGET(c.baseAPIPathForCustomer(customer), ret)
}

func (c *client) GetCurrentBillingCycle(customer string, startTime, endTime int64) (*model.CurrentBillingCycle, error) {
	ret := &model.CurrentBillingCycle{}
	return ret, c.callGET(fmt.Sprintf("%s/current_billing_cycle?start_time=%d&end_time=%d", c.baseAPIPathForCustomer(customer), startTime, endTime), ret)
}

func (c *client) GetNodeUsageFee(customer string, startTime, endTime int64) (*model.NodeUsageFee, error) {
	ret := &model.NodeUsageFee{}
	return ret, c.callGET(fmt.Sprintf("%s/node_usage_fee?start_time=%d&end_time=%d", c.baseAPIPathForCustomer(customer), startTime, endTime), ret)
}

func (c *client) GetCloudUsageFee(customer string, startTime, endTime int64) (*model.CloudUsageFee, error) {
	ret := &model.CloudUsageFee{}
	return ret, c.callGET(fmt.Sprintf("%s/cloud_usage_fee?start_time=%d&end_time=%d", c.baseAPIPathForCustomer(customer), startTime, endTime), ret)
}

func (c *client) GetTenantHeartbeatFee(customer string, startTime, endTime int64) (*model.TenantHeartbeatFee, error) {
	ret := &model.TenantHeartbeatFee{}
	return ret, c.callGET(fmt.Sprintf("%s/heartbeats?start_time=%d&end_time=%d", c.baseAPIPathForCustomer(customer), startTime, endTime), ret)
}

func (c *client) GetMonthUsageGroupByProject(customer string, year, month int) (*model.MonthUsageGroupByProject, error) {
	ret := &model.MonthUsageGroupByProject{}
	return ret, c.callGET(fmt.Sprintf("%s/month_usage_group_by_project?year=%d&month=%d", c.baseAPIPathForCustomer(customer), year, month), ret)
}

func (c *client) baseAPIPathForCustomer(customer string) string {
	return fmt.Sprintf("%s/api/v1/billing/customers/%s", c.baseURL, customer)
}

// Pagination query parameter
type Pagination struct {
	Limit         uint32
	StartingAfter string
	EndingBefore  string
	Start         int64
	End           int64
}

func (c *client) listEntities(apiPath string, pagination Pagination, output interface{}) error {
	q, err := url.ParseQuery("")
	if err != nil {
		return err
	}
	if pagination.Limit > 0 {
		q.Add("limit", fmt.Sprintf("%d", pagination.Limit))
	}
	if pagination.StartingAfter != "" {
		q.Add("starting_after", pagination.StartingAfter)
	}
	if pagination.EndingBefore != "" {
		q.Add("ending_before", pagination.EndingBefore)
	}
	if pagination.Start > 0 {
		q.Add("start_time", fmt.Sprintf("%d", pagination.Start))
	}
	if pagination.End > 0 {
		q.Add("end_time", fmt.Sprintf("%d", pagination.End))
	}
	return c.callGET(fmt.Sprintf("%s?%s", apiPath, q.Encode()), output)
}

func (c *client) callGET(apiPath string, output interface{}) error {
	return c.callWithNoPayload(apiPath, "GET", output)
}

func (c *client) callPUT(apiPath string, payload interface{}, output interface{}) error {
	return c.callWithPayload(apiPath, "PUT", payload, output)
}

func (c *client) callPOST(apiPath string, payload interface{}, output interface{}) error {
	return c.callWithPayload(apiPath, "POST", payload, output)
}

func (c *client) callDELETE(apiPath string, output interface{}) error {
	return c.callWithNoPayload(apiPath, "DELETE", output)
}

func (c *client) callWithPayload(apiPath, method string, payload interface{}, output interface{}) error {
	body, err := json.Marshal(payload)
	if err != nil {
		return err
	}
	respBody, err := c.transport.Submit(apiPath, method, bytes.NewReader(body))
	if err != nil {
		return err
	}
	return json.Unmarshal(respBody, output)
}

func (c *client) callWithNoPayload(apiPath, method string, output interface{}) error {
	body, err := c.transport.Submit(apiPath, method, nil)
	if err != nil {
		return err
	}
	return json.Unmarshal(body, output)
}

func (c *HttpTransport) Submit(apiPath, method string, payload io.Reader) ([]byte, error) {
	log.Info("call billing api", zap.String("path", apiPath))
	req, err := http.NewRequest(method, apiPath, payload)
	if err != nil {
		return nil, err
	}
	if err := c.Authenticator.Auth(req); err != nil {
		return nil, err
	}
	// #nosec
	resp, err := c.HttpClient.Do(req)
	if err != nil {
		return nil, err
	}
	// #nosec
	defer deferClose(resp.Body)
	bodyContent, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != 200 {
		return nil, fmt.Errorf("call billing failed with status code %d, msg:%s", resp.StatusCode, string(bodyContent))
	}
	return bodyContent, nil
}

func deferClose(c io.Closer) {
	if err := c.Close(); err != nil {
		log.Error("failed to close", zap.Error(err))
	}
}
