package seeddata

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"boostai-backend/internal/database"
	sharedapi "boostai-backend/internal/handlers/api/shared"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"golang.org/x/crypto/bcrypt"
)

const SeededPassword = "password123"

type studentRecord struct {
	ID        int64  `json:"id"`
	FullName  string `json:"fullname"`
	Email     string `json:"email"`
	Role      string `json:"role"`
	Active    bool   `json:"active"`
	IsDeleted bool   `json:"is_deleted"`
	CreatedAt int64  `json:"created_at"`
	UpdatedAt int64  `json:"updated_at"`
}

type classroomFile struct {
	Classroom          classroomRecord          `json:"classroom"`
	Tutor              tutorRecord              `json:"tutor"`
	ClassroomStudentRs []classroomStudentRecord `json:"classroom_student_rs"`
}

type classroomRecord struct {
	ID         int64  `json:"id"`
	Name       string `json:"name"`
	TutorID    int64  `json:"tutor_id"`
	InviteCode string `json:"invite_code"`
	CreatedAt  int64  `json:"created_at"`
	UpdatedAt  int64  `json:"updated_at"`
}

type tutorRecord struct {
	ID        int64  `json:"id"`
	FullName  string `json:"fullname"`
	Email     string `json:"email"`
	Role      string `json:"role"`
	Active    bool   `json:"active"`
	IsDeleted bool   `json:"is_deleted"`
	CreatedAt int64  `json:"created_at"`
	UpdatedAt int64  `json:"updated_at"`
}

type classroomStudentRecord struct {
	ClassroomID int64 `json:"classroom_id"`
	StudentID   int64 `json:"student_id"`
	CreatedAt   int64 `json:"created_at"`
}

type questionRecord struct {
	ID            int64   `json:"id"`
	Topic         string  `json:"topic"`
	SubTopic      *string `json:"sub_topic"`
	Tag           *string `json:"tag"`
	Difficulty    string  `json:"difficulty"`
	QuestionText  string  `json:"question_text"`
	CorrectAnswer string  `json:"correct_answer"`
	Source        string  `json:"source"`
	TeacherID     int64   `json:"teacher_id"`
	IsDeleted     bool    `json:"is_deleted"`
	CreatedAt     int64   `json:"created_at"`
	UpdatedAt     int64   `json:"updated_at"`
}

type assignmentRecord struct {
	ID        int64  `json:"id"`
	Name      string `json:"name"`
	TeacherID int64  `json:"teacher_id"`
	Topic     string `json:"topic"`
	DueDate   int64  `json:"due_date"`
	Status    string `json:"status"`
	IsDeleted bool   `json:"is_deleted"`
	CreatedAt int64  `json:"created_at"`
	UpdatedAt int64  `json:"updated_at"`
}

type assignmentQuestionRecord struct {
	ID             int64 `json:"id"`
	AssignmentID   int64 `json:"assignment_id"`
	QuestionBankID int64 `json:"question_bank_id"`
	QuestionOrder  int32 `json:"question_order"`
	CreatedAt      int64 `json:"created_at"`
}

type assignmentAssigneeRecord struct {
	ID              int64    `json:"id"`
	AssignmentID    int64    `json:"assignment_id"`
	StudentID       int64    `json:"student_id"`
	Status          string   `json:"status"`
	StartedAt       *int64   `json:"started_at"`
	SubmittedAt     *int64   `json:"submitted_at"`
	OverallScore    *float64 `json:"overall_score"`
	AIFeedback      *string  `json:"ai_feedback"`
	NextStepOutcome *string  `json:"next_step_outcome"`
	IsActive        bool     `json:"is_active"`
	CreatedAt       int64    `json:"created_at"`
}

type studentAnswerRecord struct {
	ID                       int64    `json:"id"`
	AssigneeID               int64    `json:"assignee_id"`
	AssignmentQuestionID     int64    `json:"assignment_question_id"`
	AnswerLatex              *string  `json:"answer_latex"`
	ExtractedAnswer          *string  `json:"extracted_answer"`
	SolveMode                *string  `json:"solve_mode"`
	WorkingSteps             *string  `json:"working_steps"`
	AIReasoning              *string  `json:"ai_reasoning"`
	IsCorrect                *bool    `json:"is_correct"`
	AIFeedback               *string  `json:"ai_feedback"`
	ReviewNeedsAttention     *bool    `json:"review_needs_attention"`
	ReviewIssueReason        *string  `json:"review_issue_reason"`
	ReviewCorrectnessScore   *float64 `json:"review_correctness_score"`
	ReviewUnderstandingScore *float64 `json:"review_understanding_score"`
	ReviewQuestionScore      *float64 `json:"review_question_score"`
	ReviewConfidence         *float64 `json:"review_confidence"`
	ReviewTags               []string `json:"review_tags"`
	GradingStatus            string   `json:"grading_status"`
	IsActive                 bool     `json:"is_active"`
	CreatedAt                int64    `json:"created_at"`
	AnsweredAt               *int64   `json:"_answered_at"`
	UnderSolveMode           *string  `json:"_solve_mode"`
	UnderIsCorrect           *bool    `json:"_is_correct"`
	UnderMisconceptionTag    *string  `json:"_misconception_tag"`
}

type assignmentQuestionRef struct {
	AssignmentID int64
	QuestionID   int64
	Position     int32
}

const DefaultMockDataDir = "../Mock-Data"

type Summary struct {
	Users           int    `json:"users"`
	Classrooms      int    `json:"classrooms"`
	Questions       int    `json:"questions"`
	Tags            int    `json:"tags"`
	Assignments     int    `json:"assignments"`
	AssignmentLinks int    `json:"assignment_links"`
	StudentAnswers  int    `json:"student_answers"`
	MockDataDir     string `json:"mock_data_dir"`
}

func Run(ctx context.Context, db *database.DB, mockDataDir string) (Summary, error) {
	if strings.TrimSpace(mockDataDir) == "" {
		mockDataDir = filepath.Clean(filepath.Join("..", "Mock-Data"))
	}

	var (
		students            []studentRecord
		classroomPayload    classroomFile
		questions           []questionRecord
		assignments         []assignmentRecord
		assignmentQuestions []assignmentQuestionRecord
		assignmentAssignees []assignmentAssigneeRecord
		studentAnswers      []studentAnswerRecord
	)

	if err := loadJSON(filepath.Join(mockDataDir, "students.json"), &students); err != nil {
		return Summary{}, err
	}
	if err := loadJSON(filepath.Join(mockDataDir, "classroom.json"), &classroomPayload); err != nil {
		return Summary{}, err
	}
	if err := loadJSON(filepath.Join(mockDataDir, "question_bank.json"), &questions); err != nil {
		return Summary{}, err
	}
	if err := loadJSON(filepath.Join(mockDataDir, "assignments.json"), &assignments); err != nil {
		return Summary{}, err
	}
	if err := loadJSON(filepath.Join(mockDataDir, "assignment_questions.json"), &assignmentQuestions); err != nil {
		return Summary{}, err
	}
	if err := loadJSON(filepath.Join(mockDataDir, "assignment_assignees.json"), &assignmentAssignees); err != nil {
		return Summary{}, err
	}
	if err := loadJSON(filepath.Join(mockDataDir, "student_answers.json"), &studentAnswers); err != nil {
		return Summary{}, err
	}

	if err := db.Migrate(); err != nil {
		return Summary{}, fmt.Errorf("migrate database: %w", err)
	}

	tx, err := db.Pool.Begin(ctx)
	if err != nil {
		return Summary{}, fmt.Errorf("begin transaction: %w", err)
	}
	defer tx.Rollback(ctx)

	if err := resetSeedData(ctx, tx); err != nil {
		return Summary{}, fmt.Errorf("reset data: %w", err)
	}

	if err := seedUsers(ctx, tx, classroomPayload.Tutor, students); err != nil {
		return Summary{}, fmt.Errorf("seed users: %w", err)
	}

	if err := seedClassroom(ctx, tx, classroomPayload); err != nil {
		return Summary{}, fmt.Errorf("seed classroom: %w", err)
	}

	tagIDs, err := seedQuestionsAndTags(ctx, tx, questions)
	if err != nil {
		return Summary{}, fmt.Errorf("seed questions: %w", err)
	}

	if err := seedAssignments(ctx, tx, classroomPayload.Classroom.ID, assignments); err != nil {
		return Summary{}, fmt.Errorf("seed assignments: %w", err)
	}

	if err := seedAssignmentAssignees(ctx, tx, assignmentAssignees); err != nil {
		return Summary{}, fmt.Errorf("seed assignment assignees: %w", err)
	}

	assignmentQuestionMap, err := seedAssignmentQuestions(ctx, tx, assignmentQuestions)
	if err != nil {
		return Summary{}, fmt.Errorf("seed assignment questions: %w", err)
	}

	if err := seedStudentAnswers(ctx, tx, assignmentAssignees, assignmentQuestionMap, studentAnswers); err != nil {
		return Summary{}, fmt.Errorf("seed student answers: %w", err)
	}

	if err := seedMessages(ctx, tx, classroomPayload.Tutor, students); err != nil {
		return Summary{}, fmt.Errorf("seed messages: %w", err)
	}

	if err := syncSequences(ctx, tx); err != nil {
		return Summary{}, fmt.Errorf("sync sequences: %w", err)
	}

	if err := tx.Commit(ctx); err != nil {
		return Summary{}, fmt.Errorf("commit seed transaction: %w", err)
	}

	return Summary{
		Users:           len(students) + 1,
		Classrooms:      1,
		Questions:       len(questions),
		Tags:            len(tagIDs),
		Assignments:     len(assignments),
		AssignmentLinks: len(assignmentQuestions),
		StudentAnswers:  len(studentAnswers),
		MockDataDir:     mockDataDir,
	}, nil
}

func loadJSON(path string, target any) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read %s: %w", path, err)
	}
	if err := json.Unmarshal(data, target); err != nil {
		return fmt.Errorf("parse %s: %w", path, err)
	}
	return nil
}

func resetSeedData(ctx context.Context, tx pgx.Tx) error {
	_, err := tx.Exec(ctx, `
TRUNCATE TABLE
	messages,
	message_thread_participants,
	message_threads,
	assignment_student_questions,
	student_answers,
	assignment_questions,
	assignment_assignees,
	assignments,
	question_tags,
	tags,
	questions,
	classroom_students,
	classrooms,
	users
RESTART IDENTITY CASCADE`)
	return err
}

func seedUsers(ctx context.Context, tx pgx.Tx, tutor tutorRecord, students []studentRecord) error {
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(SeededPassword), bcrypt.DefaultCost)
	if err != nil {
		return err
	}

	if err := insertUser(ctx, tx, tutor.ID, tutor.Email, string(hashedPassword), "teacher", tutor.FullName, tutor.Active && !tutor.IsDeleted, tutor.CreatedAt, tutor.UpdatedAt); err != nil {
		return err
	}

	for _, student := range students {
		if student.IsDeleted {
			continue
		}
		if err := insertUser(ctx, tx, student.ID, student.Email, string(hashedPassword), "student", student.FullName, student.Active && !student.IsDeleted, student.CreatedAt, student.UpdatedAt); err != nil {
			return err
		}
	}

	return nil
}

func seedMessages(ctx context.Context, tx pgx.Tx, tutor tutorRecord, students []studentRecord) error {
	activeStudents := make([]studentRecord, 0, len(students))
	for _, student := range students {
		if student.IsDeleted || !student.Active {
			continue
		}
		activeStudents = append(activeStudents, student)
	}

	if len(activeStudents) == 0 {
		return nil
	}

	threadSeedCount := len(activeStudents)
	if threadSeedCount > 3 {
		threadSeedCount = 3
	}

	now := time.Now().UTC()
	for idx := 0; idx < threadSeedCount; idx++ {
		student := activeStudents[idx]
		threadCreatedAt := now.Add(-time.Duration(threadSeedCount-idx) * 6 * time.Hour)
		subject := fmt.Sprintf("Study check-in for %s", firstName(student.FullName))

		var threadID int64
		if err := tx.QueryRow(ctx, `
INSERT INTO message_threads (created_by_user_id, subject, created_at, updated_at)
VALUES ($1, $2, $3, $3)
RETURNING id`, tutor.ID, subject, threadCreatedAt).Scan(&threadID); err != nil {
			return err
		}

		teacherBody := fmt.Sprintf("Hi %s, send me a quick update when you finish today's maths block. If one question feels sticky, tell me which one and I'll help.", firstName(student.FullName))
		studentBody := fmt.Sprintf("Thanks %s — I started the assignment set and I'm feeling better about the fraction questions now.", firstName(tutor.FullName))

		messageTimes := []time.Time{threadCreatedAt.Add(12 * time.Minute), threadCreatedAt.Add(54 * time.Minute)}
		if _, err := tx.Exec(ctx, `
INSERT INTO messages (thread_id, sender_user_id, body, created_at, updated_at)
VALUES ($1, $2, $3, $4, $4), ($1, $5, $6, $7, $7)`, threadID, tutor.ID, teacherBody, messageTimes[0], student.ID, studentBody, messageTimes[1]); err != nil {
			return err
		}

		teacherReadAt := pgtype.Timestamptz{Time: messageTimes[1], Valid: true}
		studentReadAt := pgtype.Timestamptz{}
		if idx == 0 {
			studentReadAt = pgtype.Timestamptz{Time: messageTimes[1], Valid: true}
		}

		if _, err := tx.Exec(ctx, `
INSERT INTO message_thread_participants (thread_id, user_id, joined_at, last_read_at)
VALUES ($1, $2, $3, $4), ($1, $5, $3, $6)`, threadID, tutor.ID, threadCreatedAt, teacherReadAt, student.ID, studentReadAt); err != nil {
			return err
		}

		if _, err := tx.Exec(ctx, `UPDATE message_threads SET updated_at = $2 WHERE id = $1`, threadID, messageTimes[1]); err != nil {
			return err
		}
	}

	return nil
}

func insertUser(ctx context.Context, tx pgx.Tx, id int64, email, passwordHash, role, fullName string, active bool, createdAtMs, updatedAtMs int64) error {
	_, err := tx.Exec(ctx, `
INSERT INTO users (id, email, password_hash, role, full_name, is_active, created_at, updated_at)
VALUES ($1, $2, $3, $4::user_role, $5, $6, $7, $8)`,
		id,
		email,
		passwordHash,
		role,
		fullName,
		active,
		msToTime(createdAtMs),
		msToTime(updatedAtMs),
	)
	return err
}

func seedClassroom(ctx context.Context, tx pgx.Tx, payload classroomFile) error {
	_, err := tx.Exec(ctx, `
INSERT INTO classrooms (id, teacher_id, name, code, description, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
		payload.Classroom.ID,
		payload.Classroom.TutorID,
		payload.Classroom.Name,
		sharedapi.NullableText(optionalString(payload.Classroom.InviteCode)),
		sharedapi.NullableText(classroomDescription(payload.Classroom.Name)),
		msToTime(payload.Classroom.CreatedAt),
		msToTime(payload.Classroom.UpdatedAt),
	)
	if err != nil {
		return err
	}

	for _, relation := range payload.ClassroomStudentRs {
		if _, err := tx.Exec(ctx, `
INSERT INTO classroom_students (classroom_id, student_id, joined_at)
VALUES ($1, $2, $3)`, relation.ClassroomID, relation.StudentID, msToTime(relation.CreatedAt)); err != nil {
			return err
		}
	}

	return nil
}

func seedQuestionsAndTags(ctx context.Context, tx pgx.Tx, questions []questionRecord) ([]int64, error) {
	tagSet := map[string]struct{}{}
	for _, question := range questions {
		if question.Tag != nil {
			tag := strings.TrimSpace(*question.Tag)
			if tag != "" {
				tagSet[tag] = struct{}{}
			}
		}
	}

	tagNames := make([]string, 0, len(tagSet))
	for tag := range tagSet {
		tagNames = append(tagNames, tag)
	}
	sort.Strings(tagNames)

	tagIDByName := make(map[string]int64, len(tagNames))
	tagIDs := make([]int64, 0, len(tagNames))
	for _, tagName := range tagNames {
		var tagID int64
		if err := tx.QueryRow(ctx, `
INSERT INTO tags (name)
VALUES ($1)
RETURNING id`, tagName).Scan(&tagID); err != nil {
			return nil, err
		}
		tagIDByName[tagName] = tagID
		tagIDs = append(tagIDs, tagID)
	}

	for _, question := range questions {
		if err := insertQuestion(ctx, tx, question); err != nil {
			return nil, err
		}
		if question.Tag == nil {
			continue
		}
		tagName := strings.TrimSpace(*question.Tag)
		if tagName == "" {
			continue
		}
		tagID := tagIDByName[tagName]
		if _, err := tx.Exec(ctx, `INSERT INTO question_tags (question_id, tag_id) VALUES ($1, $2)`, question.ID, tagID); err != nil {
			return nil, err
		}
	}

	return tagIDs, nil
}

func insertQuestion(ctx context.Context, tx pgx.Tx, record questionRecord) error {
	status := "published"
	if record.IsDeleted {
		status = "archived"
	}

	_, err := tx.Exec(ctx, `
INSERT INTO questions (id, author_teacher_id, title, prompt, topic, subject, difficulty, source, correct_answer, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5::question_topic, $6, $7::question_difficulty, $8, $9, $10::question_status, $11, $12)`,
		record.ID,
		record.TeacherID,
		questionTitle(record.QuestionText),
		record.QuestionText,
		nullableTopic(record.Topic),
		nullableSubject(record.SubTopic, record.Topic),
		nullableDifficulty(record.Difficulty),
		nullableSource(record.Source),
		sharedapi.NullableText(&record.CorrectAnswer),
		status,
		msToTime(record.CreatedAt),
		msToTime(record.UpdatedAt),
	)
	return err
}

func seedAssignments(ctx context.Context, tx pgx.Tx, classroomID int64, assignments []assignmentRecord) error {
	for _, assignment := range assignments {
		if assignment.IsDeleted {
			continue
		}

		assignmentStatus := normalizeAssignmentStatus(assignment.Status)
		publishedAt := optionalPublishedAt(assignmentStatus, assignment.CreatedAt, assignment.UpdatedAt)

		_, err := tx.Exec(ctx, `
INSERT INTO assignments (id, teacher_id, classroom_id, title, instructions, due_at, published_at, pass_threshold, status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::assignment_status, $10, $11)`,
			assignment.ID,
			assignment.TeacherID,
			classroomID,
			assignment.Name,
			sharedapi.NullableText(optionalString(assignmentInstructions(assignment.Topic))),
			optionalDueAt(assignment.DueDate),
			publishedAt,
			requiredNumeric(6.0),
			assignmentStatus,
			msToTime(assignment.CreatedAt),
			msToTime(assignment.UpdatedAt),
		)
		if err != nil {
			return err
		}
	}

	return nil
}

func seedAssignmentQuestions(ctx context.Context, tx pgx.Tx, rows []assignmentQuestionRecord) (map[int64]assignmentQuestionRef, error) {
	assignmentQuestionMap := make(map[int64]assignmentQuestionRef, len(rows))
	for _, row := range rows {
		if _, err := tx.Exec(ctx, `
INSERT INTO assignment_questions (assignment_id, question_id, position)
VALUES ($1, $2, $3)
ON CONFLICT (assignment_id, question_id) DO UPDATE
SET position = EXCLUDED.position`, row.AssignmentID, row.QuestionBankID, row.QuestionOrder); err != nil {
			return nil, err
		}
		assignmentQuestionMap[row.ID] = assignmentQuestionRef{
			AssignmentID: row.AssignmentID,
			QuestionID:   row.QuestionBankID,
			Position:     row.QuestionOrder,
		}
	}
	return assignmentQuestionMap, nil
}

func seedAssignmentAssignees(ctx context.Context, tx pgx.Tx, rows []assignmentAssigneeRecord) error {
	for _, row := range rows {
		assignedAt := firstValidMs(row.StartedAt, row.SubmittedAt, row.CreatedAt)
		_, err := tx.Exec(ctx, `
INSERT INTO assignment_assignees (assignment_id, student_id, assigned_at, ai_feedback, overall_score, pass_threshold, pass_status, next_step_outcome)
VALUES ($1, $2, $3, $4, $5, $6, $7::assignment_pass_status, $8::assignment_next_step_outcome)`,
			row.AssignmentID,
			row.StudentID,
			assignedAt,
			sharedapi.NullableText(row.AIFeedback),
			optionalNumeric(row.OverallScore),
			requiredNumeric(6.0),
			normalizePassStatus(row.OverallScore, 6.0),
			normalizeNextStepOutcome(row.NextStepOutcome),
		)
		if err != nil {
			return err
		}
	}
	return nil
}

func seedStudentAnswers(ctx context.Context, tx pgx.Tx, assignees []assignmentAssigneeRecord, assignmentQuestionMap map[int64]assignmentQuestionRef, answers []studentAnswerRecord) error {
	type assigneeRef struct {
		AssignmentID int64
		StudentID    int64
	}

	assigneeAssignment := make(map[int64]assigneeRef, len(assignees))
	for _, row := range assignees {
		assigneeAssignment[row.ID] = assigneeRef{AssignmentID: row.AssignmentID, StudentID: row.StudentID}
	}

	for _, answer := range answers {
		questionRef, ok := assignmentQuestionMap[answer.AssignmentQuestionID]
		if !ok {
			return fmt.Errorf("missing assignment question mapping for %d", answer.AssignmentQuestionID)
		}

		assigneeRef, ok := assigneeAssignment[answer.AssigneeID]
		if !ok {
			return fmt.Errorf("missing assignee mapping for %d", answer.AssigneeID)
		}

		if assigneeRef.AssignmentID != questionRef.AssignmentID {
			return fmt.Errorf("assignment mismatch for assignee %d and assignment question %d", answer.AssigneeID, answer.AssignmentQuestionID)
		}

		answerText := firstNonEmpty(answer.ExtractedAnswer, answer.AnswerLatex)
		answerStatus := normalizeAnswerStatus(answer.GradingStatus)
		solveMode := normalizeSolveMode(firstNonEmpty(answer.SolveMode, answer.UnderSolveMode))
		reviewedAt := optionalReviewedAt(answerStatus, answer.CreatedAt)

		_, err := tx.Exec(ctx, `
INSERT INTO student_answers (
	id,
	assignment_id,
	question_id,
	student_id,
	answer_text,
	ai_feedback,
	teacher_feedback,
	status,
	submitted_at,
	reviewed_at,
	created_at,
	updated_at,
	solve_mode,
	working_steps,
	is_correct,
	review_needs_attention,
	review_issue_reason,
	review_correctness_score,
	review_understanding_score,
	review_question_score,
	review_confidence,
	review_tags
 ) VALUES (
	$1, $2, $3, $4, $5, $6, $7, $8::answer_status, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22
 )`,
			answer.ID,
			assigneeRef.AssignmentID,
			questionRef.QuestionID,
			assigneeRef.StudentID,
			sharedapi.NullableText(answerText),
			sharedapi.NullableText(answer.AIFeedback),
			nil,
			answerStatus,
			optionalMsToTime(answer.AnsweredAt),
			reviewedAt,
			msToTime(answer.CreatedAt),
			deriveUpdatedAt(answer.CreatedAt, questionRef.Position),
			solveMode,
			sharedapi.NullableText(answer.WorkingSteps),
			sharedapi.NullableBool(firstNonNilBool(answer.IsCorrect, answer.UnderIsCorrect)),
			boolOrDefault(answer.ReviewNeedsAttention, false),
			sharedapi.NullableText(answer.ReviewIssueReason),
			optionalNumeric(answer.ReviewCorrectnessScore),
			optionalNumeric(answer.ReviewUnderstandingScore),
			optionalNumeric(answer.ReviewQuestionScore),
			optionalNumeric(answer.ReviewConfidence),
			stringsArray(answer.ReviewTags),
		)
		if err != nil {
			return err
		}
	}

	return nil
}

func syncSequences(ctx context.Context, tx pgx.Tx) error {
	statements := []string{
		"SELECT setval('users_id_seq', COALESCE((SELECT MAX(id) FROM users), 1))",
		"SELECT setval('classrooms_id_seq', COALESCE((SELECT MAX(id) FROM classrooms), 1))",
		"SELECT setval('questions_id_seq', COALESCE((SELECT MAX(id) FROM questions), 1))",
		"SELECT setval('tags_id_seq', COALESCE((SELECT MAX(id) FROM tags), 1))",
		"SELECT setval('assignments_id_seq', COALESCE((SELECT MAX(id) FROM assignments), 1))",
		"SELECT setval('assignment_student_questions_id_seq', COALESCE((SELECT MAX(id) FROM assignment_student_questions), 1))",
		"SELECT setval('student_answers_id_seq', COALESCE((SELECT MAX(id) FROM student_answers), 1))",
		"SELECT setval('message_threads_id_seq', COALESCE((SELECT MAX(id) FROM message_threads), 1))",
		"SELECT setval('messages_id_seq', COALESCE((SELECT MAX(id) FROM messages), 1))",
	}

	for _, statement := range statements {
		if _, err := tx.Exec(ctx, statement); err != nil {
			return err
		}
	}

	return nil
}

func msToTime(value int64) time.Time {
	if value <= 0 {
		return time.Now().UTC()
	}
	return time.UnixMilli(value).UTC()
}

func optionalMsToTime(value *int64) pgtype.Timestamptz {
	if value == nil || *value <= 0 {
		return pgtype.Timestamptz{}
	}
	return pgtype.Timestamptz{Time: time.UnixMilli(*value).UTC(), Valid: true}
}

func optionalDueAt(value int64) pgtype.Timestamptz {
	if value <= 0 {
		return pgtype.Timestamptz{}
	}
	return pgtype.Timestamptz{Time: time.UnixMilli(value).UTC(), Valid: true}
}

func deriveUpdatedAt(createdAtMs int64, position int32) time.Time {
	created := msToTime(createdAtMs)
	if position <= 0 {
		return created
	}
	return created.Add(time.Duration(position) * time.Minute)
}

func normalizeAssignmentStatus(value string) string {
	switch strings.TrimSpace(strings.ToLower(value)) {
	case "published", "assigned", "open":
		return "assigned"
	case "closed", "complete", "completed":
		return "closed"
	default:
		return "draft"
	}
}

func normalizeAnswerStatus(value string) string {
	switch strings.TrimSpace(strings.ToLower(value)) {
	case "in_progress":
		return "in_progress"
	case "submitted":
		return "submitted"
	case "reviewed", "graded":
		return "reviewed"
	default:
		return "not_started"
	}
}

func normalizeSolveMode(primary *string) string {
	if primary == nil {
		return "just_answer"
	}
	switch strings.TrimSpace(strings.ToLower(*primary)) {
	case "just_answer", "mental":
		return "just_answer"
	case "step_by_step", "calculator":
		return "step_by_step"
	case "solve_together":
		return "solve_together"
	case "handwritten", "written":
		return "handwritten"
	default:
		return "handwritten"
	}
}

func nullableTopic(value string) any {
	return normalizeQuestionTopic(value)
}

func nullableSubject(subTopic *string, topic string) any {
	if subTopic != nil {
		if trimmed := strings.TrimSpace(*subTopic); trimmed != "" {
			return sharedapi.NullableText(&trimmed)
		}
	}
	trimmed := strings.TrimSpace(topic)
	if trimmed == "" {
		return nil
	}
	return sharedapi.NullableText(&trimmed)
}

func nullableDifficulty(value string) any {
	trimmed := strings.TrimSpace(strings.ToLower(value))
	if trimmed == "" {
		return nil
	}
	return trimmed
}

func nullableSource(value string) any {
	trimmed := strings.TrimSpace(strings.ToLower(value))
	if trimmed == "" {
		return nil
	}
	return trimmed
}

func assignmentInstructions(topic string) string {
	trimmed := strings.TrimSpace(topic)
	if trimmed == "" {
		return "Complete each question and show your reasoning where needed."
	}
	return fmt.Sprintf("Complete each %s question and show your reasoning where needed.", strings.ReplaceAll(trimmed, "_", " "))
}

func questionTitle(prompt string) string {
	trimmed := strings.TrimSpace(prompt)
	if trimmed == "" {
		return "Seeded question"
	}
	if len(trimmed) <= 60 {
		return trimmed
	}
	return trimmed[:57] + "..."
}

func firstName(fullName string) string {
	parts := strings.Fields(strings.TrimSpace(fullName))
	if len(parts) == 0 {
		return "there"
	}
	return parts[0]
}

func firstNonEmpty(values ...*string) *string {
	for _, value := range values {
		if value == nil {
			continue
		}
		trimmed := strings.TrimSpace(*value)
		if trimmed == "" {
			continue
		}
		copyValue := trimmed
		return &copyValue
	}
	return nil
}

func firstNonNilBool(values ...*bool) *bool {
	for _, value := range values {
		if value != nil {
			return value
		}
	}
	return nil
}

func optionalNumeric(value *float64) pgtype.Numeric {
	if value == nil {
		return pgtype.Numeric{}
	}
	numeric, err := sharedapi.NullableFloat64AsNumeric(value)
	if err != nil {
		return pgtype.Numeric{}
	}
	return numeric
}

func stringsArray(values []string) []string {
	if len(values) == 0 {
		return []string{}
	}
	out := make([]string, 0, len(values))
	for _, value := range values {
		trimmed := strings.TrimSpace(value)
		if trimmed == "" {
			continue
		}
		out = append(out, trimmed)
	}
	return out
}

func normalizeQuestionTopic(value string) any {
	trimmed := strings.TrimSpace(strings.ToLower(value))
	if trimmed == "" {
		return nil
	}
	mapped := map[string]string{
		"place value":      "place_value",
		"place_value":      "place_value",
		"arithmetic":       "arithmetic",
		"negative numbers": "negative_numbers",
		"negative_numbers": "negative_numbers",
		"bidmas":           "bidmas",
		"fractions":        "fractions",
		"algebra":          "algebra",
		"geometry":         "geometry",
		"data":             "data",
	}
	if normalized, ok := mapped[trimmed]; ok {
		return normalized
	}
	return nil
}

func normalizePassStatus(score *float64, threshold float64) string {
	if score == nil {
		return "pending"
	}
	if *score >= threshold {
		return "pass"
	}
	return "no_pass"
}

func normalizeNextStepOutcome(value *string) any {
	if value == nil {
		return nil
	}
	trimmed := strings.TrimSpace(strings.ToLower(*value))
	switch trimmed {
	case "redo", "accept", "support":
		return trimmed
	default:
		return nil
	}
}

func requiredNumeric(value float64) pgtype.Numeric {
	numeric, err := sharedapi.NullableFloat64AsNumeric(&value)
	if err != nil {
		return pgtype.Numeric{}
	}
	return numeric
}

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

func firstValidMs(values ...any) time.Time {
	for _, value := range values {
		switch typed := value.(type) {
		case *int64:
			if typed != nil && *typed > 0 {
				return time.UnixMilli(*typed).UTC()
			}
		case int64:
			if typed > 0 {
				return time.UnixMilli(typed).UTC()
			}
		}
	}
	return time.Now().UTC()
}

func optionalPublishedAt(status string, createdAtMs, updatedAtMs int64) pgtype.Timestamptz {
	if status != "assigned" && status != "closed" {
		return pgtype.Timestamptz{}
	}
	timestamp := createdAtMs
	if updatedAtMs > 0 {
		timestamp = updatedAtMs
	}
	return optionalDueAt(timestamp)
}

func optionalReviewedAt(status string, createdAtMs int64) pgtype.Timestamptz {
	if status != "reviewed" {
		return pgtype.Timestamptz{}
	}
	return pgtype.Timestamptz{Time: msToTime(createdAtMs), Valid: true}
}

func optionalString(value string) *string {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return nil
	}
	return &trimmed
}

func classroomDescription(name string) *string {
	trimmed := strings.TrimSpace(name)
	if trimmed == "" {
		return nil
	}
	description := fmt.Sprintf("Seeded classroom for %s", trimmed)
	return &description
}
