package common

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httputil"
	url2 "net/url"
	"strconv"
	"strings"
	"time"

	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/config"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/db/model"
	"github.com/pingcap-inc/clinic/pkg/o11y"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	_const "github.com/pingcap-inc/clinic/pkg/const"
	"github.com/pingcap-inc/clinic/pkg/db"
	"github.com/pingcap-inc/clinic/pkg/rbac"
)

func Proxy(c *gin.Context, host string, ctx *ApiUserContext, targetPath string, pairs ...Pair) error {
	data, err := json.Marshal(ctx)
	if err != nil {
		return err
	}

	url := &url2.URL{
		Scheme: "http",
		Host:   host,
	}
	proxy := httputil.NewSingleHostReverseProxy(url)
	originDirector := proxy.Director
	director := func(req *http.Request) {
		req.Header.Set(UserHeader, string(data))
		req.URL.Path = targetPath

		if len(pairs) > 0 {
			q := req.URL.Query()
			for _, pair := range pairs {
				q.Add(pair.Key, pair.Value)
			}

			req.URL.RawQuery = q.Encode()
		}

		originDirector(req)
	}
	proxy.Director = director

	proxy.ServeHTTP(c.Writer, c.Request)

	return nil
}

const (
	UserHeader = "clinic-user"
)

type ApiUserContext struct {
	UserID uint
	OrgID  uint
}

type Project struct {
	ID       uint64
	TenantID uint64
}

func GetTaskStatusByDataType(dt _const.TaskType, file *db.UploadFile) db.StatusType {
	switch dt {
	case _const.MetricTask:
		return db.StatusType(file.RebuildMetricStatus)
	case _const.LogTask:
		return db.StatusType(file.RebuildLogStatus)
	// case _const.SlowLogTask:
	//	return db.StatusType(file.RebuildLogStatus)
	case _const.RebuildConfTask:
		return db.StatusType(file.RebuildConfigurationStatus)
		// case _const.ReportTask:
		//	return db.StatusType(file.ReportStatus)
	}

	return ""
}

func (q *PaginationQuery) Init() {
	q.Limit = 10
	q.Page = 1
	q.RawLimit = false
}

func ParsePaginationQuery(queryStr string) *PaginationQuery {
	// Set default values.
	p := &PaginationQuery{}
	p.Init()

	u, err := url2.Parse(queryStr)
	if err != nil {
		return p
	}
	query := u.Query()

	if limitStr := query.Get("limit"); limitStr != "" {
		if limit, err := strconv.Atoi(limitStr); err == nil {
			p.Limit = limit
			p.RawLimit = true
		}
	}

	if pageStr := query.Get("page"); pageStr != "" {
		if page, err := strconv.Atoi(pageStr); err == nil {
			p.Page = page
		}
	}

	// p.Sort = ParseSort(queryStr)
	return p
}

func CstTimeZone(t *time.Time) *time.Time {
	if t == nil {
		return t
	}

	location, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		log.Error("load Asia/Shanghai failed", zap.Error(err))
		return t
	}

	nt := t.In(location)

	return &nt
}

func InternalRoleToResponseRole(internalRoleName string) (string, int) {
	if internalRoleName == "admin" {
		return "Admin", 7
	}

	if internalRoleName == "editor" {
		return "Analyst", 5
	}

	if internalRoleName == "viewer" {
		return "Viewer", 1
	}

	return "", 0
}

func GetOrgFromAPIPath(c *gin.Context) (*session.CtxUser, *db.Organization, error) {
	u := session.GetUser(c)
	if u == nil {
		return nil, nil, errors.New("the user is not found")
	}

	oid, err := GetOrgIDFromAPIPath(c)
	if err != nil {
		return nil, nil, err
	}

	org, err := datasource.GetOrganization(u, oid)
	if err != nil {
		log.Error("query org failed", zap.Uint("org", oid), zap.Error(err))
		return nil, nil, errors.New("query organization failed")
	}

	return u, org, nil
}

func GetOrgIDFromAPIPath(c *gin.Context) (uint, error) {
	orgID, err := strconv.ParseUint(c.Param("oid"), 10, 64)
	if err != nil {
		return 0, error2.ErrNotFound.New("the org is not found")
	}
	return uint(orgID), nil
}

func GetVersion(tidbVersion string, sortedVersionsAsc []string) string {
	if strings.Contains(tidbVersion, "master") || strings.Contains(tidbVersion, "nightly") || len(sortedVersionsAsc) < 1 {
		return "master"
	}
	// as PM and TSE suggested, use `v6.5.x` for `unknown` version.
	// Pinterest misused diag tools, thus their clusters i.e. 7194894536380533749 has `unknown` version
	// N.B. real a hack patch.
	if tidbVersion == "unknown" {
		return "v6.5.x"
	}

	var result string
	for _, v := range sortedVersionsAsc {
		if CompareVersion(v, tidbVersion) >= 0 {
			result = v
			break
		}
	}
	if result == "" {
		result = sortedVersionsAsc[len(sortedVersionsAsc)-1]
	}

	return result
}

// ignore patch versio

func CompareVersion(x, y string) int {
	splitedX := strings.Split(x, ".")
	splitedY := strings.Split(y, ".")
	verLen := len(splitedX)
	if len(splitedY) < verLen {
		verLen = len(splitedY)
	}
	for i := 0; i < verLen; i++ {
		c := strings.Compare(splitedX[i], splitedY[i])
		if c != 0 {
			return c
		}
	}
	if len(x) > len(y) {
		return 1
	} else if len(x) < len(y) {
		return -1
	}

	return 0
}

func CanReadCluster(c *gin.Context, auth rbac.Auth, clusterID string) (*session.CtxUser, *db.Organization, *db.Cluster, bool, error) {
	if clusterID == "" {
		return nil, nil, nil, false, fmt.Errorf("cluster id is empty")
	}

	u, org, err := GetOrgFromAPIPath(c)
	if err != nil {
		return nil, nil, nil, false, err
	}
	cluster, err := datasource.GetCluster(u, clusterID, org.ID)
	if err != nil {
		return nil, nil, nil, false, fmt.Errorf("get cluster failed: %v, cluster_id: %s", err, clusterID)
	}
	if cluster.OrgID != org.ID {
		return nil, nil, nil, false, fmt.Errorf("cluster %s not belong to org %d", clusterID, org.ID)
	}

	if !u.Internal && org.OrgType == "cloud" {
		cfg := config.GetConfig()
		if org.SupportPlan != "Enterprise" && org.SupportPlan != "Premium" {
			openToExternal := cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateOrgs, uint64(org.DbaasOrgID))
			if !openToExternal {
				return nil, nil, nil, false, fmt.Errorf("cloud org %d not open to external", org.ID)
			}
		}
		if cluster.DeployType != o11y.DedicatedType {
			return nil, nil, nil, false, fmt.Errorf("only dedicated cluster is accessible")
		}
		projectID, err := strconv.ParseUint(cluster.ProjectID, 10, 64)
		if err != nil {
			return nil, nil, nil, false, fmt.Errorf("parse project id failed: %v", err)
		}
		ok, err := datasource.HasPermissionForCloudUser(u, org.DbaasOrgID, uint(projectID))
		if err != nil {
			return nil, nil, nil, false, fmt.Errorf("access cluster resource failed: %v, user: %d, cluster: %s", err, u.UserID, clusterID)
		}
		return u, org, cluster, ok, nil
	}

	ok, err := auth.Auth(u.UserID, u.Internal, rbac.Cluster(), rbac.ReadActing(), org.ID)
	if err != nil {
		return nil, nil, nil, false, fmt.Errorf("access cluster resource failed: %v, user: %d, cluster: %s", err, u.UserID, clusterID)
	}
	return u, org, cluster, ok, nil
}

func CheckExternalUserPermission(user *session.CtxUser, orgID uint, clusterID string) (bool, error) {
	org, err := datasource.GetOrganization(user, orgID)
	if err != nil {
		return false, err
	}

	cluster, err := datasource.GetCluster(user, clusterID, org.ID)
	if err != nil {
		return false, err
	}

	if cluster.OrgID != org.ID {
		return false, fmt.Errorf("cluster %s not belong to org %d", clusterID, org.ID)
	}

	if !user.Internal && org.OrgType == "cloud" {
		projectID, err := strconv.ParseUint(cluster.ProjectID, 10, 64)
		if err != nil {
			return false, err
		}

		ok, err := datasource.HasPermissionForCloudUser(user, org.DbaasOrgID, uint(projectID))
		if err != nil || !ok {
			return false, err
		}
	}

	return true, nil
}

func CanReadCloudCluster(c *gin.Context, auth rbac.Auth, clusterID string) (*session.CtxUser, *db.Cluster, bool, error) {
	u := session.GetUser(c)
	if u == nil {
		return nil, nil, false, errors.New("the user is not found")
	}

	if clusterID == "" {
		return nil, nil, false, fmt.Errorf("cluster id is empty")
	}
	cluster, err := model.GetAnyCluster(clusterID)
	if err != nil {
		return nil, nil, false, fmt.Errorf("get cluster failed: %v, cluster_id: %s", err, clusterID)
	}

	// force to use internal user to get org info, check permission later
	org, err := datasource.GetOrganizationByCloudID(&session.CtxUser{Internal: true, Email: "clinic@pingcap.com"}, uint(cluster.TenantID))
	if err != nil {
		return nil, nil, false, fmt.Errorf("get org failed: %v, org_id: %d", err, cluster.TenantID)
	}

	if u.Internal {
		return u, cluster, true, nil
	}

	cfg := config.GetConfig()
	if org.SupportPlan != "Enterprise" && org.SupportPlan != "Premium" {
		openToExternal := cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateOrgs, uint64(org.DbaasOrgID))
		if !openToExternal {
			return nil, nil, false, fmt.Errorf("cloud org %d not open to external", org.ID)
		}
		if cluster.DeployType != o11y.DedicatedType {
			return nil, nil, false, fmt.Errorf("only dedicated cluster is accessible")
		}
	}
	projectID, err := strconv.ParseUint(cluster.ProjectID, 10, 64)
	if err != nil {
		return nil, nil, false, fmt.Errorf("parse project id failed: %v", err)
	}
	ok, err := datasource.HasPermissionForCloudUser(u, org.DbaasOrgID, uint(projectID))
	if err != nil {
		return nil, nil, false, fmt.Errorf("access cluster resource failed: %v, user: %d, cluster: %s", err, u.UserID, clusterID)
	}
	return u, cluster, ok, nil
}

func CanReadCloudProject(c *gin.Context, auth rbac.Auth, projectID string) (*session.CtxUser, *Project, bool, error) {
	u := session.GetUser(c)
	if u == nil {
		return nil, nil, false, errors.New("the user is not found")
	}

	// get project
	id, err := strconv.ParseUint(projectID, 10, 64)
	if err != nil {
		return nil, nil, false, fmt.Errorf("failed to parse project_id: %v, project_id: %s", err, projectID)
	}

	project, err := datasource.GetProject(&session.CtxUser{Internal: true, Email: "clinic@pingcap.com"}, id)
	if err != nil {
		return nil, nil, false, fmt.Errorf("get project failed: %v, project_id: %s", err, projectID)
	}
	if project == nil {
		return nil, nil, false, fmt.Errorf("project %v is not found", projectID)
	}

	if u.Internal {
		return u, &Project{
			ID:       id,
			TenantID: project.TenantId,
		}, true, nil
	}

	// for external user, force to use internal user to get org info, check permission later
	org, err := datasource.GetOrganizationByCloudID(&session.CtxUser{Internal: true, Email: "clinic@pingcap.com"}, uint(project.TenantId))
	if err != nil {
		return nil, nil, false, fmt.Errorf("get org failed: %v, org_id: %d", err, project.TenantId)
	}

	cfg := config.GetConfig()
	if org.SupportPlan != "Enterprise" && org.SupportPlan != "Premium" {
		openToExternal := cfg.ExternalFeatureGates.FeatureEnabled(config.FeatureGateOrgs, uint64(org.DbaasOrgID))
		if !openToExternal {
			return nil, nil, false, fmt.Errorf("cloud org %d not open to external", org.ID)
		}
	}

	ok, err := datasource.HasPermissionForCloudUser(u, org.DbaasOrgID, uint(id))
	if err != nil {
		return nil, nil, false, fmt.Errorf("access project resource failed: %v, user: %d, org: %v, project: %s", err, u.UserID, org.ID, projectID)
	}
	return u, &Project{
		ID:       id,
		TenantID: project.TenantId,
	}, ok, nil
}
