package globalapi

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"sync"

	"github.com/ddliu/go-httpclient"
	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/tidbcloud/observability/pkg/common/awsauth"
	"github.com/tidbcloud/observability/pkg/common/errors"
	"github.com/tidbcloud/observability/pkg/common/message"
	"github.com/tidbcloud/observability/pkg/globalservice"
	"go.uber.org/zap"
)

// QueryClustersGlobally
// @Summary query clusters globally
// @Description  query clusters globally
// @Tags cluster
// @Accept json
// @Produce json
// @Param queryReq query message.QueryClustersReq false "query request"
// @Success 200 {object} message.Result{data=message.QueryClustersResp}
// @Failure 500 {object} message.Result
// @Router /global/clusters/ [get]
func QueryClustersGlobally(c *gin.Context) {
	log.Info("QueryClustersGlobally start")
	defer log.Info("QueryClustersGlobally end")
	query := message.QueryClustersReq{}
	err := c.ShouldBindQuery(&query)
	if err != nil {
		c.JSON(errors.ParameterInvalid.GetHttpCode(), &message.Result{
			Code:    int(errors.ParameterInvalid),
			Message: err.Error(),
		})
		return
	}

	if err := validateShowDeletedQuery(query); err != nil {
		c.JSON(errors.ParameterInvalid.GetHttpCode(), &message.Result{
			Code:    int(errors.ParameterInvalid),
			Message: err.Error(),
		})
		return
	}

	allClusterResp := new(message.QueryClustersResp)

	allRegions := globalservice.GetGlobalManager().GetGlobalMetaCache()
	total := 0
	for _, regions := range allRegions.Vendors {
		for _, r := range regions {
			total = total + len(r.Routers)
		}
	}

	headers := map[string]string{}

	wg := &sync.WaitGroup{}

	responseChannel := make(chan RegionResult, total)
	wgResponse := &sync.WaitGroup{}
	wgResponse.Add(1)
	go func() {
		for result := range responseChannel {
			err = appendRegionResult(allClusterResp, result)
			if err != nil {
				log.Error("Failed to append region result", zap.Error(err))
			}
		}
		wgResponse.Done()
	}()

	for vendor, regions := range allRegions.Vendors {
		for _, region := range regions {
			for _, lr := range region.Routers {
				routeAddress := lr.DnsAddress
				if len(routeAddress) == 0 {
					err = errors.NewErrorf(errors.GetRegionalRouteAddressError,
						"failed to get the routing address of vendor `%s` and region `%s`, logical region %s", string(vendor), region.Region, lr.LogicalRegionName)
					responseChannel <- RegionResult{err: err, primary: lr.Primary}
					continue
				}

				regionalApiUrl := buildRegionalClustersURL(routeAddress, string(vendor), region.Region, query)

				// Create a deep copy of headers for this iteration to avoid race conditions
				regionHeaders := make(map[string]string)
				for k, v := range headers {
					regionHeaders[k] = v
				}

				if lr.AccessPrincipal != "" {
					sigv4Headers, err := awsauth.GetSigV4Headers(c.Request.Context(), awsauth.SigV4AuthInfo{
						RoleArn:    lr.AccessPrincipal,
						ExternalID: lr.AccessExternalID,
						Region:     region.Region,
					}, c.Request.Method, regionalApiUrl, nil)
					if err != nil {
						log.Error("Failed to generate SigV4 headers", zap.Error(err))
						responseChannel <- RegionResult{err: err, primary: lr.Primary}
						continue
					}
					// Merge SigV4 headers into request headers
					for key, value := range sigv4Headers {
						regionHeaders[key] = value
					}
					log.Debug("Added SigV4 authentication headers for BYOC regional server",
						zap.String("roleArn", lr.AccessPrincipal))
				}
				wg.Add(1)
				go queryClustersInRegion(regionalApiUrl, regionHeaders, responseChannel, wg, lr.Primary)
			}
		}

	}

	wg.Wait()

	close(responseChannel)

	wgResponse.Wait()

	c.JSON(http.StatusOK, message.Result{
		Data: allClusterResp,
	})
}

func buildRegionalClustersURL(routeAddress, vendor, region string, query message.QueryClustersReq) string {
	params := url.Values{}
	params.Set("vendor", vendor)
	params.Set("region", region)
	params.Set("bizType", query.BizType)
	params.Set("tenantID", query.TenantID)
	params.Set("projectID", query.ProjectID)
	params.Set("applicationID", query.ApplicationID)
	params.Set("showDeleted", fmt.Sprintf("%t", query.ShowDeleted))

	return fmt.Sprintf("%s/api/v1/clusters/?%s", routeAddress, params.Encode())
}

func validateShowDeletedQuery(query message.QueryClustersReq) error {
	if !query.ShowDeleted {
		return nil
	}
	if query.BizType == "" || query.TenantID == "" || query.ApplicationID == "" {
		return errors.New(errors.ParameterInvalid, "showDeleted requires bizType, tenantID and applicationID")
	}
	return nil
}

func queryClustersInRegion(regionalApiUrl string, headers map[string]string, response chan RegionResult, wg *sync.WaitGroup, primary bool) {
	defer wg.Done()

	resp := message.QueryClustersResp{}

	httpResp, err := httpclient.Begin().WithHeaders(headers).Get(regionalApiUrl)
	if err != nil {
		response <- RegionResult{
			resp,
			err,
			primary,
		}
		return
	}

	body, err := io.ReadAll(httpResp.Body)
	if err != nil {
		response <- RegionResult{
			resp,
			errors.New(errors.IOReadFileError, err.Error()),
			primary,
		}
		return
	}

	if httpResp.StatusCode != http.StatusOK {
		response <- RegionResult{
			resp,
			errors.NewErrorf(errors.UnrecognizedError, "%s", string(body)),
			primary,
		}
		return
	}

	result := UnmarshalResult{}
	if err = json.Unmarshal(body, &result); err != nil {
		response <- RegionResult{
			resp,
			errors.New(errors.UnmarshalError, err.Error()),
			primary,
		}
		return
	}

	response <- RegionResult{
		result.Data, nil, primary,
	}
}

type UnmarshalResult struct {
	Data message.QueryClustersResp `json:"data"`
}

type RegionResult struct {
	resp    message.QueryClustersResp
	err     error
	primary bool
}

func appendRegionResult(resp *message.QueryClustersResp, regionResult RegionResult) error {
	partial := resp.Parital
	if regionResult.err != nil {
		if !regionResult.primary {
			partial = true
			log.Warn("skip non-primary region error", zap.Error(regionResult.err))
		} else {
			return regionResult.err
		}
	}

	resp.Clusters = append(resp.Clusters, regionResult.resp.Clusters...)
	resp.Parital = partial
	return nil
}
