package org

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/tidbcloud/account/internel/svc/comsvc"

	"github.com/tidbcloud/account/internel/dto"

	"github.com/tidbcloud/account/central/events/publish"

	"github.com/duke-git/lancet/v2/slice"
	"github.com/tidbcloud/account/commons/database"

	"github.com/tidbcloud/account/internel/dao"
	"github.com/tidbcloud/account/internel/dao/entity"
	org_dao "github.com/tidbcloud/account/internel/dao/org"
	"github.com/tidbcloud/account/thirdparty/dataflow"
	"github.com/tidbcloud/account/thirdparty/infra"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/tiinf/errs"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

var _ OrgSvcIface = (*OrgSvc)(nil)

var PartnerOrgTags = []string{string(entity.OrgTagAwsCppo), string(entity.OrgTagMsp), string(entity.OrgTagGcpMCPOReseller), string(entity.OrgTagResalePartner), string(entity.OrgTagAlicloud)}

type OrgSvcIface interface {
	GetOrgByID(ctx context.Context, orgId uint64) (*entity.Tenant, error)
	GetProjectIds(ctx context.Context, orgId uint64) ([]uint64, error)
	ListOrgs(ctx context.Context, param org_dao.ListOrgsParam) (*org_dao.ListOrgsResult, error)
	ListOrgsByFilter(ctx context.Context, filter org_dao.ListOrgsFilter) ([]entity.Tenant, error)
	ListOrgsByUserID(ctx context.Context, userId uint64) ([]entity.Tenant, error)
	ListTrials(ctx context.Context, orgId uint64, trialType string) ([]entity.Trial, error)
	CreateTrial(ctx context.Context, creatorUserId uint64, orgId uint64, startedAt time.Time, estimatedEndedAt, endedAt *time.Time, trialType string) (*entity.Trial, error)
	DeleteOrgByID(ctx context.Context, orgId uint64, isDeleteResource bool) error
	GetOrgByCompanyName(ctx context.Context, customName string) (*entity.Tenant, error)
	LegacyGetOrgByUser(ctx context.Context, userId uint64) (*entity.Tenant, error)
	UpdateOrg(ctx context.Context, param org_dao.UpdateOrgParam) error
}

var (
	orgSvcInitOnce sync.Once
	orgSvcInstance OrgSvcIface
)

func GetOrgSvc() (OrgSvcIface, error) {
	orgSvcInitOnce.Do(func() {
		orgSvcInstance = NewOrgSvc(database.DBConn.DB, dao.GetDaoHub())

	})
	return orgSvcInstance, nil
}

type OrgSvc struct {
	db  *gorm.DB
	hub *dao.DaoHub
}

func NewOrgSvc(db *gorm.DB, hub *dao.DaoHub) OrgSvcIface {
	return &OrgSvc{
		db:  db,
		hub: hub,
	}
}

func (s *OrgSvc) GetOrgByID(ctx context.Context, orgID uint64) (*entity.Tenant, error) {
	return s.hub.Org.GetOrgByOrgID(ctx, s.db.WithContext(ctx), orgID)
}

func (s *OrgSvc) GetOrgByCompanyName(ctx context.Context, companyName string) (*entity.Tenant, error) {
	return s.hub.Org.GetOrgByCompanyName(ctx, s.db.WithContext(ctx), companyName)
}

func (s *OrgSvc) ListOrgs(ctx context.Context, param org_dao.ListOrgsParam) (*org_dao.ListOrgsResult, error) {
	return s.hub.Org.ListOrgs(ctx, s.db.WithContext(ctx), param)
}
func (s *OrgSvc) ListOrgsByFilter(ctx context.Context, filter org_dao.ListOrgsFilter) ([]entity.Tenant, error) {
	return s.hub.Org.ListOrgsByFilter(ctx, s.db.WithContext(ctx), filter)
}

func (s *OrgSvc) ListOrgsByUserID(ctx context.Context, userID uint64) ([]entity.Tenant, error) {

	return s.hub.Cross.ListOrgsByUserID(ctx, s.db.WithContext(ctx), userID, nil)

}

func (s *OrgSvc) LegacyGetOrgByUser(ctx context.Context, userID uint64) (*entity.Tenant, error) {
	orgs, err := s.ListOrgsByUserID(ctx, userID)
	if err != nil {
		tilog.Error(ctx, "ListOrgsByUserID return error", zap.Any("userID", userID), zap.Any("err", err))
		return nil, err
	}
	if len(orgs) == 0 {
		tilog.Info(ctx, "ListOrgsByUserID return orgs empty", zap.Any("userID", userID))
		return nil, nil
	}
	return &orgs[0], nil
}

func (s *OrgSvc) ListTrials(ctx context.Context, orgID uint64, trialType string) ([]entity.Trial, error) {
	return s.hub.Org.ListTrials(ctx, s.db.WithContext(ctx), orgID, trialType)
}

func (s *OrgSvc) CreateTrial(ctx context.Context, creatorUserID uint64, orgID uint64, startedAt time.Time, estimatedEndedAt, endedAt *time.Time, trialType string) (*entity.Trial, error) {
	return s.hub.Org.CreateTrial(ctx, s.db.WithContext(ctx), creatorUserID, orgID, startedAt, estimatedEndedAt, endedAt, trialType)
}

func (s *OrgSvc) DeleteOrgByID(ctx context.Context, orgID uint64, isDeleteResource bool) error {
	logger := log.WithContextL(ctx)
	logger.Warn("DeleteOrgByID called", zap.Uint64("orgID", orgID))
	org, err := s.GetOrgByID(ctx, orgID)
	if err != nil {
		logger.Error("GetOrgByID return error", zap.Any("orgID", orgID), zap.Any("err", err))
		return err
	}
	if org == nil || (org.Status == entity.TenantStatusDeleted || org.Status == entity.TenantStatusInactive) {
		return errors.New("the org is not exist or deleted")
	}
	if slice.Contain(PartnerOrgTags, org.OrgTag) {
		msg := fmt.Sprintf("partner org %s can't be deleted", org.OrgTag)
		return errs.NewBadRequest(msg, nil)
	}

	if !isDeleteResource {
		projectIds, err := s.GetProjectIds(ctx, orgID)
		if err != nil {
			return err
		}
		resourceResult, err := infra.CheckOrgResource(ctx, orgID, projectIds)
		if err != nil {
			return err
		}
		if resourceResult.IsHaveResource {
			logger.Info("DeleteOrgByID return", zap.Any("msg", resourceResult.ResourcesMsg))
			return errs.NewBadRequest(resourceResult.ResourcesMsg, nil)
		}
	}

	// 1. delete all projects in the org
	err = s.db.Transaction(func(tx *gorm.DB) error {
		projects, err := s.hub.Project.ListActiveProjectsByOrgID(ctx, tx, orgID)
		if err != nil {
			return err
		}

		for _, project := range projects {
			err = s.hub.Project.MarkProjectAsDeletedByID(ctx, tx, project.ID.Uint64())
			logger.Info("delete all projects in org", zap.Uint64("orgID", orgID), zap.Error(err))
			if err != nil {
				return err
			}
			_, err := s.hub.Role.DeleteRoleBindingByScopeInfo(ctx, tx, entity.RoleBindingScopeProject, project.ID.Uint64(), "")
			if err != nil {
				tilog.Warn(ctx, "failed to delete project relate role_bindings", zap.Uint64("projectID", project.ID.Uint64()), zap.Error(err))
			}
		}

		// delete current org
		err = s.hub.Org.MarkOrgAsDeleted(ctx, tx, orgID)
		logger.Info("delete org", zap.Uint64("orgID", orgID), zap.Error(err))
		if err != nil {
			return err
		}
		_, err = s.hub.Role.DeleteRoleBindingByScopeInfo(ctx, tx, entity.RoleBindingScopeOrg, orgID, "")
		if err != nil {
			tilog.Warn(ctx, "failed to delete org relate role_bindings", zap.Uint64("orgID", orgID), zap.Error(err))
		}

		// delete all impersonated records
		return s.hub.Org.DeleteOrgImpersonates(ctx, tx, orgID)
	})
	if err != nil {
		return errs.NewInternalDB("failed to delete project", err)
	}

	// 2. delete all clusters(include devtier and dedicated)
	if isDeleteResource {
		infraHandler := infra.GetHandler()
		err = infraHandler.CleanFreeClusters(ctx, orgID, true)
		logger.Info("delete devtier clusters in org", zap.Uint64("orgID", orgID), zap.Error(err))
		if err != nil {
			return errs.NewInternalGRPC("failed to delete devtier clusters", err)
		}
		err = infraHandler.CleanClusters(ctx, orgID, true)
		logger.Info("delete dedicated clusters in org", zap.Uint64("orgID", orgID), zap.Error(err))
		if err != nil {
			return errs.NewInternalGRPC("failed to delete dedicated clusters", err)
		}

		// 3. delete all backups
		dpHandler := dataflow.GetHandler()
		err = dpHandler.DeleteAllBackups(ctx, orgID)
		logger.Info("delete all backups in org", zap.Uint64("orgID", orgID), zap.Error(err))
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("DP DeleteAllBackups err, org=%d", orgID), zap.Error(err))
			return errs.NewInternalGRPC("failed to delete all backups", err)
		}
	}
	if err := publish.PublishOrgEvent(ctx, dto.OrgEventData{OrgId: orgID, OrgEventType: entity.EntityEventTypeDelete}); err != nil {
		tilog.Error(ctx, "PublishOrgEvent err", zap.Any("OrgId", orgID), zap.Error(err))
	}

	return nil
}

/*
func (s *OrgSvc) DeleteOrgByIDForOps(ctx context.Context, orgID uint64) error {

		tilog.Info(ctx, "DeleteOrgByIDForOps called", zap.Uint64("orgID", orgID))

		//s.hub.User.

		// 1. check cluster  (include serverless and dedicated)
		clusterCount, err := infra.GetHandler().CountClustersByOrg(ctx, []uint64{orgID})
		if err != nil {
			return errors.Annotate(err, "get active cluster count")
		}
		if clusterCount > 0 {
			msg := fmt.Sprintf("It is forbidden to delete org, there are still %d clusters in org.", clusterCount)
			tilog.Info(ctx, msg)
			return errcode.NewInvalidInputErr(errors.Errorf(msg))
		}

		// 2. check all backups
		dpHandler := dataflow.GetHandler()
		err = dpHandler.DeleteAllBackups(ctx, orgID)
		tilog.Info(ctx, "delete all backups in org", zap.Uint64("orgID", orgID), zap.Error(err))
		if err != nil {
			tilog.Error(ctx, fmt.Sprintf("DP DeleteAllBackups err, org=%d", orgID), zap.Error(err))
			return errs.NewInternalGRPC("failed to delete all backups", err)
		}

		// 1. delete all projects in the org
		err := s.db.Transaction(func(tx *gorm.DB) error {
			projects, err := s.hub.Project.ListActiveProjectsByOrgID(ctx, tx, orgID)
			if err != nil {
				return err
			}

			for _, project := range projects {
				err = s.hub.Project.MarkProjectAsDeletedByID(ctx, tx, project.ID.Uint64())
				tilog.Info(ctx, "delete all projects in org", zap.Uint64("orgID", orgID), zap.Error(err))
				if err != nil {
					return err
				}
				_, err := s.hub.Role.DeleteRoleBindingByScopeInfo(ctx, tx, entity.RoleBindingScopeProject, project.ID.Uint64(), "")
				if err != nil {
					tilog.Warn(ctx, "failed to delete project relate role_bindings", zap.Uint64("projectID", project.ID.Uint64()), zap.Error(err))
				}
			}

			// delete current org
			err = s.hub.Org.MarkOrgAsDeleted(ctx, tx, orgID)
			logger.Info("delete org", zap.Uint64("orgID", orgID), zap.Error(err))
			if err != nil {
				return err
			}
			_, err = s.hub.Role.DeleteRoleBindingByScopeInfo(ctx, tx, entity.RoleBindingScopeOrg, orgID, "")
			if err != nil {
				tilog.Warn(ctx, "failed to delete org relate role_bindings", zap.Uint64("orgID", orgID), zap.Error(err))
			}

			// delete all impersonated records
			return s.hub.Org.DeleteOrgImpersonates(ctx, tx, orgID)
		})
		if err != nil {
			return errs.NewInternalDB("failed to delete project/org", err)
		}

		return nil
	}
*/
func (s *OrgSvc) UpdateOrg(ctx context.Context, param org_dao.UpdateOrgParam) error {
	err := s.hub.Org.UpdateOrg(ctx, s.db.WithContext(ctx), param)
	if err != nil {
		return err
	}
	if err = publish.PublishOrgEvent(ctx, dto.OrgEventData{OrgId: param.Id, OrgEventType: entity.EntityEventTypeUpdate}); err != nil {
		tilog.Error(ctx, "PublishOrgEvent err", zap.Any("orgId", param.Id), zap.Error(err))
	}

	return err
}

func (s *OrgSvc) GetProjectIds(ctx context.Context, orgId uint64) ([]uint64, error) {
	var pids []uint64
	params := dto.ListParams{
		Filters: map[string]interface{}{
			"tenant_id": orgId,
			"status":    entity.ProjectStatusActive,
		},
		SortBy: "id",
		Order:  "asc",
	}
	objs, err := comsvc.ListEntities(ctx, params, &entity.Project{})
	if err != nil {

		return pids, err
	}
	projescts := objs.([]entity.Project)

	for _, p := range projescts {
		pids = append(pids, p.ID.Uint64())
	}
	return pids, err
}
