// Path: Backend/internal/handlers/api/questions/handler.go

package questions

import (
	"boostai-backend/internal/handlers/api/shared"
	"boostai-backend/internal/http/params"
	"boostai-backend/internal/http/respond"
	authmw "boostai-backend/internal/middleware"
	"boostai-backend/internal/questiongen"
	"boostai-backend/internal/sqlc"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/jackc/pgx/v5"
)

type Handler struct {
	queries   *sqlc.Queries
	generator *questiongen.Service
}

type QuestionResponse struct {
	ID              int64      `json:"id"`
	AuthorTeacherID int64      `json:"author_teacher_id"`
	Title           string     `json:"title"`
	Prompt          string     `json:"prompt"`
	Topic           *string    `json:"topic,omitempty"`
	Subject         *string    `json:"subject,omitempty"`
	Difficulty      *string    `json:"difficulty,omitempty"`
	Source          *string    `json:"source,omitempty"`
	CorrectAnswer   *string    `json:"correct_answer,omitempty"`
	Status          string     `json:"status"`
	CreatedAt       *time.Time `json:"created_at,omitempty"`
	UpdatedAt       *time.Time `json:"updated_at,omitempty"`
}

type TagResponse struct {
	ID        int64      `json:"id"`
	Name      string     `json:"name"`
	CreatedAt *time.Time `json:"created_at,omitempty"`
}

type createQuestionRequest struct {
	AuthorTeacherID int64   `json:"author_teacher_id"`
	Title           string  `json:"title"`
	Prompt          string  `json:"prompt"`
	Topic           *string `json:"topic"`
	Subject         *string `json:"subject"`
	Difficulty      *string `json:"difficulty"`
	Source          *string `json:"source"`
	CorrectAnswer   *string `json:"correct_answer"`
	Status          string  `json:"status"`
}

type createTagRequest struct {
	Name string `json:"name"`
}

type attachTagToQuestionRequest struct {
	TagID int64 `json:"tag_id"`
}

type generateQuestionsRequest struct {
	Topic      string  `json:"topic"`
	Difficulty string  `json:"difficulty"`
	Count      int     `json:"count"`
	Seed       *int64  `json:"seed"`
	Status     *string `json:"status"`
	Source     *string `json:"source"`
}

type GeneratedQuestionResponse struct {
	Question       QuestionResponse `json:"question"`
	Tags           []string         `json:"tags"`
	WorkedSolution []string         `json:"worked_solution"`
}

type GenerateQuestionsResponse struct {
	Seed  int64                       `json:"seed"`
	Data  []GeneratedQuestionResponse `json:"data"`
	Count int                         `json:"count"`
}

func NewHandler(queries *sqlc.Queries, generator *questiongen.Service) *Handler {
	return &Handler{queries: queries, generator: generator}
}

func (h *Handler) ListQuestionsByTeacher(c *fiber.Ctx) error {
	teacherID, err := params.Int64PathParam(c, "teacherId")
	if err != nil {
		return err
	}

	ctx, cancel := shared.WithTimeout()
	defer cancel()

	questions, err := h.queries.ListQuestionsByTeacher(ctx, teacherID)
	if err != nil {
		return respond.DatabaseError(c, err)
	}

	items := make([]QuestionResponse, 0, len(questions))
	for _, question := range questions {
		items = append(items, mapQuestion(question))
	}

	return c.JSON(shared.ListResponse[QuestionResponse]{Data: items})
}

func (h *Handler) GetQuestionByID(c *fiber.Ctx) error {
	questionID, err := params.Int64PathParam(c, "questionId")
	if err != nil {
		return err
	}

	ctx, cancel := shared.WithTimeout()
	defer cancel()

	question, err := h.queries.GetQuestionByID(ctx, questionID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return respond.Error(c, fiber.StatusNotFound, "not_found", "Question not found")
		}
		return respond.DatabaseError(c, err)
	}

	return c.JSON(mapQuestion(question))
}

func (h *Handler) ListTags(c *fiber.Ctx) error {
	ctx, cancel := shared.WithTimeout()
	defer cancel()

	tags, err := h.queries.ListTags(ctx)
	if err != nil {
		return respond.DatabaseError(c, err)
	}

	items := make([]TagResponse, 0, len(tags))
	for _, tag := range tags {
		items = append(items, mapTag(tag))
	}

	return c.JSON(shared.ListResponse[TagResponse]{Data: items})
}

func (h *Handler) CreateQuestion(c *fiber.Ctx) error {
	var req createQuestionRequest
	if err := c.BodyParser(&req); err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
	}

	teacherID := authmw.CurrentUserID(c)
	if teacherID == 0 || strings.TrimSpace(req.Title) == "" || strings.TrimSpace(req.Prompt) == "" || strings.TrimSpace(req.Status) == "" {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "teacher authentication, title, prompt, and status are required")
	}

	ctx, cancel := shared.WithTimeout()
	defer cancel()

	topic, subject, err := parseQuestionTopic(req.Topic, req.Subject)
	if err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", err.Error())
	}

	difficulty, err := parseQuestionDifficulty(req.Difficulty)
	if err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", err.Error())
	}

	question, err := h.queries.CreateQuestion(ctx, sqlc.CreateQuestionParams{
		AuthorTeacherID: teacherID,
		Title:           strings.TrimSpace(req.Title),
		Prompt:          strings.TrimSpace(req.Prompt),
		Topic:           topic,
		Subject:         shared.NullableText(subject),
		Difficulty:      difficulty,
		Source:          shared.NullableText(req.Source),
		CorrectAnswer:   shared.NullableText(req.CorrectAnswer),
		Status:          sqlc.QuestionStatus(strings.TrimSpace(req.Status)),
	})
	if err != nil {
		return respond.DatabaseError(c, err)
	}

	return c.Status(fiber.StatusCreated).JSON(mapQuestion(question))
}

func (h *Handler) GenerateQuestions(c *fiber.Ctx) error {
	if h.generator == nil {
		return respond.Error(c, fiber.StatusServiceUnavailable, "generator_unavailable", "Question generator is not available")
	}

	var req generateQuestionsRequest
	if err := c.BodyParser(&req); err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
	}

	teacherID := authmw.CurrentUserID(c)
	if teacherID == 0 {
		return respond.Error(c, fiber.StatusUnauthorized, "unauthorized", "teacher authentication is required")
	}

	if strings.TrimSpace(req.Topic) == "" || strings.TrimSpace(req.Difficulty) == "" {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "topic and difficulty are required")
	}

	if req.Count < 1 || req.Count > 25 {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "count must be between 1 and 25")
	}

	topic, subject, err := parseQuestionTopic(&req.Topic, nil)
	if err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", err.Error())
	}
	if !topic.Valid {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "topic is required")
	}

	difficulty, err := parseQuestionDifficulty(&req.Difficulty)
	if err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", err.Error())
	}
	if !difficulty.Valid {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "difficulty is required")
	}

	status := sqlc.QuestionStatusDraft
	if req.Status != nil && strings.TrimSpace(*req.Status) != "" {
		normalizedStatus := sqlc.QuestionStatus(strings.ToLower(strings.TrimSpace(*req.Status)))
		switch normalizedStatus {
		case sqlc.QuestionStatusDraft, sqlc.QuestionStatusPublished, sqlc.QuestionStatusArchived:
			status = normalizedStatus
		default:
			return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "status must be draft, published, or archived")
		}
	}

	seed := int64(0)
	if req.Seed != nil {
		seed = *req.Seed
	}

	generated, usedSeed, err := h.generator.Generate(questiongen.GenerateParams{
		Topic:      topic.QuestionTopic,
		Difficulty: difficulty.QuestionDifficulty,
		Count:      req.Count,
		Seed:       seed,
	})
	if err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "generation_failed", err.Error())
	}

	ctx, cancel := shared.WithTimeout()
	defer cancel()

	source := shared.NullableText(req.Source)
	if !source.Valid {
		defaultSource := "rng_generated"
		source = shared.NullableText(&defaultSource)
	}

	responses := make([]GeneratedQuestionResponse, 0, len(generated))
	for index, item := range generated {
		title := strings.TrimSpace(item.Title)
		if title == "" {
			title = fmt.Sprintf("%s %s %d", questionTopicLabel(topic.QuestionTopic), strings.Title(string(difficulty.QuestionDifficulty)), index+1)
		}

		question, err := h.queries.CreateQuestion(ctx, sqlc.CreateQuestionParams{
			AuthorTeacherID: teacherID,
			Title:           title,
			Prompt:          strings.TrimSpace(item.Prompt),
			Topic:           topic,
			Subject:         shared.NullableText(subject),
			Difficulty:      difficulty,
			Source:          source,
			CorrectAnswer:   shared.NullableText(stringPointer(item.CorrectAnswer)),
			Status:          status,
		})
		if err != nil {
			return respond.DatabaseError(c, err)
		}

		for _, tagName := range item.Tags {
			tag, err := h.queries.CreateTag(ctx, tagName)
			if err != nil {
				return respond.DatabaseError(c, err)
			}
			if err := h.queries.AttachTagToQuestion(ctx, sqlc.AttachTagToQuestionParams{QuestionID: question.ID, TagID: tag.ID}); err != nil {
				return respond.DatabaseError(c, err)
			}
		}

		responses = append(responses, GeneratedQuestionResponse{
			Question:       mapQuestion(question),
			Tags:           item.Tags,
			WorkedSolution: item.WorkedSolution,
		})
	}

	return c.Status(fiber.StatusCreated).JSON(GenerateQuestionsResponse{
		Seed:  usedSeed,
		Data:  responses,
		Count: len(responses),
	})
}

func (h *Handler) CreateTag(c *fiber.Ctx) error {
	var req createTagRequest
	if err := c.BodyParser(&req); err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
	}

	if strings.TrimSpace(req.Name) == "" {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "name is required")
	}

	ctx, cancel := shared.WithTimeout()
	defer cancel()

	tag, err := h.queries.CreateTag(ctx, strings.TrimSpace(req.Name))
	if err != nil {
		return respond.DatabaseError(c, err)
	}

	return c.Status(fiber.StatusCreated).JSON(mapTag(tag))
}

func (h *Handler) AttachTagToQuestion(c *fiber.Ctx) error {
	questionID, err := params.Int64PathParam(c, "questionId")
	if err != nil {
		return err
	}

	var req attachTagToQuestionRequest
	if err := c.BodyParser(&req); err != nil {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "Unable to parse request body")
	}

	if req.TagID == 0 {
		return respond.Error(c, fiber.StatusBadRequest, "invalid_request", "tag_id is required")
	}

	ctx, cancel := shared.WithTimeout()
	defer cancel()

	err = h.queries.AttachTagToQuestion(ctx, sqlc.AttachTagToQuestionParams{
		QuestionID: questionID,
		TagID:      req.TagID,
	})
	if err != nil {
		return respond.DatabaseError(c, err)
	}

	return c.Status(fiber.StatusCreated).JSON(fiber.Map{
		"status":      "ok",
		"question_id": questionID,
		"tag_id":      req.TagID,
	})
}

func mapQuestion(question sqlc.Question) QuestionResponse {
	return QuestionResponse{
		ID:              question.ID,
		AuthorTeacherID: question.AuthorTeacherID,
		Title:           question.Title,
		Prompt:          question.Prompt,
		Topic:           questionTopicPointer(question.Topic),
		Subject:         shared.TextPointer(question.Subject),
		Difficulty:      questionDifficultyPointer(question.Difficulty),
		Source:          shared.TextPointer(question.Source),
		CorrectAnswer:   shared.TextPointer(question.CorrectAnswer),
		Status:          string(question.Status),
		CreatedAt:       shared.TimePointer(question.CreatedAt),
		UpdatedAt:       shared.TimePointer(question.UpdatedAt),
	}
}

func mapTag(tag sqlc.Tag) TagResponse {
	return TagResponse{
		ID:        tag.ID,
		Name:      tag.Name,
		CreatedAt: shared.TimePointer(tag.CreatedAt),
	}
}

func parseQuestionTopic(rawTopic, rawSubject *string) (sqlc.NullQuestionTopic, *string, error) {
	topicValue := strings.TrimSpace(firstNonEmpty(rawTopic, rawSubject))
	if topicValue == "" {
		return sqlc.NullQuestionTopic{}, rawSubject, nil
	}

	normalizedTopic, ok := normalizeQuestionTopic(topicValue)
	if !ok {
		return sqlc.NullQuestionTopic{}, nil, errors.New("topic must match the supported seeded subjects")
	}

	subjectLabel := questionTopicLabel(sqlc.QuestionTopic(normalizedTopic))
	return sqlc.NullQuestionTopic{QuestionTopic: sqlc.QuestionTopic(normalizedTopic), Valid: true}, &subjectLabel, nil
}

func parseQuestionDifficulty(rawDifficulty *string) (sqlc.NullQuestionDifficulty, error) {
	value := strings.TrimSpace(firstNonEmpty(rawDifficulty))
	if value == "" {
		return sqlc.NullQuestionDifficulty{}, nil
	}

	switch strings.ToLower(value) {
	case "easy":
		return sqlc.NullQuestionDifficulty{QuestionDifficulty: sqlc.QuestionDifficultyEasy, Valid: true}, nil
	case "medium":
		return sqlc.NullQuestionDifficulty{QuestionDifficulty: sqlc.QuestionDifficultyMedium, Valid: true}, nil
	case "hard":
		return sqlc.NullQuestionDifficulty{QuestionDifficulty: sqlc.QuestionDifficultyHard, Valid: true}, nil
	default:
		return sqlc.NullQuestionDifficulty{}, errors.New("difficulty must be easy, medium, or hard")
	}
}

func normalizeQuestionTopic(value string) (string, bool) {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "place value", "place_value":
		return string(sqlc.QuestionTopicPlaceValue), true
	case "arithmetic":
		return string(sqlc.QuestionTopicArithmetic), true
	case "negative numbers", "negative_numbers":
		return string(sqlc.QuestionTopicNegativeNumbers), true
	case "bidmas":
		return string(sqlc.QuestionTopicBidmas), true
	case "fractions":
		return string(sqlc.QuestionTopicFractions), true
	case "algebra":
		return string(sqlc.QuestionTopicAlgebra), true
	case "geometry":
		return string(sqlc.QuestionTopicGeometry), true
	case "data":
		return string(sqlc.QuestionTopicData), true
	default:
		return "", false
	}
}

func questionTopicLabel(topic sqlc.QuestionTopic) string {
	switch topic {
	case sqlc.QuestionTopicPlaceValue:
		return "Place Value"
	case sqlc.QuestionTopicArithmetic:
		return "Arithmetic"
	case sqlc.QuestionTopicNegativeNumbers:
		return "Negative Numbers"
	case sqlc.QuestionTopicBidmas:
		return "BIDMAS"
	case sqlc.QuestionTopicFractions:
		return "Fractions"
	case sqlc.QuestionTopicAlgebra:
		return "Algebra"
	case sqlc.QuestionTopicGeometry:
		return "Geometry"
	case sqlc.QuestionTopicData:
		return "Data"
	default:
		return ""
	}
}

func questionTopicPointer(topic sqlc.NullQuestionTopic) *string {
	if !topic.Valid {
		return nil
	}
	label := string(topic.QuestionTopic)
	return &label
}

func questionDifficultyPointer(difficulty sqlc.NullQuestionDifficulty) *string {
	if !difficulty.Valid {
		return nil
	}
	value := string(difficulty.QuestionDifficulty)
	return &value
}

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

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