package service

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

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

const (
	roleKind     = "Role"
	roleListKind = "RoleList"
)

// RoleService is the interface for role operations
type RoleService interface {
	// create role
	Create(context.Context, *rolev3.Role) (*rolev3.Role, error)
	// get role by id
	GetByID(context.Context, *rolev3.Role) (*rolev3.Role, error)
	// get role by name
	GetByName(context.Context, *rolev3.Role) (*rolev3.Role, error)
	// create or update role
	Update(context.Context, *rolev3.Role) (*rolev3.Role, error)
	// delete role
	Delete(context.Context, *rolev3.Role) (*rolev3.Role, error)
	// list roles
	List(context.Context, *rolev3.Role) (*rolev3.RoleList, error)
}

// roleService implements RoleService
type roleService struct {
	db  *bun.DB
	azc AuthzService
}

// NewRoleService return new role service
func NewRoleService(db *bun.DB, azc AuthzService) RoleService {
	return &roleService{db: db, azc: azc}
}

func (s *roleService) getPartnerOrganization(ctx context.Context, db bun.IDB, role *rolev3.Role) (uuid.UUID, uuid.UUID, error) {
	partner := role.GetMetadata().GetPartner()
	org := role.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 *roleService) deleteRolePermissionMapping(ctx context.Context, db bun.IDB, rleId uuid.UUID, role *rolev3.Role) (*rolev3.Role, error) {
	err := dao.DeleteX(ctx, db, "resource_role_id", rleId, &models.ResourceRolePermission{})
	if err != nil {
		return &rolev3.Role{}, err
	}

	drpm := authzv1.FilteredRolePermissionMapping{Role: role.GetMetadata().GetName()}
	success, err := s.azc.DeleteRolePermissionMappings(ctx, &drpm)
	if err != nil {
		return &rolev3.Role{}, fmt.Errorf("unable to delete mapping from authz; %v", err)
	}
	if !success.Res {
		fmt.Println("No roles deleted") // TODO: maybe it should not return false?
	}

	return role, nil
}

func (s *roleService) createRolePermissionMapping(ctx context.Context, db bun.IDB, role *rolev3.Role, ids parsedIds) (*rolev3.Role, error) {
	perms := role.GetSpec().GetRolepermissions()

	var items []models.ResourceRolePermission
	for _, p := range perms {
		entity, err := dao.GetIdByName(ctx, db, p, &models.ResourcePermission{})
		if err != nil {
			return role, fmt.Errorf("unable to find role permission '%v'", p)
		}
		if prm, ok := entity.(*models.ResourcePermission); ok {
			items = append(items, models.ResourceRolePermission{
				ResourceRoleId:       ids.Id,
				ResourcePermissionId: prm.ID,
			})
		} else {
			return role, fmt.Errorf("unable to find role permission '%v'", p)
		}
	}
	if len(items) > 0 {
		_, err := dao.Create(ctx, db, &items)
		if err != nil {
			return role, err
		}

		crpm := authzv1.RolePermissionMappingList{
			RolePermissionMappingList: []*authzv1.RolePermissionMapping{{
				Role:       role.GetMetadata().GetName(),
				Permission: role.Spec.Rolepermissions,
			}},
		}
		success, err := s.azc.CreateRolePermissionMappings(ctx, &crpm)
		if err != nil || !success.Res {
			return role, fmt.Errorf("unable to create mapping in authz; %v", err)
		}
	}
	return role, nil
}

func (s *roleService) Create(ctx context.Context, role *rolev3.Role) (*rolev3.Role, error) {
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, role)
	if err != nil {
		return nil, fmt.Errorf("unable to get partner and org id")
	}
	r, _ := dao.GetIdByNamePartnerOrg(ctx, s.db, role.GetMetadata().GetName(), uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Role{})
	if r != nil {
		return nil, fmt.Errorf("role '%v' already exists", role.GetMetadata().GetName())
	}

	scope := role.GetSpec().GetScope()
	if !contains([]string{"system", "organization", "project"}, strings.ToLower(scope)) {
		return nil, fmt.Errorf("unknown scope '%v'", scope)
	}

	// convert v3 spec to internal models
	rle := models.Role{
		Name:           role.GetMetadata().GetName(),
		Description:    role.GetMetadata().GetDescription(),
		CreatedAt:      time.Now(),
		ModifiedAt:     time.Now(),
		Trash:          false,
		OrganizationId: organizationId,
		PartnerId:      partnerId,
		IsGlobal:       role.GetSpec().GetIsGlobal(),
		Scope:          strings.ToLower(scope),
	}

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

	entity, err := dao.Create(ctx, tx, &rle)
	if err != nil {
		tx.Rollback()
		return &rolev3.Role{}, err
	}

	//update v3 spec
	if createdRole, ok := entity.(*models.Role); ok {
		role, err = s.createRolePermissionMapping(ctx, tx, role, parsedIds{Id: createdRole.ID, Partner: partnerId, Organization: organizationId})
		if err != nil {
			tx.Rollback()
			return &rolev3.Role{}, err
		}
	} else {
		tx.Rollback()
		return &rolev3.Role{}, fmt.Errorf("unable to create role '%v'", role.GetMetadata().GetName())
	}

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

}

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

	if rle, ok := entity.(*models.Role); ok {
		role, err = s.toV3Role(ctx, s.db, role, rle)
		if err != nil {
			return &rolev3.Role{}, err
		}
		return role, nil
	}
	return role, nil

}

func (s *roleService) GetByName(ctx context.Context, role *rolev3.Role) (*rolev3.Role, error) {
	name := role.GetMetadata().GetName()
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, role)
	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.Role{})
	if err != nil {
		return &rolev3.Role{}, err
	}

	if rle, ok := entity.(*models.Role); ok {
		role, err = s.toV3Role(ctx, s.db, role, rle)
		if err != nil {
			return &rolev3.Role{}, err
		}
	} else {
		return nil, fmt.Errorf("unable to find role")
	}
	return role, nil

}

func (s *roleService) Update(ctx context.Context, role *rolev3.Role) (*rolev3.Role, error) {
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, role)
	if err != nil {
		return nil, fmt.Errorf("unable to get partner and org id")
	}

	name := role.GetMetadata().GetName()
	entity, err := dao.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Role{})
	if err != nil {
		return role, fmt.Errorf("unable to find role '%v'", name)
	}

	if rle, ok := entity.(*models.Role); ok {
		//update role details
		rle.Name = role.Metadata.Name
		rle.Description = role.Metadata.Description
		rle.IsGlobal = role.Spec.IsGlobal
		rle.Scope = role.Spec.Scope
		rle.ModifiedAt = time.Now()

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

		_, err = dao.Update(ctx, tx, rle.ID, rle)
		if err != nil {
			tx.Rollback()
			return &rolev3.Role{}, err
		}

		role, err = s.deleteRolePermissionMapping(ctx, tx, rle.ID, role)
		if err != nil {
			tx.Rollback()
			return &rolev3.Role{}, err
		}

		role, err = s.createRolePermissionMapping(ctx, tx, role, parsedIds{Id: rle.ID, Partner: partnerId, Organization: organizationId})
		if err != nil {
			tx.Rollback()
			return &rolev3.Role{}, err
		}

		//update spec and status
		role.Spec = &rolev3.RoleSpec{
			IsGlobal: rle.IsGlobal,
			Scope:    rle.Scope,
		}

		err = tx.Commit()
		if err != nil {
			tx.Rollback()
			_log.Warn("unable to commit changes", err)
		}
		return role, nil
	}
	return &rolev3.Role{}, fmt.Errorf("unable to update role '%v'", role.GetMetadata().GetName())

}

func (s *roleService) Delete(ctx context.Context, role *rolev3.Role) (*rolev3.Role, error) {
	name := role.GetMetadata().GetName()
	partnerId, organizationId, err := s.getPartnerOrganization(ctx, s.db, role)
	if err != nil {
		return &rolev3.Role{}, fmt.Errorf("unable to get partner and org id; %v", err)
	}

	entity, err := dao.GetByNamePartnerOrg(ctx, s.db, name, uuid.NullUUID{UUID: partnerId, Valid: true}, uuid.NullUUID{UUID: organizationId, Valid: true}, &models.Role{})
	if err != nil {
		return &rolev3.Role{}, err
	}

	if rle, ok := entity.(*models.Role); ok {

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

		role, err = s.deleteRolePermissionMapping(ctx, tx, rle.ID, role)
		if err != nil {
			tx.Rollback()
			return &rolev3.Role{}, err
		}

		err = dao.Delete(ctx, tx, rle.ID, rle)
		if err != nil {
			tx.Rollback()
			return &rolev3.Role{}, err
		}

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

	return &rolev3.Role{}, fmt.Errorf("unable to delete role '%v'", role.GetMetadata().GetName())
}

func (s *roleService) toV3Role(ctx context.Context, db bun.IDB, role *rolev3.Role, rle *models.Role) (*rolev3.Role, error) {
	labels := make(map[string]string)
	labels["organization"] = role.GetMetadata().GetOrganization()
	labels["partner"] = role.GetMetadata().GetPartner()

	role.ApiVersion = apiVersion
	role.Kind = roleKind
	role.Metadata = &v3.Metadata{
		Name:         rle.Name,
		Description:  rle.Description,
		Organization: role.GetMetadata().GetOrganization(),
		Partner:      role.GetMetadata().GetPartner(),
		Labels:       labels,
		ModifiedAt:   timestamppb.New(rle.ModifiedAt),
	}
	entities, err := dao.GetRolePermissions(ctx, db, rle.ID)
	if err != nil {
		return role, err
	}
	permissions := []string{}
	for _, p := range entities {
		permissions = append(permissions, p.Name)
	}

	role.Spec = &rolev3.RoleSpec{
		IsGlobal:        rle.IsGlobal,
		Scope:           rle.Scope,
		Rolepermissions: permissions,
	}
	return role, nil
}

func (s *roleService) List(ctx context.Context, role *rolev3.Role) (*rolev3.RoleList, error) {
	var roles []*rolev3.Role
	roleList := &rolev3.RoleList{
		ApiVersion: apiVersion,
		Kind:       roleListKind,
		Metadata: &v3.ListMetadata{
			Count: 0,
		},
	}
	if len(role.Metadata.Organization) > 0 {
		orgId, err := dao.GetOrganizationId(ctx, s.db, role.Metadata.Organization)
		if err != nil {
			return roleList, err
		}
		partId, err := dao.GetPartnerId(ctx, s.db, role.Metadata.Partner)
		if err != nil {
			return roleList, err
		}
		var rles []models.Role
		entities, err := dao.List(ctx, s.db, uuid.NullUUID{UUID: partId, Valid: true}, uuid.NullUUID{UUID: orgId, Valid: true}, &rles)
		if err != nil {
			return roleList, err
		}
		if rles, ok := entities.(*[]models.Role); ok {
			for _, rle := range *rles {
				entry := &rolev3.Role{Metadata: role.GetMetadata()}
				entry, err = s.toV3Role(ctx, s.db, entry, &rle)
				if err != nil {
					return roleList, err
				}
				roles = append(roles, entry)
			}

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

	} else {
		return roleList, fmt.Errorf("missing organization id in metadata")
	}
	return roleList, nil
}
