package apikeys

import (
	"context"
	"crypto/rand"
	"encoding/base32"
	"errors"
	"fmt"
	"strings"
	"time"

	entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
	"gitlab.com/mbugroup/lti-api.git/internal/utils/secure"
	"gorm.io/gorm"
)

var (
	ErrInvalidAPIKey = errors.New("invalid api key")
	ErrInactiveKey   = errors.New("inactive api key")
)

type Principal struct {
	ID          uint
	Name        string
	Environment string
	Permissions []string
	AllArea     bool
	AreaIDs     []uint
	AllLocation bool
	LocationIDs []uint
}

type Authenticator interface {
	Authenticate(ctx context.Context, rawKey, source string) (*Principal, error)
}

type Service interface {
	Authenticator
	Create(ctx context.Context, input CreateInput) (*IssuedKey, error)
	List(ctx context.Context, environment string) ([]entity.IntegrationAPIKey, error)
	Revoke(ctx context.Context, environment, prefix string) error
}

type CreateInput struct {
	Name            string
	Environment     string
	PermissionCodes []string
	AllArea         bool
	AreaIDs         []uint
	AllLocation     bool
	LocationIDs     []uint
}

type IssuedKey struct {
	Key    string
	Record *entity.IntegrationAPIKey
}

type service struct {
	repo Repository
	now  func() time.Time
}

func NewService(db *gorm.DB) Service {
	return &service{
		repo: NewRepository(db),
		now:  time.Now,
	}
}

func (s *service) Authenticate(ctx context.Context, rawKey, source string) (*Principal, error) {
	environment, prefix, secret, err := parseRawKey(rawKey)
	if err != nil {
		return nil, ErrInvalidAPIKey
	}

	record, err := s.repo.GetByEnvironmentAndPrefix(ctx, environment, prefix)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, ErrInvalidAPIKey
		}
		return nil, err
	}

	if !strings.EqualFold(record.Status, entity.IntegrationAPIKeyStatusActive) || record.RevokedAt != nil {
		return nil, ErrInactiveKey
	}
	if !secure.Verify(record.KeyHash, secret) {
		return nil, ErrInvalidAPIKey
	}

	usedAt := s.now().UTC()
	if err := s.repo.TouchLastUsed(ctx, record.ID, usedAt, strings.TrimSpace(source)); err != nil {
		utils.Log.WithError(err).Warn("api key: failed to update last_used fields")
	}

	return &Principal{
		ID:          record.ID,
		Name:        record.Name,
		Environment: record.Environment,
		Permissions: canonicalPermissions(record.PermissionCodes),
		AllArea:     record.AllArea,
		AreaIDs:     uniqueUint(record.AreaIDs),
		AllLocation: record.AllLocation,
		LocationIDs: uniqueUint(record.LocationIDs),
	}, nil
}

func (s *service) Create(ctx context.Context, input CreateInput) (*IssuedKey, error) {
	name := strings.TrimSpace(input.Name)
	environment := strings.ToLower(strings.TrimSpace(input.Environment))
	if name == "" || environment == "" {
		return nil, fmt.Errorf("name and environment are required")
	}

	prefix, err := randomToken(10)
	if err != nil {
		return nil, err
	}
	secret, err := randomToken(24)
	if err != nil {
		return nil, err
	}

	hash, err := secure.Hash(secret, nil)
	if err != nil {
		return nil, err
	}

	record := &entity.IntegrationAPIKey{
		Name:            name,
		Environment:     environment,
		Status:          entity.IntegrationAPIKeyStatusActive,
		KeyPrefix:       prefix,
		KeyHash:         hash,
		PermissionCodes: canonicalPermissions(input.PermissionCodes),
		AllArea:         input.AllArea,
		AreaIDs:         uniqueUint(input.AreaIDs),
		AllLocation:     input.AllLocation,
		LocationIDs:     uniqueUint(input.LocationIDs),
	}

	if err := s.repo.Create(ctx, record); err != nil {
		return nil, err
	}

	return &IssuedKey{
		Key:    fmt.Sprintf("lti_%s_%s_%s", environment, prefix, secret),
		Record: record,
	}, nil
}

func (s *service) List(ctx context.Context, environment string) ([]entity.IntegrationAPIKey, error) {
	return s.repo.List(ctx, strings.ToLower(strings.TrimSpace(environment)))
}

func (s *service) Revoke(ctx context.Context, environment, prefix string) error {
	environment = strings.ToLower(strings.TrimSpace(environment))
	prefix = strings.TrimSpace(prefix)
	if environment == "" || prefix == "" {
		return fmt.Errorf("environment and prefix are required")
	}
	return s.repo.Revoke(ctx, environment, prefix, s.now().UTC())
}

func parseRawKey(rawKey string) (environment string, prefix string, secret string, err error) {
	rawKey = strings.TrimSpace(rawKey)
	parts := strings.Split(rawKey, "_")
	if len(parts) != 4 || parts[0] != "lti" {
		return "", "", "", ErrInvalidAPIKey
	}

	environment = strings.ToLower(strings.TrimSpace(parts[1]))
	prefix = strings.TrimSpace(parts[2])
	secret = strings.TrimSpace(parts[3])
	if environment == "" || prefix == "" || secret == "" {
		return "", "", "", ErrInvalidAPIKey
	}

	return environment, prefix, secret, nil
}

func randomToken(size int) (string, error) {
	buf := make([]byte, size)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}

	encoder := base32.StdEncoding.WithPadding(base32.NoPadding)
	return strings.ToLower(encoder.EncodeToString(buf)), nil
}

func canonicalPermissions(perms []string) []string {
	if len(perms) == 0 {
		return []string{}
	}

	seen := make(map[string]struct{}, len(perms))
	result := make([]string, 0, len(perms))
	for _, perm := range perms {
		perm = strings.ToLower(strings.TrimSpace(perm))
		if perm == "" {
			continue
		}
		if _, ok := seen[perm]; ok {
			continue
		}
		seen[perm] = struct{}{}
		result = append(result, perm)
	}
	return result
}

func uniqueUint(values []uint) []uint {
	if len(values) == 0 {
		return []uint{}
	}

	seen := make(map[uint]struct{}, len(values))
	result := make([]uint, 0, len(values))
	for _, value := range values {
		if value == 0 {
			continue
		}
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}
