package api

import (
	"crypto/sha256"
	"fmt"

	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/gitaly/vendored/gitalypb"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/pkg/entity"
	"go.opentelemetry.io/otel/attribute"
)

const (
	// TraceAgentIDAttr is tracing attribute that holds an agent id.
	TraceAgentIDAttr attribute.Key = "agent_id"
	// TraceAgentTypeAttr is tracing attribute that holds an agent type.
	TraceAgentTypeAttr attribute.Key = "agent_type"

	JWTAgentk = "gitlab-agent"
	JWTKAS    = "gitlab-kas"

	// The field manager name for the ones agentk  or kas own, see
	// https://kubernetes.io/docs/reference/using-api/server-side-apply/#field-management
	FieldManager = "agentk"

	// AgentNameRegex
	// https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/identity_and_auth.md#agent-identity-and-name
	AgentNameRegex = `[a-z0-9](?:[-a-z0-9]*[a-z0-9])?`

	// AgentKeyPrefix is the prefix to use for labels and annotations owned by kas/agentk.
	// See https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set.
	// See https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set.
	AgentKeyPrefix     = "agent.gitlab.com"
	AgentIDKey         = AgentKeyPrefix + "/id"
	ConfigProjectIDKey = AgentKeyPrefix + "/config_project_id"
	ProjectIDKey       = AgentKeyPrefix + "/project_id"
	// ProjectKey is the annotation/label key that has a GitLab project full path as its value, e.g. `gitlab-org/gitlab`.
	ProjectKey         = AgentKeyPrefix + "/project"
	CIPipelineIDKey    = AgentKeyPrefix + "/ci_pipeline_id"
	CIJobIDKey         = AgentKeyPrefix + "/ci_job_id"
	UsernameKey        = AgentKeyPrefix + "/username"
	EnvironmentSlugKey = AgentKeyPrefix + "/environment_slug"
	EnvironmentTierKey = AgentKeyPrefix + "/environment_tier"

	// WebSocketMaxMessageSize is an arbitrary historical limit we used from the early days.
	WebSocketMaxMessageSize = 10 * 1024 * 1024
	// GRPCMaxMessageSize is a limit that is under WebSocketMaxMessageSize.
	// Historically we didn't set gRPC limits:
	// - On the server the default value was 4MB.
	// - On the client the default value was 2GB.
	// We use 4MB explicitly to match the historical value.
	GRPCMaxMessageSize = 4 * 1024 * 1024

	// Compile time test to ensure GRPCMaxMessageSize is smaller than WebSocketMaxMessageSize.
	_ uint = WebSocketMaxMessageSize - GRPCMaxMessageSize
)

// AgentType is agent's type.
type AgentType byte

// NOTE: AgentTypeKubernetes should always be at the top to ensure backwards compatibility.
const (
	AgentTypeKubernetes AgentType = iota
	AgentTypeWorkspace
	AgentTypeUnknown
)

// String returns the string representation of the agent type
func (at AgentType) String() string {
	//nolint: exhaustive
	switch at {
	case AgentTypeKubernetes:
		return "agentk"
	case AgentTypeWorkspace:
		return "agentw"
	default:
		return "unknown"
	}
}

// ParseAgentType converts a string to an AgentType
// When parsing an agent type, we only expect real values like `agentk`, `agentw`.
// If we encounter any other value, we treat it as `AgentTypeUnknown` and raise an error.
func ParseAgentType(s string) (AgentType, error) {
	switch s {
	case "agentk":
		return AgentTypeKubernetes, nil
	case "agentw":
		return AgentTypeWorkspace, nil
	default:
		return AgentTypeUnknown, fmt.Errorf("unrecognized agent type: %s", s)
	}
}

// AgentKey is agent's key consisting of type and id.
type AgentKey struct {
	ID   int64
	Type AgentType
}

// String returns the string representation of the agent key
func (a AgentKey) String() string {
	return fmt.Sprintf("%s:%d", a.Type, a.ID)
}

// AgentToken is agent's bearer access token.
type AgentToken string

// AgentTokenWithType is agent's token and type
type AgentTokenWithType struct {
	Token AgentToken
	Type  AgentType
}

// AgentInfo is an interface to represent information about different types of agent.
type AgentInfo interface {
	GetAgentKey() AgentKey
}

// AgentkInfo contains information about an agentk.
type AgentkInfo struct {
	// Key is the agent's type and id in the database.
	Key AgentKey
	// ProjectID is the id of the configuration project of the agent.
	ProjectID int64
	// Name is the agent's name.
	// Can contain only /a-z\d-/
	Name       string
	GitalyInfo *entity.GitalyInfo
	Repository *gitalypb.Repository
	// DefaultBranch is the name of the default branch in the agent's configuration repository.
	DefaultBranch string
}

func (a *AgentkInfo) GetAgentKey() AgentKey {
	return a.Key
}

// AgentwInfo contains information about an agentw.
type AgentwInfo struct {
	// Key is the agent's type and id in the database.
	Key AgentKey
}

func (a *AgentwInfo) GetAgentKey() AgentKey {
	return a.Key
}

type ProjectInfo struct {
	ProjectID  int64
	GitalyInfo *entity.GitalyInfo
	Repository *gitalypb.Repository
	// DefaultBranch is the name of the default branch in a repository.
	DefaultBranch string
}

func AgentToken2key(token AgentToken) []byte {
	// We use only the first half of the token as a key. Under the assumption of
	// a randomly generated token of length at least 50, with an alphabet of at least
	//
	// - upper-case characters (26)
	// - lower-case characters (26),
	// - numbers (10),
	//
	// (see https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/clusters/agent_token.rb)
	//
	// we have at least 62^25 different possible token prefixes. Since the token is
	// randomly generated, to obtain the token from this hash, one would have to
	// also guess the second half, and validate it by attempting to log in (kas
	// cannot validate tokens on its own)
	n := len(token) / 2
	tokenHash := sha256.Sum256([]byte(token[:n]))
	return tokenHash[:]
}
