package admin

import (
	"context"
	"fmt"
	"sync"

	"github.com/pingcap/dbaas/admin/forms"
	"github.com/pingcap/dbaas/central/config"
	"github.com/pingcap/dbaas/central/db"
	"github.com/pingcap/dbaas/central/db/dbutil"
	"github.com/pingcap/dbaas/central/db/entity"
	"github.com/pingcap/dbaas/central/db/tenant"
	infraapi "github.com/pingcap/dbaas/central/infra-api"
	centralModel "github.com/pingcap/dbaas/central/model"
	"github.com/pingcap/dbaas/central/model/admin/grafana"
	centralBillingModel "github.com/pingcap/dbaas/central/model/billing"
	"github.com/pingcap/dbaas/central/model/cdc"
	centralTenantModel "github.com/pingcap/dbaas/central/model/tenant"
	"github.com/pingcap/errcode"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/pkgv2/log"
	mailv2 "github.com/tidbcloud/pkgv2/mail/v2"
	"github.com/tidbcloud/pkgv2/salesforce"
	"go.uber.org/zap"
	"gorm.io/gorm"
)

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
	mailFacade       mailv2.Facade
	grafanaClient    GrafanaCollector
	salesforceClient SalesForceMessenger
	api              *infraapi.API
	centralTenant    centralTenantModel.Model
	centralBilling   centralBillingModel.Model
	centralCDC       cdc.Model
}

type Project struct {
	ID   uint64
	Name string
}

type Tenant centralTenantModel.Organization

var lck sync.Mutex

func NewModel(
	log *zap.Logger,
	mailFacade mailv2.Facade,
	grafanaClient GrafanaCollector,
	salesforceClient SalesForceMessenger,
	infraAPI *infraapi.API,
	centralTenant centralTenantModel.Model,
	centralBilling centralBillingModel.Model,
	centralCDC cdc.Model,
) Model {
	return Model{
		log:              log,
		mailFacade:       mailFacade,
		grafanaClient:    grafanaClient,
		salesforceClient: salesforceClient,
		api:              infraAPI,
		centralTenant:    centralTenant,
		centralBilling:   centralBilling,
		centralCDC:       centralCDC,
	}
}

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

// User returns a UserContext
func (m Model) User(ctx context.Context, user *tenant.User) UserContext {
	return UserContext{
		Model:         m,
		tenantAndUser: user,
		log:           m.log.With(zap.String("userID", user.Email), zap.String("model", "admin")).With(log.FieldsFromContext(ctx)...),
		mailFacade:    m.mailFacade,
	}
}

func (m UserContext) BillingContext(tenantID uint64) centralBillingModel.Context {
	return m.centralBilling.User(&tenant.User{TenantID: entity.ID(tenantID)})
}

func (m UserContext) BillingModel() centralBillingModel.Model {
	return m.centralBilling
}

func (m UserContext) CDCModel() cdc.Model {
	return m.centralCDC
}

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(ctx context.Context, 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(ctx, "already impersonated this tenant", zap.Uint64("ID", req.OrgID), zap.String("email", m.tenantAndUser.Email))
				return nil
			}
			err = m.DeleteImpersonation(uint64(existRecord[0].TenantID))
			if err != nil {
				return err
			}
			return nil
		}
		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(ctx context.Context, req forms.UpdateOrgPocStatusReq) error {
	logger := log.WithContextL(ctx).With(zap.Uint64("tenantID", req.OrgID))

	err := db.Transaction(func(tx *gorm.DB) error {
		t, err := dbutil.GetTenantByID(tx, entity.ID(req.OrgID))
		if err != nil {
			log.Error(ctx, "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 req.Approve {
			// Use tenant model to enable the POC
			err := m.centralBilling.User(&tenant.User{TenantID: entity.ID(req.OrgID)}).EnableOrgPOCPlan()
			if err != nil {
				return err
			}
		} else {
			user, err := dbutil.GetUserByEmail(tx, t.Email)
			if err != nil {
				logger.Error("failed to get admin user of tenant", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to get admin user of tenant"))
			}
			consoleURL := fmt.Sprintf("https://%s", config.Global().ServerConfig.FrontendDNS)
			vars := map[string]string{
				"userName": user.FirstName,
				"url":      consoleURL,
			}
			err = m.mailFacade.SendMailWithTemplate(ctx, []string{t.Email}, config.EmailSubjectPocRejection, config.TNPoCRejected, vars, nil)
			if err != nil {
				logger.Error("failed to send POC rejection email", zap.Error(err))
				return errcode.NewInternalErr(errors.New("failed to send POC rejection email"))
			}
		}
		return nil
	})
	if err != nil {
		return err
	}
	return nil
}
