package providertokens

import (
	"context"
	"fmt"
	"strings"
	"time"

	"gorm.io/gorm"
)

// Store owns persistence for user-held, encrypted external provider tokens.
// It does not issue AgentTeam auth/session tokens or repository-scoped credentials.
type Store struct{ db *gorm.DB }

// New constructs the external provider token persistence owner.
func New(db *gorm.DB) *Store { return &Store{db: db} }

// WithDB returns the same persistence owner bound to another transaction or database handle.
func (s *Store) WithDB(db *gorm.DB) *Store { return &Store{db: db} }

func (s *Store) ensureDB() error {
	if s == nil || s.db == nil {
		return fmt.Errorf("personal access token store is not configured")
	}
	return nil
}

// ListOptions bounds metadata or ciphertext loading to one workspace owner and
// at most 200 external provider tokens. It is a store query shape, not an API DTO.
type ListOptions struct {
	WorkspaceID       string
	OwnerPrincipalID  string
	ProviderBaseURL   string
	Status            string
	ExpiresAfter      *time.Time
	ExpiresAtOrBefore *time.Time
	Limit             int
	IncludeCiphertext bool
}

// Create persists one encrypted external provider token and reloads its canonical row.
func (s *Store) Create(ctx context.Context, token PersonalAccessToken) (PersonalAccessToken, error) {
	if err := s.ensureDB(); err != nil {
		return PersonalAccessToken{}, err
	}
	token = normalize(token)
	if err := validate(token, true); err != nil {
		return PersonalAccessToken{}, err
	}
	if err := s.db.WithContext(ctx).Create(&token).Error; err != nil {
		return PersonalAccessToken{}, fmt.Errorf("create personal access token: %w", err)
	}
	return s.Get(ctx, token.WorkspaceID, token.ID)
}

// ListOwned returns a bounded, owner-scoped external provider token list.
func (s *Store) ListOwned(ctx context.Context, opts ListOptions) ([]PersonalAccessToken, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	opts.WorkspaceID = strings.TrimSpace(opts.WorkspaceID)
	opts.OwnerPrincipalID = strings.TrimSpace(opts.OwnerPrincipalID)
	if opts.WorkspaceID == "" {
		return nil, fmt.Errorf("personal access token workspace_id is required")
	}
	if opts.OwnerPrincipalID == "" {
		return nil, fmt.Errorf("personal access token owner_principal_id is required")
	}
	if opts.Limit <= 0 || opts.Limit > 200 {
		opts.Limit = 100
	}
	q := s.db.WithContext(ctx).Model(&PersonalAccessToken{}).
		Where("workspace_id = ? AND owner_principal_id = ?", opts.WorkspaceID, opts.OwnerPrincipalID)
	if !opts.IncludeCiphertext {
		q = q.Select(metadataColumns())
	}
	if providerBaseURL := strings.TrimSpace(opts.ProviderBaseURL); providerBaseURL != "" {
		q = q.Where("provider_base_url = ?", providerBaseURL)
	}
	if status := strings.TrimSpace(opts.Status); status != "" {
		q = q.Where("status = ?", status)
	}
	if opts.ExpiresAfter != nil {
		q = q.Where("(expires_at IS NULL OR expires_at > ?)", opts.ExpiresAfter.UTC())
	}
	if opts.ExpiresAtOrBefore != nil {
		q = q.Where("expires_at IS NOT NULL AND expires_at <= ?", opts.ExpiresAtOrBefore.UTC())
	}
	var tokens []PersonalAccessToken
	if err := q.Order("updated_at DESC").Order("display_name ASC").Limit(opts.Limit).Find(&tokens).Error; err != nil {
		return nil, fmt.Errorf("list personal access tokens: %w", err)
	}
	return tokens, nil
}

// Get loads one external provider token by workspace and token ID.
func (s *Store) Get(ctx context.Context, workspaceID, id string) (PersonalAccessToken, error) {
	if err := s.ensureDB(); err != nil {
		return PersonalAccessToken{}, err
	}
	var token PersonalAccessToken
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(id)).
		First(&token).Error
	if err != nil {
		return PersonalAccessToken{}, err
	}
	return token, nil
}

// GetOwned loads one external provider token scoped to its user owner.
func (s *Store) GetOwned(ctx context.Context, workspaceID, ownerPrincipalID, id string) (PersonalAccessToken, error) {
	if err := s.ensureDB(); err != nil {
		return PersonalAccessToken{}, err
	}
	var token PersonalAccessToken
	err := s.db.WithContext(ctx).
		Where("workspace_id = ? AND owner_principal_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(ownerPrincipalID), strings.TrimSpace(id)).
		First(&token).Error
	if err != nil {
		return PersonalAccessToken{}, err
	}
	return token, nil
}

// ListMetadataByIDs batch-loads metadata for explicit token IDs without ciphertext.
func (s *Store) ListMetadataByIDs(ctx context.Context, workspaceID string, ids []string) ([]PersonalAccessToken, error) {
	if err := s.ensureDB(); err != nil {
		return nil, err
	}
	workspaceID = strings.TrimSpace(workspaceID)
	ids = uniqueTrimmedStrings(ids)
	if workspaceID == "" {
		return nil, fmt.Errorf("personal access token workspace_id is required")
	}
	if len(ids) == 0 {
		return []PersonalAccessToken{}, nil
	}
	var tokens []PersonalAccessToken
	err := s.db.WithContext(ctx).
		Select(metadataColumns()).
		Where("workspace_id = ? AND id IN ?", workspaceID, ids).
		Find(&tokens).Error
	if err != nil {
		return nil, fmt.Errorf("list personal access token metadata by ids: %w", err)
	}
	return tokens, nil
}

// Update persists the mutable fields of one encrypted external provider token and reloads it.
func (s *Store) Update(ctx context.Context, token PersonalAccessToken) (PersonalAccessToken, error) {
	if err := s.ensureDB(); err != nil {
		return PersonalAccessToken{}, err
	}
	token = normalize(token)
	if err := validate(token, true); err != nil {
		return PersonalAccessToken{}, err
	}
	err := s.db.WithContext(ctx).Model(&PersonalAccessToken{}).
		Where("workspace_id = ? AND id = ?", token.WorkspaceID, token.ID).
		Updates(map[string]any{
			"display_name":        token.DisplayName,
			"provider_type":       token.ProviderType,
			"provider_base_url":   token.ProviderBaseURL,
			"username":            token.Username,
			"allowed_usages_json": token.AllowedUsagesJSON,
			"value_ciphertext":    token.ValueCiphertext,
			"status":              token.Status,
			"expires_at":          token.ExpiresAt,
			"revoked_at":          token.RevokedAt,
			"updated_by":          token.UpdatedBy,
			"updated_at":          token.UpdatedAt,
		}).Error
	if err != nil {
		return PersonalAccessToken{}, fmt.Errorf("update personal access token: %w", err)
	}
	return s.Get(ctx, token.WorkspaceID, token.ID)
}

// Delete removes one external provider token by workspace and token ID.
func (s *Store) Delete(ctx context.Context, workspaceID, id string) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	result := s.db.WithContext(ctx).
		Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(id)).
		Delete(&PersonalAccessToken{})
	if result.Error != nil {
		return fmt.Errorf("delete personal access token: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

// TouchLastUsed records the last successful provider-token use time.
func (s *Store) TouchLastUsed(ctx context.Context, workspaceID, id string, at time.Time) error {
	if err := s.ensureDB(); err != nil {
		return err
	}
	result := s.db.WithContext(ctx).Model(&PersonalAccessToken{}).
		Where("workspace_id = ? AND id = ?", strings.TrimSpace(workspaceID), strings.TrimSpace(id)).
		Update("last_used_at", at.UTC())
	if result.Error != nil {
		return fmt.Errorf("touch personal access token last used: %w", result.Error)
	}
	if result.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func metadataColumns() []string {
	return []string{
		"id",
		"workspace_id",
		"owner_principal_id",
		"display_name",
		"provider_type",
		"provider_base_url",
		"username",
		"allowed_usages_json",
		"status",
		"last_used_at",
		"expires_at",
		"revoked_at",
		"created_by",
		"updated_by",
		"created_at",
		"updated_at",
	}
}

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

func normalize(token PersonalAccessToken) PersonalAccessToken {
	token.ID = strings.TrimSpace(token.ID)
	token.WorkspaceID = strings.TrimSpace(token.WorkspaceID)
	token.OwnerPrincipalID = strings.TrimSpace(token.OwnerPrincipalID)
	token.DisplayName = strings.TrimSpace(token.DisplayName)
	token.ProviderType = strings.TrimSpace(token.ProviderType)
	token.ProviderBaseURL = strings.TrimSpace(token.ProviderBaseURL)
	token.Username = strings.TrimSpace(token.Username)
	token.AllowedUsagesJSON = strings.TrimSpace(token.AllowedUsagesJSON)
	token.ValueCiphertext = strings.TrimSpace(token.ValueCiphertext)
	token.Status = strings.TrimSpace(token.Status)
	token.CreatedBy = strings.TrimSpace(token.CreatedBy)
	token.UpdatedBy = strings.TrimSpace(token.UpdatedBy)
	return token
}

func validate(token PersonalAccessToken, requireCiphertext bool) error {
	switch {
	case token.ID == "":
		return fmt.Errorf("personal access token id is required")
	case token.WorkspaceID == "":
		return fmt.Errorf("personal access token workspace_id is required")
	case token.OwnerPrincipalID == "":
		return fmt.Errorf("personal access token owner_principal_id is required")
	case token.DisplayName == "":
		return fmt.Errorf("personal access token display_name is required")
	case token.ProviderType == "":
		return fmt.Errorf("personal access token provider_type is required")
	case token.ProviderBaseURL == "":
		return fmt.Errorf("personal access token provider_base_url is required")
	case token.Username == "":
		return fmt.Errorf("personal access token username is required")
	case token.AllowedUsagesJSON == "":
		return fmt.Errorf("personal access token allowed_usages_json is required")
	case requireCiphertext && token.ValueCiphertext == "":
		return fmt.Errorf("personal access token ciphertext is required")
	case token.Status == "":
		return fmt.Errorf("personal access token status is required")
	default:
		return nil
	}
}
