package sync

import (
	"bytes"
	"fmt"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"

	"github.com/BurntSushi/toml"
	"github.com/gin-gonic/gin"
	"github.com/yuin/goldmark"

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

func fixnbsp(s string) string {
	return strings.Replace(strings.Replace(strings.Replace(s, " ?", " ?", -1), " !", " !", -1), " :", " :", -1)
}

// GetExercices returns all exercice directories existing in a given theme, considering the given Importer.
func GetExercices(i Importer, theme *fic.Theme) ([]string, error) {
	var exercices []string

	if len(theme.Path) == 0 {
		return []string{}, nil
	} else if dirs, err := i.listDir(theme.Path); err != nil {
		return []string{}, err
	} else {
		for _, dir := range dirs {
			if _, err := i.listDir(path.Join(theme.Path, dir)); err == nil {
				if dir[0] != '.' && strings.Contains(dir, "-") {
					exercices = append(exercices, dir)
				}
			}
		}
	}

	return exercices, nil
}

func buildDependancyMap(i Importer, theme *fic.Theme) (dmap map[int64]*fic.Exercice, err error) {
	var exercices []string
	if exercices, err = GetExercices(i, theme); err != nil {
		return
	} else {
		dmap = map[int64]*fic.Exercice{}

		for _, edir := range exercices {
			var eid int
			var ename string
			eid, ename, err = parseExerciceDirname(edir)
			if err != nil {
				err = nil
				continue
			}

			var e *fic.Exercice
			e, err = theme.GetExerciceByTitle(ename)
			if err != nil {
				return
			}

			dmap[int64(eid)] = e
		}

		return
	}
}

func parseExerciceDirname(edir string) (eid int, ename string, err error) {
	edir_splt := strings.SplitN(edir, "-", 2)
	if len(edir_splt) != 2 {
		err = fmt.Errorf("%q is not a valid exercice directory: missing id prefix", edir)
		return
	}

	eid, err = strconv.Atoi(edir_splt[0])
	if err != nil {
		err = fmt.Errorf("%q: invalid exercice identifier: %s", edir, err)
		return
	}

	ename = edir_splt[1]

	return
}

// BuildExercice creates an Exercice from a given importer.
func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*fic.Exercice, exceptions *CheckExceptions) (e *fic.Exercice, p ExerciceParams, eid int, edir string, errs []error) {
	e = &fic.Exercice{}

	e.Path = epath
	edir = path.Base(epath)

	var err error
	eid, e.Title, err = parseExerciceDirname(edir)
	if err != nil {
		errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to parse exercice directory: %w", err), theme))
		return nil, p, eid, edir, errs
	}

	// Limit exceptions to this exercice
	exceptions = exceptions.GetExerciceExceptions(e)
	//log.Printf("Kept repochecker exceptions for this exercice: %v", exceptions)

	// Overwrite title if title.txt exists
	if myTitle, err := GetFileContent(i, path.Join(epath, "title.txt")); err == nil {
		myTitle = strings.TrimSpace(myTitle)
		if strings.Contains(myTitle, "\n") {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("title.txt: Title can't contain new lines"), theme))
		} else {
			e.Title = myTitle
		}
	}

	e.URLId = fic.ToURLid(e.Title)
	e.Title = fixnbsp(e.Title)

	// Texts to format using Markdown
	if i.exists(path.Join(epath, "overview.txt")) {
		e.Overview, err = GetFileContent(i, path.Join(epath, "overview.txt"))
	} else if i.exists(path.Join(epath, "overview.md")) {
		e.Overview, err = GetFileContent(i, path.Join(epath, "overview.md"))
	} else {
		err = fmt.Errorf("Unable to find overview.txt nor overview.md")
	}
	if err != nil {
		errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.txt: %s", err), theme))
	} else {
		e.Overview = fixnbsp(e.Overview)

		// Call checks hooks
		for _, h := range hooks.mdTextHooks {
			for _, err := range h(e.Overview, exceptions.GetFileExceptions("overview.md", "overview.txt")) {
				errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.md: %w", err)))
			}
		}

		var buf bytes.Buffer
		err := goldmark.Convert([]byte(strings.Split(e.Overview, "\n")[0]), &buf)
		if err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.md: an error occurs during markdown formating of the headline: %w", err), theme))
		} else {
			e.Headline = string(buf.Bytes())
		}

		if e.Overview, err = ProcessMarkdown(i, e.Overview, epath); err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.md: an error occurs during markdown formating: %w", err), theme))
		}
	}

	if i.exists(path.Join(epath, "statement.txt")) {
		e.Statement, err = GetFileContent(i, path.Join(epath, "statement.txt"))
	} else if i.exists(path.Join(epath, "statement.md")) {
		e.Statement, err = GetFileContent(i, path.Join(epath, "statement.md"))
	} else {
		err = fmt.Errorf("Unable to find statement.txt nor statement.md")
	}
	if err != nil {
		errs = append(errs, NewExerciceError(e, fmt.Errorf("statement.md: %w", err), theme))
	} else {
		// Call checks hooks
		for _, h := range hooks.mdTextHooks {
			for _, err := range h(e.Statement, exceptions.GetFileExceptions("statement.md", "statement.txt")) {
				errs = append(errs, NewExerciceError(e, fmt.Errorf("statement.md: %w", err)))
			}
		}

		if e.Statement, err = ProcessMarkdown(i, fixnbsp(e.Statement), epath); err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("statement.md: an error occurs during markdown formating: %w", err), theme))
		}
	}

	if i.exists(path.Join(epath, "finished.txt")) {
		e.Finished, err = GetFileContent(i, path.Join(epath, "finished.txt"))
		if err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("finished.txt: %w", err), theme))
		} else {
			// Call checks hooks
			for _, h := range hooks.mdTextHooks {
				for _, err := range h(e.Finished, exceptions.GetFileExceptions("finished.md", "finished.txt")) {
					errs = append(errs, NewExerciceError(e, fmt.Errorf("finished.md: %w", err)))
				}
			}

			if e.Finished, err = ProcessMarkdown(i, e.Finished, epath); err != nil {
				errs = append(errs, NewExerciceError(e, fmt.Errorf("finished.txt: an error occurs during markdown formating: %w", err), theme))
			}
		}
	}

	// Parse challenge.txt
	var md toml.MetaData
	p, md, err = parseExerciceParams(i, epath)
	if err != nil {
		errs = append(errs, NewChallengeTxtError(e, 0, err, theme))
		return
	}

	// Alert about unknown keys in challenge.txt
	if len(md.Undecoded()) > 0 {
		for _, k := range md.Undecoded() {
			errs = append(errs, NewChallengeTxtError(e, 0, fmt.Errorf("unknown key %q found, check https://srs.nemunai.re/fic/files/challenge/", k), theme))
		}
	}

	if p.Gain == 0 {
		errs = append(errs, NewChallengeTxtError(e, 0, fmt.Errorf("Undefined gain for challenge"), theme))
	} else {
		e.Gain = p.Gain
	}

	// Handle dependency
	if len(p.Dependencies) > 0 {
		if len(p.Dependencies[0].Theme) > 0 && p.Dependencies[0].Theme != theme.Name {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to treat dependency to another theme (%q): not implemented.", p.Dependencies[0].Theme), theme))
		} else {
			if dmap == nil {
				if dmap2, err := buildDependancyMap(i, theme); err != nil {
					errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to build dependency map: %w", err), theme))
				} else {
					dmap = &dmap2
				}
			}

			if dmap != nil {
				for edk, ed := range *dmap {
					if edk == p.Dependencies[0].Id {
						e.Depend = &ed.Id
						break
					}
				}
				if e.Depend == nil {
					dmap_keys := []string{}
					for k, _ := range *dmap {
						dmap_keys = append(dmap_keys, fmt.Sprintf("%d", k))
					}
					errs = append(errs, NewExerciceError(e, fmt.Errorf("Unable to find required exercice dependancy %d (available at time of processing: %s)", p.Dependencies[0].Id, strings.Join(dmap_keys, ",")), theme))
				}
			}
		}
	}

	// Handle resolutions
	resolutionFound := false

	e.VideoURI = path.Join(epath, "resolution.mp4")
	if !i.exists(e.VideoURI) {
		e.VideoURI = ""
	} else if size, err := getFileSize(i, e.VideoURI); err != nil {
		errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.mp4: %w", err), theme))
		e.VideoURI = ""
	} else if size == 0 {
		errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.mp4: The file is empty!"), theme))
		e.VideoURI = ""
	} else {
		e.VideoURI = strings.Replace(url.PathEscape(path.Join("$RFILES$", e.VideoURI)), "%2F", "/", -1)
		resolutionFound = true
	}

	writeup := path.Join(epath, "resolution.md")
	if !i.exists(writeup) {
		writeup = path.Join(epath, "resolution.txt")
	}

	if i.exists(writeup) {
		if size, err := getFileSize(i, writeup); err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err), theme))
		} else if size == 0 {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: The file is empty!"), theme))
		} else if e.Resolution, err = GetFileContent(i, writeup); err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err), theme))
		} else if e.Resolution, err = ProcessMarkdown(i, e.Resolution, epath); err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: error during markdown processing: %w", err), theme))
		} else {
			resolutionFound = true
		}
	}

	if !resolutionFound {
		errs = append(errs, NewExerciceError(e, ErrResolutionNotFound, theme))
	}

	// Call checks hooks
	for _, h := range hooks.exerciceHooks {
		for _, err := range h(e, exceptions) {
			errs = append(errs, NewExerciceError(e, err))
		}
	}
	return
}

// SyncExercice imports new or updates existing given exercice.
func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*fic.Exercice, exceptions *CheckExceptions) (e *fic.Exercice, eid int, errs []error) {
	var err error
	var p ExerciceParams
	var berrors []error

	e, p, eid, _, berrors = BuildExercice(i, theme, epath, dmap, exceptions)
	for _, e := range berrors {
		errs = append(errs, e)
	}

	if e != nil {
		// Create or update the exercice
		err = theme.SaveNamedExercice(e)
		if err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("error on exercice save: %w", err), theme))
			return
		}

		// Import eercice tags
		if _, err := e.WipeTags(); err != nil {
			errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to wipe tags: %w", err), theme))
		}
		for _, tag := range p.Tags {
			if _, err := e.AddTag(tag); err != nil {
				errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to add tag: %w", err), theme))
				return
			}
		}
	}

	return
}

// SyncExercices imports new or updates existing exercices, in a given theme.
func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (errs []error) {
	if exercices, err := GetExercices(i, theme); err != nil {
		errs = append(errs, err)
	} else {
		emap := map[string]int{}

		dmap, _ := buildDependancyMap(i, theme)

		for _, edir := range exercices {
			e, eid, cur_errs := SyncExercice(i, theme, path.Join(theme.Path, edir), &dmap, exceptions)
			if e != nil {
				emap[e.Title] = eid
				dmap[int64(eid)] = e
				errs = append(errs, cur_errs...)
			}
		}

		// Remove old exercices
		if exercices, err := theme.GetExercices(); err == nil {
			for _, ex := range exercices {
				if _, ok := emap[ex.Title]; !ok {
					ex.DeleteCascade()
				}
			}
		}
	}
	return
}

// ApiListRemoteExercices is an accessor letting foreign packages to access remote exercices list.
func ApiListRemoteExercices(c *gin.Context) {
	theme, _, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
	if theme != nil {
		exercices, err := GetExercices(GlobalImporter, theme)
		if err != nil {
			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()})
			return
		}

		c.JSON(http.StatusOK, exercices)
	} else {
		c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs))
		return
	}
}

// ApiListRemoteExercice is an accessor letting foreign packages to access remote exercice attributes.
func ApiGetRemoteExercice(c *gin.Context) {
	theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid"))
	if theme != nil {
		exercice, _, _, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions)
		if exercice != nil {
			c.JSON(http.StatusOK, exercice)
			return
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
			return
		}
	} else {
		c.JSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Errorf("%q", errs)})
		return
	}
}
