package sync

import (
	"bufio"
	"crypto"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"strings"

	"github.com/gin-gonic/gin"
	_ "golang.org/x/crypto/blake2b"

	"srs.epita.fr/fic-server/libfic"
)

type importHint struct {
	Line      int
	Hint      *fic.EHint
	FlagsDeps []int64
}

func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (hints []importHint, errs []error) {
	params, _, err := parseExerciceParams(i, exercice.Path)
	if err != nil {
		errs = append(errs, NewChallengeTxtError(exercice, 0, err))
		return
	}

	for n, hint := range params.Hints {
		h := &fic.EHint{}
		if hint.Title == "" {
			h.Title = fmt.Sprintf("Astuce #%d", n+1)
		} else {
			h.Title = fixnbsp(hint.Title)
		}
		if hint.Cost == nil {
			h.Cost = exercice.Gain / 4
		} else {
			h.Cost = *hint.Cost
		}

		if hint.Filename != "" {
			if hint.Content != "" {
				errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be filled at the same time")))
				continue
			} else if !i.Exists(path.Join(exercice.Path, "hints", hint.Filename)) {
				errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: File not found", hint.Filename)))
				continue
			} else {
				// Handle files as downloadable content
				if res, err := i.importFile(path.Join(exercice.Path, "hints", hint.Filename), func(filePath string, origin string) (interface{}, error) {
					// Calculate hash
					hash512 := crypto.BLAKE2b_512.New()
					if fd, err := os.Open(filePath); err != nil {
						return nil, err
					} else {
						defer fd.Close()

						reader := bufio.NewReader(fd)
						if _, err := io.Copy(hash512, reader); err != nil {
							return nil, err
						}
					}
					result512 := hash512.Sum(nil)

					// Special format for downloadable hints: $FILES + hexhash + path from FILES/
					return "$FILES" + hex.EncodeToString(result512) + strings.TrimPrefix(filePath, fic.FilesDir), nil
				}); err != nil {
					errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: %w", hint.Filename, err)))
					continue
				} else if s, ok := res.(string); !ok {
					errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: invalid string returned as filename", hint.Filename)))
					continue
				} else {
					h.Content = s
				}
			}
		} else if hint.Content == "" {
			errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be empty at the same time")))
			continue
		} else {
			// Call checks hooks
			for _, hk := range hooks.mdTextHooks {
				for _, err := range hk(h.Content, exercice.Language, exceptions) {
					errs = append(errs, NewHintError(exercice, h, n, err))
				}
			}

			if h.Content, err = ProcessMarkdown(i, fixnbsp(hint.Content), exercice.Path); err != nil {
				errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("error during markdown formating: %w", err)))
			}
		}

		// Call checks hooks
		for _, hook := range hooks.hintHooks {
			for _, e := range hook(h, exercice, exceptions) {
				errs = append(errs, NewHintError(exercice, h, n, e))
			}
		}

		newHint := importHint{
			Line: n + 1,
			Hint: h,
		}

		// Read dependency to flag
		for _, nf := range hint.NeedFlag {
			newHint.FlagsDeps = append(newHint.FlagsDeps, nf.Id)
		}

		hints = append(hints, newHint)
	}

	return
}

// CheckExerciceHints checks if all hints are corrects..
func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, []error) {
	exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")

	return buildExerciceHints(i, exercice, exceptions)
}

// SyncExerciceHints reads the content of hints/ directories and import it as EHint for the given challenge.
func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int64]fic.Flag, exceptions *CheckExceptions) (hintsBindings map[int]*fic.EHint, errs []error) {
	if _, err := exercice.WipeHints(); err != nil {
		errs = append(errs, err)
	} else {
		exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")

		hints, berrs := buildExerciceHints(i, exercice, exceptions)
		errs = append(errs, berrs...)

		hintsBindings = map[int]*fic.EHint{}

		for _, hint := range hints {
			// Import hint
			if h, err := exercice.AddHint(hint.Hint.Title, hint.Hint.Content, hint.Hint.Cost); err != nil {
				errs = append(errs, NewHintError(exercice, hint.Hint, hint.Line, err))
			} else {
				hintsBindings[hint.Line] = h

				// Handle hints dependencies on flags
				for _, nf := range hint.FlagsDeps {
					if f, ok := flagsBindings[nf]; ok {
						if herr := h.AddDepend(f); herr != nil {
							errs = append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: %w", nf, herr)))
						}
					} else {
						errs = append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: Unexistant flag", nf)))
					}
				}
			}
		}
	}
	return
}

// ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints.
func ApiGetRemoteExerciceHints(c *gin.Context) {
	theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
	if theme != nil {
		exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
		if exercice != nil {
			hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions)
			if hints != nil {
				c.JSON(http.StatusOK, hints)
				return
			}

			c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
			return
		}

		c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
		return
	}

	c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
}
