package service

import (
	"context"
	"dataservices/commons/config"
	"dataservices/commons/errs"
	"dataservices/commons/grpc"
	"dataservices/commons/http"
	"dataservices/ddb"
	dto "dataservices/internal/dto/dataapi"
	dto_regional "dataservices/internal/dto/dataapi_control"
	"dataservices/internal/entity"
	"dataservices/internal/repository"
	"dataservices/internal/repository_ddb"
	"fmt"
	"regexp"
	"strings"
	"sync"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const PendingToExpiredHours int64 = 24

var (
	validDomainRegexp = regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`)
	validPathRegexp   = regexp.MustCompile(`^/$|^/[A-Za-z0-9-_]+(/[A-Za-z0-9-_]+)*$`)
)

func ListBaseUrls(ctx context.Context, appId uint64) (*dto.ListBaseUrlsData, error) {
	// Global fast-fail keeps unsupported AliCloud custom-domain flows out of regional control.
	if err := GuardRegionalCapabilityForDataApp(ctx, appId, RegionalCapabilityCustomDomain); err != nil {
		return nil, err
	}
	dataapp, err := repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("query dataapp failed", err)
	}
	defaultDomains, err := listDefaultDomains(ctx, dataapp)
	if err != nil {
		return nil, err
	}
	regionSuccessfulDomainsMap, err := GetRegionSuccessfulDomainsMap(ctx, appId)
	if err != nil {
		return nil, err
	}
	baseUrls := make([]string, len(defaultDomains))
	for i, defaultDomain := range defaultDomains {
		baseUrls[i] = GenAPPServiceUrl(dataapp, getRegion(defaultDomain), regionSuccessfulDomainsMap)
	}
	return &dto.ListBaseUrlsData{BaseUrls: baseUrls}, nil
}

func ListDefaultDomains(ctx context.Context, appId uint64) (*dto.ListDefaultDomainsData, error) {
	// Global fast-fail keeps unsupported AliCloud custom-domain flows out of regional control.
	if err := GuardRegionalCapabilityForDataApp(ctx, appId, RegionalCapabilityCustomDomain); err != nil {
		return nil, err
	}
	dataapp, err := repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("query dataapp failed", err)
	}
	defaultDomainNames, err := listDefaultDomains(ctx, dataapp)
	if err != nil {
		return nil, err
	}

	defaultPath := GetAppDefaultPath(dataapp)

	regionSuccessfulDomainsMap, err := GetRegionSuccessfulDomainsMap(ctx, appId)
	if err != nil {
		return nil, err
	}
	baseUrls := make([]string, len(defaultDomainNames))
	for i, defaultDomain := range defaultDomainNames {
		baseUrls[i] = GenAPPServiceUrl(dataapp, getRegion(defaultDomain), regionSuccessfulDomainsMap)
	}

	return &dto.ListDefaultDomainsData{
		DefaultDomainNames: defaultDomainNames,
		DefaultPath:        defaultPath,
		BaseUrls:           baseUrls,
	}, nil
}

func listDefaultDomains(ctx context.Context, dataapp *entity.DataApp) ([]string, error) {
	result := make([]string, 0, 6)
	if config.IsDev() || config.IsStaging() {
		result = append(result, fmt.Sprintf("us-west-2.%s", getEndpointHost()))
		return result, nil
	}

	appClusters, err := repository.ListAppCluster(ctx, dto.ListAppClusterReq{DataappID: dataapp.ID})
	if err != nil {
		return nil, errs.NewInternalDB("list app cluster failed", err)
	}
	clusterIds := make([]uint64, len(appClusters))
	for i := 0; i < len(appClusters); i++ {
		clusterIds[i] = appClusters[i].ClusterID
	}
	clustersMap, err := grpc.ListClusterByIds(ctx, dataapp.OrgId, dataapp.ProjectId, clusterIds)
	if err != nil {
		return nil, errs.NewInternalGeneralBusiness("failed to get cluster information", err)
	}
	regionMap := make(map[string]struct{})
	for _, cluster := range clustersMap {
		if _, ok := regionMap[cluster.RegionName]; ok {
			continue
		}
		result = append(result, fmt.Sprintf("%s.%s", cluster.RegionName, getEndpointHost()))
		regionMap[cluster.RegionName] = struct{}{}
	}

	return result, nil
}

func CreateCustomDomain(ctx context.Context, appId uint64, req *dto.CustomDomainRequest, createdBy string) (*entity.CustomDomain, error) {
	dataapp, err := repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("failed to get dataapp from db", err)
	}
	if dataapp.AppType == entity.DataAppTypeChat2Query {
		return nil, errs.NewBadRequest("Chat2Query data app doesn't support custom domain", nil)
	}
	// Global fast-fail keeps unsupported AliCloud custom-domain flows out of regional control.
	if err := GuardRegionalCapabilityForDataAppRegion(ctx, appId, getRegion(req.DefaultDomainName), RegionalCapabilityCustomDomain); err != nil {
		return nil, err
	}
	if err := validateCustomDomainRequest(ctx, req, dataapp, 0); err != nil {
		return nil, err
	}

	resp, err := http.CreateCustomDomain(ctx, appId, getRegion(req.DefaultDomainName), req.CustomDomainName, req.DefaultDomainName)
	if err != nil {
		return nil, errs.NewInternalGeneralBusiness("internal http request failed", err)
	} else if resp.Code != 200 {
		tilog.Error(ctx, "failed to create customdomain in o11y", zap.Any("resp", resp))
		return nil, errs.NewInternalGeneralBusiness("internl request failed with bizerr", nil)
	}
	domainEntity := entity.CustomDomain{
		DataappId:         appId,
		ExternalAppID:     dataapp.ExternalAppID,
		CustomDomainName:  req.CustomDomainName,
		CustomPath:        req.CustomPath,
		DefaultDomainName: req.DefaultDomainName,
		CreatedBy:         createdBy,
		UpdatedBy:         createdBy,
	}

	var domain *entity.CustomDomain
	err = repository.Transaction(ctx, func(ctx context.Context) error {
		domain, err = repository.SaveCustomDomain(ctx, &domainEntity)
		if err != nil {
			return errs.NewInternalDB("failed to create customdomain in db", err)
		}
		if err = repository_ddb.PutCustomDomain(ctx, ddb.Client(), domain); err != nil {
			return errs.NewInternalDB("failed to create customdomain in ddb", err)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	domain.State = resp.State
	domain.Reason = resp.Message
	return domain, nil
}

func validateCustomDomainRequest(ctx context.Context, req *dto.CustomDomainRequest, dataapp *entity.DataApp, originCustomDomainId uint64) error {
	if strings.HasSuffix(req.CustomDomainName, "tidbcloud.com") {
		return errs.NewBadRequest("Invalid suffix for custom domain. Only non 'tidbcloud.com' domains are allowed.", nil)
	}
	if len(req.CustomDomainName) >= 255 {
		return errs.NewBadRequest("Custom domain exceeds 255 character limit.", nil)
	}
	if !validDomainRegexp.MatchString(req.CustomDomainName) {
		return errs.NewBadRequest("Invalid format for custom_domain_name. Please use a valid domain name format.", nil)
	}
	if len(req.CustomPath) >= 255 {
		return errs.NewBadRequest("Custom path exceeds 255 character limit.", nil)
	}
	if req.CustomPath == "" {
		req.CustomPath = GetAppDefaultPath(dataapp)
	}
	if !validPathRegexp.MatchString(req.CustomPath) {
		return errs.NewBadRequest("Invalid custom path format. Path must start with '/', contain alphanumeric characters, hyphens, underscores, and have no trailing slash.", nil)
	}
	if strings.HasPrefix(req.CustomPath, "/inner/dataservice") {
		return errs.NewBadRequest("Custom path cannot start with '/inner/dataservice'.", nil)
	}

	found := false
	validDefaultDomainNames, err := listDefaultDomains(ctx, dataapp)
	if err != nil {
		return err
	}
	for _, domainName := range validDefaultDomainNames {
		if req.DefaultDomainName == domainName {
			found = true
			break
		}
	}
	if !found {
		return errs.NewBadRequest("Default domain not recognized. Ensure it's among the valid defaults.", nil)
	}

	domains, err := repository.ListCustomDomainsByAppId(ctx, dataapp.ID)
	if err != nil {
		return errs.NewBadRequest("Failed to list custom domains from db", err)
	}
	for _, domain := range domains {
		if domain.ID == originCustomDomainId {
			continue
		}
		if domain.CustomDomainName == req.CustomDomainName {
			return errs.NewBadRequest("The custom domain already exists in this Data App. Each domain must be unique.", nil)
		}
		if domain.DefaultDomainName == req.DefaultDomainName {
			return errs.NewBadRequest("Only one custom domain is allowed per default domain.", nil)
		}
	}

	sameHostCustomDomains, err := repository.ListCustomDomainsByCustomDomainNameExceptExpired(ctx, req.CustomDomainName)
	if err != nil {
		return errs.NewInternalDB("Failed to list same host custom domains from db", err)
	}
	for _, domain := range sameHostCustomDomains {
		if domain.ID == originCustomDomainId {
			continue
		}
		resp, err := http.GetCustomDomainState(ctx, domain.DataappId, getRegion(domain.DefaultDomainName), domain.CustomDomainName)
		if err != nil {
			tilog.Error(ctx, "failed to get customdomain state", zap.Error(err), zap.String("external_app_id", domain.ExternalAppID))
			return errs.NewInternalGeneralBusiness("Failed to get same host custom domain status.", err)
		}
		if resp.State == string(dto_regional.CustomDomainStateSuccess) {
			return errs.NewBadRequest("This custom domain is occupied by another Data App, please use another one.", nil)
		} else {
			return errs.NewBadRequest(fmt.Sprintf("This custom domain is currently under configuration by a different Data App. If you own it, please try to configure again after %d hours.", PendingToExpiredHours), err)
		}
	}
	return nil

}

func getRegion(defaultDomainName string) string {
	parts := strings.Split(defaultDomainName, ".")
	return parts[0]
}

func ListCustomDomains(ctx context.Context, appId uint64) ([]*entity.CustomDomain, error) {
	// Global fast-fail keeps unsupported AliCloud custom-domain flows out of regional control.
	if err := GuardRegionalCapabilityForDataApp(ctx, appId, RegionalCapabilityCustomDomain); err != nil {
		return nil, err
	}
	domains, err := repository.ListCustomDomainsByAppId(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("failed to list custom domains from db", err)
	}
	var wg sync.WaitGroup
	for _, domain := range domains {
		wg.Add(1)
		go func(domain *entity.CustomDomain) {
			defer wg.Done()
			if domain.Expired {
				domain.State = string(dto_regional.CustomDomainStateExpired)
				domain.Reason = fmt.Sprintf("The DNS validation for this domain has expired for over %d hours. If this domain belongs to you, please remove this record and re-add it.", PendingToExpiredHours)
				return
			}
			resp, err := http.GetCustomDomainState(ctx, appId, getRegion(domain.DefaultDomainName), domain.CustomDomainName)
			if err != nil {
				tilog.Error(ctx, "failed to get customdomain state", zap.Error(err), zap.String("external_app_id", domain.ExternalAppID))
				return
			}
			domain.State = resp.State
			domain.Reason = resp.Message
		}(domain)
	}
	wg.Wait()
	return domains, nil
}

func DeleteCustomDomain(ctx context.Context, appId, customDomainId uint64) error {
	domain, err := repository.GetCustomDomainById(ctx, appId, customDomainId)
	if err != nil {
		return errs.NewBadRequest("failed to get custom domain from db", err)
	}
	// Global fast-fail keeps unsupported AliCloud custom-domain flows out of regional control.
	if err := GuardRegionalCapabilityForDataAppRegion(ctx, appId, getRegion(domain.DefaultDomainName), RegionalCapabilityCustomDomain); err != nil {
		return err
	}
	sameHostCustomDomains, err := repository.ListCustomDomainsByCustomDomainNameExceptExpired(ctx, domain.CustomDomainName)
	if err != nil {
		return errs.NewInternalDB("failed to list same host custom domains from db", err)
	}
	if len(sameHostCustomDomains) == 1 {
		_, err := http.DeleteCustomDomain(ctx, domain.DataappId, getRegion(domain.DefaultDomainName), domain.CustomDomainName)
		if err != nil {
			return errs.NewInternalGeneralBusiness("internal http request failed", err)
		}
	}
	if err := repository_ddb.DeleteCustomDomain(ctx, ddb.Client(), customDomainId); err != nil {
		return errs.NewInternalGeneralBusiness("failed to delete custom domain from ddb", err)
	}
	if err := repository.DeleteCustomDomainById(ctx, customDomainId); err != nil {
		return errs.NewInternalGeneralBusiness("failed to delete custom domain from db", err)
	}
	return nil
}

func EditCustomDomain(ctx context.Context, appId, customDomainId uint64, req *dto.CustomDomainRequest, updatedBy string) (*entity.CustomDomain, error) {
	domain, err := repository.GetCustomDomainById(ctx, appId, customDomainId)
	if err != nil {
		return nil, errs.NewBadRequest("failed to get custom domain from db", err)
	}
	// Global fast-fail keeps unsupported AliCloud custom-domain flows out of regional control.
	if err := GuardRegionalCapabilityForDataAppRegion(ctx, appId, getRegion(domain.DefaultDomainName), RegionalCapabilityCustomDomain); err != nil {
		return nil, err
	}
	if domain.Expired {
		return nil, errs.NewBadRequest("The record has expired, editting is not allowed. Please remove it", nil)
	}
	dataapp, err := repository.GetDataAppWithCtx(ctx, appId)
	if err != nil {
		return nil, errs.NewInternalDB("failed to get dataapp from db", err)
	}
	if err := validateCustomDomainRequest(ctx, req, dataapp, customDomainId); err != nil {
		return nil, err
	}
	if domain.DefaultDomainName != req.DefaultDomainName {
		return nil, errs.NewBadRequest("Editting default domain is not allowed.", nil)
	}

	if domain.CustomDomainName != req.CustomDomainName {
		_, err := http.DeleteCustomDomain(ctx, appId, getRegion(domain.DefaultDomainName), domain.CustomDomainName)
		if err != nil {
			return nil, errs.NewInternalGeneralBusiness("internal http delete reqeust failed", err)
		}
		resp, err := http.CreateCustomDomain(ctx, appId, getRegion(req.DefaultDomainName), req.CustomDomainName, req.DefaultDomainName)
		if err != nil {
			return nil, errs.NewInternalGeneralBusiness("internal http create reqeust failed", err)
		}
		domain.State = resp.State
		domain.Reason = resp.Message
	} else {
		resp, err := http.GetCustomDomainState(ctx, appId, getRegion(req.DefaultDomainName), req.CustomDomainName)
		if err != nil {
			return nil, errs.NewInternalGeneralBusiness("internal http get reqeust failed", err)
		}
		domain.State = resp.State
		domain.Reason = resp.Message
	}

	domain.CustomDomainName = req.CustomDomainName
	domain.CustomPath = req.CustomPath
	domain.DefaultDomainName = req.DefaultDomainName
	domain.UpdatedBy = updatedBy

	if err = repository_ddb.PutCustomDomain(ctx, ddb.Client(), domain); err != nil {
		return nil, errs.NewInternalDB("failed to update customdomain in ddb", err)
	}
	if _, err = repository.SaveCustomDomain(ctx, domain); err != nil {
		return nil, errs.NewInternalDB("failed to update customdomain in db", err)
	}

	return domain, nil
}

func GetRegionSuccessfulDomainsMap(ctx context.Context, appId uint64) (map[string]*entity.CustomDomain, error) {
	domains, err := ListCustomDomains(ctx, appId)
	if err != nil {
		return nil, err
	}
	result := make(map[string]*entity.CustomDomain)
	for _, domain := range domains {
		if domain.State != string(dto_regional.CustomDomainStateSuccess) {
			continue
		}
		result[getRegion(domain.DefaultDomainName)] = domain
	}
	return result, nil
}

func NotifyCustomDomainExpired(ctx context.Context, customDomainName, defaultDomainName string) error {
	domains, err := repository.ListCustomDomainsByCustomDomainNameExceptExpired(ctx, customDomainName)
	if err != nil {
		return errs.NewInternalDB("query custom domain failed", err)
	}
	var appId uint64
	for _, domain := range domains {
		if domain.DefaultDomainName != defaultDomainName {
			continue
		}
		domain.Expired = true
		if err = repository_ddb.PutCustomDomain(ctx, ddb.Client(), domain); err != nil {
			return errs.NewInternalDB("failed to update customdomain in ddb", err)
		}
		if _, err = repository.SaveCustomDomain(ctx, domain); err != nil {
			return errs.NewInternalDB("failed to update customdomain in db", err)
		}
		appId = domain.DataappId
	}

	if _, err = http.DeleteCustomDomain(ctx, appId, getRegion(defaultDomainName), customDomainName); err != nil {
		return errs.NewInternalGeneralBusiness("internal http request failed", err)
	}

	return nil
}
