package usecase

import (
	"context"
	"regexp"
	"strings"

	"gateway/internal/model/permission/domain"
	"gateway/internal/model/permission/domain/entity"
	"gateway/internal/model/permission/domain/enum"
	domrepo "gateway/internal/model/permission/domain/repository"
	dom "gateway/internal/model/permission/domain/usecase"
)

// roleKeyPattern enforces lower-case alphanumeric / underscore / dot keys.
var roleKeyPattern = regexp.MustCompile(`^[a-z][a-z0-9._-]{1,63}$`)

// RoleUseCaseParam injects the role + role-permission + user-role repos.
// User roles are needed so Delete can refuse when assignments still exist.
type RoleUseCaseParam struct {
	Roles           domrepo.RoleRepository
	RolePermissions domrepo.RolePermissionRepository
	UserRoles       domrepo.UserRoleRepository
}

type roleUseCase struct {
	roles     domrepo.RoleRepository
	rolePerms domrepo.RolePermissionRepository
	userRoles domrepo.UserRoleRepository
}

// NewRoleUseCase returns the tenant-scoped role manager.
func NewRoleUseCase(param RoleUseCaseParam) dom.RoleUseCase {
	return &roleUseCase{
		roles:     param.Roles,
		rolePerms: param.RolePermissions,
		userRoles: param.UserRoles,
	}
}

func (uc *roleUseCase) Create(ctx context.Context, param *dom.CreateRoleParam) (*entity.Role, error) {
	if param == nil || param.TenantID == "" {
		return nil, errb.InputMissingRequired("tenant_id")
	}
	if err := validateRoleKey(param.Key); err != nil {
		return nil, err
	}
	displayName := strings.TrimSpace(param.DisplayName)
	if displayName == "" {
		displayName = param.Key
	}
	if len(displayName) > domain.RoleDisplayNameMax {
		return nil, errb.InputInvalidRange("display_name too long")
	}
	status := param.Status
	if status == "" {
		status = enum.StatusOpen
	}
	if !status.IsValid() {
		return nil, errb.InputInvalidFormat("invalid status")
	}
	role := &entity.Role{
		TenantID:    param.TenantID,
		Key:         param.Key,
		DisplayName: displayName,
		CreatorUID:  param.CreatorUID,
		Status:      status,
		IsSystem:    false,
	}
	if err := uc.roles.Insert(ctx, role); err != nil {
		return nil, wrapRepoErr(err, "create role")
	}
	return role, nil
}

func (uc *roleUseCase) Get(ctx context.Context, tenantID, id string) (*entity.Role, error) {
	role, err := uc.roles.GetByID(ctx, tenantID, id)
	if err != nil {
		return nil, wrapRepoErr(err)
	}
	return role, nil
}

func (uc *roleUseCase) GetByKey(ctx context.Context, tenantID, key string) (*entity.Role, error) {
	role, err := uc.roles.GetByKey(ctx, tenantID, key)
	if err != nil {
		return nil, wrapRepoErr(err)
	}
	return role, nil
}

func (uc *roleUseCase) List(ctx context.Context, tenantID string) ([]*entity.Role, error) {
	roles, err := uc.roles.ListByTenant(ctx, tenantID)
	if err != nil {
		return nil, wrapRepoErr(err)
	}
	return roles, nil
}

func (uc *roleUseCase) Update(
	ctx context.Context,
	tenantID, id string,
	param *dom.UpdateRoleParam,
) (*entity.Role, error) {
	if param == nil {
		return uc.Get(ctx, tenantID, id)
	}
	existing, err := uc.roles.GetByID(ctx, tenantID, id)
	if err != nil {
		return nil, wrapRepoErr(err)
	}
	if existing.IsSystem && param.Status != nil {
		return nil, wrapRepoErr(domain.ErrRoleSystemImmutable)
	}
	update := &domrepo.RoleUpdate{}
	if param.DisplayName != nil {
		display := strings.TrimSpace(*param.DisplayName)
		if display == "" {
			return nil, errb.InputMissingRequired("display_name")
		}
		if len(display) > domain.RoleDisplayNameMax {
			return nil, errb.InputInvalidRange("display_name too long")
		}
		update.DisplayName = &display
	}
	if param.Status != nil {
		if !param.Status.IsValid() {
			return nil, errb.InputInvalidFormat("invalid status")
		}
		s := param.Status.String()
		update.Status = &s
	}
	role, err := uc.roles.Update(ctx, tenantID, id, update)
	if err != nil {
		return nil, wrapRepoErr(err, "update role")
	}
	return role, nil
}

func (uc *roleUseCase) Delete(ctx context.Context, tenantID, id string) error {
	existing, err := uc.roles.GetByID(ctx, tenantID, id)
	if err != nil {
		return wrapRepoErr(err)
	}
	if existing.IsSystem {
		return wrapRepoErr(domain.ErrRoleSystemImmutable)
	}
	assignments, err := uc.userRoles.ListByRole(ctx, tenantID, id)
	if err != nil {
		return wrapRepoErr(err, "check user roles")
	}
	if len(assignments) > 0 {
		return errb.ResPreconditionFailed("role still has assignments")
	}
	if err := uc.rolePerms.DeleteByRole(ctx, tenantID, id); err != nil {
		return wrapRepoErr(err, "delete role permissions")
	}
	if err := uc.roles.Delete(ctx, tenantID, id); err != nil {
		return wrapRepoErr(err, "delete role")
	}
	return nil
}

func validateRoleKey(key string) error {
	key = strings.TrimSpace(key)
	if key == "" {
		return errb.InputMissingRequired("key")
	}
	if len(key) < domain.RoleKeyMinLength || len(key) > domain.RoleKeyMaxLength {
		return errb.InputInvalidRange("key length")
	}
	if !roleKeyPattern.MatchString(key) {
		return wrapRepoErr(domain.ErrRoleKeyInvalid)
	}
	for _, prefix := range domain.ReservedRoleKeyPrefixes {
		if strings.HasPrefix(key, prefix) {
			return wrapRepoErr(domain.ErrRoleKeyReserved)
		}
	}
	return nil
}

var _ dom.RoleUseCase = (*roleUseCase)(nil)
