package canvaslms

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/eternal-flame-AD/yoake/config"
	"github.com/eternal-flame-AD/yoake/internal/auth"
	"github.com/eternal-flame-AD/yoake/internal/comm/model"
	"github.com/eternal-flame-AD/yoake/internal/echoerror"
	"github.com/labstack/echo/v4"
)

type Handler struct {
	conf           config.CanvasLMS
	maxn           int
	respCache      *responseCache
	respCacheMutex sync.RWMutex
	refreshPeriod  time.Duration
	comm           model.Communicator
}

type GetGradesResponse struct {
	LastRefresh string                    `json:"last_refresh"`
	Grades      []SubmissionScoreResponse `json:"grades"`
}

type SubmissionScoreResponse struct {
	Name               string
	Due                string
	AssignmentID       string
	AssignmentLegacyID string
	AssignmentURL      string
	SubmissionID       string
	SubmissionLegacyID string
	CourseID           string
	CourseLegacyID     string
	CourseName         string
	CourseCode         string

	Graded string
	Posted string

	State string

	Score          float64
	EnteredScore   float64
	PossiblePoints float64
	Grade          string
	EnteredGrade   string
	GradeHidden    bool
	GradedAt       string
	PostedAt       string

	SubmissionUserLegacyID string
	SubmissionUserID       string
	SubmissionUserName     string
	SubmissionUserSISID    string
	SubmissionUserEmail    string
}

func submissionScoreResponseFromQL(courselid, courseid, coursename, coursecode string, coursesubmission GraphSubmissionResponse) SubmissionScoreResponse {
	res := SubmissionScoreResponse{
		Name:                   coursesubmission.Assignment.Name,
		Due:                    "-",
		AssignmentID:           coursesubmission.Assignment.ID,
		AssignmentLegacyID:     coursesubmission.Assignment.IDLegacy,
		AssignmentURL:          coursesubmission.Assignment.HTMLUrl,
		SubmissionID:           coursesubmission.ID,
		SubmissionLegacyID:     coursesubmission.IDLegacy,
		CourseID:               courseid,
		CourseLegacyID:         courselid,
		CourseName:             coursename,
		CourseCode:             coursecode,
		Graded:                 coursesubmission.GradingStatus,
		Posted:                 strconv.FormatBool(coursesubmission.Posted),
		State:                  coursesubmission.State,
		Score:                  -1,
		EnteredScore:           -1,
		PossiblePoints:         coursesubmission.Assignment.PointsPossible,
		Grade:                  "-",
		EnteredGrade:           "-",
		GradeHidden:            coursesubmission.GradeHidden,
		GradedAt:               "-",
		PostedAt:               "-",
		SubmissionUserID:       coursesubmission.User.ID,
		SubmissionUserLegacyID: coursesubmission.User.IDLegacy,
		SubmissionUserName:     coursesubmission.User.Name,
	}
	if coursesubmission.Score != nil {
		res.Score = *coursesubmission.Score
	}
	if coursesubmission.EnteredScore != nil {
		res.EnteredScore = *coursesubmission.EnteredScore
	}
	if coursesubmission.Assignment.DueAt != nil {
		res.Due = *coursesubmission.Assignment.DueAt
	}
	if coursesubmission.Grade != nil {
		res.Grade = *coursesubmission.Grade
	}
	if coursesubmission.GradedAt != nil {
		res.GradedAt = *coursesubmission.GradedAt
	}
	if coursesubmission.PostedAt != nil {
		res.PostedAt = *coursesubmission.PostedAt
	}
	if coursesubmission.User.SISID != nil {
		res.SubmissionUserSISID = *coursesubmission.User.SISID
	}
	if coursesubmission.User.Email != nil {
		res.SubmissionUserEmail = *coursesubmission.User.Email
	}
	return res
}

func (h *Handler) sortResponse(compare GraphSubmissionCompareFunc) (resp []SubmissionScoreResponse, err error) {
	h.respCacheMutex.RLock()
	defer h.respCacheMutex.RUnlock()

	res := make([]GraphSubmissionResponse, h.maxn)
	resF := make([]SubmissionScoreResponse, h.maxn)
	curL := 0

	push := func(pos int, resp GraphSubmissionResponse, respF SubmissionScoreResponse) {
		for i := curL - 1; i > pos; i-- {
			res[i] = res[i-1]
			resF[i] = resF[i-1]
		}
		res[pos] = resp
		resF[pos] = respF
	}
	for _, course := range h.respCache.rawResponse.Data.AllCourses {
		for _, submission := range course.SubmissionsConnection.Nodes {
			pos := curL
			for i := curL - 1; i >= 0; i-- {
				if !compare(submission, res[i]) {
					break
				}
				pos = i
			}
			if pos < curL || curL < h.maxn {
				push(pos, submission, submissionScoreResponseFromQL(course.IDLegacy, course.ID, course.Name, course.CourseCode, submission))
				if curL < h.maxn {
					curL++
				}
			}
		}
	}
	return resF, nil
}

func (h *Handler) refresh() (hasUpdate bool, err error) {
	h.respCacheMutex.Lock()
	defer h.respCacheMutex.Unlock()

	client := http.Client{
		Timeout: h.refreshPeriod / 2,
	}
	buf := bytes.NewBufferString("")
	e := json.NewEncoder(buf)
	e.Encode(struct {
		Query         string    `json:"query"`
		OperationName string    `json:"operationName"`
		Variables     *struct{} `json:"variables"`
	}{strings.ReplaceAll(GraphQuery, "$maxn$", h.conf.MaxN), "gradeQuery", nil})
	now := time.Now()
	req, err := http.NewRequest("POST", h.conf.Endpoint, buf)
	if err != nil {
		return false, err
	}
	req.Header.Set("content-type", "application/json")
	req.Header.Set("Authorization", "Bearer "+h.conf.Token)
	resp, err := client.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {

		return false, fmt.Errorf("remote returned with %d", resp.StatusCode)
	}
	dec := json.NewDecoder(resp.Body)
	respStruct := new(GraphResponse)
	if err := dec.Decode(respStruct); err != nil {
		return false, err
	}

	hasUpdate = false
	lastUpdateTime := make(map[string]time.Time)
	for _, course := range respStruct.Data.AllCourses {
		for _, submission := range course.SubmissionsConnection.Nodes {
			lastUpdForSubmission := laterTime(submission.PostedAt, submission.GradedAt)
			if lastUpdForSubmission != nil {
				newUpdTime := parseJSONTime(*lastUpdForSubmission)
				lastUpdateTime[submission.ID] = newUpdTime

				if h.respCache != nil && (h.conf.SubmissionName == "" || submission.User.Name == h.conf.SubmissionName) {
					if lastUpdateTime, ok := h.respCache.submissionLastUpdate[submission.ID]; !ok || lastUpdateTime.UnixNano() != newUpdTime.UnixNano() {
						hasUpdate = true
					}
				}
			}
		}
	}
	h.respCache = &responseCache{
		rawResponse:          *respStruct,
		requestTime:          now,
		submissionLastUpdate: lastUpdateTime,
	}
	if hasUpdate {
		go h.SendGradeMessage(h.conf.Message.OnUpdate)
	}
	return hasUpdate, nil
}

func (h *Handler) GetInformation(key string) (data interface{}, err error) {
	switch key {
	case "recent-graded":
		return h.sortResponse(GraphSubmissionCompareByGradeTime)
	case "recent-due":
		return h.sortResponse(GraphSubmissionCompareByDue)
	}
	return nil, errors.New("unknown info request type")
}

type responseCache struct {
	rawResponse          GraphResponse
	requestTime          time.Time
	submissionLastUpdate map[string]time.Time
}

func Register(g *echo.Group, comm model.Communicator) (h *Handler, err error) {
	h = &Handler{conf: config.Config().CanvasLMS, comm: comm}
	if h.conf.Token == "" {
		return nil, errors.New("canvas token not set")
	}
	maxn, err := strconv.Atoi(h.conf.MaxN)
	if err != nil {
		return nil, err
	}
	h.maxn = maxn
	refreshperiod, err := time.ParseDuration(h.conf.Frequency)
	if err != nil {
		return nil, err
	}
	h.refreshPeriod = refreshperiod

	checkForUpdates := make(chan bool)
	go func() {
		if _, err := h.refresh(); err != nil {
			log.Panicf("cannot access graphql endpoint: %v", err)
		} else {
			go h.SendGradeMessage(h.conf.Message.OnStartup)
		}
		for forced := range checkForUpdates {
			_ = forced
			h.refresh()
			// TODO: notify if there is an update
		}
	}()
	go func() {
		for range time.NewTicker(h.refreshPeriod).C {
			checkForUpdates <- false
		}
	}()

	gradesG := g.Group("/grades", auth.RequireMiddleware(auth.RoleAdmin), func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if h.respCache == nil {
				return echoerror.NewHttp(http.StatusServiceUnavailable, errors.New("not yet initialized"))
			}
			return next(c)
		}
	})
	{
		gradesG.GET("", func(c echo.Context) error {
			if c.QueryParam("refresh") == "1" {
				if _, err := h.refresh(); err != nil {
					return fmt.Errorf("cannot access graphql endpoint: %v", err)
				}
			}
			sortQuery := c.QueryParam("sort")
			if sortQuery == "" {
				sortQuery = "recent-graded"
			}
			var res GetGradesResponse
			res.LastRefresh = h.respCache.requestTime.Format(time.RFC3339)
			switch sortQuery {
			case "recent-graded":
				if grades, err := h.sortResponse(GraphSubmissionCompareByGradeTime); err != nil {
					return err
				} else {
					res.Grades = grades
				}
				return c.JSON(http.StatusOK, res)
			case "recent-due":
				if grades, err := h.sortResponse(GraphSubmissionCompareByDue); err != nil {
					return err
				} else {
					res.Grades = grades
				}
				return c.JSON(http.StatusOK, res)
			}
			return errors.New("unknown info request type")
		})
	}

	return h, nil
}