package repotokens

import (
	"context"
	"encoding/json"
	"errors"
	"strings"

	"gorm.io/gorm"

	store "github.com/tidbcloud/AgentTeam/internal/store/repotokens"
)

const (
	AgentTokenReasonAgentCreated      = "agent_created"
	AgentTokenReasonAgentUpdated      = "agent_updated"
	AgentTokenReasonAgentDeleted      = "agent_deleted"
	AgentTokenReasonAgentDisabled     = "agent_disabled"
	AgentTokenReasonScopeRemoved      = "scope_removed"
	AgentTokenReasonCapabilityReduced = "capability_reduced"
	AgentTokenReasonSecurityResponse  = "security_response"
	AgentTokenDefaultProviderHost     = "github.com"
	AgentCapabilityRepositoryRead     = "repo:read"
	AgentCapabilityRepositoryClone    = "repo:clone"
	AgentCapabilityRepositoryWrite    = "repo:write"
	AgentPermissionWorkspaceWrite     = "agent_workspace_write"
	AgentPermissionFullAccess         = "full_access"
)

// ReconcileAgent converges a MeshX-managed agent credential to the desired
// repository scope/capabilities.
func (s *Service) ReconcileAgent(ctx context.Context, request ReconcileAgentRequest) (TokenRef, error) {
	if err := validateAgentContext(request.WorkspaceID, request.ActorPrincipalID, request.AgentID); err != nil {
		return TokenRef{}, err
	}
	request.ProviderHost = defaultProviderHost(request.ProviderHost)
	request.Capabilities = normalizeCapabilities(request.Capabilities)
	if strings.TrimSpace(request.Reason) == "" {
		request.Reason = AgentTokenReasonAgentUpdated
	}
	if !agentRepositoryAccessEnabled(request.RepositoryScope, request.Capabilities) {
		return s.RevokeAgent(ctx, RevokeAgentRequest{WorkspaceID: request.WorkspaceID, ActorPrincipalID: request.ActorPrincipalID, AgentID: request.AgentID, Reason: AgentTokenReasonScopeRemoved})
	}
	if suppressed, err := s.store.AgentSuppressed(ctx, request.WorkspaceID, request.AgentID); err != nil {
		return TokenRef{}, err
	} else if suppressed {
		return TokenRef{}, E(ErrTokenStateConflict, "agent repository access token was force revoked")
	}
	scope, err := normalizeScope(request.RepositoryScope)
	if err != nil {
		return TokenRef{}, err
	}
	capabilities, err := normalizeAndValidateCapabilities(request.Capabilities)
	if err != nil {
		return TokenRef{}, err
	}
	db, err := s.db(ctx)
	if err != nil {
		return TokenRef{}, err
	}
	var existing store.Token
	findErr := db.Where("workspace_id = ? AND agent_id = ? AND type = ? AND status <> ?", request.WorkspaceID, request.AgentID, TokenTypeAgent, StatusRevoked).
		Order("id DESC").First(&existing).Error
	if findErr == nil {
		scopeJSON, _ := json.Marshal(scope)
		capabilitiesJSON, _ := json.Marshal(capabilities)
		now := s.now()
		scopeChanged := existing.RepositoryScopeJSON != string(scopeJSON)
		capabilitiesChanged := existing.CapabilitiesJSON != string(capabilitiesJSON)
		if err := db.Transaction(func(tx *gorm.DB) error {
			if err := tx.Model(&store.Token{}).Where("id = ?", existing.ID).Updates(map[string]any{
				"provider_host": request.ProviderHost, "repository_scope_json": string(scopeJSON),
				"capabilities_json": string(capabilitiesJSON), "status": StatusActive, "updated_at": now,
			}).Error; err != nil {
				return err
			}
			events := make([]store.Event, 0, 2)
			if scopeChanged {
				event, err := newEvent(existing, EventScopeUpdated, request.ActorPrincipalID, map[string]any{
					"reason": request.Reason, "repositoryScope": scope,
				}, now)
				if err != nil {
					return err
				}
				events = append(events, event)
			}
			if capabilitiesChanged {
				event, err := newEvent(existing, EventCapabilitiesUpdated, request.ActorPrincipalID, map[string]any{
					"reason": request.Reason, "capabilities": capabilities,
				}, now)
				if err != nil {
					return err
				}
				events = append(events, event)
			}
			for i := range events {
				if err := tx.Create(&events[i]).Error; err != nil {
					return err
				}
			}
			return nil
		}); err != nil {
			return TokenRef{}, err
		}
		existing.ProviderHost, existing.RepositoryScopeJSON, existing.CapabilitiesJSON = request.ProviderHost, string(scopeJSON), string(capabilitiesJSON)
		existing.Status, existing.UpdatedAt = StatusActive, now
		return tokenRefFromModel(existing, now)
	}
	if !errors.Is(findErr, gorm.ErrRecordNotFound) {
		return TokenRef{}, findErr
	}
	token, _, err := s.create(ctx, createRequest{
		workspaceID: request.WorkspaceID, actor: request.AgentID, displayName: "Agent repository token",
		providerHost: request.ProviderHost, scope: scope, capabilities: capabilities,
		tokenType: TokenTypeAgent, agentID: request.AgentID, eventType: EventCreated,
	})
	return token, err
}

// ReconcileAgentDesiredState is the lifecycle-facing entry point used by agent
// create/update/disable hooks after they derive desired repository access from
// the agent record.
func (s *Service) ReconcileAgentDesiredState(ctx context.Context, state AgentRepositoryAccessDesiredState, reason string) (TokenRef, error) {
	if !state.Enabled {
		if strings.TrimSpace(reason) == "" {
			reason = AgentTokenReasonAgentDisabled
		}
		return s.RevokeAgent(ctx, RevokeAgentRequest{WorkspaceID: state.WorkspaceID, ActorPrincipalID: state.ActorPrincipalID, AgentID: state.AgentID, Reason: reason})
	}
	return s.ReconcileAgent(ctx, ReconcileAgentRequest{WorkspaceID: state.WorkspaceID, ActorPrincipalID: state.ActorPrincipalID, AgentID: state.AgentID, ProviderHost: state.ProviderHost, RepositoryScope: state.RepositoryScope, Capabilities: state.Capabilities, Reason: reason})
}

// RevokeAgent revokes every MeshX repository access token for an agent.
// Force=true records a non-secret suppression marker so later lifecycle
// reconciliation will not silently recreate a token after security/admin revoke.
func (s *Service) RevokeAgent(ctx context.Context, request RevokeAgentRequest) (TokenRef, error) {
	if err := validateAgentContext(request.WorkspaceID, request.ActorPrincipalID, request.AgentID); err != nil {
		return TokenRef{}, err
	}
	if strings.TrimSpace(request.Reason) == "" {
		request.Reason = AgentTokenReasonAgentDeleted
	}
	db, err := s.db(ctx)
	if err != nil {
		return TokenRef{}, err
	}
	now := s.now()
	if request.Force {
		err := s.store.SuppressAgent(ctx, store.AgentSuppression{
			WorkspaceID: request.WorkspaceID, AgentID: request.AgentID,
			ActorPrincipalID: request.ActorPrincipalID, Reason: request.Reason,
			SuppressedAt: now, CreatedAt: now, UpdatedAt: now,
		})
		if err != nil {
			return TokenRef{}, err
		}
	}
	var rows []store.Token
	if err := db.Where("workspace_id = ? AND agent_id = ? AND type = ? AND status <> ?", request.WorkspaceID, request.AgentID, TokenTypeAgent, StatusRevoked).Find(&rows).Error; err != nil {
		return TokenRef{}, err
	}
	var last TokenRef
	for _, row := range rows {
		last, err = s.revokeToken(ctx, request.WorkspaceID, request.ActorPrincipalID, row.PublicID, request.Reason, request.Force)
		if err != nil {
			return TokenRef{}, err
		}
	}
	if len(rows) == 0 {
		agentID := request.AgentID
		return TokenRef{Type: TokenTypeAgent, WorkspaceID: request.WorkspaceID, AgentID: &agentID, Status: StatusRevoked, UpdatedAt: now}, nil
	}
	return last, nil
}

// DesiredStateFromAgentRepositoryAccess centralizes the permission mapping used
// by lifecycle hooks. Only explicit supported permission modes grant repository
// capabilities: workspace-write agents receive clone/read, full-access agents
// additionally receive repo:write. Empty or unknown permission modes disable the
// desired token so AGS revokes instead of creating broad credentials.
func DesiredStateFromAgentRepositoryAccess(workspaceID, actorPrincipalID, agentID, providerHost string, scope RepositoryScope, permissionMode string) AgentRepositoryAccessDesiredState {
	var capabilities []string
	switch strings.TrimSpace(permissionMode) {
	case AgentPermissionWorkspaceWrite:
		capabilities = []string{AgentCapabilityRepositoryRead, AgentCapabilityRepositoryClone}
	case AgentPermissionFullAccess:
		capabilities = []string{AgentCapabilityRepositoryRead, AgentCapabilityRepositoryClone, AgentCapabilityRepositoryWrite}
	default:
		capabilities = []string{}
	}
	enabled := strings.TrimSpace(scope.Mode) != "" && len(capabilities) > 0
	return AgentRepositoryAccessDesiredState{WorkspaceID: workspaceID, ActorPrincipalID: actorPrincipalID, AgentID: agentID, ProviderHost: defaultProviderHost(providerHost), RepositoryScope: scope, Capabilities: capabilities, Enabled: enabled}
}

func validateAgentContext(workspaceID, actor, agentID string) error {
	if err := validateContext(workspaceID, actor); err != nil {
		return err
	}
	if strings.TrimSpace(agentID) == "" {
		return E(ErrInvalidRequest, "agentId is required")
	}
	return nil
}

func defaultProviderHost(value string) string {
	if strings.TrimSpace(value) == "" {
		return AgentTokenDefaultProviderHost
	}
	return strings.TrimSpace(value)
}

func normalizeCapabilities(values []string) []string {
	out := make([]string, 0, len(values))
	seen := map[string]struct{}{}
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		if _, ok := seen[trimmed]; ok {
			continue
		}
		seen[trimmed] = struct{}{}
		out = append(out, trimmed)
	}
	return out
}

func agentRepositoryAccessEnabled(scope RepositoryScope, capabilities []string) bool {
	return strings.TrimSpace(scope.Mode) != "" && len(capabilities) > 0
}
