package admin

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

	"github.com/pingcap/dbaas/admin/forms"
	"github.com/pingcap/dbaas/pkg/salesforce"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/central-sdk/config"
	"github.com/tidbcloud/central-sdk/db"
	"github.com/tidbcloud/central-sdk/db/dbutil"
	"github.com/tidbcloud/central-sdk/db/entity"
	"github.com/tidbcloud/central-sdk/db/tenant"
	centralModel "github.com/tidbcloud/central-sdk/model"
	"github.com/tidbcloud/central-sdk/model/admin/grafana"
	"github.com/tidbcloud/central-sdk/model/billing"
	runtimemanager "github.com/tidbcloud/central-sdk/runtime/manager"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

type Mailer interface {
	SendPocRejectionEmail(to, url, user string) error
	SendAddCreditEmail(to string, amount int64) error
}

type GrafanaCollector interface {
	Search(ctx context.Context, params ...grafana.SearchParam) ([]grafana.FoundBoard, error)
}

type SalesForceMessenger interface {
	SyncLeads(lead salesforce.Lead) error
}

type Model struct {
	log              *zap.Logger
	mail             Mailer
	billingClient    billing.Service
	grafanaClient    GrafanaCollector
	runtimes         runtimemanager.Manager
	salesforceClient SalesForceMessenger
}

type ClusterProfile struct {
	ID        uint32
	Component []*ComponentProfile
}

type ComponentProfile struct {
	Name         string
	PriceHistory []ComponentPrice
}

type ComponentPrice struct {
	StartTime int64
	Price     float64
}

type Tenant struct {
	ID                  uint64
	Name                string
	Email               string
	Country             string
	Timezone            string
	Status              string
	Plan                string
	HasDiscount         bool
	Discount            float64
	DiscountDescription string
	DiscountStartTime   int64
	DiscountEndTime     int64
	BindCreditCardAt    *time.Time
}

var lck sync.Mutex

func NewModel(log *zap.Logger, mail Mailer, billingClient billing.Service, grafanaClient GrafanaCollector, runtimes runtimemanager.Manager, salesforceClient SalesForceMessenger) Model {
	return Model{
		log:              log,
		mail:             mail,
		billingClient:    billingClient,
		grafanaClient:    grafanaClient,
		runtimes:         runtimes,
		salesforceClient: salesforceClient,
	}
}

type UserContext struct {
	Model
	tenantAndUser *tenant.User
	log           *zap.Logger
}

// User returns a UserContext
func (m Model) User(user *tenant.User) UserContext {
	return UserContext{m, user, m.log.With(zap.String("userID", user.Email), zap.String("model", "admin"))}
}

type UpdateTenantReq struct {
	ID           uint64
	Name         string
	Email        string
	UtcOffset    int32
	TidbQuota    uint32
	TikvQuota    uint32
	TiflashQuota uint32
}

type RegisterTenantReq struct {
	Name   string
	Email  string
	Credit int64
}

func (m UserContext) ListAccessLog(tenantID uint64, query *centralModel.ListAccessLogQuery) ([]centralModel.AccessLog, int64, error) {
	return centralModel.ListAuditLogs(entity.ID(tenantID), query)
}

type ImpersonateReq struct {
	OrgID uint64
}

func (m UserContext) AddImpersonation(req *ImpersonateReq) error {
	if m.tenantAndUser.ID == 0 {
		return errors.New("not a normal user")
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		t, err := dbutil.GetTenantByID(tx, entity.ID(req.OrgID))
		if err != nil {
			return err
		}
		var existRecord []entity.ImpersonatedUser
		if err := entity.NewImpersonatedUserQuerySet(tx).UserIDEq(m.tenantAndUser.ID).All(&existRecord); err != nil {
			return errors.Annotate(err, "failed to load impersonated users")
		}
		if len(existRecord) > 0 {
			if existRecord[0].TenantID == entity.ID(req.OrgID) {
				log.Info("already impersonated this tenant", zap.Uint64("ID", req.OrgID), zap.String("email", m.tenantAndUser.Email))
				return nil
			}
			return errcode.NewInvalidInputErr(errors.Errorf("already impersonate a tenant, id is %d, please delete it first", existRecord[0].TenantID))
		}
		if err := tx.Create(&entity.ImpersonatedUser{UserID: m.tenantAndUser.ID, TenantID: entity.ID(req.OrgID)}).Error; err != nil {
			return errors.Annotate(err, "failed to save impersonated users")
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("%s impersonate the tenant %s(%d)", m.tenantAndUser.Email, t.Name, t.ID))
	})
	return err
}

func (m UserContext) DeleteImpersonation(orgID uint64) error {
	if m.tenantAndUser.User.ID == 0 {
		return errors.New("not a normal user")
	}
	err := db.Transaction(func(tx *gorm.DB) error {
		var existRecord entity.ImpersonatedUser
		if err := entity.NewImpersonatedUserQuerySet(tx).PreloadTenant().TenantIDEq(entity.ID(orgID)).UserIDEq(m.tenantAndUser.ID).One(&existRecord); err != nil {
			return errors.Annotate(err, "failed to load impersonated users")
		}
		if err := existRecord.Delete(tx); err != nil {
			return errors.Annotate(err, "failed to delete impersonated users")
		}
		return dbutil.AddAccessLogNow(tx, m.tenantAndUser.TenantID, m.tenantAndUser.Email, fmt.Sprintf("%s un-impersonate the tenant %s(%d)", m.tenantAndUser.Email, existRecord.Tenant.Name, existRecord.Tenant.ID))
	})
	return err
}

func (m Model) UpdateOrganizationPocStatus(req forms.UpdateOrgPocStatusReq) error {
	err := db.Transaction(func(tx *gorm.DB) error {
		t, err := dbutil.GetTenantByID(tx, entity.ID(req.OrgID))
		if err != nil {
			log.Error("failed to get organization by ID", zap.Uint64("tenantID", req.OrgID), zap.Error(err))
			return errcode.NewInternalErr(errors.Errorf("failed to get organization by ID %d", req.OrgID))
		}

		if t.Plan != entity.TenantPlanNewUser && t.Plan != entity.TenantPlanFreeTrial {
			return errcode.NewInvalidInputErr(errors.New("only allow this API to update POC status of NewUser or FreeTrial organization"))
		}

		if req.Approve {
			t.Plan = entity.TenantPlanPOC
			err = t.Update(tx, entity.TenantDBSchema.Plan)
			if err != nil {
				log.Error("failed to update organization to POC", zap.Uint64("tenantID", req.OrgID), zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to update organization plan to POC"))
			}

			var clusters []entity.Cluster
			err = entity.NewClusterQuerySet(tx).StatusNotIn(
				entity.ClusterStatusDeleted, entity.ClusterStatusCleared).TenantIDEq(entity.ID(req.OrgID)).All(&clusters)
			if err != nil {
				log.Error("failed to get clusters of organization", zap.Uint64("tenantID", req.OrgID), zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to get clusters of organization"))
			}

			for _, cluster := range clusters {
				_, err = dbutil.CreateBackupSchedule(tx, cluster.CreatedByID, cluster.ID)
				if err != nil {
					log.Error("failed to create backup schedule for cluster", zap.Uint64("tenantID", req.OrgID), zap.Uint64("clusterID", uint64(cluster.ID)), zap.Error(err))
					return errcode.NewInternalErr(errors.New("failed to create backup schedule for cluster"))
				}
			}
		} else {
			user, err := dbutil.GetUserByEmail(tx, t.Email)
			if err != nil {
				log.Error("failed to get admin user of tenant", zap.Uint64("tenantID", req.OrgID), zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to get admin user of tenant"))
			}
			consoleURL := fmt.Sprintf("https://%s", config.Global().ServerConfig.FrontendDNS)
			err = m.mail.SendPocRejectionEmail(t.Email, consoleURL, user.FirstName)
			if err != nil {
				log.Error("failed to send POC rejection email", zap.Uint64("tenantID", req.OrgID), zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to send POC rejection email"))
			}
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}
