package datasource

import (
	"context"
	"fmt"
	"time"

	"github.com/juju/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/account/idl/pbgen/proto/account"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	cloud2 "github.com/pingcap-inc/clinic/pkg/datasource/internal/cloud"
	"github.com/pingcap-inc/clinic/pkg/datasource/internal/op"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
	"github.com/pingcap-inc/clinic/pkg/db"
	model2 "github.com/pingcap-inc/clinic/pkg/db/model"
)

var orgDatasources = map[view.DataSourceType]organizationDatasource{
	view.CloudDataSource: &cloudOrganizationDatasource{},
	view.OPDataSource:    &opOrganizationDatasource{},
}

func GetOrganization(ctx *session.CtxUser, orgID uint) (*db.Organization, error) {
	org, err := op.GetOrganization(orgID)
	if err == nil && org != nil {
		return org, nil
	}

	return cloud2.GetOrganization(ctx, orgID)
}

func GetOrganizationByCloudID(ctx *session.CtxUser, externalID uint) (*db.Organization, error) {
	return cloud2.GetOrganizationByExternalID(ctx, externalID)
}

func IsUserInCloudOrg(uctx *session.CtxUser, externalID uint) (bool, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	userResp, err := accountC.GetUserByEmail(ctx, &account.GetUserByEmailRequest{Email: uctx.Email})
	if err != nil {
		return false, errors.Annotate(err, "call grpc failed")
	}
	if userResp.GetBaseResp().GetErrCode() != 0 {
		return false, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email)
	}
	resp, err := accountC.ListOrgsByUser(ctx, &account.ListOrgsByUserRequest{UserId: userResp.User.Id})
	if err != nil {
		return false, errors.Annotate(err, "call grpc failed")
	}
	// https://github.com/tidbcloud/pkgv2/blob/main/bizerr/code.go#L35
	if resp.GetBaseResp().GetErrCode() != 0 {
		return false, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s, dbass_user_id=%d",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email, userResp.User.Id)
	}
	for _, org := range resp.Orgs {
		if uint(org.Id) == externalID {
			return true, nil
		}
	}
	return false, nil
}

func HasPermissionForCloudUser(uctx *session.CtxUser, externalOrgID, projectID uint) (bool, error) {
	if externalOrgID == 0 {
		return false, fmt.Errorf("invalid org id, org_id=%d", externalOrgID)
	}
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	userResp, err := accountC.GetUserByEmail(ctx, &account.GetUserByEmailRequest{Email: uctx.Email})
	if err != nil {
		return false, errors.Annotate(err, "call grpc failed")
	}
	if userResp.GetBaseResp().GetErrCode() != 0 {
		return false, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email)
	}
	resourceUri := fmt.Sprintf("orgs/%d/projects/list", externalOrgID)
	if projectID != 0 {
		resourceUri = fmt.Sprintf("orgs/%d/projects/%d/clusters/list", externalOrgID, projectID)
	}
	resp, err := accountC.HasPermissionForUser(ctx, &account.RbacPermissionRequest{
		UserId:      userResp.User.Id,
		ResourceUri: resourceUri,
	})
	if err != nil {
		return false, errors.Annotate(err, "call grpc failed")
	}
	// https://github.com/tidbcloud/pkgv2/blob/main/bizerr/code.go#L35
	if resp.GetBaseResp().GetErrCode() != 0 {
		return false, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s, dbass_user_id=%d",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email, userResp.User.Id)
	}
	return resp.AuthResult, nil
}

func ListOrganizations(ctx *session.CtxUser, cond *model2.ListOrganizationsCondition) ([]db.Organization, int64, error) {
	if !ctx.Internal {
		if len(cond.IDs) == 0 && len(cond.DbaasOrgIDs) == 0 {
			return nil, 0, nil
		}
		if len(cond.IDs) == 0 {
			v, total, err := cloud2.ListOrganizations(ctx, view.QueryFilter{
				Page:        cond.Page,
				Limit:       cond.PerPage,
				ExtraFilter: cond,
			})
			return v.([]db.Organization), total, err
		}
		if len(cond.DbaasOrgIDs) == 0 {
			v, total, err := op.ListOrganizations(ctx, view.QueryFilter{
				Page:        cond.Page,
				Limit:       cond.PerPage,
				ExtraFilter: cond,
			})
			return v.([]db.Organization), total, err
		}
	}

	orgs := make([]db.Organization, 0)
	total, err := joinDataSourceToSlice(cloud2.ListOrganizations, op.ListOrganizations, ctx, view.QueryFilter{
		Page:        cond.Page,
		Limit:       cond.PerPage,
		ExtraFilter: cond,
	}, &orgs)

	return orgs, total, err
}

func ListOrganizationIDsByDBaasUser(uctx *session.CtxUser) ([]db.Organization, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	userResp, err := accountC.GetUserByEmail(ctx, &account.GetUserByEmailRequest{Email: uctx.Email})
	if err != nil {
		return nil, errors.Annotate(err, "call grpc failed")
	}
	if userResp.GetBaseResp().GetErrCode() != 0 {
		return nil, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email)
	}

	resp, err := accountC.ListOrgsByUser(ctx, &account.ListOrgsByUserRequest{UserId: userResp.User.Id})
	if err != nil {
		return nil, errors.Annotate(err, "call grpc failed")
	}
	// https://github.com/tidbcloud/pkgv2/blob/main/bizerr/code.go#L35
	if resp.GetBaseResp().GetErrCode() != 0 {
		return nil, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s, dbass_user_id=%d",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email, userResp.User.Id)
	}
	orgIds := make([]uint, 0, len(resp.GetOrgs()))
	for _, org := range resp.GetOrgs() {
		orgIds = append(orgIds, uint(org.GetId()))
	}
	log.Info("list orgs by user", zap.Any("orgIds", orgIds), zap.Any("user_id", userResp.User.Id))
	orgs, _, err := cloud2.ListOrganizations(uctx, view.QueryFilter{
		Page:  1,
		Limit: len(orgIds),
		ExtraFilter: &model2.ListOrganizationsCondition{
			DbaasOrgIDs: orgIds,
		},
	})
	if err != nil {
		return nil, err
	}
	return orgs.([]db.Organization), nil
}

func ListDbaasProjects(uctx *session.CtxUser, externalID uint) ([]*account.Project, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	userResp, err := accountC.GetUserByEmail(ctx, &account.GetUserByEmailRequest{Email: uctx.Email})
	if err != nil {
		return nil, errors.Annotate(err, "call grpc failed")
	}
	if userResp.GetBaseResp().GetErrCode() != 0 {
		return nil, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s",
			userResp.GetBaseResp().GetErrCode(),
			userResp.GetBaseResp().GetErrMsg(),
			uctx.Email)
	}

	if uctx.Internal {
		resp, err := accountC.ListProjectsByOrgs(ctx, &account.ListProjectsByOrgsRequest{
			OrgIds: []uint64{uint64(externalID)},
		})
		if err != nil {
			return nil, errors.Annotate(err, "call grpc failed")
		}
		// https://github.com/tidbcloud/pkgv2/blob/main/bizerr/code.go#L35
		if resp.GetBaseResp().GetErrCode() != 0 {
			return nil, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s, dbass_user_id=%d, org_id=%d",
				userResp.GetBaseResp().GetErrCode(),
				userResp.GetBaseResp().GetErrMsg(),
				uctx.Email, userResp.User.Id, externalID)
		}
		if len(resp.Projects) == 0 {
			return nil, fmt.Errorf("no project found, email=%s, dbass_user_id=%d, org_id=%d", uctx.Email, userResp.User.Id, externalID)
		}
		return resp.Projects[0].Projects, nil
	} else {
		resp, err := accountC.ListProjectsByOrgsInUser(ctx, &account.ListProjectsByOrgsInUserRequest{
			UserId: userResp.User.Id,
			OrgIds: []uint64{uint64(externalID)},
		})
		if err != nil {
			return nil, errors.Annotate(err, "call grpc failed")
		}
		// https://github.com/tidbcloud/pkgv2/blob/main/bizerr/code.go#L35
		if resp.GetBaseResp().GetErrCode() != 0 {
			return nil, fmt.Errorf("biz code not zero, err_code=%d, err_msg=%s, email=%s, dbass_user_id=%d, org_id=%d",
				userResp.GetBaseResp().GetErrCode(),
				userResp.GetBaseResp().GetErrMsg(),
				uctx.Email, userResp.User.Id, externalID)
		}
		if len(resp.Projects) == 0 {
			return nil, fmt.Errorf("no project found, email=%s, dbass_user_id=%d, org_id=%d", uctx.Email, userResp.User.Id, externalID)
		}
		return resp.Projects[0].Projects, nil
	}
}

func LastUpdatedTime(ctx *session.CtxUser, orgID uint) (int, int64, error) {
	org, err := GetOrganization(ctx, orgID)
	if err != nil {
		return 0, 0, err
	}
	projectIDs := make([]uint64, 0)
	hideServerlessTier := false
	if !ctx.Internal && org.OrgType == "cloud" {
		hideServerlessTier = true
		projects, err := ListDbaasProjects(ctx, org.DbaasOrgID)
		if err != nil {
			return 0, 0, err
		}
		if len(projects) == 0 {
			log.Error("no project found", zap.Uint("org_id", orgID),
				zap.Uint("dbaas_org_id", org.DbaasOrgID),
				zap.Uint("user_id", ctx.UserID),
				zap.String("email", ctx.Email),
			)
			return 0, 0, nil
		}
		for _, p := range projects {
			projectIDs = append(projectIDs, p.Id)
		}
	}

	ds, ok := orgDatasources[view.DataSourceType(org.OrgType)]
	if !ok {
		return 0, 0, fmt.Errorf("unsupport org type, type=%s", org.OrgType)
	}

	return ds.lastUpdatedTime(ctx, org, projectIDs, hideServerlessTier)
}

func RealTimeListOrganizations(ctx *session.CtxUser) ([]*account.Org, int64, error) {
	return cloud2.ListRealTimeOrganizations(ctx)
}

func RealtimeGetOrganization(ctx *session.CtxUser, tenantID uint64) (*account.Org, error) {
	return cloud2.RealtimeGetOrganization(ctx, tenantID)
}

type organizationDatasource interface {
	lastUpdatedTime(ctx *session.CtxUser, org *db.Organization, projectIDs []uint64, hideServerlessTier bool) (int, int64, error)
}

type cloudOrganizationDatasource struct{}

func (cd *cloudOrganizationDatasource) lastUpdatedTime(ctx *session.CtxUser, org *db.Organization, projectIDs []uint64, hideServerlessTier bool) (int, int64, error) {
	return cloud2.LastUpdatedTime(ctx, org, projectIDs, hideServerlessTier)
}

type opOrganizationDatasource struct{}

func (cd *opOrganizationDatasource) lastUpdatedTime(ctx *session.CtxUser, org *db.Organization, projectIDs []uint64, hideServerlessTier bool) (int, int64, error) {
	return op.LastUpdatedTime(org)
}
