package service

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

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

	comerrs "dataservices/commons/errs"
	"dataservices/commons/logger"
	"dataservices/commons/metrics"
	"dataservices/commons/singleflight"
	"dataservices/commons/utils"
	"dataservices/ddb"
	"dataservices/internal/entity"
	"dataservices/internal/repository_ddb"
	comsvc "dataservices/internal/service/common"
)

var sfGroup singleflight.Group

func GetDataAppByExtId(ctx context.Context, appExtId string) (*entity.DataApp, error) {
	start := time.Now()
	metrics.DDBCacheRequestCounter.WithLabelValues("get_dataapp", "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", "get_dataapp").Observe(cost.Seconds())
	}()

	cacheManager := comsvc.NewCacheManager()
	dataapp, err := cacheManager.GetDataappBaseInfo(ctx, appExtId)
	if err == nil {
		return dataapp, nil
	} else if err == comsvc.ErrNotFound {
		return nil, comerrs.ErrDataappNotFound
	} else if err != comsvc.ErrNil {
		logger.Error(ctx, "failed to get dataapp cache", zap.String("dataapp_ext_id", appExtId), zap.Error(err))
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("get_dataapp", "loadddb").Inc()
	app, err := sfGroup.Do("getdataapp_"+appExtId, func() (any, error) {
		var (
			app *entity.DataApp
			err error
		)
		defer func() {
			var serr error
			if errs.IsError(err, comerrs.ErrDataappNotFound) {
				serr = cacheManager.SetDataappBaseInfo(ctx, appExtId, nil)
			} else if err == nil && app != nil {
				serr = cacheManager.SetDataappBaseInfo(ctx, appExtId, app)
			}
			if serr != nil {
				tilog.Error(ctx, "failed to set dataapp cache", zap.String("dataapp_ext_id", appExtId), zap.Error(err))
			}
		}()

		app, err = repository_ddb.GetDataAppByExtId(ctx, ddb.Client(), appExtId)
		if err != nil {
			if tiErr, ok := errs.AsTiError(err); ok {
				return nil, tiErr
			}
			return nil, errs.NewInternalDB("GetDataAppByExtId failed, error:"+err.Error(), err)
		}
		return app, nil
	})
	if err != nil {
		return nil, err
	}
	return app.(*entity.DataApp), nil
}

func GetDraftEndpointByPath(ctx context.Context, app *entity.DataApp, method, path string) (*entity.Endpoint, map[string]string, error) {
	start := time.Now()
	metrics.DDBCacheRequestCounter.WithLabelValues("get_draft_endpoint", "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", "get_draft_endpoint").Observe(cost.Seconds())
	}()

	// Use request path and method to check if endpoint exists in the cache
	cacheManager := comsvc.NewCacheManager()
	endpoint, err := cacheManager.GetDraftEndpoint(ctx, app.ID, method, path)
	if err == nil {
		return endpoint, nil, nil
	} else if err == comsvc.ErrNotFound {
		return nil, nil, errs.NewNotFound(fmt.Sprintf("draft endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
	} else if err != comsvc.ErrNil {
		logger.Error(ctx, "failed to get draft endpoint cache", zap.Uint64("dataapp_id", app.ID), zap.String("method", method), zap.String("path", path), zap.Error(err))
	}

	// Find endpoints through the dataapp routing tree
	pathTrie, err := GetDataappEndpointRouter(ctx, app.ID, "draft")
	if err != nil {
		return nil, nil, err
	}
	matched, data, params := pathTrie.Search(method, path)
	if !matched {
		return nil, nil, errs.NewNotFound(fmt.Sprintf("draft endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
	}

	// Using dynamic routing to check if endpoints exist in the cache
	endpointPath := data.(string)
	if endpointPath != path {
		endpoint, err = cacheManager.GetDraftEndpoint(ctx, app.ID, method, endpointPath)
		if err == nil {
			return endpoint, params, nil
		} else if err == comsvc.ErrNotFound {
			return nil, nil, errs.NewNotFound(fmt.Sprintf("draft endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
		}
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("get_draft_endpoint", "loadddb").Inc()
	// Loading endpoints from dynamoDB using singleflight
	draftEndpoint, err := sfGroup.Do(fmt.Sprintf("getdraftendpoint_%d_%s_%s", app.ID, method, endpointPath), func() (any, error) {
		var (
			endpoint *entity.Endpoint
			err      error
		)
		defer func() {
			var serr error
			if errs.IsError(err, errs.ErrNotFound) {
				serr = cacheManager.SetDraftEndpoint(ctx, app.ID, method, endpointPath, nil)
			} else if err == nil && endpoint != nil {
				serr = cacheManager.SetDraftEndpoint(ctx, app.ID, method, endpointPath, endpoint)
			}
			if serr != nil {
				tilog.Error(ctx, "failed to set draft endpoint cache", zap.Uint64("dataapp_id", app.ID), zap.String("method", method), zap.String("path", path), zap.Error(err))
			}
		}()

		draftEndpoints, err := GetDraftEndpointByName(ctx, app.ID, endpointPath)
		if err != nil {
			return nil, err
		}
		for _, draftEndpoint := range draftEndpoints {
			if strings.EqualFold(draftEndpoint.Method, method) {
				endpoint = draftEndpoint
				return endpoint, nil
			}
		}
		return nil, errs.NewNotFound(fmt.Sprintf("draft endpoint not found. app: %v, endpoint_path: %v, method: %v", app.ExternalAppID, endpoint, method), nil)
	})
	if err != nil {
		return nil, nil, err
	}
	return draftEndpoint.(*entity.Endpoint), params, nil
}

func GetDraftEndpointByID(ctx context.Context, app *entity.DataApp, endpointID uint64) (*entity.Endpoint, error) {
	start := time.Now()
	metrics.DDBCacheRequestCounter.WithLabelValues("get_draft_endpoint_by_id", "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", "get_draft_endpoint_by_id").Observe(cost.Seconds())
	}()

	// Use request path and method to check if endpoint exists in the cache
	cacheManager := comsvc.NewCacheManager()
	endpoint, err := cacheManager.GetDraftEndpointByID(ctx, app.ID, endpointID)
	if err == nil {
		return endpoint, nil
	} else if err == comsvc.ErrNotFound {
		return nil, errs.NewNotFound(fmt.Sprintf("draft endpoint not found. app: %v, endpoint_id:%d", app.ExternalAppID, endpointID), nil)
	} else if err != comsvc.ErrNil {
		logger.Error(ctx, "failed to get draft endpoint cache", zap.Uint64("dataapp_id", app.ID), zap.Uint64("endpoint_id", endpointID), zap.Error(err))
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("get_draft_endpoint_by_id", "loadddb").Inc()
	// Loading endpoints from dynamoDB using singleflight
	draftEndpoint, err := sfGroup.Do(fmt.Sprintf("getdraftendpointbyid_%d_%d", app.ID, endpointID), func() (any, error) {
		var (
			endpoint *entity.Endpoint
			err      error
		)
		defer func() {
			var serr error
			if errs.IsError(err, errs.ErrNotFound) {
				serr = cacheManager.SetDraftEndpointByID(ctx, app.ID, endpointID, nil)
			} else if err == nil && endpoint != nil {
				serr = cacheManager.SetDraftEndpointByID(ctx, app.ID, endpointID, endpoint)
			}
			if serr != nil {
				tilog.Error(ctx, "failed to set draft endpoint cache by id", zap.Uint64("dataapp_id", app.ID), zap.Uint64("endpoint_id", endpointID), zap.Error(err))
			}
		}()

		endpoint, err = GetDraftEndpoint(ctx, endpointID)
		return endpoint, err
	})
	if err != nil {
		return nil, err
	}
	return draftEndpoint.(*entity.Endpoint), nil
}

func GetDeployedEndpointByPath(ctx context.Context, app *entity.DataApp, method, path string) (*entity.EndpointPublish, map[string]string, error) {
	start := time.Now()
	metrics.DDBCacheRequestCounter.WithLabelValues("get_deployed_endpoint", "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", "get_deployed_endpoint").Observe(cost.Seconds())
	}()

	// Use request path and method to check if endpoint exists in the cache
	cacheManager := comsvc.NewCacheManager()
	endpoint, err := cacheManager.GetDeployedEndpoint(ctx, app.ID, method, path)
	if err == nil {
		return endpoint, nil, nil
	} else if err == comsvc.ErrNotFound {
		return nil, nil, errs.NewNotFound(fmt.Sprintf("deployed endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
	} else if err != comsvc.ErrNil {
		logger.Error(ctx, "failed to get deployed endpoint cache", zap.Uint64("dataapp_id", app.ID), zap.String("method", method), zap.String("path", path), zap.Error(err))
	}

	// Find endpoints through the dataapp routing tree
	pathTrie, err := GetDataappEndpointRouter(ctx, app.ID, "deployed")
	if err != nil {
		return nil, nil, err
	}
	matched, data, params := pathTrie.Search(method, path)
	if !matched {
		return nil, nil, errs.NewNotFound(fmt.Sprintf("deployed endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
	}

	// Using dynamic routing to check if endpoints exist in the cache
	endpointPath := data.(string)
	if endpointPath != path {
		endpoint, err = cacheManager.GetDeployedEndpoint(ctx, app.ID, method, endpointPath)
		if err == nil {
			return endpoint, params, nil
		} else if err == comsvc.ErrNotFound {
			return nil, nil, errs.NewNotFound(fmt.Sprintf("deployed endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
		}
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("get_deployed_endpoint", "loadddb").Inc()
	// Loading endpoints from dynamoDB using singleflight
	deployedEndpoint, err := sfGroup.Do(fmt.Sprintf("getdeployedendpoint_%d_%s_%s", app.ID, method, endpointPath), func() (any, error) {
		var (
			endpoint *entity.EndpointPublish
			err      error
		)
		defer func() {
			var serr error
			if errs.IsError(err, errs.ErrNotFound) {
				serr = cacheManager.SetDeployedEndpoint(ctx, app.ID, method, endpointPath, nil)
			} else if err == nil && endpoint != nil {
				serr = cacheManager.SetDeployedEndpoint(ctx, app.ID, method, endpointPath, endpoint)
			}
			if serr != nil {
				tilog.Error(ctx, "failed to set deployed endpoint cache", zap.Uint64("dataapp_id", app.ID), zap.String("method", method), zap.String("path", path), zap.Error(err))
			}
		}()

		endpoints, err := GetPublishedEndpointByName(ctx, app.ID, endpointPath)
		if err != nil {
			return nil, err
		}
		for _, deployedEndpoint := range endpoints {
			if strings.EqualFold(deployedEndpoint.Method, method) {
				endpoint = deployedEndpoint
				return endpoint, nil
			}
		}
		return nil, errs.NewNotFound(fmt.Sprintf("deployed endpoint not found. app: %v, request_path: %v, method: %v", app.ExternalAppID, path, method), nil)
	})
	if err != nil {
		return nil, nil, err
	}
	return deployedEndpoint.(*entity.EndpointPublish), params, nil
}

func GetDataappEndpointRouter(ctx context.Context, dataappID uint64, mode string) (*utils.PathTrie, error) {
	start := time.Now()
	metrics.DDBCacheRequestCounter.WithLabelValues("get_dataapp_router_"+mode, "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", mode+"_endpoint_router").Observe(cost.Seconds())
	}()

	cacheManager := comsvc.NewCacheManager()
	pathTrie, err := cacheManager.GetDataappEndpointRouter(ctx, dataappID, mode)
	if err == nil {
		return pathTrie, nil
	} else if err == comsvc.ErrNotFound {
		return nil, nil
	} else if err != comsvc.ErrNil {
		logger.Error(ctx, "failed to get dataapp endpoint router cache", zap.Uint64("dataapp_id", dataappID), zap.String("mode", mode), zap.Error(err))
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("get_dataapp_router_"+mode, "loadddb").Inc()
	tree, err := sfGroup.Do(fmt.Sprintf("getdataappendpointrouter_%d_%s", dataappID, mode), func() (any, error) {
		pathTrie := utils.NewPathTrie()
		if mode == "draft" {

			endpoints, err := GetAllDraftEndpointsByAppID(ctx, dataappID)
			logger.LogWithError(ctx, err, "get all draft endpoints by app id", zap.Uint64("appID", dataappID), zap.Any("endpoint_size", len(endpoints)))
			if err != nil {
				return nil, err
			}
			for _, endpoint := range endpoints {
				if endpoint.Method == "" || endpoint.Endpoint == "" {
					continue
				}
				if err := pathTrie.Insert(endpoint.Method, endpoint.Endpoint, endpoint.Endpoint); err != nil {
					tilog.Warn(ctx, "draft endpoint path conflict", zap.Uint64("endpoint_id", endpoint.ID), zap.String("method", endpoint.Method), zap.String("path", endpoint.Endpoint), zap.Error(err))
				}
			}
		} else if mode == "deployed" {

			endpoints, err := GetAllPublishedEndpointsByAppID(ctx, dataappID)
			logger.LogWithError(ctx, err, "get all deployed endpoints by app id", zap.Uint64("appID", dataappID), zap.Any("endpoint_size", len(endpoints)))
			if err != nil {
				return nil, err
			}

			for _, endpoint := range endpoints {
				if endpoint.Method == "" || endpoint.Endpoint == "" {
					continue
				}
				if err := pathTrie.Insert(endpoint.Method, endpoint.Endpoint, endpoint.Endpoint); err != nil {
					tilog.Warn(ctx, "deployed endpoint path conflict", zap.Uint64("endpoint_id", endpoint.ID), zap.String("method", endpoint.Method), zap.String("path", endpoint.Endpoint), zap.Error(err))
				}
			}
		}

		if err := cacheManager.SetDataappEndpointRouter(ctx, dataappID, mode, pathTrie); err != nil {
			tilog.Error(ctx, "failed to set dataapp endpoint router cache", zap.Uint64("dataapp_id", dataappID), zap.String("mode", mode), zap.Error(err))
		}
		return pathTrie, nil
	})
	if err != nil {
		return nil, err
	}
	return tree.(*utils.PathTrie), nil
}

func GetAllDraftEndpointsByAppID(ctx context.Context, dataAppID uint64) ([]*entity.Endpoint, errs.Error) {
	endpoints, err := repository_ddb.GetAllDraftEndpointsByAppID(ctx, ddb.Client(), dataAppID)
	if err != nil {
		if tiErr, ok := errs.AsTiError(err); ok {
			return nil, tiErr
		}
		return nil, errs.NewInternalDB("GetAllDraftEndpointsByAppID failed, error:"+err.Error(), err)
	}
	return endpoints, nil
}

func GetAllPublishedEndpointsByAppID(ctx context.Context, dataAppID uint64) ([]*entity.EndpointPublish, errs.Error) {
	endpoints, err := repository_ddb.GetAllPublishedEndpointsByAppID(ctx, ddb.Client(), dataAppID)
	if err != nil {
		if tiErr, ok := errs.AsTiError(err); ok {
			return nil, tiErr
		}
		return nil, errs.NewInternalDB("GetPublishedEndpointByName failed, error:"+err.Error(), err)
	}
	return endpoints, nil
}

func GetPublishedEndpointByName(ctx context.Context, dataAppID uint64, endpoint string) ([]*entity.EndpointPublish, errs.Error) {
	endpoints, err := repository_ddb.GetPublishedEndpointByName(ctx, ddb.Client(), dataAppID, endpoint)
	if err != nil {
		if tiErr, ok := errs.AsTiError(err); ok {
			return nil, tiErr
		}
		return nil, errs.NewInternalDB("GetPublishedEndpointByName failed, error:"+err.Error(), err)
	}
	return endpoints, nil
}

func GetDraftEndpointByName(ctx context.Context, dataAppID uint64, endpoint string) ([]*entity.Endpoint, errs.Error) {
	endpoints, err := repository_ddb.GetEndpointByName(ctx, ddb.Client(), dataAppID, endpoint)
	if err != nil {
		if tiErr, ok := errs.AsTiError(err); ok {
			return nil, tiErr
		}
		return nil, errs.NewInternalDB("GetDraftEndpointByName failed, error:"+err.Error(), err)
	}
	return endpoints, nil
}

func GetDraftEndpoint(ctx context.Context, endpointId uint64) (*entity.Endpoint, errs.Error) {
	endpoint, err := repository_ddb.GetEndpoint(ctx, ddb.Client(), endpointId)
	if err != nil {
		if tiErr, ok := errs.AsTiError(err); ok {
			return nil, tiErr
		}
		return nil, errs.NewInternalDB("GetDraftEndpoint failed, error:"+err.Error(), err)
	}
	return endpoint, nil
}

func HasClusterPermission(ctx context.Context, dataappID, clusterID uint64) (bool, error) {
	start := time.Now()
	metrics.DDBCacheRequestCounter.WithLabelValues("has_cluster_permission", "request").Inc()
	defer func() {
		cost := time.Since(start)
		metrics.ExecutionLinkDuration.WithLabelValues("endpoint_exec", "has_cluster_perm").Observe(cost.Seconds())
		tilog.Info(ctx, "HasClusterPermission completed",
			zap.Uint64("dataapp_id", dataappID),
			zap.Uint64("cluster_id", clusterID),
			zap.Duration("cost", time.Since(start)))
	}()

	cacheManager := comsvc.NewCacheManager()
	_, err := cacheManager.GetAppCluster(ctx, dataappID, clusterID)
	if err == nil {
		return true, nil
	} else if err == comsvc.ErrNotFound {
		return false, nil
	} else if err != comsvc.ErrNil {
		logger.Error(ctx, "failed to get app cluster permission cache", zap.Uint64("dataapp_id", dataappID), zap.Uint64("cluster_id", clusterID), zap.Error(err))
	}

	metrics.DDBCacheRequestCounter.WithLabelValues("has_cluster_permission", "loadddb").Inc()
	result, err := sfGroup.Do(fmt.Sprintf("getappclusterpermission_%d_%d", dataappID, clusterID), func() (any, error) {
		appCluster, err := repository_ddb.GetAppCluster(ctx, ddb.Client(), dataappID, clusterID)
		if err == nil || errs.IsError(err, errs.ErrNotFound) {
			if err := cacheManager.SetAppCluster(ctx, dataappID, clusterID, appCluster); err != nil {
				logger.LogWithError(ctx, err, "failed to set app cluster cache", zap.Uint64("dataapp_id", dataappID), zap.Uint64("cluster_id", clusterID))
			}
			return appCluster != nil, nil
		} else {
			return false, err
		}
	})
	if err != nil {
		return false, err
	}
	return result.(bool), nil
}

func GetCustomDomain(ctx context.Context, customDomainName, externalAppId string) (*entity.CustomDomain, errs.Error) {
	domain, err := repository_ddb.GetCustomDomain(ctx, ddb.Client(), customDomainName, externalAppId)
	if err != nil {
		return nil, errs.NewNotFound("Not found", err)
	}
	return domain, nil
}

func ListCustomDomains(ctx context.Context, customDomainName string) ([]*entity.CustomDomain, errs.Error) {
	domains, err := repository_ddb.ListCustomDomains(ctx, ddb.Client(), customDomainName)
	if err != nil {
		return nil, errs.NewInternalDB("Failed to retrive domain information", err)
	}
	sort.Slice(domains, func(i, j int) bool {
		return len(domains[i].CustomPath) > len(domains[j].CustomPath)
	})
	return domains, nil
}
