package handler

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strings"
	"time"

	"github.com/micro/micro/v3/service/errors"
	"github.com/micro/micro/v3/service/logger"
	pb "github.com/micro/services/currency/proto"
	"github.com/patrickmn/go-cache"
)

var (
	re = regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
)

type Currency struct {
	Api   string
	Cache *cache.Cache
}

func (c *Currency) Codes(ctx context.Context, req *pb.CodesRequest, rsp *pb.CodesResponse) error {
	// try the cache
	if codes, ok := c.Cache.Get("codes"); ok {
		rsp.Codes = codes.([]*pb.Code)
		return nil
	}

	resp, err := http.Get(c.Api + "/codes")
	if err != nil {
		logger.Errorf("Failed to get codes: %v\n", err)
		return errors.InternalServerError("currency.codes", "failed to get codes")
	}
	defer resp.Body.Close()

	b, _ := ioutil.ReadAll(resp.Body)

	if resp.StatusCode != 200 {
		logger.Errorf("Failed to get codes (non 200): %d %v\n", resp.StatusCode, string(b))
		return errors.InternalServerError("currency.codes", "failed to get codes")
	}

	var respBody map[string]interface{}

	if err := json.Unmarshal(b, &respBody); err != nil {
		logger.Errorf("Failed to unmarshal codes: %v\n", err)
		return errors.InternalServerError("currency.codes", "failed to get codes")
	}

	codes, ok := respBody["supported_codes"].([]interface{})
	if !ok {
		logger.Errorf("Failed to convert rates to map[string]interface{}: %v\n", ok)
		return errors.InternalServerError("currency.rates", "failed to get rates")
	}

	for _, code := range codes {
		c := code.([]interface{})
		rsp.Codes = append(rsp.Codes, &pb.Code{
			Name:     c[0].(string),
			Currency: c[1].(string),
		})
	}

	// set for a period of time
	c.Cache.Set("codes", rsp.Codes, time.Hour)

	return nil
}

func (c *Currency) History(ctx context.Context, req *pb.HistoryRequest, rsp *pb.HistoryResponse) error {
	if len(req.Code) == 0 {
		return errors.BadRequest("currency.rates", "missing code")
	}
	if len(req.Code) != 3 {
		return errors.BadRequest("currency.rates", "code is invalid")
	}

	if len(req.Date) == 0 {
		return errors.BadRequest("currency.history", "missing date")
	}

	if !re.MatchString(req.Date) {
		return errors.BadRequest("currency.history", "invalid date")
	}

	// try the cache
	if rates, ok := c.Cache.Get("history:" + req.Code + req.Date); ok {
		rsp.Code = req.Code
		rsp.Date = req.Date
		rsp.Rates = rates.(map[string]float64)
		return nil
	}

	parts := strings.Split(req.Date, "-")

	resp, err := http.Get(fmt.Sprintf("%s/history/%s/%s/%s/%s", c.Api, req.Code, parts[0], parts[1], parts[2]))
	if err != nil {
		logger.Errorf("Failed to get historic rates: %v\n", err)
		return errors.InternalServerError("currency.history", "failed to get history")
	}
	defer resp.Body.Close()

	b, _ := ioutil.ReadAll(resp.Body)

	if resp.StatusCode != 200 {
		logger.Errorf("Failed to get historic rates (non 200): %d %v\n", resp.StatusCode, string(b))
		return errors.InternalServerError("currency.history", "failed to get history")
	}

	var respBody map[string]interface{}

	if err := json.Unmarshal(b, &respBody); err != nil {
		logger.Errorf("Failed to unmarshal historic rates: %v\n", err)
		return errors.InternalServerError("currency.history", "failed to get history")
	}

	rates, ok := respBody["conversion_rates"].(map[string]interface{})
	if !ok {
		logger.Errorf("Failed to convert historic rates to map[string]interface{}: %v\n", ok)
		return errors.InternalServerError("currency.history", "failed to get history")
	}

	rsp.Code = req.Code
	rsp.Date = req.Date
	rsp.Rates = make(map[string]float64)

	for code, rate := range rates {
		rsp.Rates[code], _ = rate.(float64)
	}

	// set for a period of time
	c.Cache.Set("history:"+req.Code+req.Date, rsp.Rates, time.Hour*24)

	return nil
}

func (c *Currency) Rates(ctx context.Context, req *pb.RatesRequest, rsp *pb.RatesResponse) error {
	if len(req.Code) == 0 {
		return errors.BadRequest("currency.rates", "missing code")
	}
	if len(req.Code) != 3 {
		return errors.BadRequest("currency.rates", "code is invalid")
	}

	// try the cache
	if rates, ok := c.Cache.Get("rates:" + req.Code); ok {
		rsp.Code = req.Code
		rsp.Rates = rates.(map[string]float64)
		return nil
	}

	resp, err := http.Get(c.Api + "/latest/" + req.Code)
	if err != nil {
		logger.Errorf("Failed to get rates: %v\n", err)
		return errors.InternalServerError("currency.rates", "failed to get rates")
	}
	defer resp.Body.Close()

	b, _ := ioutil.ReadAll(resp.Body)

	if resp.StatusCode != 200 {
		logger.Errorf("Failed to get rates (non 200): %d %v\n", resp.StatusCode, string(b))
		return errors.InternalServerError("currency.rates", "failed to get rates")
	}

	var respBody map[string]interface{}

	if err := json.Unmarshal(b, &respBody); err != nil {
		logger.Errorf("Failed to unmarshal rates: %v\n", err)
		return errors.InternalServerError("currency.rates", "failed to get rates")
	}

	rates, ok := respBody["conversion_rates"].(map[string]interface{})
	if !ok {
		logger.Errorf("Failed to convert rates to map[string]interface{}: %v\n", ok)
		return errors.InternalServerError("currency.rates", "failed to get rates")
	}

	rsp.Code = req.Code
	rsp.Rates = make(map[string]float64)

	for code, rate := range rates {
		rsp.Rates[code], _ = rate.(float64)
	}

	// set for a period of time
	c.Cache.Set("rates:"+req.Code, rsp.Rates, cache.DefaultExpiration)

	return nil
}

func (c *Currency) Convert(ctx context.Context, req *pb.ConvertRequest, rsp *pb.ConvertResponse) error {
	if len(req.From) != 3 {
		return errors.BadRequest("currency.convert", "invalid from code")
	}
	if len(req.To) != 3 {
		return errors.BadRequest("currency.convert", "invalid to code")
	}

	uri := fmt.Sprintf("%s/pair/%s/%s", c.Api, req.From, req.To)

	// try the cache
	if req.Amount == 0 {
		rate, ok := c.Cache.Get("pair:" + req.From + req.To)
		if ok {
			rsp.From = req.From
			rsp.To = req.To
			rsp.Rate = rate.(float64)
			return nil
		}
	}

	if req.Amount > 0.0 {
		uri = fmt.Sprintf("%s/%v", uri, req.Amount)
	}

	resp, err := http.Get(uri)
	if err != nil {
		logger.Errorf("Failed to convert: %v\n", err)
		return errors.InternalServerError("currency.convert", "failed to convert")
	}
	defer resp.Body.Close()

	b, _ := ioutil.ReadAll(resp.Body)

	if resp.StatusCode != 200 {
		logger.Errorf("Failed to get convert (non 200): %d %v\n", resp.StatusCode, string(b))
		return errors.InternalServerError("currency.convert", "failed to convert")
	}

	var respBody map[string]interface{}

	if err := json.Unmarshal(b, &respBody); err != nil {
		logger.Errorf("Failed to unmarshal conversion: %v\n", err)
		return errors.InternalServerError("currency.convet", "failed to convert")
	}

	rsp.From = req.From
	rsp.To = req.To
	rsp.Rate, _ = respBody["conversion_rate"].(float64)
	rsp.Amount, _ = respBody["conversion_result"].(float64)

	// save for a period of time
	c.Cache.Set("pair:"+req.From+req.To, rsp.Rate, cache.DefaultExpiration)

	return nil
}
