package agents

import (
	"context"
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/core/identifiers"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/plan"
)

type AgentStore interface {
	CreateAgentForUser(ctx context.Context, userID, workspaceID, machineID string, params agent.CreateAgentParams) (agent.AgentView, error)
	DeleteAgentForWorkspace(ctx context.Context, userID, workspaceID, agentID string) (string, error)
	GetAgentForUser(ctx context.Context, userID, workspaceID, agentID string) (agent.AgentView, error)
	UpsertAgentRoleMaterialization(ctx context.Context, input agent.AgentRoleMaterializationInput) error
}

type RoleRealizer interface {
	RealizeRoleForAgent(ctx context.Context, input RealizeRoleForAgentInput) (RealizationResult, error)
	RoleMaterialization(ctx context.Context, input RealizeRoleForAgentInput) (agent.AgentRoleMaterialization, error)
}

type RoleResourcePlanner interface {
	RoleResourceImpact(ctx context.Context, input RealizeRoleForAgentInput) (ResourceImpact, error)
	CountWorkspaceResource(ctx context.Context, input RealizeRoleForAgentInput, metric plan.MetricKey, scope plan.QuotaScope) (int64, error)
}

type Service struct {
	agents         AgentStore
	realizer       RoleRealizer
	resourcePlan   RoleResourcePlanner
	quotaStore     WorkspaceResourceLimitStore
	billingEnabled bool
}

type WorkspaceResourceLimitStore interface {
	LoadWorkspaceResourceLimit(ctx context.Context, workspaceID string, ownerUserID string, metric plan.MetricKey) (plan.WorkspaceResourceLimit, error)
}

type CreateFromRoleInput struct {
	UserID          string
	WorkspaceID     string
	MachineID       string
	Agent           agent.CreateAgentParams
	Source          Source
	RoleDisplayName string
	Realization     RealizationOptions
}

type Source struct {
	Kind      string
	PresetID  string
	RoleID    string
	VersionID string
	Locale    string
}

type RealizeRoleForAgentInput struct {
	UserID          string
	WorkspaceID     string
	AgentID         string
	Source          Source
	RoleDisplayName string
	Realization     RealizationOptions
}

type RealizationOptions struct {
	DryRun         bool
	Apply          bool
	SkipOptional   bool
	StorageScope   string
	TemplateLocale string
}

type RealizationResult struct {
	RoleID  string
	AgentID string
	DryRun  bool
	Apply   bool
	Items   []RealizationItem
}

type ResourceImpact struct {
	ManagedMemories   int64
	SkillRepositories int64
}

type RealizationItem struct {
	ManifestItemType string
	ManifestItemName string
	Action           string
	Status           string
	ResourceID       string
	AccessLevel      string
	Branch           string
	Message          string
	Required         bool
}

type CreateFromRoleResult struct {
	Created            agent.AgentView
	Realization        RealizationResult
	AgentRoleBindingID string
	Partial            bool
}

type ApplyRoleResult struct {
	Realization RealizationResult
	Partial     bool
}

type ValidationError struct{ Message string }

func (e *ValidationError) Error() string { return e.Message }

type RealizationError struct {
	Result     RealizationResult
	Cause      error
	CleanupErr error
}

func (e *RealizationError) Error() string {
	if e == nil {
		return ""
	}
	if e.CleanupErr != nil {
		return fmt.Sprintf("role realization failed; rollback cleanup failed: %v", e.CleanupErr)
	}
	return "role realization failed"
}

func (e *RealizationError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.Cause
}

func (s *Service) CreateAgentFromRole(ctx context.Context, input CreateFromRoleInput) (CreateFromRoleResult, error) {
	if s == nil || s.agents == nil || s.realizer == nil {
		return CreateFromRoleResult{}, fmt.Errorf("roleagents service is not configured")
	}
	if err := validateSource(input.Source); err != nil {
		return CreateFromRoleResult{}, err
	}

	created, err := s.agents.CreateAgentForUser(ctx, input.UserID, input.WorkspaceID, input.MachineID, input.Agent)
	if err != nil {
		return CreateFromRoleResult{}, err
	}

	realizationOptions := input.Realization
	if !realizationOptions.DryRun && !realizationOptions.Apply {
		realizationOptions.Apply = true
	}
	realizeInput := RealizeRoleForAgentInput{
		UserID:          input.UserID,
		WorkspaceID:     input.WorkspaceID,
		AgentID:         created.AgentID,
		Source:          input.Source,
		RoleDisplayName: input.RoleDisplayName,
		Realization:     realizationOptions,
	}
	if err := s.checkResourceQuota(ctx, realizeInput); err != nil {
		cleanupErr := rollbackCreate(ctx, s.agents, input, created.AgentID)
		return CreateFromRoleResult{}, &RealizationError{Cause: err, CleanupErr: cleanupErr}
	}
	realization, err := s.realizer.RealizeRoleForAgent(ctx, realizeInput)
	if err != nil {
		cleanupErr := rollbackCreate(ctx, s.agents, input, created.AgentID)
		return CreateFromRoleResult{}, &RealizationError{Cause: err, CleanupErr: cleanupErr}
	}

	requiredFailed, partial := classify(realization.Items)
	if requiredFailed {
		cleanupErr := rollbackCreate(ctx, s.agents, input, created.AgentID)
		return CreateFromRoleResult{}, &RealizationError{Result: realization, CleanupErr: cleanupErr}
	}
	agentRoleBindingID := ""
	if !realizationOptions.DryRun && realizationOptions.Apply {
		agentRoleBindingID, err = identifiers.NewEntityID("arb")
		if err != nil {
			cleanupErr := rollbackCreate(ctx, s.agents, input, created.AgentID)
			return CreateFromRoleResult{}, &RealizationError{Cause: fmt.Errorf("generate agent role binding id: %w", err), CleanupErr: cleanupErr}
		}
		if err := s.upsertRoleMaterialization(ctx, RealizeRoleForAgentInput{
			UserID:      input.UserID,
			WorkspaceID: input.WorkspaceID,
			AgentID:     created.AgentID,
			Source:      input.Source,
		}, realization, agentRoleBindingID); err != nil {
			cleanupErr := rollbackCreate(ctx, s.agents, input, created.AgentID)
			return CreateFromRoleResult{}, &RealizationError{Cause: err, CleanupErr: cleanupErr}
		}
	}

	return CreateFromRoleResult{Created: created, Realization: realization, AgentRoleBindingID: agentRoleBindingID, Partial: partial}, nil
}

func (s *Service) ApplyRoleToAgent(ctx context.Context, input RealizeRoleForAgentInput) (ApplyRoleResult, error) {
	if s == nil || s.agents == nil || s.realizer == nil {
		return ApplyRoleResult{}, fmt.Errorf("roleagents service is not configured")
	}
	if err := validateSource(input.Source); err != nil {
		return ApplyRoleResult{}, err
	}
	if strings.TrimSpace(input.AgentID) == "" {
		return ApplyRoleResult{}, &ValidationError{Message: "agentId is required"}
	}
	if _, err := s.agents.GetAgentForUser(ctx, input.UserID, input.WorkspaceID, input.AgentID); err != nil {
		return ApplyRoleResult{}, err
	}

	realizationOptions := input.Realization
	if !realizationOptions.DryRun && !realizationOptions.Apply {
		realizationOptions.Apply = true
	}
	input.Realization = realizationOptions
	if err := s.checkResourceQuota(ctx, input); err != nil {
		return ApplyRoleResult{}, err
	}
	realization, err := s.realizer.RealizeRoleForAgent(ctx, input)
	if err != nil {
		return ApplyRoleResult{}, err
	}

	requiredFailed, partial := classify(realization.Items)
	if requiredFailed {
		return ApplyRoleResult{}, &RealizationError{Result: realization}
	}
	if !input.Realization.DryRun && input.Realization.Apply {
		if err := s.upsertRoleMaterialization(ctx, input, realization, ""); err != nil {
			return ApplyRoleResult{}, err
		}
	}
	return ApplyRoleResult{Realization: realization, Partial: partial}, nil
}

func (s *Service) checkResourceQuota(ctx context.Context, input RealizeRoleForAgentInput) error {
	if !s.billingEnabled || input.Realization.DryRun || !input.Realization.Apply {
		return nil
	}
	impact, err := s.resourcePlan.RoleResourceImpact(ctx, input)
	if err != nil {
		return err
	}
	if impact.ManagedMemories > 0 {
		if err := s.checkWorkspaceResourceQuota(ctx, input, plan.MetricManagedMemories, impact.ManagedMemories); err != nil {
			return err
		}
	}
	if impact.SkillRepositories > 0 {
		if err := s.checkWorkspaceResourceQuota(ctx, input, plan.MetricSkillRepositories, impact.SkillRepositories); err != nil {
			return err
		}
	}
	return nil
}

func (s *Service) checkWorkspaceResourceQuota(ctx context.Context, input RealizeRoleForAgentInput, metric plan.MetricKey, delta int64) error {
	limit, err := s.quotaStore.LoadWorkspaceResourceLimit(ctx, input.WorkspaceID, input.UserID, metric)
	if err != nil {
		return err
	}
	if limit.Limit < 0 {
		return nil
	}
	current, err := s.resourcePlan.CountWorkspaceResource(ctx, input, metric, limit.Scope)
	if err != nil {
		return err
	}
	if current+delta <= limit.Limit {
		return nil
	}
	return plan.QuotaExceededError{
		OrganizationID: limit.OrganizationID,
		PlanKey:        limit.PlanKey,
		PlanName:       limit.PlanName,
		Metric:         limit.Metric,
		Limit:          limit.Limit,
		Current:        current,
		Delta:          delta,
		Scope:          limit.Scope,
	}
}

func (s *Service) upsertRoleMaterialization(ctx context.Context, input RealizeRoleForAgentInput, realization RealizationResult, bindingID string) error {
	materializationInput := input
	if strings.TrimSpace(realization.RoleID) != "" {
		materializationInput.Source = Source{Kind: "my-role", RoleID: strings.TrimSpace(realization.RoleID), VersionID: strings.TrimSpace(input.Source.VersionID)}
	}
	materialization, err := s.realizer.RoleMaterialization(ctx, materializationInput)
	if err != nil {
		return err
	}
	if requestedVersionID := strings.TrimSpace(input.Source.VersionID); requestedVersionID != "" && strings.TrimSpace(materialization.RoleVersionID) != requestedVersionID {
		return fmt.Errorf("role version %q is unavailable for materialization", requestedVersionID)
	}
	source := input.Source.MaterializationSource()
	if source.BindingKind == "preset_derived" && strings.TrimSpace(realization.RoleID) != "" {
		source.RoleID = strings.TrimSpace(realization.RoleID)
	}
	if err := s.agents.UpsertAgentRoleMaterialization(ctx, agent.AgentRoleMaterializationInput{
		UserID:         input.UserID,
		WorkspaceID:    input.WorkspaceID,
		AgentID:        input.AgentID,
		BindingID:      bindingID,
		Source:         source,
		RoleID:         firstNonEmpty(realization.RoleID, materialization.RoleID),
		RoleVersionID:  materialization.RoleVersionID,
		RealizationID:  materialization.RealizationID,
		DefinitionPath: materialization.ActiveDefinitionPath,
		ManifestItems:  materialization.ManifestItems,
	}); err != nil {
		return fmt.Errorf("save agent role binding: %w", err)
	}
	return nil
}

func rollbackCreate(ctx context.Context, agents AgentStore, input CreateFromRoleInput, agentID string) error {
	_, err := agents.DeleteAgentForWorkspace(ctx, input.UserID, input.WorkspaceID, agentID)
	return err
}

func validateSource(source Source) error {
	switch strings.TrimSpace(source.Kind) {
	case "preset":
		if strings.TrimSpace(source.PresetID) == "" {
			return &ValidationError{Message: "source.presetId is required"}
		}
	case "my-role":
		if strings.TrimSpace(source.RoleID) == "" {
			return &ValidationError{Message: "source.roleId is required"}
		}
		if strings.TrimSpace(source.VersionID) == "" {
			return &ValidationError{Message: "source.versionId is required"}
		}
	default:
		return &ValidationError{Message: "source.kind must be preset or my-role"}
	}
	return nil
}

func classify(items []RealizationItem) (requiredFailed bool, partial bool) {
	for _, item := range items {
		switch strings.TrimSpace(item.Status) {
		case "failed", "error", "unresolved", "skipped":
			if item.Required {
				return true, partial
			}
			partial = true
		}
	}
	return false, partial
}

func (s Source) MaterializationSource() agent.AgentRoleMaterializationSource {
	source := agent.AgentRoleMaterializationSource{
		Kind:          strings.TrimSpace(s.Kind),
		PresetID:      strings.TrimSpace(s.PresetID),
		RoleID:        strings.TrimSpace(s.RoleID),
		RoleVersionID: strings.TrimSpace(s.VersionID),
		Locale:        strings.TrimSpace(s.Locale),
	}
	switch strings.TrimSpace(s.Kind) {
	case "preset":
		source.BindingKind = "preset_derived"
		source.SourcePresetRoleID = strings.TrimSpace(s.PresetID)
	case "my-role":
		source.BindingKind = "my_role"
	}
	return source
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

type ServiceOptions struct {
	BillingEnabled  bool
	QuotaStore      WorkspaceResourceLimitStore
	ResourcePlanner RoleResourcePlanner
}

func NewService(agents AgentStore, realizer RoleRealizer, options ...ServiceOptions) *Service {
	if agents == nil || realizer == nil {
		return nil
	}
	opt := ServiceOptions{}
	if len(options) > 0 {
		opt = options[0]
	}
	resourcePlan := opt.ResourcePlanner
	if resourcePlan == nil {
		resourcePlan, _ = realizer.(RoleResourcePlanner)
	}
	if opt.BillingEnabled && (opt.QuotaStore == nil || resourcePlan == nil) {
		return nil
	}
	return &Service{agents: agents, realizer: realizer, resourcePlan: resourcePlan, quotaStore: opt.QuotaStore, billingEnabled: opt.BillingEnabled}
}
