// Package actionsrunner adapts the official GitHub Actions runner protocol to
// the MeshX automation runner control plane.
package actionsrunner

import (
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"math/big"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/automation/dsl"
	"github.com/tidbcloud/AgentTeam/internal/service/automation"
	automationstore "github.com/tidbcloud/AgentTeam/internal/store/automation"
	"gorm.io/gorm"
)

const (
	defaultPollWaitSeconds = 1
	defaultLeaseSeconds    = 300
	brokerAccessTokenTTL   = 24 * time.Hour

	runServiceAccessTokenTTL      = 7 * 24 * time.Hour
	maxOfficialRunnerRunSteps     = 200
	officialRunStepTimeoutMinutes = 30
	clientAssertionMaxTTL         = 10 * time.Minute

	accessTokenScopeBroker     = "broker"
	accessTokenScopeRunService = "run_service"
)

// AutomationControlPlane is the MeshX automation service surface used by the
// official-runner adapter.
type AutomationControlPlane interface {
	LookupAutomationRunnerRegistrationToken(ctx context.Context, runnerToken string) (automation.AutomationRunnerRegistrationLookup, error)
	RegisterAutomationActionsRunnerCredential(ctx context.Context, runnerToken string, credential automationstore.AutomationActionsRunnerCredential) error
	CreateOrReuseAutomationRunnerSessionForRunner(ctx context.Context, workspaceID string, input automation.AutomationRunnerSessionMutation) (automation.AutomationRunnerSession, error)
	HeartbeatAutomationRunnerSessionByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerHeartbeatMutation) (automation.AutomationRunnerHeartbeat, error)
	RevokeAutomationRunnerSessionByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerSessionMutation) error
	AcquireAutomationRunnerJobsByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerAcquireMutation) ([]automation.AutomationRunnerJob, error)
	AcceptAutomationRunnerJobByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerJobMutation) (automationstore.AutomationJobRun, error)
	RenewAutomationRunnerJobByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerJobRenewMutation) (automation.AutomationRunnerJobLease, error)
	CompleteAutomationRunnerJobByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerJobMutation) (automationstore.AutomationJobRun, error)
	FailAutomationRunnerJobByTokenHash(ctx context.Context, workspaceID string, runnerTokenHash string, input automation.AutomationRunnerJobMutation) (automationstore.AutomationJobRun, error)
}

// Service translates official runner registration, broker, and run-service
// calls into MeshX automation runner operations.
type Service struct {
	store           *automationstore.Store
	automations     AutomationControlPlane
	now             func() time.Time
	pollWaitSeconds int
}

// NewService creates an official actions/runner compatibility service.
func NewService(store *automationstore.Store, automations AutomationControlPlane, now func() time.Time) *Service {
	return &Service{store: store, automations: automations, now: now, pollWaitSeconds: defaultPollWaitSeconds}
}

// RunnerRegistrationBootstrap validates the official config token.
func (s *Service) RunnerRegistrationBootstrap(ctx context.Context, registrationToken string, brokerURL string) (RegistrationBootstrap, error) {
	if err := s.ensureReady(); err != nil {
		return RegistrationBootstrap{}, err
	}
	if _, err := s.automations.LookupAutomationRunnerRegistrationToken(ctx, registrationToken); err != nil {
		return RegistrationBootstrap{}, err
	}
	return RegistrationBootstrap{BrokerURL: ensureTrailingSlash(brokerURL)}, nil
}

// ValidateRegistrationToken validates the config token before the API layer
// returns fixed runner-admin metadata.
func (s *Service) ValidateRegistrationToken(ctx context.Context, registrationToken string) error {
	if err := s.ensureReady(); err != nil {
		return err
	}
	_, err := s.automations.LookupAutomationRunnerRegistrationToken(ctx, registrationToken)
	return err
}

// RegisterRunner stores official OAuth client metadata for a MeshX runner.
func (s *Service) RegisterRunner(ctx context.Context, registrationToken string, input RegisterRunnerInput, brokerURL string, oauthURL string) (RunnerRegistration, error) {
	if err := s.ensureReady(); err != nil {
		return RunnerRegistration{}, err
	}
	publicKey := strings.TrimSpace(input.PublicKey)
	if publicKey == "" {
		return RunnerRegistration{}, automation.ErrInvalidArgument
	}
	lookup, err := s.automations.LookupAutomationRunnerRegistrationToken(ctx, registrationToken)
	if err != nil {
		return RunnerRegistration{}, err
	}
	clientID, err := newUUID()
	if err != nil {
		return RunnerRegistration{}, fmt.Errorf("generate actions runner client id: %w", err)
	}
	officialRunnerID := officialRunnerID(lookup.WorkspaceID, lookup.RunnerID, clientID)
	now := s.currentTime()
	credential := automationstore.AutomationActionsRunnerCredential{
		ClientID:         clientID,
		WorkspaceID:      lookup.WorkspaceID,
		RunnerID:         lookup.RunnerID,
		OfficialRunnerID: officialRunnerID,
		PublicKey:        publicKey,
		CreatedAt:        now,
		UpdatedAt:        now,
	}
	if err := s.automations.RegisterAutomationActionsRunnerCredential(ctx, registrationToken, credential); err != nil {
		return RunnerRegistration{}, err
	}
	name := strings.TrimSpace(input.Name)
	if name == "" {
		name = lookup.Name
	}
	if name == "" {
		name = lookup.RunnerID
	}
	return RunnerRegistration{OfficialRunnerID: officialRunnerID, RunnerName: name, ClientID: clientID, BrokerURL: ensureTrailingSlash(brokerURL), OAuthURL: oauthURL}, nil
}

// OAuthTokenExchange validates the runner JWT assertion and returns a
// MeshX-backed bearer token for broker/run-service calls.
func (s *Service) OAuthTokenExchange(ctx context.Context, clientAssertion string, audience string) (OAuthToken, error) {
	if err := s.ensureReady(); err != nil {
		return OAuthToken{}, err
	}
	claims, err := parseClientAssertion(clientAssertion)
	if err != nil {
		return OAuthToken{}, err
	}
	credential, err := s.store.GetActionsRunnerCredentialByClientID(ctx, claims.Subject)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return OAuthToken{}, automation.ErrUnauthorized
	}
	if err != nil {
		return OAuthToken{}, err
	}
	now := s.currentTime()
	if err := verifyClientAssertion(clientAssertion, claims, credential.ClientID, credential.PublicKey, audience, now); err != nil {
		return OAuthToken{}, err
	}
	sessionID, err := newUUID()
	if err != nil {
		return OAuthToken{}, fmt.Errorf("generate actions runner session id: %w", err)
	}
	session, err := s.automations.CreateOrReuseAutomationRunnerSessionForRunner(ctx, credential.WorkspaceID, automation.AutomationRunnerSessionMutation{RunnerID: credential.RunnerID, SessionID: sessionID})
	if err != nil {
		return OAuthToken{}, err
	}
	sessionTokenHash := strings.TrimSpace(session.SessionTokenHash)
	if sessionTokenHash == "" {
		sessionTokenHash = nativeSessionTokenHash(session.SessionToken)
	}
	token, err := s.issueAccessToken(ctx, credential.WorkspaceID, credential.RunnerID, session.SessionID, sessionTokenHash, accessTokenScopeBroker, brokerAccessTokenTTL)
	if err != nil {
		return OAuthToken{}, err
	}
	return OAuthToken{AccessToken: token, ExpiresIn: int(brokerAccessTokenTTL / time.Second)}, nil
}

// CreateBrokerSession validates the MeshX-backed access token.
func (s *Service) CreateBrokerSession(ctx context.Context, accessToken string, ownerName string) (BrokerSession, error) {
	claims, err := s.authenticate(ctx, accessToken, accessTokenScopeBroker)
	if err != nil {
		return BrokerSession{}, err
	}
	if ownerName = strings.TrimSpace(ownerName); ownerName == "" {
		ownerName = claims.RunnerID
	}
	return BrokerSession{SessionID: claims.SessionID, OwnerName: ownerName}, nil
}

// CloseBrokerSession validates a broker session close request.
func (s *Service) CloseBrokerSession(ctx context.Context, accessToken string) error {
	claims, err := s.authenticate(ctx, accessToken, accessTokenScopeBroker)
	if err != nil {
		return err
	}
	return s.automations.RevokeAutomationRunnerSessionByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerSessionMutation{RunnerID: claims.RunnerID, SessionID: claims.SessionID})
}

// NextMessage long-polls MeshX for one runner job and returns a broker message.
func (s *Service) NextMessage(ctx context.Context, accessToken string, runServiceURL string) (*TaskAgentMessage, error) {
	claims, err := s.authenticate(ctx, accessToken, accessTokenScopeBroker)
	if err != nil {
		return nil, err
	}
	jobs, err := s.automations.AcquireAutomationRunnerJobsByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerAcquireMutation{
		RunnerID:     claims.RunnerID,
		SessionID:    claims.SessionID,
		Limit:        1,
		LeaseSeconds: defaultLeaseSeconds,
		WaitSeconds:  s.pollWaitSeconds,
	})
	if err != nil {
		return nil, err
	}
	if len(jobs) == 0 {
		return nil, nil
	}
	ref := map[string]any{
		"id":                strconv.FormatInt(jobs[0].QueueJobID, 10),
		"runner_request_id": strconv.FormatInt(jobs[0].QueueJobID, 10),
		"run_service_url":   ensureTrailingSlash(runServiceURL),
		"billing_owner_id":  "meshx",
	}
	body, _ := json.Marshal(ref)
	return &TaskAgentMessage{MessageID: jobs[0].QueueJobID, MessageType: "RunnerJobRequest", Body: string(body)}, nil
}

// AcquireJob accepts the MeshX lease and returns a minimal official runner job.
func (s *Service) AcquireJob(ctx context.Context, accessToken string, request AcquireJobRequest) (map[string]any, error) {
	claims, err := s.authenticate(ctx, accessToken, accessTokenScopeBroker)
	if err != nil {
		return nil, err
	}
	queueJobID, err := parseQueueJobID(request.JobMessageID)
	if err != nil {
		return nil, err
	}
	jobRun, err := s.automations.AcceptAutomationRunnerJobByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerJobMutation{RunnerID: claims.RunnerID, SessionID: claims.SessionID, QueueJobID: queueJobID})
	if err != nil {
		return nil, err
	}
	job, err := s.store.GetJob(ctx, queueJobID)
	if err != nil {
		return nil, err
	}
	run, err := s.store.GetRun(ctx, claims.WorkspaceID, job.RunID)
	if err != nil {
		return nil, err
	}
	message, err := agentJobRequestMessage(ensureTrailingSlash(request.RunServiceURL), s.currentTime(), run, job, jobRun)
	if err != nil {
		s.failAcceptedJob(ctx, claims, queueJobID, err.Error(), true)
		return nil, automation.ErrInvalidArgument
	}
	runServiceToken, err := s.issueAccessToken(ctx, claims.WorkspaceID, claims.RunnerID, claims.SessionID, claims.SessionTokenHash, accessTokenScopeRunService, runServiceAccessTokenTTL)
	if err != nil {
		s.failAcceptedJob(ctx, claims, queueJobID, "issue actions runner run-service token: "+err.Error(), false)
		return nil, err
	}
	if err := setAgentJobAccessToken(message, runServiceToken); err != nil {
		s.failAcceptedJob(ctx, claims, queueJobID, err.Error(), true)
		return nil, err
	}
	return message, nil
}

func (s *Service) failAcceptedJob(ctx context.Context, claims authenticatedAccessToken, queueJobID int64, message string, terminal bool) {
	_, _ = s.automations.FailAutomationRunnerJobByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerJobMutation{
		RunnerID:        claims.RunnerID,
		SessionID:       claims.SessionID,
		QueueJobID:      queueJobID,
		Message:         message,
		TerminalFailure: terminal,
	})
}

// RenewJob extends the MeshX queue lease for the official runner job.
func (s *Service) RenewJob(ctx context.Context, accessToken string, request RenewJobRequest) (RenewJobResponse, error) {
	claims, err := s.authenticate(ctx, accessToken, accessTokenScopeRunService)
	if err != nil {
		return RenewJobResponse{}, err
	}
	queueJobID, err := queueJobIDFromGUID(request.JobID)
	if err != nil {
		return RenewJobResponse{}, err
	}
	lease, err := s.automations.RenewAutomationRunnerJobByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerJobRenewMutation{RunnerID: claims.RunnerID, SessionID: claims.SessionID, QueueJobID: queueJobID, LeaseSeconds: defaultLeaseSeconds})
	if err != nil {
		return RenewJobResponse{}, err
	}
	if lease.LeaseUntil == nil {
		return RenewJobResponse{}, nil
	}
	return RenewJobResponse{LockedUntil: *lease.LeaseUntil}, nil
}

// CompleteJob maps the official runner conclusion to MeshX job completion.
func (s *Service) CompleteJob(ctx context.Context, accessToken string, request CompleteJobRequest) error {
	claims, err := s.authenticate(ctx, accessToken, accessTokenScopeRunService)
	if err != nil {
		return err
	}
	queueJobID, err := queueJobIDFromGUID(request.JobID)
	if err != nil {
		return err
	}
	outputJSON := completionOutputJSON(request)
	if successfulConclusion(request.Conclusion) {
		_, err = s.automations.CompleteAutomationRunnerJobByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerJobMutation{RunnerID: claims.RunnerID, SessionID: claims.SessionID, QueueJobID: queueJobID, OutputJSON: outputJSON})
		return err
	}
	_, err = s.automations.FailAutomationRunnerJobByTokenHash(ctx, claims.WorkspaceID, claims.SessionTokenHash, automation.AutomationRunnerJobMutation{RunnerID: claims.RunnerID, SessionID: claims.SessionID, QueueJobID: queueJobID, OutputJSON: outputJSON, Message: "actions runner concluded " + conclusionString(request.Conclusion), TerminalFailure: true})
	return err
}

func (s *Service) authenticate(ctx context.Context, accessToken string, allowedScopes ...string) (authenticatedAccessToken, error) {
	if err := s.ensureReady(); err != nil {
		return authenticatedAccessToken{}, err
	}
	now := s.currentTime()
	claims, err := decodeAccessToken(accessToken, now)
	if err != nil {
		return authenticatedAccessToken{}, err
	}
	if !accessTokenScopeAllowed(claims.Scope, allowedScopes) {
		return authenticatedAccessToken{}, automation.ErrUnauthorized
	}
	token, err := s.store.GetActiveActionsRunnerAccessTokenByHash(ctx, accessTokenHash(accessToken), now)
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return authenticatedAccessToken{}, automation.ErrUnauthorized
	}
	if err != nil {
		return authenticatedAccessToken{}, err
	}
	if token.WorkspaceID != claims.WorkspaceID || token.RunnerID != claims.RunnerID || token.ExpiresAt.Unix() != claims.ExpiresAt || token.SessionID != claims.SessionID || token.TokenScope != claims.Scope || strings.TrimSpace(token.SessionTokenHash) == "" {
		return authenticatedAccessToken{}, automation.ErrUnauthorized
	}
	_, err = s.automations.HeartbeatAutomationRunnerSessionByTokenHash(ctx, claims.WorkspaceID, token.SessionTokenHash, automation.AutomationRunnerHeartbeatMutation{RunnerID: claims.RunnerID, SessionID: claims.SessionID})
	if err != nil {
		return authenticatedAccessToken{}, err
	}
	return authenticatedAccessToken{accessTokenClaims: claims, SessionTokenHash: token.SessionTokenHash}, nil
}

func (s *Service) issueAccessToken(ctx context.Context, workspaceID string, runnerID string, sessionID string, sessionTokenHash string, scope string, ttl time.Duration) (string, error) {
	now := s.currentTime()
	scope = strings.TrimSpace(scope)
	if scope == "" {
		return "", fmt.Errorf("actions runner access token scope is required")
	}
	sessionTokenHash = strings.TrimSpace(sessionTokenHash)
	if sessionTokenHash == "" {
		return "", fmt.Errorf("actions runner session token hash is required")
	}
	if ttl <= 0 {
		ttl = brokerAccessTokenTTL
	}
	expiresAt := now.Add(ttl)
	token, err := encodeAccessToken(accessTokenClaims{
		WorkspaceID:     workspaceID,
		RunnerID:        runnerID,
		SessionID:       sessionID,
		Scope:           scope,
		IssuedAt:        now.Unix(),
		ExpiresAt:       expiresAt.Unix(),
		OrchestrationID: sessionID,
	})
	if err != nil {
		return "", err
	}
	if err := s.store.CreateActionsRunnerAccessToken(ctx, automationstore.AutomationRunnerToken{
		TokenHash:        accessTokenHash(token),
		WorkspaceID:      workspaceID,
		RunnerID:         runnerID,
		TokenScope:       scope,
		SessionID:        strings.TrimSpace(sessionID),
		SessionTokenHash: sessionTokenHash,
		ExpiresAt:        expiresAt,
		CreatedAt:        now,
		UpdatedAt:        now,
	}); err != nil {
		return "", err
	}
	return token, nil
}

func accessTokenScopeAllowed(scope string, allowedScopes []string) bool {
	scope = strings.TrimSpace(scope)
	if scope == "" || len(allowedScopes) == 0 {
		return false
	}
	for _, allowed := range allowedScopes {
		if scope == strings.TrimSpace(allowed) {
			return true
		}
	}
	return false
}

func (s *Service) ensureReady() error {
	if s == nil || s.store == nil || s.automations == nil {
		return fmt.Errorf("actions runner service is not configured")
	}
	return nil
}

func (s *Service) currentTime() time.Time {
	if s != nil && s.now != nil {
		return s.now()
	}
	return time.Now().UTC()
}

func ensureTrailingSlash(raw string) string {
	raw = strings.TrimSpace(raw)
	if raw == "" || strings.HasSuffix(raw, "/") {
		return raw
	}
	return raw + "/"
}

func parseQueueJobID(raw string) (int64, error) {
	id, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
	if err != nil || id <= 0 {
		return 0, automation.ErrInvalidArgument
	}
	return id, nil
}

func queueJobGUID(queueJobID int64) string {
	var bytes [16]byte
	sum := sha256.Sum256([]byte("meshx actions runner queue job"))
	copy(bytes[:8], sum[:8])
	binary.BigEndian.PutUint64(bytes[8:], uint64(queueJobID))
	return uuidFromBytes(bytes[:])
}

func queueJobIDFromGUID(raw string) (int64, error) {
	decoded, err := hex.DecodeString(strings.ReplaceAll(strings.TrimSpace(raw), "-", ""))
	if err != nil || len(decoded) != 16 {
		return 0, automation.ErrInvalidArgument
	}
	id := binary.BigEndian.Uint64(decoded[8:])
	if id == 0 || id > uint64(^uint64(0)>>1) {
		return 0, automation.ErrInvalidArgument
	}
	return int64(id), nil
}

func newUUID() (string, error) {
	var bytes [16]byte
	if _, err := rand.Read(bytes[:]); err != nil {
		return "", err
	}
	bytes[6] = (bytes[6] & 0x0f) | 0x40
	bytes[8] = (bytes[8] & 0x3f) | 0x80
	return uuidFromBytes(bytes[:]), nil
}

func uuidFromBytes(bytes []byte) string {
	if len(bytes) != 16 {
		return ""
	}
	return fmt.Sprintf("%s-%s-%s-%s-%s",
		hex.EncodeToString(bytes[0:4]),
		hex.EncodeToString(bytes[4:6]),
		hex.EncodeToString(bytes[6:8]),
		hex.EncodeToString(bytes[8:10]),
		hex.EncodeToString(bytes[10:16]),
	)
}

func guidFromString(value string) string {
	sum := sha256.Sum256([]byte(value))
	bytes := sum[:16]
	bytes[6] = (bytes[6] & 0x0f) | 0x50
	bytes[8] = (bytes[8] & 0x3f) | 0x80
	return uuidFromBytes(bytes)
}

func officialRunnerID(values ...string) int64 {
	sum := sha256.Sum256([]byte(strings.Join(values, "|")))
	id := int64(binary.BigEndian.Uint64(sum[:8]) & (1<<63 - 1))
	if id == 0 {
		return 1
	}
	return id
}

type accessTokenClaims struct {
	WorkspaceID     string `json:"workspace_id"`
	RunnerID        string `json:"runner_id"`
	SessionID       string `json:"session_id"`
	Scope           string `json:"scope"`
	IssuedAt        int64  `json:"iat"`
	ExpiresAt       int64  `json:"exp"`
	TokenID         string `json:"jti,omitempty"`
	OrchestrationID string `json:"orch_id,omitempty"`
}

type authenticatedAccessToken struct {
	accessTokenClaims
	SessionTokenHash string
}

func encodeAccessToken(claims accessTokenClaims) (string, error) {
	if strings.TrimSpace(claims.TokenID) == "" {
		tokenID, err := newUUID()
		if err != nil {
			return "", err
		}
		claims.TokenID = tokenID
	}
	header, err := json.Marshal(map[string]string{"typ": "JWT", "alg": "none"})
	if err != nil {
		return "", err
	}
	payload, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}
	var signature [32]byte
	if _, err := rand.Read(signature[:]); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(header) + "." +
		base64.RawURLEncoding.EncodeToString(payload) + "." +
		base64.RawURLEncoding.EncodeToString(signature[:]), nil
}

func decodeAccessToken(token string, now time.Time) (accessTokenClaims, error) {
	token = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(token), "Bearer "))
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	if _, err := base64.RawURLEncoding.DecodeString(parts[2]); err != nil {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	header, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	var tokenHeader struct {
		Type      string `json:"typ"`
		Algorithm string `json:"alg"`
	}
	if err := json.Unmarshal(header, &tokenHeader); err != nil {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	if !strings.EqualFold(strings.TrimSpace(tokenHeader.Type), "JWT") {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	var claims accessTokenClaims
	if err := json.Unmarshal(payload, &claims); err != nil {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	if strings.TrimSpace(claims.WorkspaceID) == "" || strings.TrimSpace(claims.RunnerID) == "" || strings.TrimSpace(claims.SessionID) == "" || strings.TrimSpace(claims.Scope) == "" {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	if claims.IssuedAt <= 0 || claims.ExpiresAt <= 0 || claims.ExpiresAt <= claims.IssuedAt {
		return accessTokenClaims{}, automation.ErrUnauthorized
	}
	if !now.IsZero() {
		unix := now.Unix()
		if unix >= claims.ExpiresAt || claims.IssuedAt > unix+60 {
			return accessTokenClaims{}, automation.ErrUnauthorized
		}
	}
	return claims, nil
}

func accessTokenHash(token string) string {
	token = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(token), "Bearer "))
	sum := sha256.Sum256([]byte(token))
	return hex.EncodeToString(sum[:])
}

func nativeSessionTokenHash(token string) string {
	token = strings.TrimSpace(token)
	if token == "" {
		return ""
	}
	sum := sha256.Sum256([]byte(token))
	return hex.EncodeToString(sum[:])
}

type clientAssertionClaims struct {
	Subject   string `json:"sub"`
	Issuer    string `json:"iss"`
	Audience  string `json:"aud"`
	NotBefore int64  `json:"nbf"`
	ExpiresAt int64  `json:"exp"`
}

func parseClientAssertion(assertion string) (clientAssertionClaims, error) {
	parts := strings.Split(strings.TrimSpace(assertion), ".")
	if len(parts) != 3 {
		return clientAssertionClaims{}, automation.ErrUnauthorized
	}
	payload, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return clientAssertionClaims{}, automation.ErrUnauthorized
	}
	var claims clientAssertionClaims
	if err := json.Unmarshal(payload, &claims); err != nil {
		return clientAssertionClaims{}, automation.ErrUnauthorized
	}
	if strings.TrimSpace(claims.Subject) == "" {
		return clientAssertionClaims{}, automation.ErrUnauthorized
	}
	return claims, nil
}

func verifyClientAssertion(assertion string, claims clientAssertionClaims, clientID string, publicKeyXML string, audience string, now time.Time) error {
	parts := strings.Split(strings.TrimSpace(assertion), ".")
	if len(parts) != 3 {
		return automation.ErrUnauthorized
	}
	headerBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
	if err != nil {
		return automation.ErrUnauthorized
	}
	var header struct {
		Algorithm string `json:"alg"`
	}
	if err := json.Unmarshal(headerBytes, &header); err != nil || header.Algorithm != "PS256" {
		return automation.ErrUnauthorized
	}
	if claims.Subject != clientID || claims.Issuer != clientID {
		return automation.ErrUnauthorized
	}
	if strings.TrimSpace(audience) != "" && claims.Audience != strings.TrimSpace(audience) {
		return automation.ErrUnauthorized
	}
	if claims.ExpiresAt <= 0 {
		return automation.ErrUnauthorized
	}
	if !now.IsZero() {
		unix := now.Unix()
		if claims.NotBefore > 0 && unix+60 < claims.NotBefore {
			return automation.ErrUnauthorized
		}
		if unix-60 > claims.ExpiresAt {
			return automation.ErrUnauthorized
		}
		if claims.ExpiresAt > unix+int64(clientAssertionMaxTTL/time.Second)+60 {
			return automation.ErrUnauthorized
		}
		if claims.NotBefore > 0 && claims.ExpiresAt-claims.NotBefore > int64(clientAssertionMaxTTL/time.Second)+120 {
			return automation.ErrUnauthorized
		}
	}
	publicKey, err := parseRSAPublicKeyXML(publicKeyXML)
	if err != nil {
		return automation.ErrUnauthorized
	}
	signature, err := base64.RawURLEncoding.DecodeString(parts[2])
	if err != nil {
		return automation.ErrUnauthorized
	}
	sum := sha256.Sum256([]byte(parts[0] + "." + parts[1]))
	if err := rsa.VerifyPSS(publicKey, crypto.SHA256, sum[:], signature, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}); err != nil {
		return automation.ErrUnauthorized
	}
	return nil
}

type rsaKeyValue struct {
	Modulus  string `xml:"Modulus"`
	Exponent string `xml:"Exponent"`
}

func parseRSAPublicKeyXML(raw string) (*rsa.PublicKey, error) {
	var key rsaKeyValue
	if err := xml.Unmarshal([]byte(strings.TrimSpace(raw)), &key); err != nil {
		return nil, err
	}
	modulus, err := base64.StdEncoding.DecodeString(strings.TrimSpace(key.Modulus))
	if err != nil {
		return nil, err
	}
	exponentBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(key.Exponent))
	if err != nil {
		return nil, err
	}
	exponent := 0
	for _, b := range exponentBytes {
		exponent = exponent<<8 + int(b)
	}
	if exponent == 0 {
		return nil, fmt.Errorf("rsa exponent is empty")
	}
	return &rsa.PublicKey{N: new(big.Int).SetBytes(modulus), E: exponent}, nil
}

func successfulConclusion(value any) bool {
	switch typed := value.(type) {
	case nil:
		return false
	case float64:
		return typed == 0
	case string:
		normalized := strings.ToLower(strings.TrimSpace(typed))
		return normalized == "succeeded" || normalized == "success" || normalized == "0"
	default:
		return false
	}
}

func conclusionString(value any) string {
	if text, ok := value.(string); ok {
		return strings.TrimSpace(text)
	}
	if value == nil {
		return "unknown"
	}
	return fmt.Sprint(value)
}

func completionOutputJSON(request CompleteJobRequest) string {
	payload := map[string]any{}
	if len(request.Outputs) > 0 {
		payload["outputs"] = request.Outputs
	}
	if len(request.StepResults) > 0 {
		payload["step_results"] = request.StepResults
	}
	if len(request.Annotations) > 0 {
		payload["annotations"] = request.Annotations
	}
	if len(request.Telemetry) > 0 {
		payload["telemetry"] = request.Telemetry
	}
	if len(payload) == 0 {
		return "{}"
	}
	encoded, _ := json.Marshal(payload)
	return string(encoded)
}

func setAgentJobAccessToken(message map[string]any, accessToken string) error {
	resources, ok := message["resources"].(map[string]any)
	if !ok {
		return fmt.Errorf("actions runner job message is missing resources")
	}
	endpoints, ok := resources["endpoints"].([]map[string]any)
	if !ok {
		return fmt.Errorf("actions runner job message is missing endpoints")
	}
	for _, endpoint := range endpoints {
		if endpoint["name"] != "SystemVssConnection" {
			continue
		}
		authorization, ok := endpoint["authorization"].(map[string]any)
		if !ok {
			return fmt.Errorf("actions runner SystemVssConnection endpoint is missing authorization")
		}
		parameters, ok := authorization["Parameters"].(map[string]string)
		if !ok {
			return fmt.Errorf("actions runner SystemVssConnection endpoint is missing authorization parameters")
		}
		parameters["AccessToken"] = accessToken
		return nil
	}
	return fmt.Errorf("actions runner job message is missing SystemVssConnection endpoint")
}

func agentJobRequestMessage(runServiceURL string, now time.Time, run automationstore.AutomationRun, job automationstore.AutomationJob, jobRun automationstore.AutomationJobRun) (map[string]any, error) {
	if strings.TrimSpace(runServiceURL) == "" {
		return nil, fmt.Errorf("actions run service url is required")
	}
	var spec dsl.JobSpec
	if err := json.Unmarshal([]byte(jobRun.InputJSON), &spec); err != nil {
		return nil, fmt.Errorf("decode automation job input: %w", err)
	}
	if strings.TrimSpace(spec.If) != "" {
		return nil, fmt.Errorf("automation job if is not supported by the official runner adapter")
	}
	if len(spec.Env) > 0 {
		return nil, fmt.Errorf("automation job env is not supported by the official runner adapter")
	}
	if len(spec.Steps) > maxOfficialRunnerRunSteps {
		return nil, fmt.Errorf("automation job has too many official runner steps")
	}
	steps := make([]map[string]any, 0, len(spec.Steps))
	for idx, step := range spec.Steps {
		stepPath := fmt.Sprintf("automation job %s step %d", jobRun.JobID, idx)
		if strings.TrimSpace(step.If) != "" {
			return nil, fmt.Errorf("%s if is not supported by the official runner adapter", stepPath)
		}
		if len(step.Env) > 0 {
			return nil, fmt.Errorf("%s env is not supported by the official runner adapter", stepPath)
		}
		if strings.TrimSpace(step.Uses) != "" {
			return nil, fmt.Errorf("%s uses action %q is not supported by the official runner adapter", stepPath, step.Uses)
		}
		script := strings.TrimSpace(step.Run)
		if script == "" {
			continue
		}
		stepName := strings.TrimSpace(step.ID)
		if stepName == "" {
			stepName = fmt.Sprintf("__run_%d", idx+1)
		}
		displayName := strings.TrimSpace(step.Name)
		if displayName == "" {
			displayName = stepName
		}
		steps = append(steps, map[string]any{
			"type":             4,
			"id":               guidFromString(jobRun.ID + ":step:" + strconv.Itoa(idx)),
			"name":             stepName,
			"displayName":      displayName,
			"contextName":      stepName,
			"condition":        "success()",
			"enabled":          true,
			"timeoutInMinutes": officialRunStepTimeoutMinutes,
			"reference": map[string]any{
				"type": 3,
			},
			"inputs": mappingToken(map[string]string{
				"script": script,
			}),
		})
	}
	if len(steps) == 0 {
		return nil, fmt.Errorf("automation job has no supported run steps")
	}
	lockedUntil := now.UTC()
	if job.LeaseUntil != nil {
		lockedUntil = *job.LeaseUntil
	}
	planID := guidFromString(run.WorkspaceID + ":" + run.ID)
	jobID := queueJobGUID(job.ID)
	variables := map[string]map[string]any{
		"system.culture":        variableValue("en-US"),
		"system.jobRequestType": variableValue("RunnerJobRequest"),
		"GITHUB_ACTIONS":        variableValue("true"),
		"GITHUB_REPOSITORY":     variableValue(run.RepositoryFullName),
		"GITHUB_REF":            variableValue(run.GitRef),
		"GITHUB_SHA":            variableValue(run.CommitSHA),
		"GITHUB_AUTOMATION":     variableValue(run.Name),
		"GITHUB_JOB":            variableValue(jobRun.JobID),
		"GITHUB_RUN_ID":         variableValue(run.ID),
	}
	baseURL := serviceBaseURL(runServiceURL)
	return map[string]any{
		"messageType":    "RunnerJobRequest",
		"plan":           map[string]any{"planId": planID, "planType": "Build", "version": 1},
		"timeline":       map[string]any{"id": guidFromString(jobRun.ID + ":timeline")},
		"jobId":          jobID,
		"jobDisplayName": firstNonEmpty(strings.TrimSpace(spec.Name), jobRun.JobID),
		"jobName":        jobRun.JobID,
		"requestId":      job.ID,
		"lockedUntil":    lockedUntil.UTC().Format(time.RFC3339Nano),
		"billingOwnerId": "meshx",
		"resources": map[string]any{
			"endpoints": []map[string]any{
				{
					"name": "SystemVssConnection",
					"type": "SystemVssConnection",
					"url":  runServiceURL,
					"authorization": map[string]any{
						"scheme":     "OAuth",
						"Parameters": map[string]string{},
					},
					"Data":    map[string]string{},
					"isReady": true,
				},
			},
		},
		"contextData": map[string]any{
			"github": pipelineContextDictionary(map[string]any{
				"api_url":        pipelineContextString(baseURL + "/api/v3"),
				"graphql_url":    pipelineContextString(baseURL + "/api/graphql"),
				"job":            pipelineContextString(jobRun.JobID),
				"ref":            pipelineContextString(run.GitRef),
				"repository":     pipelineContextString(run.RepositoryFullName),
				"run_attempt":    pipelineContextString(strconv.Itoa(job.Attempt)),
				"run_id":         pipelineContextString(run.ID),
				"server_url":     pipelineContextString(baseURL),
				"sha":            pipelineContextString(run.CommitSHA),
				"automation":     pipelineContextString(run.Name),
				"automation_ref": pipelineContextString(run.RepositoryFullName + "/" + run.DefinitionPath + "@" + run.GitRef),
				"automation_sha": pipelineContextString(run.CommitSHA),
			}),
		},
		"variables": variables,
		"steps":     steps,
	}, nil
}

func serviceBaseURL(runServiceURL string) string {
	trimmed := strings.TrimRight(strings.TrimSpace(runServiceURL), "/")
	return strings.TrimSuffix(trimmed, "/_runs")
}

func pipelineContextDictionary(values map[string]any) map[string]any {
	out := map[string]any{"t": 2}
	if len(values) == 0 {
		return out
	}
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	items := make([]map[string]any, 0, len(keys))
	for _, key := range keys {
		items = append(items, map[string]any{"k": key, "v": values[key]})
	}
	out["d"] = items
	return out
}

func pipelineContextString(value string) map[string]any {
	return map[string]any{"s": value}
}

func mappingToken(values map[string]string) map[string]any {
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	items := make([]map[string]any, 0, len(keys))
	for _, key := range keys {
		items = append(items, map[string]any{"Key": key, "Value": values[key]})
	}
	return map[string]any{"type": 2, "map": items}
}

func variableValue(value string) map[string]any {
	return map[string]any{"value": value, "isSecret": false}
}

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