package service

import (
	"context"
	"database/sql"
	"fmt"
	"strings"
	"time"

	"github.com/RafayLabs/rcloud-base/internal/dao"
	"github.com/RafayLabs/rcloud-base/internal/models"
	"github.com/RafayLabs/rcloud-base/pkg/query"
	authzv1 "github.com/RafayLabs/rcloud-base/proto/types/authz"
	commonv3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
	v3 "github.com/RafayLabs/rcloud-base/proto/types/commonpb/v3"
	userv3 "github.com/RafayLabs/rcloud-base/proto/types/userpb/v3"
	"github.com/google/uuid"
	bun "github.com/uptrace/bun"
	"google.golang.org/protobuf/types/known/timestamppb"
)

const (
	groupKind     = "Group"
	groupListKind = "GroupList"
)

// GroupService is the interface for group operations
type GroupService interface {
	// create group
	Create(context.Context, *userv3.Group) (*userv3.Group, error)
	// get group by id
	GetByID(context.Context, *userv3.Group) (*userv3.Group, error)
	// get group by name
	GetByName(context.Context, *userv3.Group) (*userv3.Group, error)
	// create or update group
	Update(context.Context, *userv3.Group) (*userv3.Group, error)
	// delete group
	Delete(context.Context, *userv3.Group) (*userv3.Group, error)
	// list groups
	List(context.Context, ...query.Option) (*userv3.GroupList, error)
}

// groupService implements GroupService
type groupService struct {
	db  *bun.DB
	azc AuthzService
}

// NewGroupService return new group service
func NewGroupService(db *bun.DB, azc AuthzService) GroupService {
	return &groupService{db: db, azc: azc}
}

func (s *groupService) deleteGroupRoleRelaitons(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) {
	// delete previous entries
	// TODO: single delete command
	err := dao.DeleteX(ctx, db, "group_id", groupId, &models.GroupRole{})
	if err != nil {
		return &userv3.Group{}, err
	}
	err = dao.DeleteX(ctx, db, "group_id", groupId, &models.ProjectGroupRole{})
	if err != nil {
		return &userv3.Group{}, err
	}
	err = dao.DeleteX(ctx, db, "group_id", groupId, &models.ProjectGroupNamespaceRole{})
	if err != nil {
		return &userv3.Group{}, err
	}

	_, err = s.azc.DeletePolicies(ctx, &authzv1.Policy{Sub: "g:" + group.GetMetadata().GetName()})
	if err != nil {
		return &userv3.Group{}, fmt.Errorf("unable to delete group-role relations from authz; %v", err)
	}
	return group, nil
}

// Map roles to groups
func (s *groupService) createGroupRoleRelations(ctx context.Context, db bun.IDB, group *userv3.Group, ids parsedIds) (*userv3.Group, error) {
	// TODO: add transactions
	projectNamespaceRoles := group.GetSpec().GetProjectNamespaceRoles()

	var pgrs []models.ProjectGroupRole
	var grs []models.GroupRole
	var ps []*authzv1.Policy
	for _, pnr := range projectNamespaceRoles {
		role := pnr.GetRole()
		entity, err := dao.GetByName(ctx, db, role, &models.Role{})
		if err != nil {
			return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role)
		}
		var roleId uuid.UUID
		var roleName string
		var scope string
		if rle, ok := entity.(*models.Role); ok {
			roleId = rle.ID
			roleName = rle.Name
			scope = strings.ToLower(rle.Scope)
		} else {
			return &userv3.Group{}, fmt.Errorf("unable to find role '%v'", role)
		}

		project := pnr.GetProject()
		org := group.GetMetadata().GetOrganization()

		switch scope {
		case "system":
			gr := models.GroupRole{
				Trash:          false,
				RoleId:         roleId,
				PartnerId:      ids.Partner,
				OrganizationId: ids.Organization,
				GroupId:        ids.Id,
				Active:         true,
			}
			grs = append(grs, gr)
			ps = append(ps, &authzv1.Policy{
				Sub:  "g:" + group.GetMetadata().GetName(),
				Ns:   "*",
				Proj: "*",
				Org:  "*",
				Obj:  role,
			})
		case "organization":
			if org == "" {
				return &userv3.Group{}, fmt.Errorf("no org name provided for role '%v'", roleName)
			}
			gr := models.GroupRole{
				Trash:          false,
				RoleId:         roleId,
				PartnerId:      ids.Partner,
				OrganizationId: ids.Organization,
				GroupId:        ids.Id,
				Active:         true,
			}
			grs = append(grs, gr)
			ps = append(ps, &authzv1.Policy{
				Sub:  "g:" + group.GetMetadata().GetName(),
				Ns:   "*",
				Proj: "*",
				Org:  org,
				Obj:  role,
			})
		case "project":
			if org == "" {
				return &userv3.Group{}, fmt.Errorf("no org name provided for role '%v'", roleName)
			}
			if project == "" {
				return &userv3.Group{}, fmt.Errorf("no project name provided for role '%v'", roleName)
			}
			projectId, err := dao.GetProjectId(ctx, s.db, project)
			if err != nil {
				return &userv3.Group{}, fmt.Errorf("unable to find project '%v'", project)
			}
			pgr := models.ProjectGroupRole{
				Trash:          false,
				RoleId:         roleId,
				PartnerId:      ids.Partner,
				OrganizationId: ids.Organization,
				GroupId:        ids.Id,
				ProjectId:      projectId,
				Active:         true,
			}
			pgrs = append(pgrs, pgr)

			ps = append(ps, &authzv1.Policy{
				Sub:  "g:" + group.GetMetadata().GetName(),
				Ns:   "*",
				Proj: project,
				Org:  org,
				Obj:  role,
			})
		}
	}
	if len(pgrs) > 0 {
		_, err := dao.Create(ctx, db, &pgrs)
		if err != nil {
			return &userv3.Group{}, err
		}
	}
	if len(grs) > 0 {
		_, err := dao.Create(ctx, db, &grs)
		if err != nil {
			return &userv3.Group{}, err
		}
	}

	if len(ps) > 0 {
		success, err := s.azc.CreatePolicies(ctx, &authzv1.Policies{Policies: ps})
		if err != nil || !success.Res {
			return &userv3.Group{}, fmt.Errorf("unable to create mapping in authz; %v", err)
		}
	}

	return group, nil
}

func (s *groupService) deleteGroupAccountRelations(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) {
	err := dao.DeleteX(ctx, db, "group_id", groupId, &models.GroupAccount{})
	if err != nil {
		return &userv3.Group{}, fmt.Errorf("unable to delete user; %v", err)
	}

	_, err = s.azc.DeleteUserGroups(ctx, &authzv1.UserGroup{Grp: "g:" + group.GetMetadata().GetName()})
	if err != nil {
		return &userv3.Group{}, fmt.Errorf("unable to delete group-user relations from authz; %v", err)
	}
	return group, nil
}

// Update the users(account) mapped to each group
func (s *groupService) createGroupAccountRelations(ctx context.Context, db bun.IDB, groupId uuid.UUID, group *userv3.Group) (*userv3.Group, error) {
	// TODO: add transactions
	var grpaccs []models.GroupAccount
	var ugs []*authzv1.UserGroup
	for _, account := range unique(group.GetSpec().GetUsers()) {
		// FIXME: do combined lookup
		entity, err := dao.GetIdByTraits(ctx, db, account, &models.KratosIdentities{})
		if err != nil {
			return &userv3.Group{}, fmt.Errorf("unable to find user '%v'", account)
		}
		if acc, ok := entity.(*models.KratosIdentities); ok {
			grp := models.GroupAccount{
				CreatedAt:  time.Now(),
				ModifiedAt: time.Now(),
				Trash:      false,
				AccountId:  acc.ID,
				GroupId:    groupId,
				Active:     true,
			}
			grpaccs = append(grpaccs, grp)
			ugs = append(ugs, &authzv1.UserGroup{
				Grp:  "g:" + group.GetMetadata().GetName(),
				User: "u:" + account,
			})
		}
	}
	if len(grpaccs) == 0 {
		return group, nil
	}
	_, err := dao.Create(ctx, db, &grpaccs)
	if err != nil {
		return &userv3.Group{}, err
	}

	// TODO: revert our db inserts if this fails
	// Just FYI, the success can be false if we delete the db directly but casbin has it available internally
	_, err = s.azc.CreateUserGroups(ctx, &authzv1.UserGroups{UserGroups: ugs})
	if err != nil {
		return &userv3.Group{}, fmt.Errorf("unable to create mapping in authz; %v", err)
	}

	return group, nil
}

// TODO: move this to utils, make it accept two strings (names)
func (s *groupService) getPartnerOrganization(ctx context.Context, db bun.IDB, group *userv3.Group) (uuid.UUID, uuid.UUID, error) {
	partner := group.GetMetadata().GetPartner()
	org := group.GetMetadata().GetOrganization()
	partnerId, err := dao.GetPartnerId(ctx, db, partner)
	if err != nil {
		return uuid.Nil, uuid.Nil, err
	}
	organizationId, err := dao.GetOrganizationId(ctx, db, org)
	if err != nil {
		return partnerId, uuid.Nil, err
	}
	return partnerId, organizationId, nil

}

func (s *groupService) Create(ctx context.Context, group *userv3.Group) (*userv3.Group, error) {
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, group)
	if err != nil {
		return nil, fmt.Errorf("unable to get partner and org id")
	}
	g, _ := dao.GetIdByNamePartnerOrg(ctx, s.db, group.GetMetadata().GetName(), uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{})
	if g != nil {
		return nil, fmt.Errorf("group '%v' already exists", group.GetMetadata().GetName())
	}
	//convert v3 spec to internal models
	grp := models.Group{
		Name:           group.GetMetadata().GetName(),
		Description:    group.GetMetadata().GetDescription(),
		CreatedAt:      time.Now(),
		ModifiedAt:     time.Now(),
		Trash:          false,
		OrganizationId: organizationId,
		PartnerId:      partnerId,
		Type:           group.GetSpec().GetType(),
	}

	tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
	if err != nil {
		return &userv3.Group{}, err
	}

	entity, err := dao.Create(ctx, tx, &grp)
	if err != nil {
		tx.Rollback() // TODO: check errors for rollback (and do what?)
		return &userv3.Group{}, err
	}

	//update v3 spec
	if grp, ok := entity.(*models.Group); ok {
		// we can get previous group using the id, find users/roles from that and delete those
		group, err = s.createGroupAccountRelations(ctx, tx, grp.ID, group)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}

		group, err = s.createGroupRoleRelations(ctx, tx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId})
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}

		err = tx.Commit()
		if err != nil {
			tx.Rollback()
			_log.Warn("unable to commit changes", err)
		}
		return group, nil
	}
	tx.Rollback()
	return &userv3.Group{}, fmt.Errorf("unable to create group")
}

func (s *groupService) toV3Group(ctx context.Context, db bun.IDB, group *userv3.Group, grp *models.Group) (*userv3.Group, error) {
	labels := make(map[string]string)
	labels["organization"] = group.GetMetadata().GetOrganization()
	labels["partner"] = group.GetMetadata().GetPartner()

	group.ApiVersion = apiVersion
	group.Kind = groupKind
	group.Metadata = &v3.Metadata{
		Name:         grp.Name,
		Description:  grp.Description,
		Organization: group.GetMetadata().GetOrganization(),
		Partner:      group.GetMetadata().GetPartner(),
		Labels:       labels,
		ModifiedAt:   timestamppb.New(grp.ModifiedAt),
	}
	users, err := dao.GetUsers(ctx, db, grp.ID)
	if err != nil {
		return &userv3.Group{}, err
	}
	userNames := []string{}
	for _, u := range users {
		userNames = append(userNames, u.Traits["email"].(string))
	}

	roles, err := dao.GetGroupRoles(ctx, db, grp.ID)
	if err != nil {
		return &userv3.Group{}, err
	}
	group.Spec = &userv3.GroupSpec{
		Type:                  grp.Type,
		Users:                 userNames,
		ProjectNamespaceRoles: roles,
	}
	return group, nil
}

func (s *groupService) GetByID(ctx context.Context, group *userv3.Group) (*userv3.Group, error) {
	id := group.GetMetadata().GetId()
	uid, err := uuid.Parse(id)
	if err != nil {
		return &userv3.Group{}, err
	}
	entity, err := dao.GetByID(ctx, s.db, uid, &models.Group{})
	if err != nil {
		return &userv3.Group{}, err
	}

	if grp, ok := entity.(*models.Group); ok {
		return s.toV3Group(ctx, s.db, group, grp)
	}
	return group, nil

}

func (s *groupService) GetByName(ctx context.Context, group *userv3.Group) (*userv3.Group, error) {
	name := group.GetMetadata().GetName()
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, group)
	if err != nil {
		return nil, fmt.Errorf("unable to get partner and org id")
	}
	entity, err := dao.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{})
	if err != nil {
		return &userv3.Group{}, err
	}

	if grp, ok := entity.(*models.Group); ok {
		return s.toV3Group(ctx, s.db, group, grp)
	}
	return group, nil

}

func (s *groupService) Update(ctx context.Context, group *userv3.Group) (*userv3.Group, error) {
	// TODO: inform when unchanged
	name := group.GetMetadata().GetName()
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, group)
	if err != nil {
		return nil, fmt.Errorf("unable to get partner and org id")
	}
	entity, err := dao.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{})
	if err != nil {
		return &userv3.Group{}, fmt.Errorf("no group found with name '%v'", name)
	}

	if grp, ok := entity.(*models.Group); ok {
		// TODO: are we not letting them update org/partner?
		grp.Name = group.Metadata.Name
		grp.Description = group.Metadata.Description
		grp.Type = group.Spec.Type
		grp.ModifiedAt = time.Now()

		tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
		if err != nil {
			return &userv3.Group{}, err
		}

		// update account/role links
		group, err = s.deleteGroupAccountRelations(ctx, tx, grp.ID, group)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}
		group, err = s.createGroupAccountRelations(ctx, tx, grp.ID, group)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}
		group, err = s.deleteGroupRoleRelaitons(ctx, tx, grp.ID, group)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}
		group, err = s.createGroupRoleRelations(ctx, tx, group, parsedIds{Id: grp.ID, Partner: partnerId, Organization: organizationId})
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}

		_, err = dao.Update(ctx, tx, grp.ID, grp)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}

		err = tx.Commit()
		if err != nil {
			tx.Rollback()
			_log.Warn("unable to commit changes", err)
		}

		// update spec and status
		group.Spec = &userv3.GroupSpec{
			Type:                  grp.Type,
			Users:                 group.Spec.Users, // TODO: update from db resp or no update?
			ProjectNamespaceRoles: group.Spec.ProjectNamespaceRoles,
		}
	}

	return group, nil
}

func (s *groupService) Delete(ctx context.Context, group *userv3.Group) (*userv3.Group, error) {
	name := group.GetMetadata().GetName()
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, group)
	if err != nil {
		return &userv3.Group{}, fmt.Errorf("unable to get partner and org id")
	}
	entity, err := dao.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Group{})
	if err != nil {
		return &userv3.Group{}, err
	}
	if grp, ok := entity.(*models.Group); ok {

		tx, err := s.db.BeginTx(ctx, &sql.TxOptions{})
		if err != nil {
			return &userv3.Group{}, err
		}

		group, err = s.deleteGroupRoleRelaitons(ctx, tx, grp.ID, group)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}
		group, err = s.deleteGroupAccountRelations(ctx, tx, grp.ID, group)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}
		err = dao.Delete(ctx, tx, grp.ID, grp)
		if err != nil {
			tx.Rollback()
			return &userv3.Group{}, err
		}

		err = tx.Commit()
		if err != nil {
			tx.Rollback()
			_log.Warn("unable to commit changes", err)
		}
		return group, nil
	}

	return &userv3.Group{}, fmt.Errorf("unable to delete group")
}

func (s *groupService) List(ctx context.Context, opts ...query.Option) (*userv3.GroupList, error) {
	var groups []*userv3.Group
	groupList := &userv3.GroupList{
		ApiVersion: apiVersion,
		Kind:       groupListKind,
		Metadata: &v3.ListMetadata{
			Count: 0,
		},
	}

	queryOptions := commonv3.QueryOptions{}
	for _, opt := range opts {
		opt(&queryOptions)
	}

	orgId, err := dao.GetOrganizationId(ctx, s.db, queryOptions.Organization)
	if err != nil {
		return groupList, err
	}
	partId, err := dao.GetPartnerId(ctx, s.db, queryOptions.Partner)
	if err != nil {
		return groupList, err
	}
	var grps []models.Group
	entities, err := dao.ListFiltered(ctx, s.db,
		uuid.NullUUID{UUID: partId, Valid: true}, uuid.NullUUID{UUID: orgId, Valid: true},
		&grps,
		queryOptions.Q,
		queryOptions.OrderBy,
		queryOptions.Order,
		int(queryOptions.Limit),
		int(queryOptions.Offset),
	)
	if err != nil {
		return groupList, err
	}
	if grps, ok := entities.(*[]models.Group); ok {
		for _, grp := range *grps {
			entry := &userv3.Group{Metadata: &commonv3.Metadata{
				Organization: queryOptions.Organization,
				Partner:      queryOptions.Partner,
			}}
			entry, err = s.toV3Group(ctx, s.db, entry, &grp)
			if err != nil {
				return groupList, err
			}
			groups = append(groups, entry)
		}

		//update the list metadata and items response
		groupList.Metadata = &v3.ListMetadata{
			Count: int64(len(groups)),
		}
		groupList.Items = groups
	}

	return groupList, nil
}
