package controller

import (
	"bytes"
	"crawler/config"
	"crawler/model"
	"crawler/utility"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"io"
	"log"
	"net/http"
	"reflect"
	"strconv"
	"strings"
	"time"
)

type G2AInterface interface {
	Init()
	Products(c *gin.Context)
	Order(c *gin.Context)
}

type G2A struct {
	ApiURL          string
	IsTest          bool
	Credentials     config.G2ACredential
	G2AProductModel model.G2AProductModel
	G2AReportModel  model.G2AReportModel
	G2AOrderModel   model.G2AOrderModel
	G2AErrorModel   model.G2AErrorModel
	G2AErrorResponse
	OrderDetail model.OrderDetailModel
	ShopConfig  model.ShopConfigModel
}

// G2A 오류
type G2AErrorResponse struct {
	Status  any
	Message any
	Code    any
}

// G2A 환경설정 조회
func (this *G2A) Init() {
	this.IsTest = this.ShopConfig.G2AIsTest()

	if this.IsTest == true {
		this.Credentials = config.G2A.Sandbox
	} else {
		switch config.Env.DeveloperEnv {
		case config.LOCAL:
			this.Credentials = config.G2A.Sandbox
		case config.DEV:
			this.Credentials = config.G2A.Export
		default:
			log.Panic("G2A 설정이 잘못되었습니다.")
		}
	}

	var checksum = sha256.Sum256([]byte(fmt.Sprintf("%s%s%s",
		this.Credentials.Client.ID, this.Credentials.Email, this.Credentials.Client.Secret,
	)))

	this.Credentials.Hash = fmt.Sprintf("%x", checksum[:])
}

// G2A 상품 조회
func (this *G2A) Products(c *gin.Context) {
	this.Init()

	fmt.Println("G2A 상품 수집을 시작합니다.")

	var (
		result                             = this.G2AProductModel.G2AProductResult
		total, insert, update, error, page = 0, 0, 0, 0, 1
		startTime                          = time.Now()
		endTime                            float64
		output                             = func(msg string) string {
			s := fmt.Sprintf("Total : %d\n", total)
			s += fmt.Sprintf("Error: %d\n", error)
			s += fmt.Sprintf("Insert: %d\n", insert)
			s += fmt.Sprintf("Update: %d\n", update)
			s += fmt.Sprintf("Page: %d\n", page)
			s += fmt.Sprintf("소요시간: %f초\n", endTime)
			return msg + "\n" + s
		}
	)

	for {

		this.ApiURL = fmt.Sprintf("%s/products?page=%d", this.Credentials.API, page)
		receivedDTO := this.HttpGetRequest()

		byteData, _ := json.Marshal(receivedDTO)
		err := json.Unmarshal(byteData, &result)
		utility.Check(err, config.ERROR_LOG_PATH_G2A)

		// 상품 수집
		if len(result.Docs) > 0 {
			for i, product := range result.Docs {
				if this.G2AProductModel.IsExists(product.ID) {
					update++
				} else {
					insert++
				}

				if this.IsTest {
					result.Docs[i].IsTest = 1
				} else {
					result.Docs[i].IsTest = 0
				}
			}

			err = this.G2AProductModel.Insert(result.Docs)
			if err != nil {
				error++
				utility.SendMessageToG2AError(output("[G2A 상품수집 중 오류]"))
				utility.Check(err, config.ERROR_LOG_PATH_G2A)
				break
			}

			total += len(result.Docs)
			page++
			fmt.Printf("G2A 상품 수집 중... / total: %d, page: %d\n", total, page)
		} else {
			endTime = time.Since(startTime).Seconds()
			fmt.Printf("G2A 상품 수집 종료! / total: %d, page: %d\n", total, page)
			break
		}
	}

	// 통계 저장
	this.G2AReportModel.Insert(model.G2AReport{
		TotalCnt:  total,
		InsertCnt: insert,
		UpdateCnt: update,
		ProcessAt: endTime,
		LastPage:  page,
	})

	// 텔레그램 알림
	utility.SendMessageToG2AProduct(output("[G2A 상품수집 완료]"))

	c.JSON(http.StatusOK, gin.H{
		"total":  total,
		"insert": insert,
		"update": update,
		"page":   page,
	})
}

// G2A 품절 확인 후 상품 정보 갱신
func (this *G2A) CheckOutOfStock(c *gin.Context) {
	this.Init()

	fmt.Println("G2A 상품 품절 확인")

	var (
		result    = this.G2AProductModel.G2AProductResult
		productID = c.Query("id")
		inStock   = "yes"
	)

	if productID == "" {
		c.JSON(http.StatusOK, gin.H{
			"inStock": "no",
		})
		return
	}

	this.ApiURL = fmt.Sprintf("%s/products?id=%s", this.Credentials.API, productID)
	receivedDTO := this.HttpGetRequest()

	byteData, _ := json.Marshal(receivedDTO)
	err := json.Unmarshal(byteData, &result)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	// 상품 정보 확인
	if len(result.Docs) > 0 {
		for _, product := range result.Docs {
			// 조회된 상품 정보를 한번 갱신한다.
			err = this.G2AProductModel.Update(product)
			utility.Check(err, config.ERROR_LOG_PATH_G2A)

			if product.Qty <= 0 { // 재고가 0 이하면 품절이다.
				inStock = "no"
				break
			}
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"inStock": inStock,
	})
}

// G2A 주문
// Add an Order -> Pay for an order
func (this *G2A) Order(c *gin.Context) {
	var requestKey = c.Query("requestKey")
	if requestKey == "" {
		c.JSON(http.StatusForbidden, G2AErrorResponse{
			Code:    http.StatusForbidden,
			Message: "잘못된 요청입니다.",
		})
		return
	}

	// base64_decode 처리
	rawRequestKey, err := base64.URLEncoding.DecodeString(requestKey)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)
	requestKey = string(rawRequestKey)

	var (
		key = utility.MakeMD5(config.ENCRYPT_KEY)
		iv  = config.ENCRYPT_IV
	)

	// 복호화
	decryptCode, err := utility.AesDecrypt(requestKey, key, iv)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	// 이미 주문한 주문건이 있는지 확인
	if this.G2AOrderModel.IsExists(decryptCode) {
		c.JSON(http.StatusOK, gin.H{
			"result": "OK",
		})
		return
	}

	id := strings.Split(decryptCode, "/")
	var (
		orderID, _       = strconv.Atoi(id[0])
		orderDetailID, _ = strconv.Atoi(id[1])
		productID        = id[2]
	)

	if this.OrderDetail.IsExists(orderID, orderDetailID) == false {
		c.JSON(http.StatusNotFound, G2AErrorResponse{
			Code:    http.StatusNotFound,
			Message: "비 정상적인 주문서 입니다.",
		})
		return
	}

	//orderDetail, err := this.OrderDetail.Info(orderID, orderDetailID)
	//utility.Check(err, config.ERROR_LOG_PATH_G2A)

	tmpProductID, _ := strconv.Atoi(productID)
	G2AProductInfo, err := this.G2AProductModel.Info(tmpProductID)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	var params = this.G2AOrderModel.G2AOrderParams
	params.ProductID = productID
	params.Currency = "KRW"
	params.MaxPrice = G2AProductInfo.RetailMinPrice

	if err := c.ShouldBind(&params); err != nil {
		c.JSON(http.StatusBadRequest, G2AErrorResponse{
			Code:    http.StatusBadRequest,
			Message: err.Error(),
		})
		return
	}

	this.Init()

	// 주문을 요청한다.
	this.ApiURL = fmt.Sprintf("%s/order", this.Credentials.API)
	var (
		orderResult = this.G2AOrderModel.G2AOrderResult
		receivedDTO = this.HttpPostRequest(params)
		typeName    = reflect.TypeOf(receivedDTO).Name()
	)

	if typeName == "G2AErrorModel" {
		c.JSON(http.StatusBadRequest, receivedDTO)
		return
	}

	byteData, _ := json.Marshal(receivedDTO)
	err = json.Unmarshal(byteData, &orderResult)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	// 주문 번호를 결제 처리한다.
	this.ApiURL = fmt.Sprintf("%s/order/pay/%s", this.Credentials.API, orderResult.OrderID)
	var orderPayResult = this.G2AOrderModel.G2AOrderPayResult
	receivedDTO = this.HttpPutRequest()
	typeName = reflect.TypeOf(receivedDTO).Name()

	if typeName == "G2AErrorModel" {
		c.JSON(http.StatusBadRequest, receivedDTO)
		return
	}

	byteData, _ = json.Marshal(receivedDTO)
	err = json.Unmarshal(byteData, &orderPayResult)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	// 결제 확인
	if orderPayResult.Status == false {
		c.JSON(http.StatusOK, gin.H{
			"message": "결제에 실패하였습니다.",
		})
		return
	}

	// 결제 및 주문이 제대로 되었는지 확인한다.
	this.ApiURL = fmt.Sprintf("%s/order/details/%s", this.Credentials.API, orderResult.OrderID)
	var orderDetailResult = this.G2AOrderModel.G2AOrderDetailResult
	receivedDTO = this.HttpGetRequest()
	typeName = reflect.TypeOf(receivedDTO).Name()

	if typeName == "G2AErrorModel" {
		c.JSON(http.StatusBadRequest, receivedDTO)
		return
	}

	byteData, _ = json.Marshal(receivedDTO)
	err = json.Unmarshal(byteData, &orderDetailResult)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	time.Sleep(1 * time.Second)

	// 결제 및 주문이 제대로 되었는지 확인한다.
	this.ApiURL = fmt.Sprintf("%s/order/key/%s", this.Credentials.API, orderResult.OrderID)
	var orderKeyResult = this.G2AOrderModel.G2AOrderKeyResult
	receivedDTO = this.HttpGetRequest()
	typeName = reflect.TypeOf(receivedDTO).Name()

	if typeName == "G2AErrorModel" {
		c.JSON(http.StatusBadRequest, receivedDTO)
		return
	}

	byteData, _ = json.Marshal(receivedDTO)
	err = json.Unmarshal(byteData, &orderKeyResult)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	// 주문 기록 저장
	this.G2AOrderModel.Insert(model.G2AOrder{
		RequestKey:           decryptCode,
		RequestOrderID:       orderID,
		RequestOrderDetailID: orderDetailID,
		Status:               orderDetailResult.Status,
		OrderID:              orderResult.OrderID,
		ProductID:            params.ProductID,
		Code:                 orderKeyResult.Key,
		Price:                orderDetailResult.Price,
		Currency:             orderDetailResult.Currency,
		TransactionID:        orderPayResult.TransactionID,
	})

	// 텔레그램 알림
	utility.SendMessageToG2AOrder(func() string {
		msg := fmt.Sprintf("[G2A 주문완료]\n")
		msg += fmt.Sprintf("Order ID : %d\n", orderID)
		msg += fmt.Sprintf("Order Detail ID : %d\n", orderDetailID)
		msg += fmt.Sprintf("Status : %s\n", orderDetailResult.Status)
		msg += fmt.Sprintf("Code: %s\n", orderKeyResult.Key)
		msg += fmt.Sprintf("Price: %f\n", orderDetailResult.Price)
		msg += fmt.Sprintf("Currency: %s\n", orderDetailResult.Currency)
		return msg
	}())

	c.JSON(http.StatusOK, gin.H{
		"result": "OK",
	})
}

// HTTP Get CALL
func (this *G2A) HttpGetRequest() interface{} {
	req, err := http.NewRequest("GET", this.ApiURL, nil)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	req.Header.Set("Accept", "application/json")
	req.Header.Add("Authorization", fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))

	client := &http.Client{
		Timeout: 8 * time.Second,
	}
	res, err := client.Do(req)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	if res.StatusCode != 200 {
		return this.SetG2AError(data, nil)
	}

	var receivedDTO interface{}
	err = json.Unmarshal(data, &receivedDTO)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	return receivedDTO
}

// HTTP Post CALL
func (this *G2A) HttpPostRequest(params model.G2AOrderParams) interface{} {
	payloadBytes, err := json.Marshal(params)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	body := bytes.NewReader(payloadBytes)

	req, err := http.NewRequest("POST", this.ApiURL, body)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	req.Header.Set("Content-Type", "application/json")
	req.Header.Add("Authorization", fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))

	client := &http.Client{
		Timeout: 8 * time.Second,
	}
	res, err := client.Do(req)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	if res.StatusCode != 200 {
		return this.SetG2AError(data, payloadBytes)
	}

	var receivedDTO interface{}
	err = json.Unmarshal(data, &receivedDTO)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	return receivedDTO
}

// HTTP Put CALL
func (this *G2A) HttpPutRequest() interface{} {
	req, err := http.NewRequest("PUT", this.ApiURL, nil)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	req.Header.Set("Content-type", "application/json")
	req.Header.Set("Content-Length", "0")
	req.Header.Add("Authorization", fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))

	fmt.Println(fmt.Sprintf("%s, %s", this.Credentials.Client.ID, this.Credentials.Hash))

	client := &http.Client{
		Timeout: 8 * time.Second,
	}
	res, err := client.Do(req)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)
	defer res.Body.Close()

	data, err := io.ReadAll(res.Body)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	if res.StatusCode != 200 {
		return this.SetG2AError(data, nil)
	}

	var receivedDTO interface{}
	err = json.Unmarshal(data, &receivedDTO)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	return receivedDTO
}

// G2A 주문 API 오류 발생 처리
func (this *G2A) SetG2AError(data []byte, params interface{}) model.G2AErrorModel {
	var G2AError = this.G2AErrorModel
	err := json.Unmarshal(data, &G2AError)
	utility.Check(err, config.ERROR_LOG_PATH_G2A)

	G2AError.URL = this.ApiURL
	if params != nil {
		G2AError.Params = string(params.([]byte))
	}
	G2AError.Save()

	// 텔레그램 알림
	if err != nil {
		utility.SendMessageToG2AError(fmt.Sprintf("[G2A 주문 중 오류]\n%s", err.Error()))
	}

	return G2AError
}
