package sync

import (
	"fmt"
	"math/rand"
	"net/http"
	"path"
	"sort"
	"strconv"
	"strings"
	"unicode"

	"github.com/gin-gonic/gin"

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

// isFullGraphic detects if some rune are not graphic one.
// This function is usefull to display warning when importing key ending with \r.
func isFullGraphic(s string) bool {
	for _, c := range s {
		if !unicode.IsGraphic(c) {
			return false
		}
	}
	return true
}

func validatorRegexp(vre string) (validator_regexp *string) {
	if len(vre) > 0 {
		validator_regexp = &vre
	} else {
		validator_regexp = nil
	}
	return
}

func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bool, separator string) (raw string, prep string, errs []error) {
	// Concatenate array
	if f, ok := input.([]interface{}); ok {
		if len(validatorRe) > 0 {
			errs = append(errs, fmt.Errorf("ValidatorRe cannot be defined for this kind of flag."))
			validatorRe = ""
		}

		if len(separator) == 0 {
			separator = ","
		} else if len(separator) > 1 {
			separator = string(separator[0])
			errs = append(errs, fmt.Errorf("separator truncated to %q", separator))
		}

		var fitems []string
		for i, v := range f {
			if g, ok := v.(string); ok {
				if strings.Index(g, separator) != -1 {
					errs = append(errs, fmt.Errorf("flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.", separator))
					return
				} else {
					fitems = append(fitems, g)
				}
			} else {
				errs = append(errs, fmt.Errorf("item %d has an invalid type: can only be string, is %T.", i, g))
				return
			}
		}

		ignord := "f"
		if !ordered {
			sort.Strings(fitems)
			ignord = "t"
		}

		nbLines := 0
		if showLines {
			if len(fitems) > 9 {
				errs = append(errs, fmt.Errorf("too much items in vector to use ShowLines features, max 9."))
			} else {
				nbLines = len(fitems)
			}
		}

		raw = strings.Join(fitems, separator) + separator

		prep = fmt.Sprintf("`%s%s%d", separator, ignord, nbLines)
	} else if f, ok := input.(int64); ok {
		raw = fmt.Sprintf("%d", f)
	} else if f, ok := input.(float64); ok {
		raw = strconv.FormatFloat(f, 'f', -1, 64)
	} else if f, ok := input.(string); !ok {
		errs = append(errs, fmt.Errorf("has an invalid type: can only be []string or string, not %T", input))
		return
	} else {
		raw = f
	}
	return
}

func buildLabelFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, exceptions *CheckExceptions) (f *fic.FlagLabel, errs []error) {
	if len(flag.Label) == 0 {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label cannot be empty.")))
		return
	}

	// Call checks hooks
	for _, h := range hooks.mdTextHooks {
		for _, err := range h(flag.Label, exercice.Language, exceptions.Filter2ndCol(strconv.Itoa(flagline))) {
			errs = append(errs, NewFlagError(exercice, &flag, flagline, err))
		}
	}

	if mdlabel, err := ProcessMarkdown(GlobalImporter, flag.Label, exercice.Path); err != nil {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("unable to parse property label as Markdown: %w", err)))
	} else {
		if strings.Count(flag.Label, "\n\n") == 0 {
			flag.Label = mdlabel[3 : len(mdlabel)-4]
		} else {
			flag.Label = mdlabel
		}
	}

	if flag.Raw != nil {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("raw cannot be defined.")))
	}

	if len(flag.Choice) != 0 {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("choices cannot be defined.")))
	}

	f = &fic.FlagLabel{
		Order:   int8(flagline),
		Label:   flag.Label,
		Variant: flag.Variant,
	}

	// Call checks hooks
	for _, h := range hooks.flagLabelHooks {
		for _, e := range h(f, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) {
			errs = append(errs, NewFlagError(exercice, &flag, flagline, e))
		}
	}
	return
}

func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defaultLabel string, exceptions *CheckExceptions) (f *fic.Flag, choices []*fic.FlagChoice, errs []error) {
	if len(flag.Label) == 0 {
		flag.Label = defaultLabel
	}

	if len(flag.Variant) != 0 {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("variant is not defined for this kind of flag.")))
	}

	if flag.Label[0] == '`' {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label should not begin with `.")))
		flag.Label = flag.Label[1:]
	}

	raw, prep, terrs := getRawKey(flag.Raw, flag.CaptureRe, flag.Ordered, flag.ShowLines, flag.Separator)

	if len(terrs) > 0 {
		for _, terr := range terrs {
			errs = append(errs, NewFlagError(exercice, &flag, flagline, terr))
		}
		f = nil
		return
	}
	flag.Label = prep + flag.Label

	if len(flag.Label) > 255 {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("label is too long (max 255 chars per label).")))
	}

	if (flag.Type == "text" && !isFullGraphic(strings.Replace(raw, "\n", "", -1))) || (flag.Type != "text" && !isFullGraphic(raw)) {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("WARNING non-printable characters in flag, is this really expected?")))
	}

	hashedFlag, err := fic.ComputeHashedFlag([]byte(raw), !flag.CaseSensitive, flag.NoTrim, validatorRegexp(flag.CaptureRe), flag.SortReGroups)
	if err != nil {
		errs = append(errs, NewFlagError(exercice, &flag, flagline, err))
		return
	}
	fk := &fic.FlagKey{
		Type:          flag.Type,
		IdExercice:    exercice.Id,
		Order:         int8(flagline),
		Label:         flag.Label,
		Placeholder:   flag.Placeholder,
		Help:          flag.Help,
		Unit:          flag.Unit,
		IgnoreCase:    !flag.CaseSensitive,
		Multiline:     flag.Type == "text",
		CaptureRegexp: validatorRegexp(flag.CaptureRe),
		SortReGroups:  flag.SortReGroups,
		Checksum:      hashedFlag[:],
		ChoicesCost:   flag.ChoicesCost,
		BonusGain:     flag.BonusGain,
	}

	// Call checks hooks
	for _, h := range hooks.flagKeyHooks {
		for _, e := range h(fk, raw, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) {
			errs = append(errs, NewFlagError(exercice, &flag, flagline, e))
		}
	}

	fl := fic.Flag(fk)
	f = &fl

	if len(flag.Choice) > 0 || (flag.Type == "ucq" || flag.Type == "radio") {
		// Import choices
		hasOne := false

		if !flag.NoShuffle {
			rand.Shuffle(len(flag.Choice), func(i, j int) {
				flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i]
			})
		}

		for _, choice := range flag.Choice {
			val, prep, terrs := getRawKey(choice.Value, "", false, false, "")
			if len(terrs) > 0 {
				for _, terr := range terrs {
					errs = append(errs, NewFlagError(exercice, &flag, flagline, terr))
				}
				continue
			}

			if len(choice.Label) == 0 {
				choice.Label = val
			}
			choice.Label = prep + choice.Label

			if !flag.CaseSensitive {
				val = strings.ToLower(val)
			}

			fc := &fic.FlagChoice{
				Label: choice.Label,
				Value: val,
			}

			// Call checks hooks
			for _, h := range hooks.flagChoiceHooks {
				for _, e := range h(fc, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) {
					errs = append(errs, NewFlagError(exercice, &flag, flagline, e))
				}
			}

			choices = append(choices, fc)

			if val == "true" || val == "false" {
				errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("value can't be %q, this is not a MCQ, the value has to be meaningful. The value is shown to players as response identifier.", val)))
			}

			if val == raw || (!flag.CaseSensitive && val == strings.ToLower(raw)) {
				hasOne = true
			}
		}
		if !hasOne {
			errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("no valid answer defined.")))
		}

		// Call checks hooks
		for _, h := range hooks.flagKeyWithChoicesHooks {
			for _, e := range h(fk, raw, choices, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) {
				errs = append(errs, NewFlagError(exercice, &flag, flagline, e))
			}
		}
	}
	return
}

type importFlag struct {
	Line      int
	Flag      fic.Flag
	JustifyOf *fic.MCQ_entry
	Choices   []*fic.FlagChoice
	FilesDeps []string
	FlagsDeps []int64
}

func iface2Number(input interface{}, output *string) error {
	if input != nil {
		if v, ok := input.(int64); ok {
			*output = fmt.Sprintf("%d", v)
		} else if v, ok := input.(float64); ok {
			*output = strconv.FormatFloat(v, 'f', -1, 64)
			fmt.Printf("%s\n", *output)
		} else {
			return fmt.Errorf("has an invalid type: expected int or float, got %T", input)
		}
	}
	return nil
}

// buildExerciceFlag read challenge.txt and extract all flags.
func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nline int, exceptions *CheckExceptions) (ret []importFlag, errs []error) {
	switch strings.ToLower(flag.Type) {
	case "":
		flag.Type = "key"
	case "label":
		flag.Type = "label"
	case "key":
		flag.Type = "key"
	case "number":
		var smin, smax, sstep string
		err := iface2Number(flag.NumberMin, &smin)
		if err != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("min %w", err)))
		}

		err = iface2Number(flag.NumberMax, &smax)
		if err != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("max %w", err)))
		}

		err = iface2Number(flag.NumberStep, &sstep)
		if err != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("step %w", err)))
		}
		flag.Type = fmt.Sprintf("number,%s,%s,%s", smin, smax, sstep)
	case "text":
		flag.Type = "text"
	case "vector":
		flag.Type = "vector"
	case "ucq":
		flag.Type = "ucq"
	case "radio":
		flag.Type = "radio"
	case "mcq":
		flag.Type = "mcq"
	default:
		errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'ucq', 'radio' or 'vector'")))
		return
	}

	if !strings.HasPrefix(flag.Type, "number") {
		if flag.NumberMin != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property min undefined for this kind of flag: should the type be 'number'")))
		} else if flag.NumberMax != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property max undefined for this kind of flag: should the type be 'number'")))
		} else if flag.NumberStep != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property step undefined for this kind of flag: should the type be 'number'")))
		}
	}

	if len(flag.Help) > 0 {
		if mdhelp, err := ProcessMarkdown(i, flag.Help, exercice.Path); err != nil {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("unable to parse property help as Markdown: %w", err)))
		} else {
			flag.Help = mdhelp[3 : len(mdhelp)-4]
		}
	}

	if flag.Type == "label" {
		addedFlag, berrs := buildLabelFlag(exercice, flag, nline+1, exceptions)
		errs = append(errs, berrs...)
		if addedFlag != nil {
			ret = append(ret, importFlag{
				Line: nline + 1,
				Flag: addedFlag,
			})
		}
	} else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" {
		addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag", exceptions)
		errs = append(errs, berrs...)
		if addedFlag != nil {
			ret = append(ret, importFlag{
				Line:    nline + 1,
				Flag:    *addedFlag,
				Choices: choices,
			})
		}
	} else if flag.Type == "mcq" {
		addedFlag := fic.MCQ{
			IdExercice: exercice.Id,
			Order:      int8(nline + 1),
			Title:      flag.Label,
			Entries:    []*fic.MCQ_entry{},
		}

		hasOne := false
		isJustified := false

		if len(flag.Variant) != 0 {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("variant is not defined for this kind of flag")))
		}

		if !flag.NoShuffle {
			rand.Shuffle(len(flag.Choice), func(i, j int) {
				flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i]
			})
		}

		for cid, choice := range flag.Choice {
			var val bool

			if choice.Raw != nil {
				if hasOne && !isJustified {
					errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ")))
					continue
				}

				val = true
				isJustified = true
			} else if p, ok := choice.Value.(bool); ok {
				val = p
				if isJustified {
					errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ")))
					continue
				}
			} else if choice.Value == nil {
				val = false
			} else {
				errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("choice %d: incorrect type for value: %T is not boolean.", cid, choice.Value)))
				continue
			}

			entry := &fic.MCQ_entry{
				Label:    choice.Label,
				Response: val,
			}
			addedFlag.Entries = append(addedFlag.Entries, entry)

			if isJustified && choice.Raw != nil {
				addedFlag, choices, berrs := buildKeyFlag(exercice, choice.ExerciceFlag, nline+1, "Flag correspondant", exceptions)
				if len(berrs) > 0 {
					errs = append(errs, berrs...)
				}
				if addedFlag != nil {
					ret = append(ret, importFlag{
						Line:      nline + 1,
						Flag:      *addedFlag,
						JustifyOf: entry,
						Choices:   choices,
					})
				}
			}
		}

		// Call checks hooks
		for _, h := range hooks.flagMCQHooks {
			for _, e := range h(&addedFlag, addedFlag.Entries, exercice, exceptions) {
				errs = append(errs, NewFlagError(exercice, &flag, nline+1, e))
			}
		}

		ret = append([]importFlag{importFlag{
			Line: nline + 1,
			Flag: &addedFlag,
		}}, ret...)
	}
	return
}

// buildExerciceFlags read challenge.txt and extract all flags.
func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (flags map[int64]importFlag, flagids []int64, errs []error) {
	params, gerrs := getExerciceParams(i, exercice)
	if len(gerrs) > 0 {
		return flags, flagids, gerrs
	}

	flags = map[int64]importFlag{}

	for nline, flag := range params.Flags {
		if flag.Id == 0 {
			// TODO: should be more smart than that. Perhaps search to increment if possible.
			flag.Id = rand.Int63()
		}

		// Ensure flag ID is unique
		for _, ok := flags[flag.Id]; ok; _, ok = flags[flag.Id] {
			errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("identifier already used (%d), using a random one.", flag.Id)))
			flag.Id = rand.Int63()
		}

		newFlags, ferrs := buildExerciceFlag(i, exercice, flag, nline, exceptions)
		if len(ferrs) > 0 {
			errs = append(errs, ferrs...)
		}
		if len(newFlags) > 0 {
			for _, newFlag := range newFlags {
				fId := flag.Id
				for _, ok := flags[fId]; ok; _, ok = flags[fId] {
					fId = rand.Int63()
				}

				// Read dependency to flag
				for _, nf := range flag.NeedFlag {
					if len(nf.Theme) > 0 {
						errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("dependancy on another scenario is not implemented yet.")))
					}
					newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf.Id)
				}
				for _, nf := range flag.NeedFlags {
					newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf)
				}

				// Read dependency to file
				for _, lf := range flag.LockedFile {
					newFlag.FilesDeps = append(newFlag.FilesDeps, lf.Filename)
				}

				flags[fId] = newFlag
				flagids = append(flagids, fId)
			}
		}
	}

	return
}

// CheckExerciceFlags checks if all flags for the given challenge are correct.
func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exceptions *CheckExceptions) (rf []fic.Flag, errs []error) {
	exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")

	flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions)
	errs = append(errs, berrs...)

	for _, flagid := range flagsids {
		if flag, ok := flags[flagid]; ok {
			// Check dependency to flag
			for _, nf := range flag.FlagsDeps {
				if _, ok := flags[nf]; !ok {
					errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on flag id=%d: id not defined", nf)))
				}
			}

			// Check dependency loop
			deps := flag.FlagsDeps
			for i := 0; i < len(deps); i++ {
				if deps[i] == flagid {
					errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag dependency loop detected: flag id=%d: depends on itself", flagid)))
					break
				}

			deploppadd:
				for _, d := range flags[deps[i]].FlagsDeps {
					for _, dd := range deps {
						if dd == d {
							continue deploppadd
						}
					}
					deps = append(deps, d)
				}
			}

			// Check dependency to file
			for _, lf := range flag.FilesDeps {
				found := false
				for _, f := range files {
					if f == lf {
						found = true
						break
					}
				}
				if !found {
					errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on %s: No such file", lf)))
				}
			}

			rf = append(rf, flag.Flag)
		}
	}

	return
}

// ExerciceFlagsMap builds the flags bindings between challenge.txt and DB.
func ExerciceFlagsMap(i Importer, exercice *fic.Exercice) (kmap map[int64]fic.Flag) {
	flags, flagids, _ := buildExerciceFlags(i, exercice, nil)

	kmap = map[int64]fic.Flag{}

	for _, flagid := range flagids {
		if flag, ok := flags[flagid]; ok {
			if addedFlag, err := flag.Flag.RecoverId(); err == nil {
				kmap[flagid] = addedFlag
			}
		}
	}

	return
}

// SyncExerciceFlags imports all kind of flags for the given challenge.
func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (kmap map[int64]fic.Flag, errs []error) {
	if _, err := exercice.WipeFlags(); err != nil {
		errs = append(errs, err)
	} else if _, err := exercice.WipeMCQs(); err != nil {
		errs = append(errs, err)
	} else {
		exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")

		flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions)
		errs = append(errs, berrs...)

		kmap = map[int64]fic.Flag{}

		// Import flags
		for _, flagid := range flagids {
			if flag, ok := flags[flagid]; ok {
				if flag.JustifyOf != nil {
					if f, ok := flag.Flag.(*fic.FlagKey); ok {
						f.Label = fmt.Sprintf("%%%d%%%s", flag.JustifyOf.Id, f.Label)
					}
				}

				if addedFlag, err := exercice.AddFlag(flag.Flag); err != nil {
					errs = append(errs, NewFlagError(exercice, nil, flag.Line, err))
				} else {
					if f, ok := addedFlag.(*fic.FlagKey); ok {
						for _, choice := range flag.Choices {
							if _, err := f.AddChoice(choice); err != nil {
								errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("choice #FIXME: %w", err)))
							}
						}
					}

					kmap[flagid] = addedFlag

					// Import dependency to flag
					for _, nf := range flag.FlagsDeps {
						if rf, ok := kmap[nf]; !ok {
							errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to flag id=%d: id not defined, perhaps not available at time of processing", nf)))
						} else if err := addedFlag.AddDepend(rf); err != nil {
							errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to id=%d: %w", nf, err)))
						}
					}

					// Import dependency to file
					for _, lf := range flag.FilesDeps {
						if rf, err := exercice.GetFileByFilename(lf); err != nil {
							errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err)))
						} else if err := rf.AddDepend(addedFlag); err != nil {
							errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err)))
						}
					}
				}
			}
		}
	}

	return
}

// ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags.
func ApiGetRemoteExerciceFlags(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 {
			flags, errs := CheckExerciceFlags(GlobalImporter, exercice, []string{}, eexceptions)
			if flags != nil {
				c.JSON(http.StatusOK, flags)
				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))
	return
}
