package service

import (
	"context"
	"fmt"
	"log/slog"
	"regexp"
	"strings"
	"sync"

	"github.com/owncord/server/db"
	"github.com/owncord/server/permissions"
)

// RoleService owns create/edit/delete/reorder of roles. It is the only writer
// of the roles table outside migrations.
//
// Every mutation is checked against the ACTOR's role position rather than
// against a bit alone: MANAGE_ROLES says "may manage roles", the hierarchy says
// "which ones". Without the position rule any holder of the bit could edit the
// role above them — or grant themselves ADMINISTRATOR through a role they
// create — which is the same privilege-escalation hole ChangeUserRole closed
// for role *assignment*.
type RoleService struct {
	st    Store
	perms *PermissionService
	// mu serializes the read-check-write mutations: position uniqueness and
	// the role cap are enforced against a ListRoles snapshot, not by a DB
	// constraint, so two interleaved mutations can both see the same free
	// slot. Single-process server — one lock covers every writer.
	mu sync.Mutex
}

// NewRoleService creates a RoleService.
func NewRoleService(st Store, perms *PermissionService) *RoleService {
	return &RoleService{st: st, perms: perms}
}

const (
	// maxRoleNameLen bounds the name so a role label cannot be used to blow up
	// every member list and permission modal that renders it.
	maxRoleNameLen = 32
	// maxRoles bounds how many roles a server can hold. Reorder normalizes
	// positions into 1..N strictly below the actor, so N must stay well under
	// the owner position (100) for the hierarchy to remain expressible.
	maxRoles = 64
)

// hexColorRe matches the two CSS hex forms the client renders (#rgb, #rrggbb).
// Anything else — named colors, rgb(), a bare hex without the hash — is
// rejected rather than normalized: the value is written straight into a style
// attribute by the desktop client and the admin panel.
var hexColorRe = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`)

// RoleInput carries the mutable fields of a role. Pointer fields are "leave
// unchanged" on update; on create a nil Position means "just below the actor".
type RoleInput struct {
	Name        *string
	Color       *string // points at "" to clear the color
	Permissions *int64
	Position    *int
}

// RoleWithMembers is a role plus how many users currently hold it. The count
// is what makes the delete confirmation honest ("12 members move to Member").
type RoleWithMembers struct {
	db.Role
	MemberCount int `json:"member_count"`
}

// actorRole loads the actor's role through the permission cache and verifies
// MANAGE_ROLES (ADMINISTRATOR bypasses). Every failure is Forbidden: an
// unresolvable role must never authorize a role mutation.
func (s *RoleService) actorRole(ctx context.Context, actorID int64) (*db.Role, error) {
	if s.perms == nil {
		return nil, fmt.Errorf("%w: permission service unavailable", ErrForbidden)
	}
	role, err := s.perms.GetRoleForUser(ctx, actorID)
	if err != nil || role == nil {
		return nil, fmt.Errorf("%w: failed to load actor role", ErrForbidden)
	}
	if !permissions.HasServerPerm(role.Permissions, permissions.ManageRoles) {
		return nil, fmt.Errorf("%w: missing %s permission", ErrForbidden, permissions.Name(permissions.ManageRoles))
	}
	return role, nil
}

// requireBelowActor enforces the hierarchy rule shared by edit, delete and
// reorder: the target role must sit strictly below the actor's own position.
// Equality is refused too, so a peer cannot rewrite the role they both hold.
func requireBelowActor(actor *db.Role, target *db.Role) error {
	if target.Position >= actor.Position {
		return fmt.Errorf("%w: cannot manage a role at or above your own rank", ErrForbidden)
	}
	return nil
}

// requireGrantable enforces "you cannot hand out what you do not hold": every
// bit being ADDED must be present in the actor's own mask. Removing a bit the
// actor lacks is allowed — that is a de-escalation, and refusing it would make
// an over-permissioned role impossible to wind down by anyone but an admin.
// ADMINISTRATOR bypasses, which is what lets the owner grant anything.
func requireGrantable(actor *db.Role, oldPerms, newPerms int64) error {
	if permissions.HasAdmin(actor.Permissions) {
		return nil
	}
	added := newPerms &^ oldPerms
	if missing := added &^ actor.Permissions; missing != 0 {
		return fmt.Errorf("%w: cannot grant a permission your own role lacks (%s)",
			ErrForbidden, permissions.Name(missing&-missing))
	}
	return nil
}

// validateName trims, bounds and uniqueness-checks a role name. excludeID is
// the role being renamed (0 on create) so a no-op rename is not a collision.
func (s *RoleService) validateName(ctx context.Context, raw string, excludeID int64) (string, error) {
	name := strings.TrimSpace(raw)
	if name == "" {
		return "", fmt.Errorf("%w: name is required", ErrBadRequest)
	}
	if len([]rune(name)) > maxRoleNameLen {
		return "", fmt.Errorf("%w: name must be at most %d characters", ErrBadRequest, maxRoleNameLen)
	}
	existing, err := s.st.GetRoleByName(ctx, name)
	if err != nil {
		return "", fmt.Errorf("%w: failed to check role name: %v", ErrInternal, err)
	}
	if existing != nil && existing.ID != excludeID {
		return "", fmt.Errorf("%w: a role named %q already exists", ErrBadRequest, existing.Name)
	}
	return name, nil
}

// validateColor normalizes an optional color. A nil pointer means "unset"; an
// empty string clears it.
func validateColor(raw *string) (*string, error) {
	if raw == nil {
		return nil, nil
	}
	c := strings.TrimSpace(*raw)
	if c == "" {
		return nil, nil
	}
	if !hexColorRe.MatchString(c) {
		return nil, fmt.Errorf("%w: color must be a hex value like #5865F2", ErrBadRequest)
	}
	c = strings.ToUpper(c)
	return &c, nil
}

// validatePosition bounds a requested position: non-negative and strictly below
// the actor's own rank.
func validatePosition(actor *db.Role, pos int) error {
	if pos < 0 {
		return fmt.Errorf("%w: position must not be negative", ErrBadRequest)
	}
	if pos >= actor.Position {
		return fmt.Errorf("%w: cannot place a role at or above your own rank", ErrForbidden)
	}
	return nil
}

// ListRoles returns every role, highest position first, with member counts.
// Gated on MANAGE_ROLES like the mutations — the panel section that shows it is.
func (s *RoleService) ListRoles(ctx context.Context, actorID int64) ([]RoleWithMembers, error) {
	if _, err := s.actorRole(ctx, actorID); err != nil {
		return nil, err
	}
	roles, err := s.st.ListRoles(ctx)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
	}
	counts, err := s.st.CountRoleMembers(ctx)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to count role members: %v", ErrInternal, err)
	}
	out := make([]RoleWithMembers, 0, len(roles))
	for _, r := range roles {
		out = append(out, RoleWithMembers{Role: *r, MemberCount: counts[r.ID]})
	}
	return out, nil
}

// CreateRole creates a role strictly below the actor's own rank.
func (s *RoleService) CreateRole(ctx context.Context, actorID int64, in RoleInput) (*db.Role, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	actor, err := s.actorRole(ctx, actorID)
	if err != nil {
		return nil, err
	}
	if in.Name == nil {
		return nil, fmt.Errorf("%w: name is required", ErrBadRequest)
	}
	name, err := s.validateName(ctx, *in.Name, 0)
	if err != nil {
		return nil, err
	}
	color, err := validateColor(in.Color)
	if err != nil {
		return nil, err
	}

	// Unknown bits are dropped rather than rejected, matching the channel
	// override handlers — a client sending a wider mask than this build knows
	// about must not be able to persist bits nothing enforces.
	perms := int64(0)
	if in.Permissions != nil {
		perms = *in.Permissions & permissions.AllPerms
	}
	// Every bit is "added" on create, so the grantable check runs against 0.
	if err := requireGrantable(actor, 0, perms); err != nil {
		return nil, err
	}

	existing, err := s.st.ListRoles(ctx)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
	}
	if len(existing) >= maxRoles {
		return nil, fmt.Errorf("%w: server already has the maximum of %d roles", ErrBadRequest, maxRoles)
	}

	// Positions must stay unique: every hierarchy comparison uses >=/<=, so two
	// roles sharing a position read as equal rank and can never manage each
	// other's members. Track the slots already taken.
	taken := make(map[int]bool, len(existing))
	for _, rl := range existing {
		taken[rl.Position] = true
	}

	// Default placement is the highest free slot below the actor — directly
	// below when that is free, which is what a manager creating a deputy role
	// expects, but stepping past any occupied position so a second create does
	// not collide with the first.
	position := actor.Position - 1
	if in.Position != nil {
		position = *in.Position
		// Rank first: an at/above-rank position is a hierarchy violation
		// (ErrForbidden) regardless of whether it also happens to be occupied.
		if err := validatePosition(actor, position); err != nil {
			return nil, err
		}
		if taken[position] {
			return nil, fmt.Errorf("%w: position %d is already used by another role", ErrBadRequest, position)
		}
	} else {
		for position > 0 && taken[position] {
			position--
		}
		if position <= 0 {
			return nil, fmt.Errorf("%w: no free position below your rank — reorder existing roles first", ErrBadRequest)
		}
		if err := validatePosition(actor, position); err != nil {
			return nil, err
		}
	}

	role, err := s.st.CreateRole(ctx, name, color, perms, position)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to create role: %v", ErrInternal, err)
	}

	db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_create", "role", role.ID,
		fmt.Sprintf("created role %s (permissions=%#x position=%d)", role.Name, role.Permissions, role.Position))
	slog.Info("role created", "actor_id", actorID, "role_id", role.ID, "name", role.Name)
	return role, nil
}

// UpdateRole applies a partial change to a role below the actor's rank.
// Returns the updated role and whether the permission mask actually moved —
// the caller uses that to decide between a cheap roles_update broadcast and a
// full visibility re-sync.
func (s *RoleService) UpdateRole(ctx context.Context, actorID, roleID int64, in RoleInput) (updated *db.Role, permsChanged bool, err error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	actor, err := s.actorRole(ctx, actorID)
	if err != nil {
		return nil, false, err
	}
	role, err := s.st.GetRoleByID(ctx, roleID)
	if err != nil {
		return nil, false, fmt.Errorf("%w: failed to fetch role: %v", ErrInternal, err)
	}
	if role == nil {
		return nil, false, fmt.Errorf("%w: role not found", ErrNotFound)
	}
	if err := requireBelowActor(actor, role); err != nil {
		return nil, false, err
	}

	name := role.Name
	if in.Name != nil {
		if name, err = s.validateName(ctx, *in.Name, role.ID); err != nil {
			return nil, false, err
		}
	}
	color := role.Color
	if in.Color != nil {
		if color, err = validateColor(in.Color); err != nil {
			return nil, false, err
		}
	}
	perms := role.Permissions
	if in.Permissions != nil {
		perms = *in.Permissions & permissions.AllPerms
		if err := requireGrantable(actor, role.Permissions, perms); err != nil {
			return nil, false, err
		}
	}
	position := role.Position
	if in.Position != nil {
		position = *in.Position
		if err := validatePosition(actor, position); err != nil {
			return nil, false, err
		}
		// Positions must stay unique (see CreateRole): tied positions read
		// as equal rank in every >=/<= hierarchy comparison. Moving onto a
		// slot another role holds is refused; re-stating our own is fine.
		if position != role.Position {
			existing, err := s.st.ListRoles(ctx)
			if err != nil {
				return nil, false, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
			}
			for _, rl := range existing {
				if rl.ID != role.ID && rl.Position == position {
					return nil, false, fmt.Errorf("%w: position %d is already used by another role", ErrBadRequest, position)
				}
			}
		}
	}

	if err := s.st.UpdateRole(ctx, role.ID, name, color, perms, position); err != nil {
		return nil, false, fmt.Errorf("%w: failed to update role: %v", ErrInternal, err)
	}

	db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_update", "role", role.ID,
		fmt.Sprintf("updated role %s (permissions=%#x position=%d)", name, perms, position))
	slog.Info("role updated", "actor_id", actorID, "role_id", role.ID, "name", name)

	return &db.Role{
		ID:          role.ID,
		Name:        name,
		Color:       color,
		Permissions: perms,
		Position:    position,
		IsDefault:   role.IsDefault,
	}, perms != role.Permissions, nil
}

// DeleteRole removes a role below the actor's rank, moving its members onto the
// default role. Returns the deleted role, the fallback its members landed on,
// and the ids of those members so the caller can invalidate and re-sync them.
func (s *RoleService) DeleteRole(ctx context.Context, actorID, roleID int64) (deleted, fallback *db.Role, movedUserIDs []int64, err error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	actor, err := s.actorRole(ctx, actorID)
	if err != nil {
		return nil, nil, nil, err
	}
	role, err := s.st.GetRoleByID(ctx, roleID)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("%w: failed to fetch role: %v", ErrInternal, err)
	}
	if role == nil {
		return nil, nil, nil, fmt.Errorf("%w: role not found", ErrNotFound)
	}
	if err := requireBelowActor(actor, role); err != nil {
		return nil, nil, nil, err
	}
	// Belt and braces: the position rule already puts the owner role out of
	// reach (nothing is above position 100), but the seeded owner is named
	// explicitly so a database whose positions were edited by hand cannot make
	// the server ownerless.
	if role.ID == permissions.OwnerRoleID || role.Position >= permissions.OwnerRolePosition {
		return nil, nil, nil, fmt.Errorf("%w: the Owner role cannot be deleted", ErrBadRequest)
	}
	if role.IsDefault {
		return nil, nil, nil, fmt.Errorf("%w: the default role cannot be deleted — every member falls back to it", ErrBadRequest)
	}

	fallback, err = s.st.GetDefaultRole(ctx)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("%w: failed to resolve the default role: %v", ErrInternal, err)
	}
	if fallback == nil {
		// Fail closed: without a fallback the members would be orphaned on a
		// role id that no longer exists.
		return nil, nil, nil, fmt.Errorf("%w: no default role is configured", ErrInternal)
	}

	movedUserIDs, err = s.st.DeleteRoleReassigning(ctx, role.ID, fallback.ID)
	if err != nil {
		return nil, nil, nil, fmt.Errorf("%w: failed to delete role: %v", ErrInternal, err)
	}
	// The members' cached masks are the deleted role's until this drops them.
	if s.perms != nil {
		for _, uid := range movedUserIDs {
			s.perms.InvalidateUser(uid)
		}
	}

	db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_delete", "role", role.ID,
		fmt.Sprintf("deleted role %s (%d members reassigned to %s)", role.Name, len(movedUserIDs), fallback.Name))
	slog.Warn("role deleted", "actor_id", actorID, "role_id", role.ID, "name", role.Name,
		"members_reassigned", len(movedUserIDs))
	return role, fallback, movedUserIDs, nil
}

// ReorderRoles rewrites the positions of every role the actor may manage.
// orderedIDs is highest-rank-first and must name exactly the set of roles
// strictly below the actor — a partial list is refused rather than silently
// leaving the omitted roles wherever they were, which is how positions
// collided in the first place.
//
// Positions are normalized to N..1, so they stay unique, stay strictly below
// the actor (N < actor.Position, enforced by maxRoles), and never collide with
// the untouched roles above the actor.
func (s *RoleService) ReorderRoles(ctx context.Context, actorID int64, orderedIDs []int64) ([]*db.Role, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	actor, err := s.actorRole(ctx, actorID)
	if err != nil {
		return nil, err
	}
	roles, err := s.st.ListRoles(ctx)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
	}

	manageable := make(map[int64]*db.Role, len(roles))
	for _, r := range roles {
		if r.Position < actor.Position {
			manageable[r.ID] = r
		}
	}
	if len(orderedIDs) != len(manageable) {
		return nil, fmt.Errorf("%w: the order must list all %d roles below your own rank", ErrBadRequest, len(manageable))
	}
	seen := make(map[int64]bool, len(orderedIDs))
	for _, id := range orderedIDs {
		if seen[id] {
			return nil, fmt.Errorf("%w: role %d listed twice", ErrBadRequest, id)
		}
		seen[id] = true
		if _, ok := manageable[id]; !ok {
			// Covers both "unknown role" and "role at or above your rank"; the
			// two are deliberately indistinguishable to the caller.
			return nil, fmt.Errorf("%w: role %d is not yours to reorder", ErrForbidden, id)
		}
	}
	if len(orderedIDs) >= actor.Position {
		return nil, fmt.Errorf("%w: too many roles to place below your own rank", ErrBadRequest)
	}

	positions := make(map[int64]int, len(orderedIDs))
	for i, id := range orderedIDs {
		positions[id] = len(orderedIDs) - i
	}
	if err := s.st.SetRolePositions(ctx, positions); err != nil {
		return nil, fmt.Errorf("%w: failed to reorder roles: %v", ErrInternal, err)
	}

	db.WriteAudit(context.WithoutCancel(ctx), s.st, actorID, "role_reorder", "role", 0,
		fmt.Sprintf("reordered %d roles", len(orderedIDs)))
	slog.Info("roles reordered", "actor_id", actorID, "count", len(orderedIDs))

	updated, err := s.st.ListRoles(ctx)
	if err != nil {
		return nil, fmt.Errorf("%w: failed to list roles: %v", ErrInternal, err)
	}
	return updated, nil
}

// AffectedUserIDs returns the ids of the users holding roleID, and whether the
// lookup succeeded. Handlers use it to invalidate exactly the permission-cache
// entries a role edit touches; on ok=false the caller must fall back to a
// blanket invalidation — a nil list treated as "nobody" silently leaves stale
// masks in place.
func (s *RoleService) AffectedUserIDs(ctx context.Context, roleID int64) ([]int64, bool) {
	ids, err := s.st.ListUserIDsByRole(ctx, roleID)
	if err != nil {
		slog.Warn("role service: failed to list role members", "role_id", roleID, "err", err)
		return nil, false
	}
	return ids, true
}
