package graphql

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.41

import (
	"context"
	"encoding/json"
	"fmt"
	"math/rand"
	"strconv"
	"time"

	"github.com/go-jet/jet/v2/postgres"
	"github.com/programme-lv/backend/internal/database/proglv/public/model"
	"github.com/programme-lv/backend/internal/database/proglv/public/table"
	"github.com/programme-lv/tester/pkg/messaging"
	amqp "github.com/rabbitmq/amqp091-go"
)

// EnqueueSubmissionForPublishedTaskVersion is the resolver for the enqueueSubmissionForPublishedTaskVersion field.
func (r *mutationResolver) EnqueueSubmissionForPublishedTaskVersion(ctx context.Context, taskID string, languageID string, submissionCode string) (*Submission, error) {
	// validate task id
	taskIDInt64, err := strconv.ParseInt(taskID, 10, 64)
	if err != nil {
		return nil, err
	}

	// authenticate user
	user, err := r.GetUserFromContext(ctx)
	if err != nil {
		return nil, err
	}

	// fetch task published version id
	var task model.Tasks
	err = postgres.SELECT(table.Tasks.AllColumns).FROM(table.Tasks).
		WHERE(table.Tasks.ID.EQ(postgres.Int64(taskIDInt64))).Query(r.PostgresDB, &task)
	if err != nil {
		return nil, err
	}
	if task.PublishedVersionID == nil {
		return nil, fmt.Errorf("task has no published version")
	}

	// fetch programming language
	var language model.ProgrammingLanguages
	err = postgres.SELECT(table.ProgrammingLanguages.AllColumns).FROM(table.ProgrammingLanguages).
		WHERE(table.ProgrammingLanguages.ID.EQ(postgres.String(languageID))).Query(r.PostgresDB, &language)
	if err != nil {
		return nil, err
	}

	// TODO: do all inserts in one transaction

	// create a new evaluation
	evaluation := model.Evaluations{
		EvalStatusID:  "IQ",
		TaskVersionID: int64(*task.PublishedVersionID),
	}

	insertStmt := table.Evaluations.INSERT(
		table.Evaluations.EvalStatusID,
		table.Evaluations.TaskVersionID,
	).MODEL(evaluation).RETURNING(table.Evaluations.ID)
	err = insertStmt.Query(r.PostgresDB, &evaluation)
	if err != nil {
		return nil, err
	}

	// create a new submission
	submission := model.TaskSubmissions{
		UserID:            user.ID,
		TaskID:            task.ID,
		ProgrammingLangID: language.ID,
		Submission:        submissionCode,
		Hidden:            false,
		VisibleEvalID:     &evaluation.ID,
	}

	insertStmt = table.TaskSubmissions.INSERT(
		table.TaskSubmissions.UserID,
		table.TaskSubmissions.TaskID,
		table.TaskSubmissions.ProgrammingLangID,
		table.TaskSubmissions.Submission,
		table.TaskSubmissions.Hidden,
		table.TaskSubmissions.VisibleEvalID,
	).MODEL(submission).RETURNING(table.TaskSubmissions.ID)
	err = insertStmt.Query(r.PostgresDB, &submission)
	if err != nil {
		return nil, err
	}

	// link the evaluation to the submission
	submissionEvaluation := model.SubmissionEvaluations{
		SubmissionID: submission.ID,
		EvaluationID: evaluation.ID,
	}

	insertStmt = table.SubmissionEvaluations.INSERT(
		table.SubmissionEvaluations.SubmissionID,
		table.SubmissionEvaluations.EvaluationID,
	).MODEL(submissionEvaluation).RETURNING(table.SubmissionEvaluations.ID)
	err = insertStmt.Query(r.PostgresDB, &submissionEvaluation)
	if err != nil {
		return nil, err
	}

	// publish submission
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	body := messaging.EvaluationRequest{
		TaskVersionId: int64(*task.PublishedVersionID),
		Submission: messaging.Submission{
			SourceCode: submissionCode,
			LanguageId: languageID,
		},
	}

	correlation := messaging.Correlation{
		HasEvaluationId: true,
		EvaluationId:    evaluation.ID,
		UnixMillis:      time.Now().UnixMilli(),
		RandomInt63:     rand.Int63(),
	}

	bodyJson, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}

	correlationJson, err := json.Marshal(correlation)
	if err != nil {
		return nil, err
	}

	ch, err := r.SubmissionRMQ.Channel()
	if err != nil {
		return nil, err
	}
	defer ch.Close()

	q, err := ch.QueueDeclare("eval_q", true, false, false, false, nil)
	if err != nil {
		return nil, err
	}

	err = ch.PublishWithContext(ctx, "", q.Name, false, false, amqp.Publishing{
		ContentType:   "application/json",
		Body:          bodyJson,
		ReplyTo:       "res_q",
		CorrelationId: string(correlationJson),
	})
	if err != nil {
		return nil, err
	}

	return &Submission{
		ID:   strconv.FormatInt(submission.ID, 10),
		Task: nil,
		Language: &ProgrammingLanguage{
			ID:       language.ID,
			FullName: language.FullName,
			MonacoID: language.MonacoID,
		},
		Submission: submissionCode,
	}, nil
}

// ListPublicSubmissions is the resolver for the listPublicSubmissions field.
func (r *queryResolver) ListPublicSubmissions(ctx context.Context) ([]*Submission, error) {
	/*
		we first select task submissions, then select published task versions,
		join with programming languages, join with users, join with evaluations and join with runtime statistics,
		join with evaluations
	*/
	selectStmt := postgres.SELECT(
		table.TaskSubmissions.AllColumns,
		table.Tasks.AllColumns,
		table.TaskVersions.AllColumns,
		table.ProgrammingLanguages.AllColumns,
		table.Users.AllColumns,
		table.Evaluations.AllColumns,
		table.RuntimeStatistics.AllColumns,
	).FROM(table.TaskSubmissions.
		LEFT_JOIN(table.Tasks, table.TaskSubmissions.TaskID.EQ(table.Tasks.ID)).
		INNER_JOIN(table.TaskVersions, table.Tasks.PublishedVersionID.EQ(table.TaskVersions.ID)).
		INNER_JOIN(table.ProgrammingLanguages, table.TaskSubmissions.ProgrammingLangID.EQ(table.ProgrammingLanguages.ID)).
		INNER_JOIN(table.Users, table.TaskSubmissions.UserID.EQ(table.Users.ID)).
		INNER_JOIN(table.Evaluations, table.TaskSubmissions.VisibleEvalID.EQ(table.Evaluations.ID)).
		LEFT_JOIN(table.RuntimeStatistics, table.Evaluations.TestRuntimeStatisticsID.EQ(table.RuntimeStatistics.ID))).
		WHERE(table.TaskSubmissions.Hidden.EQ(postgres.Bool(false))).
		ORDER_BY(table.TaskSubmissions.CreatedAt.DESC())

	var submissionRows []struct {
		model.TaskSubmissions
		model.Tasks
		model.TaskVersions
		model.ProgrammingLanguages
		model.Users
		model.Evaluations
		RuntimeStatistics *model.RuntimeStatistics
	}
	err := selectStmt.Query(r.PostgresDB, &submissionRows)
	if err != nil {
		return nil, err
	}

	var gqlSubmissions []*Submission
	for _, submissionRow := range submissionRows {
		submission := Submission{ID: strconv.FormatInt(submissionRow.TaskSubmissions.ID, 10)}
		submission.Submission = submissionRow.TaskSubmissions.Submission

		// fill task field
		task := Task{ID: strconv.FormatInt(submissionRow.Tasks.ID, 10)}
		task.Code = submissionRow.TaskVersions.ShortCode
		task.Name = submissionRow.TaskVersions.FullName
		submission.Task = &task

		// fill language field
		language := ProgrammingLanguage{ID: submissionRow.ProgrammingLanguages.ID}
		language.FullName = submissionRow.ProgrammingLanguages.FullName
		language.MonacoID = submissionRow.ProgrammingLanguages.MonacoID
		submission.Language = &language

		// fill user fields
		submission.Username = submissionRow.Users.Username

		// fill evaluation field
		evaluation := Evaluation{ID: strconv.FormatInt(submissionRow.Evaluations.ID, 10)}
		evaluation.Status = submissionRow.Evaluations.EvalStatusID
		evaluation.TotalScore = int(submissionRow.Evaluations.EvalTotalScore)
		if submissionRow.Evaluations.EvalPossibleScore != nil {
			possibleScore := int(*submissionRow.Evaluations.EvalPossibleScore)
			evaluation.PossibleScore = &possibleScore
		}
		if submissionRow.RuntimeStatistics != nil {
			runtimeStatistics := RuntimeStatistics{
				AvgTimeMs:   int(submissionRow.RuntimeStatistics.AvgTimeMillis),
				MaxTimeMs:   int(submissionRow.RuntimeStatistics.MaximumTimeMillis),
				AvgMemoryKb: int(submissionRow.RuntimeStatistics.AvgMemoryKibibytes),
				MaxMemoryKb: int(submissionRow.RuntimeStatistics.MaximumMemoryKibibytes),
			}
			evaluation.RuntimeStatistics = &runtimeStatistics
		}
		submission.Evaluation = &evaluation

		// fill created at field
		submission.CreatedAt = submissionRow.TaskSubmissions.CreatedAt.Format(time.RFC3339)
		gqlSubmissions = append(gqlSubmissions, &submission)
	}

	return gqlSubmissions, nil
}

// GetSubmission is the resolver for the getSubmission field.
func (r *queryResolver) GetSubmission(ctx context.Context, id string) (*Submission, error) {
	idInt64, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		return nil, err
	}
	/*
		we first select task submissions, then select published task versions,
		join with programming languages, join with users, join with evaluations and join with runtime statistics,
		join with evaluations
	*/
	selectSubmissionStmt := postgres.SELECT(
		table.TaskSubmissions.AllColumns,
		table.Tasks.AllColumns,
		table.TaskVersions.AllColumns,
		table.ProgrammingLanguages.AllColumns,
		table.Users.AllColumns,
		table.Evaluations.AllColumns,
		table.RuntimeStatistics.AllColumns,
	).FROM(table.TaskSubmissions.
		LEFT_JOIN(table.Tasks, table.TaskSubmissions.TaskID.EQ(table.Tasks.ID)).
		INNER_JOIN(table.TaskVersions, table.Tasks.PublishedVersionID.EQ(table.TaskVersions.ID)).
		INNER_JOIN(table.ProgrammingLanguages, table.TaskSubmissions.ProgrammingLangID.EQ(table.ProgrammingLanguages.ID)).
		INNER_JOIN(table.Users, table.TaskSubmissions.UserID.EQ(table.Users.ID)).
		INNER_JOIN(table.Evaluations, table.TaskSubmissions.VisibleEvalID.EQ(table.Evaluations.ID)).
		LEFT_JOIN(table.RuntimeStatistics, table.Evaluations.TestRuntimeStatisticsID.EQ(table.RuntimeStatistics.ID))).
		WHERE(table.TaskSubmissions.ID.EQ(postgres.Int64(idInt64))).
		ORDER_BY(table.TaskSubmissions.CreatedAt.DESC())

	var submissionRow struct {
		model.TaskSubmissions
		model.Tasks
		model.TaskVersions
		model.ProgrammingLanguages
		model.Users
		model.Evaluations
		RuntimeStatistics *model.RuntimeStatistics
	}
	err = selectSubmissionStmt.Query(r.PostgresDB, &submissionRow)
	if err != nil {
		return nil, err
	}

	selectTestsStmt := postgres.SELECT(
		table.EvaluationTestResults.ID,
		table.EvaluationTestResults.TaskVTestID,
		table.EvaluationTestResults.EvalStatusID,
		table.RuntimeData.TimeMillis,
		table.RuntimeData.MemoryKibibytes).
		FROM(table.EvaluationTestResults.
			INNER_JOIN(table.RuntimeData, table.EvaluationTestResults.ExecRDataID.EQ(table.RuntimeData.ID))).
		WHERE(table.EvaluationTestResults.EvaluationID.EQ(postgres.Int64(submissionRow.Evaluations.ID)))

	var testRows []struct {
		model.EvaluationTestResults
		model.RuntimeData
	}

	err = selectTestsStmt.Query(r.PostgresDB, &testRows)
	if err != nil {
		return nil, err
	}

	submission := Submission{ID: strconv.FormatInt(submissionRow.TaskSubmissions.ID, 10)}
	submission.Submission = submissionRow.TaskSubmissions.Submission

	// fill task field
	task := Task{ID: strconv.FormatInt(submissionRow.Tasks.ID, 10)}
	task.Code = submissionRow.TaskVersions.ShortCode
	task.Name = submissionRow.TaskVersions.FullName
	submission.Task = &task

	// fill language field
	language := ProgrammingLanguage{ID: submissionRow.ProgrammingLanguages.ID}
	language.FullName = submissionRow.ProgrammingLanguages.FullName
	language.MonacoID = submissionRow.ProgrammingLanguages.MonacoID
	submission.Language = &language

	// fill user fields
	submission.Username = submissionRow.Users.Username

	// fill evaluation field
	evaluation := Evaluation{ID: strconv.FormatInt(submissionRow.Evaluations.ID, 10)}
	evaluation.Status = submissionRow.Evaluations.EvalStatusID
	evaluation.TotalScore = int(submissionRow.Evaluations.EvalTotalScore)
	if submissionRow.Evaluations.EvalPossibleScore != nil {
		possibleScore := int(*submissionRow.Evaluations.EvalPossibleScore)
		evaluation.PossibleScore = &possibleScore
	}
	if submissionRow.RuntimeStatistics != nil {
		runtimeStatistics := RuntimeStatistics{
			AvgTimeMs:   int(submissionRow.RuntimeStatistics.AvgTimeMillis),
			MaxTimeMs:   int(submissionRow.RuntimeStatistics.MaximumTimeMillis),
			AvgMemoryKb: int(submissionRow.RuntimeStatistics.AvgMemoryKibibytes),
			MaxMemoryKb: int(submissionRow.RuntimeStatistics.MaximumMemoryKibibytes),
		}
		evaluation.RuntimeStatistics = &runtimeStatistics
	}
	if submissionRow.Evaluations.CompilationDataID != nil {
		// we will have to unfortunately fetch compilation data from the database
		var compilationData model.RuntimeData
		err = postgres.SELECT(table.RuntimeData.AllColumns).FROM(table.RuntimeData).
			WHERE(table.RuntimeData.ID.EQ(postgres.Int64(*submissionRow.Evaluations.CompilationDataID))).
			Query(r.PostgresDB, &compilationData)
		if err != nil {
			return nil, err
		}
		skip := false
		skip = skip || compilationData.TimeMillis == nil
		skip = skip || compilationData.MemoryKibibytes == nil
		skip = skip || compilationData.ExitCode == nil
		skip = skip || compilationData.Stdout == nil
		skip = skip || compilationData.Stderr == nil
		if !skip {
			evaluation.Compilation = &CompilationDetails{
				TimeMs:   int(*compilationData.TimeMillis),
				MemoryKb: int(*compilationData.MemoryKibibytes),
				ExitCode: int(*compilationData.ExitCode),
				Stdout:   *compilationData.Stdout,
				Stderr:   *compilationData.Stderr,
			}
		}
	}
	var tests []*TestResult
	for _, testRow := range testRows {
		testResultTypes := map[string]TestResultType{
			"AC":  TestResultTypeAc,
			"PT":  TestResultTypePt,
			"WA":  TestResultTypeWa,
			"PE":  TestResultTypePe,
			"TLE": TestResultTypeTle,
			"MLE": TestResultTypeMle,
			"ILE": TestResultTypeIle,
			"IG":  TestResultTypeIg,
			"RE":  TestResultTypeRe,
			"SV":  TestResultTypeSv,
			"ISE": TestResultTypeIse,
		}
		testResultType, ok := testResultTypes[testRow.EvalStatusID]
		if !ok {
			continue
		}
		if testRow.RuntimeData.TimeMillis == nil || testRow.RuntimeData.MemoryKibibytes == nil {
			continue
		}
		test := TestResult{
			Result:   testResultType,
			TimeMs:   int(*testRow.RuntimeData.TimeMillis),
			MemoryKb: int(*testRow.RuntimeData.MemoryKibibytes),
		}
		tests = append(tests, &test)
	}
	evaluation.TestResults = tests
	submission.Evaluation = &evaluation

	// fill created at field
	submission.CreatedAt = submissionRow.TaskSubmissions.CreatedAt.Format(time.RFC3339)

	return &submission, nil
}
