package internal_api

import (
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode"

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

	error2 "github.com/pingcap-inc/clinic/pkg/api/error"
	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource"
	"github.com/pingcap-inc/clinic/pkg/datasource/clustersearch"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
)

const (
	defaultClusterMetadataPageSize = 50
	maxClusterMetadataPageSize     = 200
	maxClusterMetadataPage         = 1_000_000
	maxClusterMetadataTextLength   = 255
)

var (
	errClusterMetadataNotFound = errors.New("cluster metadata not found")
	deletedClusterStatuses     = map[string]struct{}{
		"deleted":        {},
		"deletecomplete": {},
		"cleared":        {},
	}
)

type clusterMetadataListOptions struct {
	Page        int
	PageSize    int
	Query       string
	ShowDeleted bool
}

// clusterMetadataDataSource is a narrow adapter over Clinic's pre-existing
// dashboard and cluster datasources. It deliberately owns no query or storage
// logic; the interface only makes the internal HTTP adapter unit-testable.
type clusterMetadataDataSource interface {
	ListOrganizationAndCluster(*session.CtxUser, *view.ListOrganizationAndClusterFilter, int, int) ([]*view.DashboardItemExternal, int64, error)
	GetOrganizationByCloudID(*session.CtxUser, uint) (*db.Organization, error)
	ListClusters(*session.CtxUser, string, uint, uint, int, int, bool, bool) ([]db.Cluster, int64, error)
	GetCluster(*session.CtxUser, string, uint) (*db.Cluster, error)
}

type clinicClusterMetadataDataSource struct{}

func (clinicClusterMetadataDataSource) ListOrganizationAndCluster(
	user *session.CtxUser,
	filter *view.ListOrganizationAndClusterFilter,
	page, pageSize int,
) ([]*view.DashboardItemExternal, int64, error) {
	return datasource.ListOrganizationAndCluster(user, filter, page, pageSize)
}

func (clinicClusterMetadataDataSource) GetOrganizationByCloudID(user *session.CtxUser, orgID uint) (*db.Organization, error) {
	return datasource.GetOrganizationByCloudID(user, orgID)
}

func (clinicClusterMetadataDataSource) ListClusters(
	user *session.CtxUser,
	query string,
	orgID, projectID uint,
	page, pageSize int,
	hideServerless, hideDeleted bool,
) ([]db.Cluster, int64, error) {
	return datasource.ListClusters(user, query, orgID, projectID, page, pageSize, hideServerless, hideDeleted)
}

func (clinicClusterMetadataDataSource) GetCluster(user *session.CtxUser, clusterID string, orgID uint) (*db.Cluster, error) {
	return datasource.GetCluster(user, clusterID, orgID)
}

// DataProxyClusterMetadataService adapts Clinic's existing metadata services
// to the stable contract consumed by Data Proxy. orgId always means the TiDB
// Cloud/DBAAS organization ID, never the Clinic organizations table key.
type DataProxyClusterMetadataService struct {
	dataSource clusterMetadataDataSource
}

func NewDataProxyClusterMetadataService() *DataProxyClusterMetadataService {
	return newDataProxyClusterMetadataService(clinicClusterMetadataDataSource{})
}

func newDataProxyClusterMetadataService(dataSource clusterMetadataDataSource) *DataProxyClusterMetadataService {
	return &DataProxyClusterMetadataService{dataSource: dataSource}
}

func (s *DataProxyClusterMetadataService) GetCluster(c *gin.Context) {
	clusterID := c.Param("clusterId")
	if err := validateClusterMetadataID(clusterID); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	user, ok := dataProxyInternalUser(c)
	if !ok {
		return
	}

	items, _, err := s.dataSource.ListOrganizationAndCluster(user, &view.ListOrganizationAndClusterFilter{
		ClusterIDs:   []string{clusterID},
		Hide_deleted: false,
	}, 1, 2)
	if err != nil {
		s.writeDataSourceError(c, clusterID, err)
		return
	}
	for _, item := range items {
		cluster, mapErr := dataProxyClusterMetadataFromDashboard(item)
		if mapErr != nil || cluster.ClusterID != clusterID {
			continue
		}
		dbaasOrgID, parseErr := strconv.ParseUint(cluster.OrgID, 10, 64)
		if parseErr != nil {
			continue
		}
		org, resolveErr := s.resolveCloudOrganization(user, dbaasOrgID)
		if resolveErr != nil {
			if errors.Is(resolveErr, errClusterMetadataNotFound) {
				continue
			}
			s.writeDataSourceError(c, clusterID, resolveErr)
			return
		}
		if !dashboardItemBelongsToOrganization(item, org) {
			continue
		}
		c.JSON(http.StatusOK, cluster)
		return
	}
	s.writeDataSourceError(c, clusterID, errClusterMetadataNotFound)
}

func (s *DataProxyClusterMetadataService) GetOrgCluster(c *gin.Context) {
	orgID, ok := parseDataProxyOrgID(c)
	if !ok {
		return
	}
	clusterID := c.Param("clusterId")
	if err := validateClusterMetadataID(clusterID); err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	user, ok := dataProxyInternalUser(c)
	if !ok {
		return
	}

	cluster, org, err := s.resolveOwnedCluster(user, orgID, clusterID)
	if err != nil {
		s.writeDataSourceError(c, clusterID, err)
		return
	}

	result := dataProxyClusterMetadataFromCluster(cluster, org)
	c.JSON(http.StatusOK, result)
}

// resolveOwnedCluster resolves a TiDB Cloud/DBAAS organization and delegates
// cluster lookup to Clinic's existing detail datasource. Internal Data Proxy
// capabilities reuse this helper so ownership and fallback behavior stay
// identical to GetOrgCluster.
func (s *DataProxyClusterMetadataService) resolveOwnedCluster(
	user *session.CtxUser,
	orgID uint64,
	clusterID string,
) (*db.Cluster, *db.Organization, error) {
	org, err := s.resolveCloudOrganization(user, orgID)
	if err != nil {
		return nil, nil, err
	}
	cluster, err := s.dataSource.GetCluster(user, clusterID, org.ID)
	if err != nil {
		exists, fallbackErr := s.dashboardClusterBelongsToOrganization(user, clusterID, org)
		if fallbackErr != nil {
			return nil, nil, fmt.Errorf(
				"detail datasource failed: %v; dashboard lookup failed: %w", err, fallbackErr)
		}
		if !exists {
			return nil, nil, errClusterMetadataNotFound
		}
		return nil, nil, fmt.Errorf("existing Clinic detail datasource failed for a known cluster: %v", err)
	}
	if cluster == nil || cluster.ClusterID != clusterID || cluster.OrgID != org.ID ||
		cluster.TenantID != uint64(org.DbaasOrgID) {
		log.Warn("existing Clinic detail datasource returned mismatched cluster ownership",
			zap.String("requested_cluster_id", clusterID), zap.Uint64("requested_dbaas_org_id", orgID))
		return nil, nil, errClusterMetadataNotFound
	}

	// Some existing cluster datasources do not preload the Organization
	// association. The shared o11y resolver relies on it for dedicated and
	// next-generation lookups, so populate it on a copy after ownership checks.
	resolvedCluster := *cluster
	resolvedCluster.Organization = *org
	return &resolvedCluster, org, nil
}

func (s *DataProxyClusterMetadataService) ListOrgClusters(c *gin.Context) {
	orgID, ok := parseDataProxyOrgID(c)
	if !ok {
		return
	}
	opts, err := parseClusterMetadataListOptions(c)
	if err != nil {
		_ = c.Error(error2.ErrBadRequest.New("%s", err.Error()))
		return
	}
	user, ok := dataProxyInternalUser(c)
	if !ok {
		return
	}

	org, err := s.resolveCloudOrganization(user, orgID)
	if err != nil {
		s.writeDataSourceError(c, "", err)
		return
	}
	clusters, total, err := s.dataSource.ListClusters(
		user,
		opts.Query,
		org.ID,
		0,
		opts.Page,
		opts.PageSize,
		false,
		!opts.ShowDeleted,
	)
	if err != nil {
		s.writeDataSourceError(c, "", err)
		return
	}

	items := make([]DataProxyClusterMetadata, 0, len(clusters))
	for i := range clusters {
		if clusters[i].ClusterID == "" || clusters[i].OrgID != org.ID ||
			clusters[i].TenantID != uint64(org.DbaasOrgID) {
			s.writeDataSourceError(c, clusters[i].ClusterID,
				fmt.Errorf("existing Clinic list datasource returned inconsistent cluster ownership"))
			return
		}
		items = append(items, dataProxyClusterMetadataFromCluster(&clusters[i], org))
	}
	c.JSON(http.StatusOK, DataProxyClusterMetadataPage{
		Items:    items,
		Page:     opts.Page,
		PageSize: opts.PageSize,
		Total:    total,
	})
}

func (s *DataProxyClusterMetadataService) resolveCloudOrganization(user *session.CtxUser, orgID uint64) (*db.Organization, error) {
	org, err := s.dataSource.GetOrganizationByCloudID(user, uint(orgID))
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, errClusterMetadataNotFound
		}
		return nil, err
	}
	if org == nil || org.ID == 0 || uint64(org.DbaasOrgID) != orgID || org.OrgType != string(view.CloudDataSource) {
		return nil, errClusterMetadataNotFound
	}
	return org, nil
}

func (s *DataProxyClusterMetadataService) dashboardClusterBelongsToOrganization(
	user *session.CtxUser,
	clusterID string,
	org *db.Organization,
) (bool, error) {
	items, _, err := s.dataSource.ListOrganizationAndCluster(user, &view.ListOrganizationAndClusterFilter{
		ClusterIDs:   []string{clusterID},
		Hide_deleted: false,
	}, 1, 2)
	if err != nil {
		return false, err
	}
	for _, item := range items {
		if item != nil && item.DashboardItemInternal != nil && item.ClusterID == clusterID &&
			dashboardItemBelongsToOrganization(item, org) {
			return true, nil
		}
	}
	return false, nil
}

func (s *DataProxyClusterMetadataService) writeDataSourceError(c *gin.Context, clusterID string, err error) {
	switch {
	case errors.Is(err, errClusterMetadataNotFound), errors.Is(err, gorm.ErrRecordNotFound):
		_ = c.Error(error2.ErrNotFound.New("cluster not found"))
	case errors.Is(err, clustersearch.ErrTextSearchPolicyUnavailable):
		_ = c.Error(error2.ErrServiceUnavailable.New("%s", err.Error()))
	default:
		log.Error("query cluster metadata through existing Clinic datasource failed",
			zap.String("cluster_id", clusterID), zap.Error(err))
		_ = c.Error(error2.ErrInternal.New("failed to query cluster metadata"))
	}
}

func dataProxyInternalUser(c *gin.Context) (*session.CtxUser, bool) {
	user := session.GetUser(c)
	if user == nil || !user.Internal {
		_ = c.Error(error2.ErrForbidden.New("internal Clinic user is required"))
		return nil, false
	}
	return user, true
}

func dataProxyClusterMetadataFromDashboard(item *view.DashboardItemExternal) (DataProxyClusterMetadata, error) {
	if item == nil || item.DashboardItemInternal == nil || item.ClusterID == "" {
		return DataProxyClusterMetadata{}, errClusterMetadataNotFound
	}
	orgID, err := strconv.ParseUint(item.TenantID, 10, 64)
	if err != nil || orgID == 0 {
		return DataProxyClusterMetadata{}, errClusterMetadataNotFound
	}
	createdAt := ""
	if item.ClusterCreatedAt > 0 {
		createdAt = time.UnixMilli(item.ClusterCreatedAt).UTC().Format(time.RFC3339)
	}
	return DataProxyClusterMetadata{
		ClusterID:    item.ClusterID,
		OrgID:        strconv.FormatUint(orgID, 10),
		Name:         item.ClusterName,
		Status:       item.ClusterStatus,
		Deleted:      item.ClusterDeletedAt > 0 || isDeletedClusterStatus(item.ClusterStatus),
		Version:      item.ClusterVersion,
		Provider:     item.ClusterProviderName,
		Region:       item.ClusterRegionName,
		DeployType:   item.ClusterDeployType,
		DeployTypeV2: item.ClusterDeployTypeV2,
		CreatedAt:    createdAt,
		DiagnosticContext: DataProxyDiagnosticContext{
			ProjectID: item.ProjectID,
			ParentID:  item.ParentID,
			ShootName: item.ClusterShootName,
		},
	}, nil
}

func dataProxyClusterMetadataFromCluster(cluster *db.Cluster, org *db.Organization) DataProxyClusterMetadata {
	createdAt := ""
	if !cluster.CreatedAt.IsZero() {
		createdAt = cluster.CreatedAt.UTC().Format(time.RFC3339)
	}
	keyspaceID := ""
	if cluster.KeyspaceID != 0 {
		keyspaceID = strconv.FormatUint(uint64(cluster.KeyspaceID), 10)
	}
	return DataProxyClusterMetadata{
		ClusterID:    cluster.ClusterID,
		OrgID:        strconv.FormatUint(uint64(org.DbaasOrgID), 10),
		Name:         cluster.Name,
		Status:       cluster.Status,
		Deleted:      cluster.DeletedAt.Valid || isDeletedClusterStatus(cluster.Status),
		Version:      cluster.Version,
		Provider:     cluster.Provider,
		Region:       cluster.Region,
		DeployType:   cluster.DeployType,
		DeployTypeV2: cluster.DeployTypeV2,
		CreatedAt:    createdAt,
		DiagnosticContext: DataProxyDiagnosticContext{
			ProjectID:  cluster.ProjectID,
			ParentID:   cluster.ParentID,
			ShootName:  cluster.ShootName,
			KeyspaceID: keyspaceID,
		},
	}
}

func dashboardItemBelongsToOrganization(item *view.DashboardItemExternal, org *db.Organization) bool {
	if item == nil || item.DashboardItemInternal == nil || org == nil {
		return false
	}
	dbaasOrgID, err := strconv.ParseUint(item.TenantID, 10, 64)
	if err != nil || dbaasOrgID != uint64(org.DbaasOrgID) {
		return false
	}
	clinicOrgID, err := strconv.ParseUint(item.OrgID, 10, 64)
	return err == nil && clinicOrgID == uint64(org.ID)
}

func isDeletedClusterStatus(status string) bool {
	_, ok := deletedClusterStatuses[strings.ToLower(strings.TrimSpace(status))]
	return ok
}

func parseDataProxyOrgID(c *gin.Context) (uint64, bool) {
	orgID, err := strconv.ParseUint(c.Param("orgId"), 10, 64)
	if err != nil || orgID == 0 || uint64(uint(orgID)) != orgID {
		_ = c.Error(error2.ErrBadRequest.New("orgId must be a positive TiDB Cloud/DBAAS org ID"))
		return 0, false
	}
	return orgID, true
}

func parseClusterMetadataListOptions(c *gin.Context) (clusterMetadataListOptions, error) {
	page, err := parseClusterMetadataPositiveInt(c.Query("page"), 1, maxClusterMetadataPage)
	if err != nil {
		return clusterMetadataListOptions{}, errors.New("page must be between 1 and 1000000")
	}
	pageSize, err := parseClusterMetadataPositiveInt(c.Query("pageSize"), defaultClusterMetadataPageSize, maxClusterMetadataPageSize)
	if err != nil {
		return clusterMetadataListOptions{}, errors.New("pageSize must be between 1 and 200")
	}
	showDeleted := false
	if raw := c.Query("showDeleted"); raw != "" {
		showDeleted, err = strconv.ParseBool(raw)
		if err != nil {
			return clusterMetadataListOptions{}, errors.New("showDeleted must be true or false")
		}
	}
	query := strings.TrimSpace(c.Query("query"))
	if len(query) > maxClusterMetadataTextLength {
		return clusterMetadataListOptions{}, errors.New("query must not exceed 255 bytes")
	}
	return clusterMetadataListOptions{
		Page:        page,
		PageSize:    pageSize,
		Query:       query,
		ShowDeleted: showDeleted,
	}, nil
}

func parseClusterMetadataPositiveInt(raw string, defaultValue, maxValue int) (int, error) {
	if raw == "" {
		return defaultValue, nil
	}
	value, err := strconv.Atoi(raw)
	if err != nil || value < 1 || value > maxValue {
		return 0, fmt.Errorf("integer out of range")
	}
	return value, nil
}

func validateClusterMetadataID(value string) error {
	if value == "" {
		return errors.New("clusterId is required")
	}
	if len(value) > maxClusterMetadataTextLength {
		return errors.New("clusterId must not exceed 255 bytes")
	}
	if strings.Contains(value, "/") {
		return errors.New("clusterId must not contain slash characters")
	}
	for _, r := range value {
		if unicode.IsControl(r) {
			return errors.New("clusterId must not contain control characters")
		}
	}
	return nil
}
