package maas

import (
	"context"
	"fmt"
	"slices"
	"strings"

	maasavailability "github.com/tidbcloud/AgentTeam/internal/maas/availability"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/organization"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

const (
	ReasonModelRetired           = "model_retired"
	ReasonNotEntitled            = "not_entitled"
	ReasonEntitlementPending     = "entitlement_pending"
	ReasonEntitlementSuspended   = "entitlement_suspended"
	ReasonZeroBalance            = "zero_balance"
	ReasonBalanceUnavailable     = "balance_unavailable"
	ReasonWorkspaceNotAuthorized = "workspace_not_authorized"
	ReasonFeatureUnsupported     = "feature_unsupported"
	ReasonWireFamilyUnsupported  = "wire_family_unsupported"
	ReasonDaemonUpgradeRequired  = "daemon_upgrade_required"
	ReasonRuntimeUnavailable     = "runtime_unavailable"

	authorizationAllActive = "all_active_hosted_inventory"
	authorizationSelected  = "selected_hosted_models"

	inventoryStatusActive   = "active_inventory"
	entitlementStatusActive = "eligible_entitled"
	balanceStatusPositive   = "positive_balance"
	runtimeStatusSupported  = "runtime_supported"
)

type OrganizationStore interface {
	GetWorkspaceOrganizationForUser(ctx context.Context, userID string, workspaceID string) (organization.OrganizationSummary, error)
	GetHostedModelSelectionForWorkspace(ctx context.Context, organizationID string, workspaceID string) (organization.OrganizationHostedModelSelectionView, error)
}

type AvailabilityClient interface {
	LoadHostedAvailability(ctx context.Context, organizationID string) (maasavailability.Availability, error)
}

type Dependencies struct {
	Organizations OrganizationStore
	Availability  AvailabilityClient
}

// Validator enforces AgentTeam-hosted model admission before save/start/capability issue.
type Validator struct {
	organizations OrganizationStore
	availability  AvailabilityClient
}

func NewValidator(deps Dependencies) *Validator {
	return &Validator{
		organizations: deps.Organizations,
		availability:  deps.Availability,
	}
}

type AgentBindingValidation struct {
	OrganizationID            string
	UserID                    string
	WorkspaceID               string
	ModelReference            string
	LLMMode                   string
	Runtime                   string
	Features                  []string
	MachineSupportsDirectMaaS *bool
}

// ValidateAgentBindingForUser resolves the caller's organization membership
// before enforcing one hosted agent binding for the selected workspace.
func (validator *Validator) ValidateAgentBindingForUser(ctx context.Context, params AgentBindingValidation) error {
	if agent.NormalizeAgentLLMMode(params.LLMMode) != agent.AgentLLMModeAgentTeamHosted {
		return nil
	}
	if validator == nil || validator.organizations == nil {
		return hostedAdmissionConflict(ReasonRuntimeUnavailable)
	}
	workspaceID := strings.TrimSpace(params.WorkspaceID)
	userID := strings.TrimSpace(params.UserID)
	if workspaceID == "" {
		return storeerr.BadRequest("workspaceId is required")
	}
	if userID == "" {
		return storeerr.BadRequest("userId is required")
	}

	org, err := validator.organizations.GetWorkspaceOrganizationForUser(ctx, userID, workspaceID)
	if err != nil {
		return err
	}
	params.OrganizationID = org.ID
	params.WorkspaceID = workspaceID
	return validator.ValidateAgentBinding(ctx, params)
}

// ValidateAgentBinding enforces one AgentTeam-hosted model binding when the caller
// already knows the authoritative organization/workspace context.
func (validator *Validator) ValidateAgentBinding(ctx context.Context, params AgentBindingValidation) error {
	if agent.NormalizeAgentLLMMode(params.LLMMode) != agent.AgentLLMModeAgentTeamHosted {
		return nil
	}
	if validator == nil || validator.organizations == nil || validator.availability == nil {
		return hostedAdmissionConflict(ReasonRuntimeUnavailable)
	}
	if params.MachineSupportsDirectMaaS != nil && !*params.MachineSupportsDirectMaaS {
		return hostedAdmissionConflict(ReasonDaemonUpgradeRequired)
	}

	organizationID := strings.TrimSpace(params.OrganizationID)
	workspaceID := strings.TrimSpace(params.WorkspaceID)
	modelRef := strings.TrimSpace(params.ModelReference)
	if organizationID == "" {
		return storeerr.BadRequest("organizationId is required")
	}
	if workspaceID == "" {
		return storeerr.BadRequest("workspaceId is required")
	}
	if modelRef == "" {
		return storeerr.BadRequest("model is required")
	}

	selection, err := validator.organizations.GetHostedModelSelectionForWorkspace(ctx, organizationID, workspaceID)
	if err != nil {
		return err
	}
	if !authorizationCoversModel(selection, modelRef) {
		return hostedAdmissionConflict(ReasonWorkspaceNotAuthorized)
	}

	availability, err := validator.availability.LoadHostedAvailability(ctx, organizationID)
	if err != nil {
		return fmt.Errorf("load hosted availability for admission: %w", hostedAdmissionConflict(ReasonRuntimeUnavailable))
	}
	model, ok := findAvailabilityModel(availability, modelRef)
	if !ok {
		return hostedAdmissionConflict(ReasonModelRetired)
	}
	if !model.Enabled {
		return hostedAdmissionConflict(firstUnavailableReason(model))
	}
	if !supportsFeatures(model.MessagesFeatures, params.Features) {
		return hostedAdmissionConflict(ReasonFeatureUnsupported)
	}
	if !supportsRuntimeWireFamily(model, params.Runtime) {
		return hostedAdmissionConflict(ReasonWireFamilyUnsupported)
	}
	return nil
}

func hostedAdmissionConflict(reason string) error {
	trimmed := strings.TrimSpace(reason)
	if trimmed == "" {
		trimmed = ReasonRuntimeUnavailable
	}
	return storeerr.Conflict(trimmed)
}

func authorizationCoversModel(selection organization.OrganizationHostedModelSelectionView, modelRef string) bool {
	switch strings.TrimSpace(selection.AuthorizationMode) {
	case authorizationAllActive:
		return true
	case authorizationSelected:
		return slices.Contains(normalizeStrings(selection.SelectedModelRefs), strings.TrimSpace(modelRef))
	default:
		return false
	}
}

func findAvailabilityModel(availability maasavailability.Availability, modelRef string) (maasavailability.Model, bool) {
	trimmedModelRef := strings.TrimSpace(modelRef)
	for _, model := range availability.Models {
		if strings.TrimSpace(model.ModelReference) == trimmedModelRef {
			return model, true
		}
	}
	return maasavailability.Model{}, false
}

func supportsFeatures(supported []string, requested []string) bool {
	if len(requested) == 0 {
		return true
	}
	normalizedSupported := normalizeStrings(supported)
	for _, feature := range normalizeStrings(requested) {
		if !slices.Contains(normalizedSupported, feature) {
			return false
		}
	}
	return true
}

func supportsRuntimeWireFamily(model maasavailability.Model, runtime string) bool {
	runtimeID := strings.TrimSpace(runtime)
	if runtimeID == "" {
		return true
	}
	families := normalizeStrings(model.ExecutableWireAPIFamilies)
	if len(families) == 0 {
		families = normalizeStrings(model.SupportedFamilies)
	}
	if len(families) == 0 {
		return true
	}
	return slices.Contains(families, requiredWireFamilyForRuntime(runtimeID))
}

func requiredWireFamilyForRuntime(runtime string) string {
	switch strings.TrimSpace(runtime) {
	case "claude":
		return "messages"
	case "codex":
		return "responses"
	default:
		return "chat_completions"
	}
}

func firstUnavailableReason(model maasavailability.Model) string {
	for _, reason := range normalizeStrings(model.Reasons) {
		return reason
	}
	if strings.TrimSpace(model.InventoryStatus) != inventoryStatusActive {
		return ReasonModelRetired
	}
	switch strings.TrimSpace(model.EntitlementStatus) {
	case entitlementStatusActive:
	default:
		if strings.TrimSpace(model.EntitlementStatus) != "" {
			return strings.TrimSpace(model.EntitlementStatus)
		}
		return ReasonNotEntitled
	}
	switch strings.TrimSpace(model.BalanceStatus) {
	case balanceStatusPositive:
	default:
		if strings.TrimSpace(model.BalanceStatus) != "" {
			return strings.TrimSpace(model.BalanceStatus)
		}
		return ReasonZeroBalance
	}
	switch strings.TrimSpace(model.RuntimeStatus) {
	case runtimeStatusSupported:
	default:
		if strings.TrimSpace(model.RuntimeStatus) == ReasonFeatureUnsupported {
			return ReasonFeatureUnsupported
		}
		return ReasonRuntimeUnavailable
	}
	return ReasonRuntimeUnavailable
}

func normalizeStrings(items []string) []string {
	normalized := make([]string, 0, len(items))
	seen := make(map[string]struct{}, len(items))
	for _, item := range items {
		trimmed := strings.TrimSpace(item)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		normalized = append(normalized, trimmed)
	}
	return normalized
}
