package service

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

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// APIKeyService handles API key business logic.
type APIKeyService struct {
	repo     port.APIKeyRepository
	adminKey string
}

// NewAPIKeyService creates a new API key service.
func NewAPIKeyService(repo port.APIKeyRepository, adminKey string) *APIKeyService {
	return &APIKeyService{
		repo:     repo,
		adminKey: adminKey,
	}
}

// CreateKeyRequest contains parameters for creating a new API key.
type CreateKeyRequest struct {
	Name       string
	Scopes     []domain.Scope
	ProjectIDs []domain.ProjectID
	AllowedIPs []string // CIDR notation; nil = no restriction
	ExpiresIn  time.Duration
	CreatedBy  string
}

// CreateKeyResult contains the newly created key and its secret.
type CreateKeyResult struct {
	Key    *domain.APIKey
	Secret string
}

// Create generates a new API key.
func (s *APIKeyService) Create(ctx context.Context, req CreateKeyRequest) (*CreateKeyResult, error) {
	// Generate key components using auth-compatible format:
	// identifier: 4 random bytes → 8 hex chars
	// random:     16 random bytes → 32 hex chars
	// Full key:   rdev_sk_<identifier>_<random>
	idBytes := make([]byte, 4)
	if _, err := rand.Read(idBytes); err != nil {
		return nil, fmt.Errorf("generate identifier: %w", err)
	}
	identifier := hex.EncodeToString(idBytes)

	randomBytes := make([]byte, 16)
	if _, err := rand.Read(randomBytes); err != nil {
		return nil, fmt.Errorf("generate random: %w", err)
	}
	random := hex.EncodeToString(randomBytes)

	fullKey := fmt.Sprintf("rdev_sk_%s_%s", identifier, random)

	// Hash the full key (what the user receives and sends back for auth)
	keyHash := hashKey(fullKey)

	// Calculate expiration
	var expiresAt *time.Time
	if req.ExpiresIn > 0 {
		t := time.Now().Add(req.ExpiresIn)
		expiresAt = &t
	}

	// Create key
	key := &domain.APIKey{
		Name:       req.Name,
		KeyPrefix:  identifier,
		Scopes:     req.Scopes,
		ProjectIDs: req.ProjectIDs,
		AllowedIPs: req.AllowedIPs,
		ExpiresAt:  expiresAt,
		CreatedBy:  req.CreatedBy,
	}

	if err := s.repo.Create(ctx, key, keyHash); err != nil {
		return nil, fmt.Errorf("store key: %w", err)
	}

	return &CreateKeyResult{
		Key:    key,
		Secret: fullKey,
	}, nil
}

// GetByHash retrieves an API key by its raw key value.
func (s *APIKeyService) GetByHash(ctx context.Context, rawKey string) (*domain.APIKey, error) {
	keyHash := hashKey(rawKey)
	return s.repo.GetByHash(ctx, keyHash)
}

// Get retrieves an API key by ID.
func (s *APIKeyService) Get(ctx context.Context, id domain.APIKeyID) (*domain.APIKey, error) {
	return s.repo.Get(ctx, id)
}

// List returns all API keys.
func (s *APIKeyService) List(ctx context.Context) ([]*domain.APIKey, error) {
	return s.repo.List(ctx)
}

// Revoke marks an API key as revoked.
func (s *APIKeyService) Revoke(ctx context.Context, id domain.APIKeyID) error {
	return s.repo.Revoke(ctx, id)
}

// UpdateLastUsed updates the last used timestamp for a key.
func (s *APIKeyService) UpdateLastUsed(ctx context.Context, id domain.APIKeyID) error {
	return s.repo.UpdateLastUsed(ctx, id)
}

// Validate checks a raw API key and returns the associated APIKey if valid.
// It checks for admin key, looks up by hash, and verifies the key is active.
// On success it asynchronously updates the last-used timestamp.
func (s *APIKeyService) Validate(ctx context.Context, rawKey string) (*domain.APIKey, error) {
	// Check admin key first
	if s.adminKey != "" && rawKey == s.adminKey {
		return &domain.APIKey{
			ID:        "admin",
			Name:      "Super Admin",
			KeyPrefix: "admin",
			Scopes:    []domain.Scope{domain.ScopeAdmin},
		}, nil
	}

	keyHash := hashKey(rawKey)

	apiKey, err := s.repo.GetByHash(ctx, keyHash)
	if err != nil {
		return nil, err
	}

	if apiKey.IsRevoked() {
		return nil, domain.ErrKeyRevoked
	}
	if apiKey.IsExpired() {
		return nil, domain.ErrKeyExpired
	}

	// Update last_used_at asynchronously (fire-and-forget: intentionally detached from
	// request context since this is a non-critical audit update that should not block
	// validation or be cancelled when request completes)
	go func() {
		_ = s.repo.UpdateLastUsed(context.Background(), apiKey.ID)
	}()

	return apiKey, nil
}

// ValidateAdminKey checks if the provided key matches the admin key.
func (s *APIKeyService) ValidateAdminKey(key string) bool {
	return s.adminKey != "" && key == s.adminKey
}

// AdminKey returns the admin key (for creating admin APIKey struct).
func (s *APIKeyService) AdminKey() string {
	return s.adminKey
}

// hashKey creates a SHA-256 hash of a key.
func hashKey(key string) string {
	hash := sha256.Sum256([]byte(key))
	return hex.EncodeToString(hash[:])
}

// ParseExpiration converts a duration string to time.Duration.
// Supported formats: "30d", "60d", "90d", "1y", "never" (or empty)
func ParseExpiration(s string) (time.Duration, error) {
	switch s {
	case "", "never":
		return 0, nil
	case "30d":
		return 30 * 24 * time.Hour, nil
	case "60d":
		return 60 * 24 * time.Hour, nil
	case "90d":
		return 90 * 24 * time.Hour, nil
	case "1y":
		return 365 * 24 * time.Hour, nil
	default:
		return 0, fmt.Errorf("invalid expiration format: %s", s)
	}
}
