package repotokens

import (
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strconv"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

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

const (
	plaintextPrefix     = "ags_rat_"
	defaultLimit        = 50
	maxLimit            = 100
	defaultSyncLeaseTTL = 30 * time.Minute
	maxSyncLeaseTTL     = 2 * time.Hour
)

// Service owns repository credential issuance, one-time plaintext handling,
// owner authorization, audit, and lifecycle over the dedicated persistence
// store.
type Service struct {
	store *store.Store
	now   func() time.Time
}

// NewService constructs the repository credential lifecycle service over its
// dedicated store. A nil clock uses UTC wall time.
func NewService(tokenStore *store.Store, now func() time.Time) *Service {
	if now == nil {
		now = func() time.Time { return time.Now().UTC() }
	}
	return &Service{store: tokenStore, now: now}
}

// List returns owner-visible human credential metadata for one workspace. It
// excludes agent tokens, sync leases, plaintext, and verifier hashes.
func (s *Service) List(ctx context.Context, request ListRequest) (ListResponse, error) {
	if err := validateContext(request.WorkspaceID, request.ActorPrincipalID); err != nil {
		return ListResponse{}, err
	}
	if typ := strings.TrimSpace(request.Type); typ != "" && typ != TokenTypeHuman {
		return ListResponse{}, E(ErrPermissionDenied, "repository access token API only exposes human tokens")
	}
	limit := normalizeLimit(request.Limit)
	cursor, err := parseCursor(request.Cursor)
	if err != nil {
		return ListResponse{}, err
	}
	db, err := s.db(ctx)
	if err != nil {
		return ListResponse{}, err
	}
	q := db.Where("workspace_id = ? AND owner_principal_id = ? AND type = ?", strings.TrimSpace(request.WorkspaceID), strings.TrimSpace(request.ActorPrincipalID), TokenTypeHuman)
	if cursor != 0 {
		q = q.Where("id < ?", cursor)
	}
	if status := strings.TrimSpace(request.Status); status != "" {
		q = q.Where("status = ?", status)
	}
	if host := normalizeProviderHost(request.ProviderHost); host != "" {
		q = q.Where("provider_host = ?", host)
	}
	var rows []store.Token
	if err := q.Order("id DESC").Limit(limit + 1).Find(&rows).Error; err != nil {
		return ListResponse{}, err
	}
	nextCursor := ""
	if len(rows) > limit {
		nextCursor = strconv.FormatUint(uint64(rows[limit-1].ID), 10)
		rows = rows[:limit]
	}
	items := make([]TokenRef, 0, len(rows))
	for _, row := range rows {
		item, err := tokenRefFromModel(row, s.now())
		if err != nil {
			return ListResponse{}, err
		}
		items = append(items, item)
	}
	return ListResponse{Items: items, NextCursor: nextCursor}, nil
}

// CreateHuman issues an owner-scoped repository credential and returns its
// plaintext exactly once with the non-secret service projection.
func (s *Service) CreateHuman(ctx context.Context, request CreateHumanRequest) (CreateHumanResponse, error) {
	if err := validateContext(request.WorkspaceID, request.ActorPrincipalID); err != nil {
		return CreateHumanResponse{}, err
	}
	token, plaintext, err := s.create(ctx, createRequest{
		workspaceID: request.WorkspaceID, actor: request.ActorPrincipalID,
		displayName: request.DisplayName, providerHost: request.ProviderHost,
		scope: request.RepositoryScope, capabilities: request.Capabilities,
		expiresAt: request.ExpiresAt, tokenType: TokenTypeHuman, eventType: EventCreated,
	})
	if err != nil {
		return CreateHumanResponse{}, err
	}
	return CreateHumanResponse{Token: token, Plaintext: plaintext}, nil
}

// CreateSyncLease issues a bounded, short-lived repository sync credential and
// returns its plaintext exactly once. The service caps lease lifetime.
func (s *Service) CreateSyncLease(ctx context.Context, request CreateSyncLeaseRequest) (CreateSyncLeaseResponse, error) {
	if err := validateContext(request.WorkspaceID, request.ActorPrincipalID); err != nil {
		return CreateSyncLeaseResponse{}, err
	}
	now := s.now()
	expiresAt := request.ExpiresAt
	if expiresAt == nil {
		value := now.Add(defaultSyncLeaseTTL)
		expiresAt = &value
	}
	if !expiresAt.After(now) || expiresAt.After(now.Add(maxSyncLeaseTTL)) {
		return CreateSyncLeaseResponse{}, E(ErrInvalidRequest, "sync lease expiry must be within two hours")
	}
	displayName := strings.TrimSpace(request.DisplayName)
	if displayName == "" {
		displayName = "Repository sync lease"
	}
	token, plaintext, err := s.create(ctx, createRequest{
		workspaceID: request.WorkspaceID, actor: request.ActorPrincipalID,
		displayName: displayName, providerHost: request.ProviderHost,
		scope: request.RepositoryScope, capabilities: request.Capabilities,
		expiresAt: expiresAt, tokenType: TokenTypeSyncLease, eventType: EventLeaseCreated,
	})
	if err != nil {
		return CreateSyncLeaseResponse{}, err
	}
	return CreateSyncLeaseResponse{Token: token, Plaintext: plaintext}, nil
}

type createRequest struct {
	workspaceID, actor, displayName, providerHost, tokenType, agentID, eventType string
	scope                                                                        RepositoryScope
	capabilities                                                                 []string
	expiresAt                                                                    *time.Time
}

func (s *Service) create(ctx context.Context, request createRequest) (TokenRef, string, error) {
	displayName := strings.TrimSpace(request.displayName)
	if displayName == "" || len(displayName) > 255 {
		return TokenRef{}, "", E(ErrInvalidRequest, "displayName is required and must not exceed 255 characters")
	}
	providerHost := normalizeProviderHost(request.providerHost)
	if providerHost == "" {
		return TokenRef{}, "", E(ErrInvalidRequest, "providerHost is required")
	}
	scope, err := normalizeScope(request.scope)
	if err != nil {
		return TokenRef{}, "", err
	}
	capabilities, err := normalizeAndValidateCapabilities(request.capabilities)
	if err != nil {
		return TokenRef{}, "", err
	}
	now := s.now()
	if request.expiresAt != nil && !request.expiresAt.After(now) {
		return TokenRef{}, "", E(ErrInvalidRequest, "expiresAt must be in the future")
	}
	scopeJSON, _ := json.Marshal(scope)
	capabilitiesJSON, _ := json.Marshal(capabilities)
	plaintext, err := newPlaintext()
	if err != nil {
		return TokenRef{}, "", err
	}
	publicID, err := identifiers.NewEntityID("rat")
	if err != nil {
		return TokenRef{}, "", err
	}
	agsTokenID, err := identifiers.NewEntityID("ags_tok")
	if err != nil {
		return TokenRef{}, "", err
	}
	hash, fingerprint := verifier(plaintext)
	row := store.Token{
		PublicID: publicID, AGSTokenID: agsTokenID, VerifierHash: hash, Fingerprint: fingerprint,
		Type: request.tokenType, DisplayName: displayName, OwnerPrincipalID: strings.TrimSpace(request.actor),
		WorkspaceID: strings.TrimSpace(request.workspaceID), AgentID: strings.TrimSpace(request.agentID),
		ProviderHost: providerHost, RepositoryScopeJSON: string(scopeJSON), CapabilitiesJSON: string(capabilitiesJSON),
		Status: StatusActive, ExpiresAt: request.expiresAt, CreatedAt: now, UpdatedAt: now,
	}
	event, err := newEvent(row, request.eventType, request.actor, map[string]any{
		"repositoryScope": scope, "capabilities": capabilities,
	}, now)
	if err != nil {
		return TokenRef{}, "", err
	}
	events := []store.Event{event}
	if request.tokenType == TokenTypeHuman {
		revealed, eventErr := newEvent(row, EventPlaintextRevealed, request.actor, map[string]any{
			"repositoryScope": scope, "capabilities": capabilities,
		}, now)
		if eventErr != nil {
			return TokenRef{}, "", eventErr
		}
		events = append(events, revealed)
	}
	created, err := s.store.CreateToken(ctx, row, events...)
	if err != nil {
		return TokenRef{}, "", err
	}
	ref, err := tokenRefFromModel(created, now)
	return ref, plaintext, err
}

// Get returns a human credential only when it belongs to the authenticated
// principal in the requested workspace; hidden tokens map to not found.
func (s *Service) Get(ctx context.Context, request GetRequest) (TokenRef, error) {
	if err := validateGetLike(request.WorkspaceID, request.ActorPrincipalID, request.ID); err != nil {
		return TokenRef{}, err
	}
	row, err := s.getModel(ctx, request.WorkspaceID, request.ID)
	if err != nil {
		return TokenRef{}, err
	}
	if row.Type != TokenTypeHuman || row.OwnerPrincipalID != strings.TrimSpace(request.ActorPrincipalID) {
		return TokenRef{}, E(ErrTokenNotFound, "repository access token is not visible to the current principal")
	}
	return tokenRefFromModel(row, s.now())
}

// Revoke disables an owner-visible human credential and appends its audit
// event atomically. Repeated revocation is idempotent.
func (s *Service) Revoke(ctx context.Context, request RevokeRequest) (TokenRef, error) {
	if _, err := s.Get(ctx, GetRequest{WorkspaceID: request.WorkspaceID, ActorPrincipalID: request.ActorPrincipalID, ID: request.ID}); err != nil {
		return TokenRef{}, err
	}
	return s.revokeToken(ctx, request.WorkspaceID, request.ActorPrincipalID, request.ID, request.Reason, false)
}

func (s *Service) revokeToken(ctx context.Context, workspaceID, actor, publicID, reason string, force bool) (TokenRef, error) {
	db, err := s.db(ctx)
	if err != nil {
		return TokenRef{}, err
	}
	if strings.TrimSpace(reason) == "" {
		reason = "user_requested"
	}
	now := s.now()
	var row store.Token
	err = db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
			Where("workspace_id = ? AND public_id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(publicID)).
			First(&row).Error; err != nil {
			return err
		}
		if row.Status == StatusRevoked {
			return nil
		}
		if err := tx.Model(&store.Token{}).Where("id = ?", row.ID).Updates(map[string]any{
			"status": StatusRevoked, "revoked_at": now, "updated_at": now,
		}).Error; err != nil {
			return err
		}
		row.Status, row.RevokedAt, row.UpdatedAt = StatusRevoked, &now, now
		event, err := newEvent(row, EventRevoked, actor, map[string]any{"reason": reason, "force": force}, now)
		if err != nil {
			return err
		}
		return tx.Create(&event).Error
	})
	if err != nil {
		return TokenRef{}, mapStoreError(err)
	}
	return tokenRefFromModel(row, now)
}

// Events returns the bounded audit history of an owner-visible human
// credential; it never reveals plaintext or verifier material.
func (s *Service) Events(ctx context.Context, request EventListRequest) (EventListResponse, error) {
	if _, err := s.Get(ctx, GetRequest{WorkspaceID: request.WorkspaceID, ActorPrincipalID: request.ActorPrincipalID, ID: request.ID}); err != nil {
		return EventListResponse{}, err
	}
	row, err := s.getModel(ctx, request.WorkspaceID, request.ID)
	if err != nil {
		return EventListResponse{}, err
	}
	limit := normalizeLimit(request.Limit)
	cursor, err := parseCursor(request.Cursor)
	if err != nil {
		return EventListResponse{}, err
	}
	db, err := s.db(ctx)
	if err != nil {
		return EventListResponse{}, err
	}
	q := db.Where("token_id = ?", row.ID)
	if cursor != 0 {
		q = q.Where("id < ?", cursor)
	}
	var rows []store.Event
	if err := q.Order("id DESC").Limit(limit + 1).Find(&rows).Error; err != nil {
		return EventListResponse{}, err
	}
	nextCursor := ""
	if len(rows) > limit {
		nextCursor = strconv.FormatUint(uint64(rows[limit-1].ID), 10)
		rows = rows[:limit]
	}
	items := make([]Event, 0, len(rows))
	for _, row := range rows {
		item, err := eventFromModel(row)
		if err != nil {
			return EventListResponse{}, err
		}
		items = append(items, item)
	}
	return EventListResponse{Items: items, NextCursor: nextCursor}, nil
}

// ValidateCredential verifies a presented repository credential and separates
// expected authentication failures from lookup errors. It returns no verifier
// or stored plaintext.
func (s *Service) ValidateCredential(ctx context.Context, plaintext string) (TokenRef, ValidationFailure, error) {
	plaintext = strings.TrimSpace(plaintext)
	if !strings.HasPrefix(plaintext, plaintextPrefix) {
		return TokenRef{}, ValidationFailureUnknownToken, nil
	}
	hash, _ := verifier(plaintext)
	db, err := s.db(ctx)
	if err != nil {
		return TokenRef{}, ValidationFailureLookupError, err
	}
	var row store.Token
	if err := db.Where("verifier_hash = ?", hash).First(&row).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return TokenRef{}, ValidationFailureUnknownToken, nil
		}
		return TokenRef{}, ValidationFailureLookupError, err
	}
	ref, err := tokenRefFromModel(row, s.now())
	if err != nil {
		return TokenRef{}, ValidationFailureLookupError, err
	}
	switch ref.Status {
	case StatusActive:
		return ref, ValidationFailureNone, nil
	case StatusRevoked:
		return TokenRef{}, ValidationFailureRevokedToken, nil
	case StatusExpired:
		return TokenRef{}, ValidationFailureExpiredToken, nil
	default:
		return TokenRef{}, ValidationFailureInactiveToken, nil
	}
}

// AuthorizeRepository enforces the validated credential's repository scope and
// capability. It does not replace AgentTeam membership or resource authorization.
func (s *Service) AuthorizeRepository(token TokenRef, repository, capability string) error {
	if token.Status != StatusActive || !scopeCovers(token.RepositoryScope, repository) || !hasCapability(token.Capabilities, capability) {
		return E(ErrPermissionDenied, "repository credential does not grant requested access")
	}
	return nil
}

// Touch best-effort records credential use for observability. Failure is
// intentionally ignored and never changes an authorization decision.
func (s *Service) Touch(ctx context.Context, token TokenRef) {
	if strings.TrimSpace(token.ID) == "" {
		return
	}
	db, err := s.db(ctx)
	if err != nil {
		return
	}
	now := s.now()
	_ = db.Model(&store.Token{}).Where("public_id = ?", token.ID).Update("last_used_at", now).Error
}

func (s *Service) getModel(ctx context.Context, workspaceID, publicID string) (store.Token, error) {
	if strings.TrimSpace(workspaceID) == "" || strings.TrimSpace(publicID) == "" {
		return store.Token{}, E(ErrInvalidRequest, "workspace and token id are required")
	}
	row, err := s.store.GetByPublicID(ctx, workspaceID, publicID)
	if err != nil {
		return store.Token{}, mapStoreError(err)
	}
	return row, nil
}

func (s *Service) db(ctx context.Context) (*gorm.DB, error) {
	if s == nil || s.store == nil {
		return nil, E(ErrTokenStateConflict, "repository access token store is not configured")
	}
	return s.store.DB(ctx)
}

func validateContext(workspaceID, actor string) error {
	if strings.TrimSpace(workspaceID) == "" {
		return E(ErrPermissionDenied, "workspace context is required")
	}
	if strings.TrimSpace(actor) == "" {
		return E(ErrPermissionDenied, "authenticated principal is required")
	}
	return nil
}

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

func normalizeProviderHost(value string) string { return strings.ToLower(strings.TrimSpace(value)) }

func normalizeScope(scope RepositoryScope) (RepositoryScope, error) {
	if strings.TrimSpace(scope.Mode) != "repositories" || strings.TrimSpace(scope.PolicyRef) != "" {
		return RepositoryScope{}, E(ErrInvalidRequest, "repositoryScope.mode must be repositories")
	}
	out := RepositoryScope{Mode: "repositories"}
	seen := map[string]struct{}{}
	for _, raw := range scope.Repositories {
		repo := normalizeRepository(raw)
		owner, name, ok := strings.Cut(repo, "/")
		if !ok || owner == "" || name == "" || strings.Contains(name, "/") {
			return RepositoryScope{}, E(ErrInvalidRequest, "repositoryScope.repositories must contain owner/repo names")
		}
		key := strings.ToLower(repo)
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out.Repositories = append(out.Repositories, repo)
	}
	if len(out.Repositories) == 0 {
		return RepositoryScope{}, E(ErrInvalidRequest, "repositoryScope.repositories is required")
	}
	return out, nil
}

func normalizeAndValidateCapabilities(values []string) ([]string, error) {
	allowed := map[string]bool{"repo:read": true, "repo:clone": true, "repo:write": true}
	out, seen := []string{}, map[string]bool{}
	for _, raw := range values {
		value := strings.TrimSpace(raw)
		if value == "" {
			continue
		}
		if !allowed[value] {
			return nil, E(ErrInvalidRequest, "unsupported repository access token capability")
		}
		if !seen[value] {
			seen[value] = true
			out = append(out, value)
		}
	}
	if len(out) == 0 {
		return nil, E(ErrInvalidRequest, "capabilities are required")
	}
	return out, nil
}

func normalizeRepository(value string) string {
	value = strings.Trim(strings.TrimSpace(value), "/")
	return strings.TrimSuffix(value, ".git")
}

func scopeCovers(scope RepositoryScope, repository string) bool {
	repository = normalizeRepository(repository)
	for _, candidate := range scope.Repositories {
		if strings.EqualFold(normalizeRepository(candidate), repository) {
			return true
		}
	}
	return false
}

func hasCapability(values []string, required string) bool {
	for _, value := range values {
		if strings.TrimSpace(value) == strings.TrimSpace(required) {
			return true
		}
	}
	return false
}

func tokenRefFromModel(row store.Token, now time.Time) (TokenRef, error) {
	var scope RepositoryScope
	if err := json.Unmarshal([]byte(row.RepositoryScopeJSON), &scope); err != nil {
		return TokenRef{}, err
	}
	var capabilities []string
	if err := json.Unmarshal([]byte(row.CapabilitiesJSON), &capabilities); err != nil {
		return TokenRef{}, err
	}
	var agentID *string
	if row.AgentID != "" {
		value := row.AgentID
		agentID = &value
	}
	status := row.Status
	if status == StatusActive && row.ExpiresAt != nil && !row.ExpiresAt.After(now) {
		status = StatusExpired
	}
	return TokenRef{
		ID: row.PublicID, AGSTokenID: row.AGSTokenID, Type: row.Type, DisplayName: row.DisplayName,
		OwnerPrincipalID: row.OwnerPrincipalID, WorkspaceID: row.WorkspaceID, AgentID: agentID,
		ProviderHost: row.ProviderHost, RepositoryScope: scope, Capabilities: capabilities,
		Status: status, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt,
		LastUsedAt: row.LastUsedAt, ExpiresAt: row.ExpiresAt, RevokedAt: row.RevokedAt,
	}, nil
}

func eventFromModel(row store.Event) (Event, error) {
	metadata := map[string]any{}
	if row.MetadataJSON != "" {
		if err := json.Unmarshal([]byte(row.MetadataJSON), &metadata); err != nil {
			return Event{}, err
		}
	}
	return Event{ID: row.EventID, TokenID: row.PublicTokenID, AGSTokenID: row.AGSTokenID, Type: row.Type, ActorPrincipalID: row.ActorPrincipalID, CreatedAt: row.CreatedAt, Metadata: metadata}, nil
}

func newEvent(token store.Token, eventType, actor string, metadata map[string]any, now time.Time) (store.Event, error) {
	eventID, err := identifiers.NewEntityID("ratevt")
	if err != nil {
		return store.Event{}, err
	}
	encoded, err := json.Marshal(metadata)
	if err != nil {
		return store.Event{}, err
	}
	if strings.TrimSpace(actor) == "" {
		actor = "system"
	}
	return store.Event{EventID: eventID, TokenID: token.ID, PublicTokenID: token.PublicID, AGSTokenID: token.AGSTokenID, Type: eventType, ActorPrincipalID: actor, MetadataJSON: string(encoded), CreatedAt: now}, nil
}

func newPlaintext() (string, error) {
	value := make([]byte, 24)
	if _, err := rand.Read(value); err != nil {
		return "", fmt.Errorf("generate repository access token: %w", err)
	}
	return plaintextPrefix + hex.EncodeToString(value), nil
}

func verifier(plaintext string) (string, string) {
	sum := sha256.Sum256([]byte(plaintext))
	encoded := hex.EncodeToString(sum[:])
	return "sha256:" + encoded, encoded[:16]
}

func normalizeLimit(limit int) int {
	if limit <= 0 {
		return defaultLimit
	}
	if limit > maxLimit {
		return maxLimit
	}
	return limit
}

func parseCursor(cursor string) (uint, error) {
	if strings.TrimSpace(cursor) == "" {
		return 0, nil
	}
	value, err := strconv.ParseUint(strings.TrimSpace(cursor), 10, 64)
	if err != nil {
		return 0, E(ErrInvalidRequest, "cursor is invalid")
	}
	return uint(value), nil
}

func mapStoreError(err error) error {
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return E(ErrTokenNotFound, "repository access token not found")
	}
	return err
}
