package usecase

import (
	"context"
	"errors"
	"encoding/json"
	"lune/talentscale/internal/domain"
	"time"
	"sort"
	"fmt"
	"math"

	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
)

type assessmentUsecase struct {
	repo  domain.AssessmentRepository
	redis *redis.Client
}

func NewAssessmentUsecase(repo domain.AssessmentRepository, rdb *redis.Client) domain.AssessmentUsecase {
	return &assessmentUsecase{repo: repo, redis: rdb}
}

func (u *assessmentUsecase) StartSession(ctx context.Context, candidateID uuid.UUID, testTypeID uuid.UUID) (*domain.AssessmentSession, error) {
	// Check if active session already exists
	sessions, err := u.repo.GetSessionsByCandidate(ctx, candidateID)
	if err == nil {
		for _, s := range sessions {
			if s.TestTypeID == testTypeID && (s.Status == "PENDING" || s.Status == "ON_PROGRESS") {
				return &s, nil
			}
		}
	}

	session := &domain.AssessmentSession{
		ID:          uuid.New(),
		CandidateID: candidateID,
		TestTypeID:  testTypeID,
		Status:      "PENDING",
		CreatedAt:   time.Now(),
		UpdatedAt:   time.Now(),
	}

	if err := u.repo.CreateSession(ctx, session); err != nil {
		return nil, err
	}
	return session, nil
}

func (u *assessmentUsecase) GetQuestions(ctx context.Context, sessionID uuid.UUID) ([]domain.Question, error) {
	session, err := u.repo.GetSessionByID(ctx, sessionID)
	if err != nil {
		return nil, errors.New("session not found")
	}

	// Update status to ON_PROGRESS if it was PENDING
	if session.Status == "PENDING" {
		if err := u.repo.UpdateSessionStatus(ctx, sessionID, "ON_PROGRESS"); err != nil {
			return nil, err
		}
	}

	return u.repo.GetQuestionsByTestType(ctx, session.TestTypeID)
}

func (u *assessmentUsecase) SubmitAnswer(ctx context.Context, answer *domain.CandidateAnswer) error {
	session, err := u.repo.GetSessionByID(ctx, answer.SessionID)
	if err != nil {
		return errors.New("session not found")
	}
	if session.Status != "ON_PROGRESS" {
		return errors.New("session is not active")
	}

	answer.ID = uuid.New()
	answer.AnsweredAt = time.Now()
	
	// Scoring Logic
	q, err := u.repo.GetQuestionByID(ctx, answer.QuestionID)
	if err == nil {
		// 🧮 1. DISC Scoring
		if q.QuestionType == "disc" || q.QuestionType == "most_least_choice" {
			var discAnswer struct {
				Plus  string `json:"plus"`
				Minus string `json:"minus"`
			}
			if err := json.Unmarshal([]byte(answer.Answer), &discAnswer); err == nil {
				totalScore := 0.0
				for _, item := range q.Items {
					metaData, _ := json.Marshal(item.Meta)
					var meta struct {
						Label string  `json:"label"`
						Trait string  `json:"trait"`
						Rank  float64 `json:"rank"`
					}
					json.Unmarshal(metaData, &meta)

					if meta.Label == discAnswer.Plus {
						totalScore += meta.Rank
					} else if meta.Label == discAnswer.Minus {
						totalScore -= meta.Rank
					}
				}
				answer.Score = totalScore
				return u.repo.SaveAnswer(ctx, answer)
			}
		}

		// 🧮 2. MBTI (Likert Scale) Scoring
		if q.QuestionType == "mbti" {
			var val float64
			if err := json.Unmarshal([]byte(answer.Answer), &val); err == nil {
				answer.Score = val // Save raw scale (-2 to +2)
				return u.repo.SaveAnswer(ctx, answer)
			}
		}

		// 🧮 3. Essay (No auto-score)
		if q.QuestionType == "essay" {
			answer.Score = 0
			return u.repo.SaveAnswer(ctx, answer)
		}

		// 🧮 4. MCQ / Pattern Matrix Scoring
		var key struct {
			CorrectIndexes []int `json:"correct_indexes"`
		}
		keyData, _ := json.Marshal(q.AnswerKey)
		json.Unmarshal(keyData, &key)

		var userAnswers []int
		json.Unmarshal([]byte(answer.Answer), &userAnswers)

		isCorrect := false
		if len(userAnswers) == len(key.CorrectIndexes) && len(key.CorrectIndexes) > 0 {
			sort.Ints(userAnswers)
			sort.Ints(key.CorrectIndexes)
			isCorrect = true
			for i := range userAnswers {
				if userAnswers[i] != key.CorrectIndexes[i] {
					isCorrect = false
					break
				}
			}
		}

		answer.IsCorrect = &isCorrect
		if isCorrect {
			answer.Score = q.ScoringWeight
		} else {
			answer.Score = 0
		}
	}
	
	return u.repo.SaveAnswer(ctx, answer)
}

func (u *assessmentUsecase) CompleteSession(ctx context.Context, sessionID uuid.UUID) error {
	session, err := u.repo.GetSessionByID(ctx, sessionID)
	if err != nil {
		return errors.New("session not found")
	}
	if session.Status == "COMPLETED" {
		return nil
	}

	return u.repo.UpdateSessionStatus(ctx, sessionID, "COMPLETED")
}

func (u *assessmentUsecase) GetDISCResults(ctx context.Context, sessionID uuid.UUID) (any, error) {
	// ⚡ REDIS CACHE CHECK
	cacheKey := fmt.Sprintf("disc_results:%s", sessionID.String())
	if val, err := u.redis.Get(ctx, cacheKey).Result(); err == nil {
		var cachedResult any
		if err := json.Unmarshal([]byte(val), &cachedResult); err == nil {
			return cachedResult, nil
		}
	}

	session, err := u.repo.GetSessionByID(ctx, sessionID)
	if err != nil {
		return nil, err
	}

	answers, err := u.repo.GetAnswersBySession(ctx, sessionID)
	if err != nil {
		return nil, err
	}

	questions, err := u.repo.GetQuestionsByTestType(ctx, session.TestTypeID)
	if err != nil {
		return nil, err
	}

	questionMap := make(map[uuid.UUID]domain.Question)
	for _, q := range questions {
		questionMap[q.ID] = q
	}

	// 3. Aggregate Scoring
	most := map[string]float64{"D": 0, "I": 0, "S": 0, "C": 0}
	least := map[string]float64{"D": 0, "I": 0, "S": 0, "C": 0}

	for _, ans := range answers {
		q, ok := questionMap[ans.QuestionID]
		if !ok || (q.QuestionType != "disc" && q.QuestionType != "most_least_choice") {
			continue
		}

		var discAnswer struct {
			Plus  string `json:"plus"`
			Minus string `json:"minus"`
		}
		if err := json.Unmarshal([]byte(ans.Answer), &discAnswer); err != nil {
			continue
		}

		for _, item := range q.Items {
			metaData, _ := json.Marshal(item.Meta)
			var meta struct {
				Label string  `json:"label"`
				Trait string  `json:"trait"`
				Rank  float64 `json:"rank"`
			}
			json.Unmarshal(metaData, &meta)

			if meta.Label == discAnswer.Plus {
				most[meta.Trait] += meta.Rank
			}
			if meta.Label == discAnswer.Minus {
				least[meta.Trait] += meta.Rank
			}
		}
	}

	change := make(map[string]float64)
	for _, trait := range []string{"D", "I", "S", "C"} {
		change[trait] = most[trait] - least[trait]
	}

	result := map[string]any{
		"most":   most,
		"least":  least,
		"change": change,
		"summary": map[string]string{
			"dominant_trait": getDominantTrait(most),
			"personality":    getPersonalityDescription(getDominantTrait(most)),
		},
	}

	resJSON, _ := json.Marshal(result)
	u.redis.Set(ctx, cacheKey, resJSON, 1*time.Hour)

	return result, nil
}

func (u *assessmentUsecase) GetMBTIResults(ctx context.Context, sessionID uuid.UUID) (any, error) {
	cacheKey := fmt.Sprintf("mbti_results:%s", sessionID.String())
	if val, err := u.redis.Get(ctx, cacheKey).Result(); err == nil {
		var cachedResult any
		json.Unmarshal([]byte(val), &cachedResult)
		return cachedResult, nil
	}

	session, err := u.repo.GetSessionByID(ctx, sessionID)
	if err != nil {
		return nil, err
	}

	answers, err := u.repo.GetAnswersBySession(ctx, sessionID)
	if err != nil {
		return nil, err
	}

	questions, err := u.repo.GetQuestionsByTestType(ctx, session.TestTypeID)
	if err != nil {
		return nil, err
	}

	questionMap := make(map[uuid.UUID]domain.Question)
	for _, q := range questions {
		questionMap[q.ID] = q
	}

	dimScores := map[string]float64{"EI": 0, "SN": 0, "TF": 0, "JP": 0}
	dimCounts := map[string]int{"EI": 0, "SN": 0, "TF": 0, "JP": 0}

	for _, ans := range answers {
		q, ok := questionMap[ans.QuestionID]
		if !ok || q.QuestionType != "mbti" {
			continue
		}

		metaData, _ := json.Marshal(q.Config)
		var meta struct {
			Dimension string `json:"dimension"`
		}
		json.Unmarshal(metaData, &meta)

		if meta.Dimension != "" {
			dimScores[meta.Dimension] += ans.Score
			dimCounts[meta.Dimension]++
		}
	}

	dimensions := []struct {
		Key   string
		Left  string
		Right string
	}{
		{"EI", "I", "E"},
		{"SN", "S", "N"},
		{"TF", "T", "F"},
		{"JP", "J", "P"},
	}

	mbtiType := ""
	details := make(map[string]any)

	for _, d := range dimensions {
		score := dimScores[d.Key]
		trait := ""
		percentage := 0.0

		if score <= 0 {
			trait = d.Left
			maxPossible := float64(dimCounts[d.Key] * 2)
			if maxPossible > 0 {
				percentage = (math.Abs(score) / maxPossible) * 100
			}
		} else {
			trait = d.Right
			maxPossible := float64(dimCounts[d.Key] * 2)
			if maxPossible > 0 {
				percentage = (score / maxPossible) * 100
			}
		}
		
		mbtiType += trait
		details[d.Key] = map[string]any{
			"trait":      trait,
			"score":      score,
			"percentage": math.Round(percentage),
			"label":      getMBTILabel(trait),
		}
	}

	result := map[string]any{
		"type":    mbtiType,
		"details": details,
		"summary": getMBTIProfile(mbtiType),
	}

	resJSON, _ := json.Marshal(result)
	u.redis.Set(ctx, cacheKey, resJSON, 1*time.Hour)

	return result, nil
}

func getDominantTrait(scores map[string]float64) string {
	max := -1.0
	trait := ""
	for t, s := range scores {
		if s > max {
			max = s
			trait = t
		}
	}
	return trait
}

func getPersonalityDescription(trait string) string {
	descriptions := map[string]string{
		"D": "Dominance: Result-oriented, firm, and decisive.",
		"I": "Influence: Social, outgoing, and persuasive.",
		"S": "Steadiness: Calm, supportive, and reliable.",
		"C": "Compliance: Analytical, systematic, and accurate.",
	}
	return descriptions[trait]
}

func getMBTILabel(trait string) string {
	labels := map[string]string{
		"E": "Extraversion", "I": "Introversion",
		"S": "Sensing", "N": "Intuition",
		"T": "Thinking", "F": "Feeling",
		"J": "Judging", "P": "Perceiving",
	}
	return labels[trait]
}

func getMBTIProfile(mbtiType string) string {
	profiles := map[string]string{
		"INTJ": "Architect", "INTP": "Logician", "ENTJ": "Commander", "ENTP": "Debater",
		"INFJ": "Advocate", "INFP": "Mediator", "ENFJ": "Protagonist", "ENFP": "Campaigner",
		"ISTJ": "Logistician", "ISFJ": "Defender", "ESTJ": "Executive", "ESFJ": "Consul",
		"ISTP": "Virtuoso", "ISFP": "Adventurer", "ESTP": "Entrepreneur", "ESFP": "Entertainer",
	}
	if p, ok := profiles[mbtiType]; ok {
		return p
	}
	return "Kepribadian MBTI."
}
