package roles

import (
	"context"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/identity/trusted"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
	rolesstore "github.com/tidbcloud/AgentTeam/internal/store/resource/roles"
)

// AgentOwnershipSource is the authorization seam for Role operations that
// target a private AgentTeam agent.
type AgentOwnershipSource interface {
	RequireOwnedAgent(ctx context.Context, workspaceID, subject, agentID string) error
}

// ClaimsAgentOwnershipSource reads ownership only from the request identity
// installed after AgentTeam's verified-claims boundary.
type ClaimsAgentOwnershipSource struct{}

func (ClaimsAgentOwnershipSource) RequireOwnedAgent(ctx context.Context, workspaceID, subject, agentID string) error {
	identity, ok := trusted.IdentityFromContext(ctx)
	if !ok {
		return repos.ErrForbidden
	}
	if strings.TrimSpace(identity.Subject) == "" || strings.TrimSpace(identity.Subject) != strings.TrimSpace(subject) {
		return repos.ErrForbidden
	}
	claims := identity.RawClaims
	if claimString(claims, "agent_ownership_workspace_id") != strings.TrimSpace(workspaceID) {
		return repos.ErrForbidden
	}
	agentID = strings.TrimSpace(agentID)
	if agentID == "" {
		return repos.ErrForbidden
	}
	for _, ownedID := range claimedAgentIDs(claims) {
		if ownedID == agentID {
			return nil
		}
	}
	return repos.ErrForbidden
}

func claimedAgentIDs(claims map[string]any) []string {
	var ids []string
	if values, ok := claims["owned_agents"].([]any); ok {
		for _, value := range values {
			item, ok := value.(map[string]any)
			if !ok {
				continue
			}
			id := claimString(item, "id")
			if id == "" {
				id = claimString(item, "agent_id")
			}
			ids = append(ids, id)
		}
	}
	switch values := claims["owned_agent_ids"].(type) {
	case []string:
		ids = append(ids, values...)
	case []any:
		for _, value := range values {
			if id, ok := value.(string); ok {
				ids = append(ids, id)
			}
		}
	case string:
		ids = append(ids, strings.Split(values, ",")...)
	}
	for index := range ids {
		ids[index] = strings.TrimSpace(ids[index])
	}
	return ids
}

func claimString(claims map[string]any, key string) string {
	value, _ := claims[key].(string)
	return strings.TrimSpace(value)
}

type admittedRoleUpdate struct {
	current        Role
	input          Mutation
	visibility     string
	contentChanged bool
	contentItems   []rolesstore.RolePackageManifestItem
}

func admitRoleCreate(input Mutation) (Mutation, []rolesstore.RolePackageManifestItem, error) {
	if strings.TrimSpace(input.Name) == "" {
		return Mutation{}, nil, invalidRoleArgument("role name is required")
	}
	if strings.TrimSpace(input.Definition) == "" {
		return Mutation{}, nil, invalidRoleArgument("role definition is required")
	}
	portable, items, err := canonicalizePortableRole(input.Definition, input.RuntimeHints, input.Attachments)
	if err != nil {
		return Mutation{}, nil, err
	}
	input.Definition = portable.Definition
	input.RuntimeHints = portable.RuntimeHints
	input.Attachments = attachmentsFromRoleManifest(portable.RolePackageManifest)
	return input, items, nil
}

func (s *Service) admitRoleUpdate(
	ctx context.Context,
	workspaceID, subject, roleID, expectedVersionID string,
	patch RoleUpdatePatch,
) (admittedRoleUpdate, error) {
	if patch.Name == nil && patch.Description == nil && patch.Definition == nil && patch.Visibility == nil {
		return admittedRoleUpdate{}, invalidRoleArgument("role update requires at least one field")
	}
	current, err := s.GetRole(ctx, workspaceID, subject, roleID)
	if err != nil {
		return admittedRoleUpdate{}, err
	}
	if !roleCanManage(current.Role) {
		return admittedRoleUpdate{}, repos.ErrForbidden
	}
	if strings.TrimSpace(expectedVersionID) == "" || current.RoleVersionID != strings.TrimSpace(expectedVersionID) {
		return admittedRoleUpdate{}, ErrRoleVersionConflict
	}
	input := Mutation{
		Name: current.Name, Description: current.Description, Locale: current.Locale,
		Scenario: current.Scenario, Icon: current.Icon, Tags: append([]string(nil), current.Tags...),
		Definition: current.Definition, RuntimeHints: current.RuntimeHints,
		Attachments: attachmentsFromRoleManifest(current.RolePackageManifest),
	}
	active := current.IsActive
	input.IsActive = &active
	if patch.Name != nil {
		input.Name = *patch.Name
	}
	if patch.Description != nil {
		input.Description = *patch.Description
	}
	contentChanged := patch.Definition != nil
	if contentChanged {
		input.Definition = *patch.Definition
	}
	visibility := current.Visibility
	if patch.Visibility != nil {
		visibility, err = normalizeRoleVisibility(*patch.Visibility)
		if err != nil {
			return admittedRoleUpdate{}, err
		}
	}
	if strings.TrimSpace(input.Name) == "" {
		return admittedRoleUpdate{}, invalidRoleArgument("role name is required")
	}
	var contentItems []rolesstore.RolePackageManifestItem
	if contentChanged {
		input, contentItems, err = admitRoleCreate(input)
		if err != nil {
			return admittedRoleUpdate{}, err
		}
	}
	return admittedRoleUpdate{
		current: current, input: input, visibility: visibility,
		contentChanged: contentChanged, contentItems: contentItems,
	}, nil
}

func (s *Service) admitRoleDelete(
	ctx context.Context,
	workspaceID, subject, roleID, expectedVersionID string,
) (Role, error) {
	current, err := s.GetRole(ctx, workspaceID, subject, roleID)
	if err != nil {
		return Role{}, err
	}
	if !roleCanManage(current.Role) {
		return Role{}, repos.ErrForbidden
	}
	if strings.TrimSpace(expectedVersionID) == "" || current.RoleVersionID != strings.TrimSpace(expectedVersionID) {
		return Role{}, ErrRoleVersionConflict
	}
	return current, nil
}

func (s *Service) admitRoleApply(
	ctx context.Context,
	workspaceID, subject, roleID string,
	input RealizationRequest,
) (Role, RealizationImpact, error) {
	if s.ownership == nil {
		return Role{}, RealizationImpact{}, repos.ErrForbidden
	}
	if err := s.ownership.RequireOwnedAgent(ctx, workspaceID, subject, input.AgentID); err != nil {
		return Role{}, RealizationImpact{}, err
	}
	current, err := s.GetRole(ctx, workspaceID, subject, roleID)
	if err != nil {
		return Role{}, RealizationImpact{}, err
	}
	versionID := strings.TrimSpace(input.RoleVersionID)
	if versionID == "" || current.RoleVersionID != versionID {
		return Role{}, RealizationImpact{}, ErrRoleVersionConflict
	}
	impact, err := s.realizationImpact(ctx, subject, current, input)
	return current, impact, err
}

func (s *Service) PreflightCreateRoleOperation(
	_ context.Context,
	workspaceID, subject string,
	input Mutation,
) error {
	if err := s.validate(workspaceID, subject); err != nil {
		return err
	}
	_, _, err := admitRoleCreate(input)
	return err
}

func (s *Service) PreflightUpdateRoleOperation(
	ctx context.Context,
	workspaceID, subject, roleID, expectedVersionID string,
	patch RoleUpdatePatch,
) error {
	if err := s.validate(workspaceID, subject); err != nil {
		return err
	}
	_, err := s.admitRoleUpdate(ctx, workspaceID, subject, roleID, expectedVersionID, patch)
	return err
}

func (s *Service) PreflightDeleteRoleOperation(
	ctx context.Context,
	workspaceID, subject, roleID, expectedVersionID string,
) error {
	if err := s.validate(workspaceID, subject); err != nil {
		return err
	}
	_, err := s.admitRoleDelete(ctx, workspaceID, subject, roleID, expectedVersionID)
	return err
}

func (s *Service) PreflightApplyRoleOperation(
	ctx context.Context,
	workspaceID, subject, roleID string,
	input RealizationRequest,
) (RealizationImpact, error) {
	if err := s.validate(workspaceID, subject); err != nil {
		return RealizationImpact{}, err
	}
	_, impact, err := s.admitRoleApply(ctx, workspaceID, subject, roleID, input)
	return impact, err
}
