package main

import (
	"boostai-backend/internal/config"
	"boostai-backend/internal/database"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"math"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
)

var historicalAssignmentIDs = map[int64]bool{
	3001: true,
	3002: true,
	3003: true,
	3004: true,
	3005: true,
}

type assignmentRecord struct {
	ID           int64  `json:"id"`
	Name         string `json:"name"`
	Status       string `json:"status"`
	MaximumMarks int    `json:"maximum_marks"`
}

type assigneeRecord struct {
	ID              int64    `json:"id"`
	AssignmentID    int64    `json:"assignment_id"`
	StudentID       int64    `json:"student_id"`
	Status          string   `json:"status"`
	OverallScore    *float64 `json:"overall_score"`
	AiFeedback      *string  `json:"ai_feedback"`
	NextStepOutcome *string  `json:"next_step_outcome"`
}

type assignmentQuestionRecord struct {
	ID             int64 `json:"id"`
	AssignmentID   int64 `json:"assignment_id"`
	QuestionBankID int64 `json:"question_bank_id"`
}

type studentAnswerRecord struct {
	AssigneeID                int64    `json:"assignee_id"`
	AssignmentQuestionID      int64    `json:"assignment_question_id"`
	AnswerLatex               string   `json:"answer_latex"`
	AiReasoning               string   `json:"ai_reasoning"`
	AiFeedback                *string  `json:"ai_feedback"`
	SolveMode                 *string  `json:"solve_mode"`
	UnderSolveMode            *string  `json:"_solve_mode"`
	IsCorrect                 *bool    `json:"is_correct"`
	UnderIsCorrect            *bool    `json:"_is_correct"`
	MisconceptionTag          *string  `json:"_misconception_tag"`
	QuestionTopic             *string  `json:"_question_topic"`
	ReviewNeedsAttention      *bool    `json:"review_needs_attention"`
	ReviewIssueReason         *string  `json:"review_issue_reason"`
	ReviewUnderstandingScore  *float64 `json:"review_understanding_score"`
	ReviewCorrectnessScore    *float64 `json:"review_correctness_score"`
	ReviewQuestionScore       *float64 `json:"review_question_score"`
	ReviewConfidence          *float64 `json:"review_confidence"`
	ReviewTags                []string `json:"review_tags"`
	CreatedAt                 int64    `json:"created_at"`
	AnsweredAt                *int64   `json:"_answered_at"`
}

type questionReviewUpdate struct {
	AssignmentID            int64
	StudentID               int64
	QuestionID              int64
	IsCorrect               bool
	AIFeedback              string
	NeedsAttention          bool
	IssueReason             string
	CorrectnessScore        float64
	UnderstandingScore      float64
	QuestionScore           float64
	Confidence              float64
	ReviewTags              []string
	Topic                   string
	QuestionContribution    float64
	AnsweredAt              time.Time
	HasAnsweredAt           bool
}

type assigneeSummary struct {
	AssignmentID      int64
	StudentID         int64
	AssignmentName    string
	OverallScore      *float64
	AIFeedback        string
	NextStepOutcome   string
	QuestionUpdates   []questionReviewUpdate
	CorrectCount      int
	NeedsAttentionCnt int
}

func main() {
	cfg := config.Load()
	db, err := database.NewPostgres(cfg.DatabaseURL)
	if err != nil {
		log.Fatalf("failed to connect to database: %v", err)
	}
	defer db.Close()

	mockDataDir, err := resolveMockDataDir()
	if err != nil {
		log.Fatalf("failed to resolve mock data directory: %v", err)
	}

	assignments, err := readJSON[[]assignmentRecord](filepath.Join(mockDataDir, "assignments.json"))
	if err != nil {
		log.Fatalf("failed to read assignments.json: %v", err)
	}
	assignees, err := readJSON[[]assigneeRecord](filepath.Join(mockDataDir, "assignment_assignees.json"))
	if err != nil {
		log.Fatalf("failed to read assignment_assignees.json: %v", err)
	}
	assignmentQuestions, err := readJSON[[]assignmentQuestionRecord](filepath.Join(mockDataDir, "assignment_questions.json"))
	if err != nil {
		log.Fatalf("failed to read assignment_questions.json: %v", err)
	}
	studentAnswers, err := readJSON[[]studentAnswerRecord](filepath.Join(mockDataDir, "student_answers.json"))
	if err != nil {
		log.Fatalf("failed to read student_answers.json: %v", err)
	}

	assignmentByID := map[int64]assignmentRecord{}
	for _, item := range assignments {
		assignmentByID[item.ID] = item
	}

	assigneeByID := map[int64]assigneeRecord{}
	for _, item := range assignees {
		assigneeByID[item.ID] = item
	}

	questionIDByAssignmentQuestionID := map[int64]int64{}
	for _, item := range assignmentQuestions {
		questionIDByAssignmentQuestionID[item.ID] = item.QuestionBankID
	}

	summaries := map[string]*assigneeSummary{}
	for _, row := range studentAnswers {
		assignee, ok := assigneeByID[row.AssigneeID]
		if !ok || !historicalAssignmentIDs[assignee.AssignmentID] {
			continue
		}
		questionID, ok := questionIDByAssignmentQuestionID[row.AssignmentQuestionID]
		if !ok {
			continue
		}
		assignment := assignmentByID[assignee.AssignmentID]
		key := fmt.Sprintf("%d:%d", assignee.AssignmentID, assignee.StudentID)
		summary := summaries[key]
		if summary == nil {
			summary = &assigneeSummary{
				AssignmentID:   assignee.AssignmentID,
				StudentID:      assignee.StudentID,
				AssignmentName: assignment.Name,
			}
			summaries[key] = summary
		}

		update := buildQuestionReviewUpdate(assignee, questionID, row)
		summary.QuestionUpdates = append(summary.QuestionUpdates, update)
		if update.IsCorrect {
			summary.CorrectCount++
		}
		if update.NeedsAttention {
			summary.NeedsAttentionCnt++
		}
	}

	for _, summary := range summaries {
		finalizeAssigneeSummary(summary)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	tx, err := db.Pool.Begin(ctx)
	if err != nil {
		log.Fatalf("failed to begin transaction: %v", err)
	}
	defer tx.Rollback(ctx)

	updatedAnswers := 0
	updatedAssignees := 0
	for _, summary := range summaries {
		for _, update := range summary.QuestionUpdates {
			_, err := tx.Exec(ctx, `
				UPDATE student_answers
				SET is_correct = $4,
				    ai_feedback = $5,
				    review_needs_attention = $6,
				    review_issue_reason = $7,
				    review_correctness_score = $8,
				    review_understanding_score = $9,
				    review_question_score = $10,
				    review_confidence = $11,
				    review_tags = $12,
				    updated_at = NOW()
				WHERE assignment_id = $1
				  AND student_id = $2
				  AND question_id = $3
			`,
				update.AssignmentID,
				update.StudentID,
				update.QuestionID,
				update.IsCorrect,
				nullableString(update.AIFeedback),
				update.NeedsAttention,
				nullableString(update.IssueReason),
				update.CorrectnessScore,
				update.UnderstandingScore,
				update.QuestionScore,
				update.Confidence,
				update.ReviewTags,
			)
			if err != nil {
				log.Fatalf("failed to update student answer (%d/%d/%d): %v", update.AssignmentID, update.StudentID, update.QuestionID, err)
			}
			updatedAnswers++
		}

		var overall any
		var passStatus string
		if summary.OverallScore == nil {
			passStatus = "pending"
			overall = nil
		} else {
			overall = *summary.OverallScore
			if *summary.OverallScore >= 6.0 {
				passStatus = "pass"
			} else {
				passStatus = "no_pass"
			}
		}

		_, err := tx.Exec(ctx, `
			UPDATE assignment_assignees
			SET overall_score = $3,
			    ai_feedback = $4,
			    pass_status = $5,
			    pass_status_override = NULL
			WHERE assignment_id = $1
			  AND student_id = $2
		`, summary.AssignmentID, summary.StudentID, overall, nullableString(summary.AIFeedback), passStatus)
		if err != nil {
			log.Fatalf("failed to update assignment assignee (%d/%d): %v", summary.AssignmentID, summary.StudentID, err)
		}
		updatedAssignees++
	}

	if err := tx.Commit(ctx); err != nil {
		log.Fatalf("failed to commit transaction: %v", err)
	}

	log.Printf("historical review backfill complete: %d answers updated, %d assignees updated", updatedAnswers, updatedAssignees)
}

func buildQuestionReviewUpdate(assignee assigneeRecord, questionID int64, row studentAnswerRecord) questionReviewUpdate {
	isCorrect := false
	if row.IsCorrect != nil {
		isCorrect = *row.IsCorrect
	} else if row.UnderIsCorrect != nil {
		isCorrect = *row.UnderIsCorrect
	} else {
		isCorrect = !strings.HasPrefix(strings.ToLower(strings.TrimSpace(row.AiReasoning)), "incorrect")
	}

	solveMode := firstNonEmptyString(row.SolveMode, row.UnderSolveMode)
	understanding := valueOrElse(row.ReviewUnderstandingScore, deriveUnderstandingScore(isCorrect, solveMode))
	confidence := valueOrElse(row.ReviewConfidence, deriveConfidenceScore(isCorrect, solveMode))
	needsAttention := boolOrElse(row.ReviewNeedsAttention, !isCorrect || understanding < 0.72)
	issueReason := stringOrElse(row.ReviewIssueReason, deriveIssueReason(row.AiReasoning, row.MisconceptionTag))
	aiFeedback := stringOrElse(row.AiFeedback, row.AiReasoning)
	reviewTags := sanitizeTags(row.ReviewTags, row.MisconceptionTag)
	correctness := valueOrElse(row.ReviewCorrectnessScore, 1.0)
	questionScore := valueOrElse(row.ReviewQuestionScore, 1.0)
	questionContribution := ((boolToFloat(isCorrect)) + understanding) / 2

	var answeredAt time.Time
	var hasAnsweredAt bool
	if row.AnsweredAt != nil && *row.AnsweredAt > 0 {
		answeredAt = time.UnixMilli(*row.AnsweredAt).UTC()
		hasAnsweredAt = true
	} else if row.CreatedAt > 0 {
		answeredAt = time.UnixMilli(row.CreatedAt).UTC()
		hasAnsweredAt = true
	}

	return questionReviewUpdate{
		AssignmentID:         assignee.AssignmentID,
		StudentID:            assignee.StudentID,
		QuestionID:           questionID,
		IsCorrect:            isCorrect,
		AIFeedback:           aiFeedback,
		NeedsAttention:       needsAttention,
		IssueReason:          issueReason,
		CorrectnessScore:     roundToThree(correctness),
		UnderstandingScore:   roundToThree(understanding),
		QuestionScore:        roundToThree(questionScore),
		Confidence:           roundToThree(confidence),
		ReviewTags:           reviewTags,
		Topic:                stringOrElse(row.QuestionTopic, "general"),
		QuestionContribution: questionContribution,
		AnsweredAt:           answeredAt,
		HasAnsweredAt:        hasAnsweredAt,
	}
}

func finalizeAssigneeSummary(summary *assigneeSummary) {
	if len(summary.QuestionUpdates) == 0 {
		return
	}
	var total float64
	topicScores := map[string][]float64{}
	for _, item := range summary.QuestionUpdates {
		total += item.QuestionContribution
		topicScores[item.Topic] = append(topicScores[item.Topic], item.QuestionContribution)
	}
	overall := roundToTwo(total / float64(len(summary.QuestionUpdates)) * 10)
	summary.OverallScore = &overall

	type topicAvg struct {
		name string
		avg  float64
	}
	var weakest []topicAvg
	for topic, scores := range topicScores {
		var subtotal float64
		for _, score := range scores {
			subtotal += score
		}
		weakest = append(weakest, topicAvg{name: topic, avg: subtotal / float64(len(scores))})
	}
	if len(weakest) > 1 {
		sort.Slice(weakest, func(i, j int) bool {
			if weakest[i].avg == weakest[j].avg {
				return weakest[i].name < weakest[j].name
			}
			return weakest[i].avg < weakest[j].avg
		})
	}
	weakestTopics := []string{}
	for i, item := range weakest {
		if i >= 2 {
			break
		}
		weakestTopics = append(weakestTopics, displayTopic(item.name))
	}
	weakestTopicText := "general fluency"
	if len(weakestTopics) > 0 {
		weakestTopicText = strings.Join(weakestTopics, ", ")
	}
	summary.NextStepOutcome = "accept"
	if overall < 4.5 {
		summary.NextStepOutcome = "redo"
	} else if overall < 6.0 {
		summary.NextStepOutcome = "support"
	}
	summary.AIFeedback = fmt.Sprintf(
		"Student completed %s with %d/%d correct responses. Overall score is %.2f/10. The weakest areas were %s. %d question(s) need extra attention.",
		summary.AssignmentName,
		summary.CorrectCount,
		len(summary.QuestionUpdates),
		overall,
		weakestTopicText,
		summary.NeedsAttentionCnt,
	)
}

func deriveUnderstandingScore(isCorrect bool, solveMode string) float64 {
	if isCorrect {
		return map[string]float64{
			"step_by_step": 0.95,
			"handwritten": 0.85,
			"just_answer": 0.75,
			"solve_together": 0.65,
		}[defaultSolveMode(solveMode)]
	}
	return map[string]float64{
		"step_by_step": 0.40,
		"handwritten": 0.32,
		"just_answer": 0.20,
		"solve_together": 0.28,
	}[defaultSolveMode(solveMode)]
}

func deriveConfidenceScore(isCorrect bool, solveMode string) float64 {
	if isCorrect {
		return map[string]float64{
			"step_by_step": 0.82,
			"handwritten": 0.78,
			"just_answer": 0.90,
			"solve_together": 0.62,
		}[defaultSolveMode(solveMode)]
	}
	return map[string]float64{
		"step_by_step": 0.55,
		"handwritten": 0.60,
		"just_answer": 0.72,
		"solve_together": 0.50,
	}[defaultSolveMode(solveMode)]
}

func deriveIssueReason(aiReasoning string, misconception *string) string {
	if misconception != nil && strings.TrimSpace(*misconception) != "" {
		switch strings.TrimSpace(*misconception) {
		case "add_tops_add_bottoms":
			return "The student added the numerator and denominator directly instead of finding a common denominator."
		case "fraction_op_confusion":
			return "The student confused the fraction operation and did not apply the correct method."
		case "fraction_general_uncertainty":
			return "The student shows insecure understanding of equivalent or comparable fractions."
		case "place_value_misalignment":
			return "The student misread place value, causing digits to be aligned incorrectly."
		case "arithmetic_slip":
			return "The final answer is wrong, suggesting a careless arithmetic slip rather than a secure method."
		case "scaffolding_dependence":
			return "The student appears dependent on scaffolding and does not show secure independent understanding."
		case "word_problem_interpretation":
			return "The student did not translate the word problem into the correct calculation."
		default:
			return strings.TrimSpace(*misconception)
		}
	}
	text := strings.TrimSpace(aiReasoning)
	if text == "" {
		return "The answer shows incomplete understanding of the method."
	}
	return text
}

func defaultSolveMode(value string) string {
	value = strings.TrimSpace(value)
	if value == "" {
		return "just_answer"
	}
	return value
}

func displayTopic(value string) string {
	value = strings.ReplaceAll(strings.TrimSpace(value), "_", " ")
	parts := strings.Fields(value)
	for i, part := range parts {
		parts[i] = strings.ToUpper(part[:1]) + part[1:]
	}
	return strings.Join(parts, " ")
}

func boolToFloat(value bool) float64 {
	if value {
		return 1
	}
	return 0
}

func roundToTwo(value float64) float64 {
	return math.Round(value*100) / 100
}

func roundToThree(value float64) float64 {
	return math.Round(value*1000) / 1000
}

func readJSON[T any](path string) (T, error) {
	var zero T
	data, err := os.ReadFile(path)
	if err != nil {
		return zero, err
	}
	var value T
	if err := json.Unmarshal(data, &value); err != nil {
		return zero, err
	}
	return value, nil
}

func resolveMockDataDir() (string, error) {
	if value := strings.TrimSpace(os.Getenv("MOCK_DATA_DIR")); value != "" {
		return value, nil
	}
	candidates := []string{
		filepath.Join("..", "Mock-Data"),
		filepath.Join(".", "Mock-Data"),
		filepath.Join("..", "..", "Mock-Data"),
	}
	for _, candidate := range candidates {
		if info, err := os.Stat(candidate); err == nil && info.IsDir() {
			return candidate, nil
		}
	}
	return "", errors.New("Mock-Data directory not found; set MOCK_DATA_DIR")
}

func valueOrElse(value *float64, fallback float64) float64 {
	if value != nil {
		return *value
	}
	return fallback
}

func boolOrElse(value *bool, fallback bool) bool {
	if value != nil {
		return *value
	}
	return fallback
}

func stringOrElse(value *string, fallback string) string {
	if value != nil && strings.TrimSpace(*value) != "" {
		return strings.TrimSpace(*value)
	}
	return strings.TrimSpace(fallback)
}

func firstNonEmptyString(values ...*string) string {
	for _, value := range values {
		if value != nil && strings.TrimSpace(*value) != "" {
			return strings.TrimSpace(*value)
		}
	}
	return ""
}

func sanitizeTags(tags []string, misconception *string) []string {
	seen := map[string]bool{}
	result := make([]string, 0, len(tags)+1)
	for _, tag := range tags {
		tag = strings.TrimSpace(tag)
		if tag == "" || seen[tag] {
			continue
		}
		seen[tag] = true
		result = append(result, tag)
	}
	if misconception != nil {
		tag := strings.TrimSpace(*misconception)
		if tag != "" && !seen[tag] {
			result = append(result, tag)
		}
	}
	return result
}

func nullableString(value string) any {
	if strings.TrimSpace(value) == "" {
		return nil
	}
	return strings.TrimSpace(value)
}
