package sso

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/redis/go-redis/v9"

	"gitlab.com/mbugroup/lti-api.git/internal/cache"
	"gitlab.com/mbugroup/lti-api.git/internal/config"
	"gitlab.com/mbugroup/lti-api.git/internal/utils"
)

const (
	profileCachePrefix = "sso:profile:user:"
	profileCacheTTL    = time.Minute
)

var (
	profileClient = &http.Client{Timeout: 5 * time.Second}

	profileLocalCache sync.Map // map[string]cachedProfile
)

type cachedProfile struct {
	Profile   *UserProfile
	ExpiresAt time.Time
}

// UserProfile represents the enriched user information returned by the central SSO.
type UserProfile struct {
	UserID      uint
	Roles       []Role
	Permissions []Permission
	AreaIDs     []uint
	LocationIDs []uint
	AllArea     bool
	AllLocation bool
}

// Role describes a role assignment from the SSO profile response.
type Role struct {
	ID           uint
	Key          string
	Name         string
	ClientID     uint
	ClientAlias  string
	ClientName   string
	Permissions  []Permission
	RawReference json.RawMessage `json:"-"`
}

// Permission describes a granular permission entry from the SSO profile.
type Permission struct {
	ID          uint
	Name        string
	Action      string
	ClientID    uint
	ClientAlias string
	ClientName  string
}

// PermissionNames returns a de-duplicated slice of permission identifiers in canonical form.
func (p *UserProfile) PermissionNames() []string {
	if p == nil || len(p.Permissions) == 0 {
		return nil
	}
	set := make(map[string]struct{}, len(p.Permissions))
	for _, perm := range p.Permissions {
		name := canonicalPermissionName(perm.Name)
		if name != "" {
			set[name] = struct{}{}
		}
	}
	out := make([]string, 0, len(set))
	for name := range set {
		out = append(out, name)
	}
	return out
}

// FetchProfile retrieves the SSO profile for the authenticated user, using Redis/in-memory
// caching to reduce load on the SSO service. Only end-user tokens (subject user:ID) are supported.
func FetchProfile(ctx context.Context, token string, verification *VerificationResult) (*UserProfile, error) {
	if verification == nil || verification.UserID == 0 {
		return nil, errors.New("profile only available for user tokens")
	}
	key := profileCacheKey(verification.UserID)

	if profile := loadProfileFromLocalCache(key); profile != nil {
		return profile, nil
	}

	if profile := loadProfileFromRedis(ctx, key); profile != nil {
		storeProfileInLocalCache(key, profile)
		return profile, nil
	}

	profile, err := fetchProfileFromSSO(ctx, token)
	if err != nil {
		return nil, err
	}

	storeProfileInLocalCache(key, profile)
	storeProfileInRedis(ctx, key, profile)
	return profile, nil
}

func fetchProfileFromSSO(ctx context.Context, token string) (*UserProfile, error) {
	endpoint := strings.TrimSpace(config.SSOGetMeURL)
	if endpoint == "" {
		return nil, errors.New("sso get-me endpoint not configured")
	}

	if ctx == nil {
		ctx = context.Background()
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, fmt.Errorf("build profile request: %w", err)
	}
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Authorization", "Bearer "+token)
	if cookieName := strings.TrimSpace(config.SSOAccessCookieName); cookieName != "" {
		req.Header.Set("Cookie", fmt.Sprintf("%s=%s", cookieName, token))
	}

	resp, err := profileClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("fetch profile: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 400 {
		return nil, fmt.Errorf("fetch profile: status %d", resp.StatusCode)
	}

	var envelope userInfoEnvelope
	if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
		return nil, fmt.Errorf("decode profile: %w", err)
	}

	roles := envelope.getRoles()
	areaIDs := envelope.getAreaIDs()
	locationIDs := envelope.getLocationIDs()
	allArea := envelope.getAllArea()
	allLocation := envelope.getAllLocation()
	profile := &UserProfile{}

	// Attempt to infer user id if provided.
	if envelope.User != nil && envelope.User.ID > 0 {
		profile.UserID = uint(envelope.User.ID)
	}

	perms := make([]Permission, 0)
	convertedRoles := make([]Role, 0, len(roles))
	for _, r := range roles {
		role := Role{
			ID:          uint(r.ID),
			Key:         strings.TrimSpace(r.Key),
			Name:        strings.TrimSpace(r.Name),
			ClientAlias: strings.TrimSpace(r.Client.Alias),
			ClientName:  strings.TrimSpace(r.Client.Name),
			ClientID:    uint(r.Client.ID),
		}
		rolePerms := make([]Permission, 0, len(r.Permissions))
		for _, p := range r.Permissions {
			perm := Permission{
				ID:          uint(p.ID),
				Name:        strings.TrimSpace(p.Name),
				Action:      strings.TrimSpace(p.Action),
				ClientAlias: strings.TrimSpace(p.Client.Alias),
				ClientName:  strings.TrimSpace(p.Client.Name),
				ClientID:    uint(p.Client.ID),
			}
			if perm.Name != "" {
				rolePerms = append(rolePerms, perm)
				perms = append(perms, perm)
			}
		}
		role.Permissions = rolePerms
		convertedRoles = append(convertedRoles, role)
	}
	profile.Roles = convertedRoles
	profile.Permissions = perms
	profile.AreaIDs = areaIDs
	profile.LocationIDs = locationIDs
	profile.AllArea = allArea
	profile.AllLocation = allLocation

	return profile, nil
}

func loadProfileFromLocalCache(key string) *UserProfile {
	if value, ok := profileLocalCache.Load(key); ok {
		if cached, ok := value.(cachedProfile); ok {
			if time.Now().Before(cached.ExpiresAt) && cached.Profile != nil {
				return cached.Profile
			}
			profileLocalCache.Delete(key)
		}
	}
	return nil
}

func loadProfileFromRedis(ctx context.Context, key string) *UserProfile {
	client := cache.Redis()
	if client == nil {
		return nil
	}

	data, err := client.Get(ctx, key).Bytes()
	if err != nil {
		if !errors.Is(err, redis.Nil) {
			utils.Log.WithError(err).Warn("sso profile redis lookup failed")
		}
		return nil
	}

	var profile UserProfile
	if err := json.Unmarshal(data, &profile); err != nil {
		utils.Log.WithError(err).Warn("sso profile redis decode failed")
		return nil
	}

	return &profile
}

func storeProfileInLocalCache(key string, profile *UserProfile) {
	if profile == nil {
		return
	}
	profileLocalCache.Store(key, cachedProfile{
		Profile:   profile,
		ExpiresAt: time.Now().Add(profileCacheTTL),
	})
}

func storeProfileInRedis(ctx context.Context, key string, profile *UserProfile) {
	client := cache.Redis()
	if client == nil || profile == nil {
		return
	}

	data, err := json.Marshal(profile)
	if err != nil {
		utils.Log.WithError(err).Warn("sso profile redis encode failed")
		return
	}

	if err := client.Set(ctx, key, data, profileCacheTTL).Err(); err != nil {
		utils.Log.WithError(err).Warn("sso profile redis store failed")
	}
}

func profileCacheKey(userID uint) string {
	return profileCachePrefix + strconv.FormatUint(uint64(userID), 10)
}

// InvalidateProfileCache clears cached profile data for the given user in both local and Redis caches.
func InvalidateProfileCache(ctx context.Context, userID uint) {
	if userID == 0 {
		return
	}
	key := profileCacheKey(userID)
	profileLocalCache.Delete(key)

	client := cache.Redis()
	if client == nil {
		return
	}
	if ctx == nil {
		ctx = context.Background()
	}
	if err := client.Del(ctx, key).Err(); err != nil && !errors.Is(err, redis.Nil) {
		utils.Log.WithError(err).Warn("sso profile redis delete failed")
	}
}

func canonicalPermissionName(name string) string {
	return strings.ToLower(strings.TrimSpace(name))
}

// userInfoEnvelope handles the varying shapes returned by the SSO userinfo endpoint.
type userInfoEnvelope struct {
	Roles       []userInfoRole `json:"roles"`
	AreaIDs     []uint         `json:"area_ids"`
	LocationIDs []uint         `json:"location_ids"`
	AllArea     bool           `json:"all_area"`
	AllLocation bool           `json:"all_location"`
	Data        *struct {
		ID          int64          `json:"id"`
		Roles       []userInfoRole `json:"roles"`
		AreaIDs     []uint         `json:"area_ids"`
		LocationIDs []uint         `json:"location_ids"`
		AllArea     bool           `json:"all_area"`
		AllLocation bool           `json:"all_location"`
	} `json:"data"`
	User *struct {
		ID int64 `json:"id"`
	} `json:"user"`
}

func (e *userInfoEnvelope) getRoles() []userInfoRole {
	if len(e.Roles) > 0 {
		return e.Roles
	}
	if e.Data != nil && len(e.Data.Roles) > 0 {
		if e.User == nil && e.Data.ID > 0 {
			e.User = &struct {
				ID int64 `json:"id"`
			}{ID: e.Data.ID}
		}
		return e.Data.Roles
	}
	return nil
}

func (e *userInfoEnvelope) getAreaIDs() []uint {
	if len(e.AreaIDs) > 0 {
		return e.AreaIDs
	}
	if e.Data != nil && len(e.Data.AreaIDs) > 0 {
		return e.Data.AreaIDs
	}
	return nil
}

func (e *userInfoEnvelope) getLocationIDs() []uint {
	if len(e.LocationIDs) > 0 {
		return e.LocationIDs
	}
	if e.Data != nil && len(e.Data.LocationIDs) > 0 {
		return e.Data.LocationIDs
	}
	return nil
}

func (e *userInfoEnvelope) getAllArea() bool {
	if e.AllArea {
		return true
	}
	if e.Data != nil && e.Data.AllArea {
		return true
	}
	return false
}

func (e *userInfoEnvelope) getAllLocation() bool {
	if e.AllLocation {
		return true
	}
	if e.Data != nil && e.Data.AllLocation {
		return true
	}
	return false
}

type userInfoRole struct {
	ID          int64             `json:"id"`
	Key         string            `json:"key"`
	Name        string            `json:"name"`
	Client      userInfoClient    `json:"client"`
	Permissions []userInfoPermRaw `json:"permissions"`
}

type userInfoClient struct {
	ID    int64  `json:"id"`
	Name  string `json:"name"`
	Alias string `json:"alias"`
}

type userInfoPermRaw struct {
	ID      int64          `json:"id"`
	Name    string         `json:"name"`
	Action  string         `json:"action"`
	Client  userInfoClient `json:"client"`
	Details any            `json:"details"`
}
