package apikeys

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

	"update_server/internal/db"
)

const (
	rawKeyPrefix        = "upsk_"
	rawKeyBytes         = 32
	rawKeyPreviewLength = 17
	createRetryLimit    = 4
)

var (
	ErrUnauthenticated = errors.New("api key unauthenticated")
	ErrUnauthorized    = errors.New("api key unauthorized")
)

type Permission string

const (
	PermissionDownload       Permission = "can_download"
	PermissionUpload         Permission = "can_upload"
	PermissionDelete         Permission = "can_delete"
	PermissionManageProjects Permission = "can_manage_projects"
)

type Service struct {
	store *db.Store
}

type CreateParams struct {
	Name              string
	Description       string
	ScopeMode         db.ScopeMode
	CanDownload       bool
	CanUpload         bool
	CanDelete         bool
	CanManageProjects bool
	ExpiresAt         *time.Time
	ProjectIDs        []int64
	TagIDs            []int64
	CreatedByUserID   *int64
}

type UpdateParams struct {
	Name              string
	Description       string
	ScopeMode         db.ScopeMode
	CanDownload       bool
	CanUpload         bool
	CanDelete         bool
	CanManageProjects bool
	ExpiresAt         *time.Time
	ProjectIDs        []int64
	TagIDs            []int64
}

type CreateResult struct {
	APIKey *db.APIKey
	RawKey string
}

func NewService(store *db.Store) *Service {
	return &Service{store: store}
}

func (s *Service) Create(ctx context.Context, params CreateParams) (*CreateResult, error) {
	normalized, err := normalizeCreateParams(params)
	if err != nil {
		return nil, err
	}

	for attempt := 0; attempt < createRetryLimit; attempt++ {
		rawKey, keyPrefix, keyHash, err := generateAPIKey()
		if err != nil {
			return nil, err
		}

		var created *db.APIKey
		err = s.store.WithTx(ctx, func(tx *db.TxStore) error {
			inserted, err := tx.APIKeys.Create(ctx, db.CreateAPIKeyParams{
				Name:              normalized.Name,
				KeyPrefix:         keyPrefix,
				KeyHash:           keyHash,
				Description:       normalized.Description,
				ScopeMode:         normalized.ScopeMode,
				CanDownload:       normalized.CanDownload,
				CanUpload:         normalized.CanUpload,
				CanDelete:         normalized.CanDelete,
				CanManageProjects: normalized.CanManageProjects,
				IsActive:          true,
				ExpiresAt:         normalized.ExpiresAt,
				CreatedByUserID:   normalized.CreatedByUserID,
			})
			if err != nil {
				return err
			}

			if err := syncScopeAccess(ctx, tx.APIKeys, inserted.ID, normalized.ScopeMode, normalized.ProjectIDs, normalized.TagIDs); err != nil {
				return err
			}

			created = inserted
			return nil
		})
		if err != nil {
			if errors.Is(err, db.ErrConflict) {
				continue
			}

			return nil, fmt.Errorf("create api key: %w", err)
		}

		return &CreateResult{
			APIKey: created,
			RawKey: rawKey,
		}, nil
	}

	return nil, fmt.Errorf("create api key: could not generate a unique key")
}

func (s *Service) Update(ctx context.Context, apiKeyID int64, params UpdateParams) (*db.APIKey, error) {
	normalized, err := normalizeUpdateParams(params)
	if err != nil {
		return nil, err
	}

	var updated *db.APIKey
	if err := s.store.WithTx(ctx, func(tx *db.TxStore) error {
		if _, err := tx.APIKeys.GetByID(ctx, apiKeyID); err != nil {
			return err
		}

		switch {
		case normalized.ScopeMode == db.ScopeModeAllProjects:
			if err := tx.APIKeys.ClearProjectAccess(ctx, apiKeyID); err != nil {
				return err
			}
			if err := tx.APIKeys.ClearTagAccess(ctx, apiKeyID); err != nil {
				return err
			}
		case normalized.ScopeMode.UsesProjectRules():
			if err := tx.APIKeys.ClearTagAccess(ctx, apiKeyID); err != nil {
				return err
			}
		case normalized.ScopeMode.UsesTagRules():
			if err := tx.APIKeys.ClearProjectAccess(ctx, apiKeyID); err != nil {
				return err
			}
		}

		record, err := tx.APIKeys.Update(ctx, apiKeyID, db.UpdateAPIKeyParams{
			Name:              normalized.Name,
			Description:       normalized.Description,
			ScopeMode:         normalized.ScopeMode,
			CanDownload:       normalized.CanDownload,
			CanUpload:         normalized.CanUpload,
			CanDelete:         normalized.CanDelete,
			CanManageProjects: normalized.CanManageProjects,
			ExpiresAt:         normalized.ExpiresAt,
		})
		if err != nil {
			return err
		}

		if err := syncScopeAccess(ctx, tx.APIKeys, apiKeyID, normalized.ScopeMode, normalized.ProjectIDs, normalized.TagIDs); err != nil {
			return err
		}

		updated = record
		return nil
	}); err != nil {
		if errors.Is(err, db.ErrNotFound) {
			return nil, err
		}

		return nil, fmt.Errorf("update api key: %w", err)
	}

	return s.store.APIKeys.GetByID(ctx, updated.ID)
}

func (s *Service) SetActive(ctx context.Context, apiKeyID int64, isActive bool) error {
	if err := s.store.APIKeys.SetActive(ctx, apiKeyID, isActive); err != nil {
		return err
	}

	return nil
}

func (s *Service) Authenticate(ctx context.Context, rawKey string) (*AuthState, error) {
	rawKey = strings.TrimSpace(rawKey)
	if rawKey == "" {
		return nil, ErrUnauthenticated
	}

	record, err := s.store.APIKeys.GetByHash(ctx, hashAPIKey(rawKey))
	if err != nil {
		if errors.Is(err, db.ErrNotFound) {
			return nil, ErrUnauthenticated
		}

		return nil, fmt.Errorf("lookup api key: %w", err)
	}

	now := time.Now().UTC()
	if !record.IsActive || record.Expired(now) {
		return nil, ErrUnauthenticated
	}

	if err := s.store.APIKeys.TouchLastUsedAt(ctx, record.ID, now); err != nil {
		return nil, fmt.Errorf("touch api key last_used_at: %w", err)
	}

	record.LastUsedAt = &now

	return &AuthState{APIKey: *record}, nil
}

func (s *Service) List(ctx context.Context) ([]db.APIKeyListItem, error) {
	return s.store.APIKeys.List(ctx)
}

func (s *Service) GetByID(ctx context.Context, apiKeyID int64) (*db.APIKey, error) {
	return s.store.APIKeys.GetByID(ctx, apiKeyID)
}

func (s *Service) ListProjectAccess(ctx context.Context, apiKeyID int64) ([]db.Project, error) {
	return s.store.APIKeys.ListProjectAccess(ctx, apiKeyID)
}

func (s *Service) ListTagAccess(ctx context.Context, apiKeyID int64) ([]db.Tag, error) {
	return s.store.APIKeys.ListTagAccess(ctx, apiKeyID)
}

func (s *Service) ListAccessibleProjects(ctx context.Context, apiKey db.APIKey) ([]db.Project, error) {
	return s.store.APIKeys.ListAccessibleProjects(ctx, apiKey.ID, apiKey.ScopeMode)
}

func (s *Service) CanAccessProject(ctx context.Context, apiKey db.APIKey, projectID int64) (bool, error) {
	return s.store.APIKeys.HasProjectAccess(ctx, apiKey.ID, apiKey.ScopeMode, projectID)
}

func HasPermission(apiKey db.APIKey, permission Permission) bool {
	switch permission {
	case PermissionDownload:
		return apiKey.CanDownload
	case PermissionUpload:
		return apiKey.CanUpload
	case PermissionDelete:
		return apiKey.CanDelete
	case PermissionManageProjects:
		return apiKey.CanManageProjects
	default:
		return false
	}
}

func generateAPIKey() (rawKey string, keyPrefix string, keyHash string, err error) {
	bytes := make([]byte, rawKeyBytes)
	if _, err := rand.Read(bytes); err != nil {
		return "", "", "", fmt.Errorf("generate api key: %w", err)
	}

	body := base64.RawURLEncoding.EncodeToString(bytes)
	rawKey = rawKeyPrefix + body

	keyPrefix = rawKey
	if len(keyPrefix) > rawKeyPreviewLength {
		keyPrefix = keyPrefix[:rawKeyPreviewLength]
	}

	return rawKey, keyPrefix, hashAPIKey(rawKey), nil
}

func hashAPIKey(rawKey string) string {
	sum := sha256.Sum256([]byte(rawKey))
	return hex.EncodeToString(sum[:])
}

func syncScopeAccess(ctx context.Context, repo *db.APIKeyRepository, apiKeyID int64, scopeMode db.ScopeMode, projectIDs []int64, tagIDs []int64) error {
	switch {
	case scopeMode == db.ScopeModeAllProjects:
		if err := repo.ClearProjectAccess(ctx, apiKeyID); err != nil {
			return err
		}
		if err := repo.ClearTagAccess(ctx, apiKeyID); err != nil {
			return err
		}
	case scopeMode.UsesProjectRules():
		if err := repo.ClearTagAccess(ctx, apiKeyID); err != nil {
			return err
		}
		if err := repo.ReplaceProjectAccess(ctx, apiKeyID, projectIDs); err != nil {
			return err
		}
	case scopeMode.UsesTagRules():
		if err := repo.ClearProjectAccess(ctx, apiKeyID); err != nil {
			return err
		}
		if err := repo.ReplaceTagAccess(ctx, apiKeyID, tagIDs); err != nil {
			return err
		}
	default:
		return fmt.Errorf("unsupported scope mode %q", scopeMode)
	}

	return nil
}

func normalizeCreateParams(params CreateParams) (CreateParams, error) {
	normalized := params
	normalized.Name = strings.TrimSpace(normalized.Name)
	normalized.Description = strings.TrimSpace(normalized.Description)

	switch {
	case normalized.Name == "":
		return CreateParams{}, fmt.Errorf("api key name is required")
	case !normalized.ScopeMode.Valid():
		return CreateParams{}, fmt.Errorf("api key scope mode is required")
	case !hasAnyPermission(normalized):
		return CreateParams{}, fmt.Errorf("select at least one permission")
	}

	normalized.ProjectIDs, normalized.TagIDs = normalizedScopeIDs(normalized.ScopeMode, normalized.ProjectIDs, normalized.TagIDs)
	return normalized, nil
}

func normalizeUpdateParams(params UpdateParams) (UpdateParams, error) {
	normalized := params
	normalized.Name = strings.TrimSpace(normalized.Name)
	normalized.Description = strings.TrimSpace(normalized.Description)

	switch {
	case normalized.Name == "":
		return UpdateParams{}, fmt.Errorf("api key name is required")
	case !normalized.ScopeMode.Valid():
		return UpdateParams{}, fmt.Errorf("api key scope mode is required")
	case !hasAnyPermission(normalized):
		return UpdateParams{}, fmt.Errorf("select at least one permission")
	}

	normalized.ProjectIDs, normalized.TagIDs = normalizedScopeIDs(normalized.ScopeMode, normalized.ProjectIDs, normalized.TagIDs)
	return normalized, nil
}

func normalizedScopeIDs(scopeMode db.ScopeMode, projectIDs []int64, tagIDs []int64) ([]int64, []int64) {
	switch {
	case scopeMode.UsesProjectRules():
		return dedupeIDs(projectIDs), nil
	case scopeMode.UsesTagRules():
		return nil, dedupeIDs(tagIDs)
	default:
		return nil, nil
	}
}

func dedupeIDs(values []int64) []int64 {
	seen := make(map[int64]struct{}, len(values))
	deduped := make([]int64, 0, len(values))

	for _, value := range values {
		if value <= 0 {
			continue
		}

		if _, ok := seen[value]; ok {
			continue
		}

		seen[value] = struct{}{}
		deduped = append(deduped, value)
	}

	return deduped
}

func hasAnyPermission(params interface {
	GetCanDownload() bool
	GetCanUpload() bool
	GetCanDelete() bool
	GetCanManageProjects() bool
}) bool {
	return params.GetCanDownload() || params.GetCanUpload() || params.GetCanDelete() || params.GetCanManageProjects()
}

func (p CreateParams) GetCanDownload() bool       { return p.CanDownload }
func (p CreateParams) GetCanUpload() bool         { return p.CanUpload }
func (p CreateParams) GetCanDelete() bool         { return p.CanDelete }
func (p CreateParams) GetCanManageProjects() bool { return p.CanManageProjects }

func (p UpdateParams) GetCanDownload() bool       { return p.CanDownload }
func (p UpdateParams) GetCanUpload() bool         { return p.CanUpload }
func (p UpdateParams) GetCanDelete() bool         { return p.CanDelete }
func (p UpdateParams) GetCanManageProjects() bool { return p.CanManageProjects }
