package main

import (
	"flag"
	"io/ioutil"
	"log"
	"math/rand"
	"os"
	"os/signal"
	"path"
	"strconv"
	"strings"
	"syscall"
	"time"

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

	"gopkg.in/fsnotify.v1"
)

var TeamsDir string
var SubmissionDir string

func watchsubdir(watcher *fsnotify.Watcher, pathname string) error {
	log.Println("Watch new directory:", pathname)
	if err := watcher.Add(pathname); err != nil {
		return err
	}

	if ds, err := ioutil.ReadDir(pathname); err != nil {
		return err
	} else {
		for _, d := range ds {
			p := path.Join(pathname, d.Name())
			if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
				if err := watchsubdir(watcher, p); err != nil {
					return err
				}
			} else if d.Mode().IsRegular() {
				go treat(p)
			}
		}
		return nil
	}
}

func walkAndTreat(pathname string) error {
	if ds, err := ioutil.ReadDir(pathname); err != nil {
		return err
	} else {
		for _, d := range ds {
			p := path.Join(pathname, d.Name())
			if d.IsDir() && d.Name() != ".tmp" && d.Mode()&os.ModeSymlink == 0 {
				if err := walkAndTreat(p); err != nil {
					return err
				}
			} else if d.Mode().IsRegular() {
				treat(p)
			}
		}
		return nil
	}
}

var ChStarted = false
var lastRegeneration time.Time
var skipInitialGeneration = false

func reloadSettings(config *settings.Settings) {
	allowRegistration = config.AllowRegistration
	canJoinTeam = config.CanJoinTeam
	denyTeamCreation = config.DenyTeamCreation
	canResetProgression = config.WorkInProgress && config.CanResetProgression
	fic.HintCoefficient = config.HintCurCoefficient
	fic.WChoiceCoefficient = config.WChoiceCurCoefficient
	fic.ExerciceCurrentCoefficient = config.ExerciceCurCoefficient
	ChStarted = config.Start.Unix() > 0 && time.Since(config.Start) >= 0
	fic.PartialValidation = config.PartialValidation
	fic.UnlockedChallengeDepth = config.UnlockedChallengeDepth
	fic.UnlockedChallengeUpTo = config.UnlockedChallengeUpTo
	fic.UnlockedStandaloneExercices = config.UnlockedStandaloneExercices
	fic.UnlockedStandaloneExercicesByThemeStepValidation = config.UnlockedStandaloneExercicesByThemeStepValidation
	fic.UnlockedStandaloneExercicesByStandaloneExerciceValidation = config.UnlockedStandaloneExercicesByStandaloneExerciceValidation
	fic.DisplayAllFlags = config.DisplayAllFlags

	fic.FirstBlood = config.FirstBlood
	fic.SubmissionCostBase = config.SubmissionCostBase
	fic.SubmissionUniqueness = config.SubmissionUniqueness
	fic.GlobalScoreCoefficient = config.GlobalScoreCoefficient
	fic.CountOnlyNotGoodTries = config.CountOnlyNotGoodTries
	fic.DiscountedFactor = config.DiscountedFactor
}

func main() {
	var dsn = flag.String("dsn", fic.DSNGenerator(), "DSN to connect to the MySQL server")
	flag.StringVar(&generatorSocket, "generator", "./GENERATOR/generator.socket", "Path to the generator socket")
	flag.StringVar(&settings.SettingsDir, "settings", "./SETTINGSDIST", "Base directory where load and save settings")
	flag.StringVar(&SubmissionDir, "submission", "./submissions", "Base directory where save submissions")
	flag.StringVar(&TeamsDir, "teams", "./TEAMS", "Base directory where save teams JSON files")
	var debugINotify = flag.Bool("debuginotify", false, "Show skipped inotofy events")
	flag.Parse()

	log.SetPrefix("[checker] ")

	settings.SettingsDir = path.Clean(settings.SettingsDir)
	SubmissionDir = path.Clean(SubmissionDir)
	TeamsDir = path.Clean(TeamsDir)

	rand.Seed(time.Now().UnixNano())

	log.Println("Creating submission directory...")
	if _, err := os.Stat(path.Join(SubmissionDir, ".tmp")); os.IsNotExist(err) {
		if err := os.MkdirAll(path.Join(SubmissionDir, ".tmp"), 0700); err != nil {
			log.Fatal("Unable to create submission directory: ", err)
		}
	}

	log.Println("Opening DB...")
	if err := fic.DBInit(*dsn); err != nil {
		log.Fatal("Cannot open the database: ", err)
	}
	defer fic.DBClose()

	// Load configuration
	settings.LoadAndWatchSettings(path.Join(settings.SettingsDir, settings.SettingsFile), reloadSettings)

	log.Println("Registering directory events...")
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	if err := watchsubdir(watcher, SubmissionDir); err != nil {
		log.Fatal(err)
	}

	// Register SIGUSR1 and SIGTERM
	interrupt1 := make(chan os.Signal, 1)
	signal.Notify(interrupt1, syscall.SIGUSR1)
	interrupt := make(chan os.Signal, 1)
	signal.Notify(interrupt, syscall.SIGTERM)

	watchedNotify := fsnotify.Create

loop:
	for {
		select {
		case <-interrupt:
			break loop
		case <-interrupt1:
			log.Println("SIGUSR1 received, retreating all files in queue...")
			walkAndTreat(SubmissionDir)
			log.Println("SIGUSR1 treated.")
		case ev := <-watcher.Events:
			if d, err := os.Lstat(ev.Name); err == nil && ev.Op&fsnotify.Create == fsnotify.Create && d.Mode().IsDir() && d.Mode()&os.ModeSymlink == 0 && d.Name() != ".tmp" {
				// Register new subdirectory
				if err := watchsubdir(watcher, ev.Name); err != nil {
					log.Println(err)
				}
			} else if err == nil && ev.Op&watchedNotify == watchedNotify && d.Mode().IsRegular() {
				if *debugINotify {
					log.Println("Treating event:", ev, "for", ev.Name)
				}
				go treat(ev.Name)
			} else if err == nil && ev.Op&fsnotify.Write == fsnotify.Write {
				log.Println("FSNOTIFY WRITE SEEN. Prefer looking at them, as it appears files are not atomically moved.")
				watchedNotify = fsnotify.Write
				go treat(ev.Name)
			} else if err == nil && *debugINotify {
				log.Println("Skipped event:", ev, "for", ev.Name)
			}
		case err := <-watcher.Errors:
			log.Println("error:", err)
		}
	}
}

func treat(raw_path string) {
	// Extract
	spath := strings.Split(strings.TrimPrefix(raw_path, SubmissionDir), "/")

	if len(spath) == 3 {
		if spath[1] == "_registration" {
			treatRegistration(raw_path, spath[2])
			return
		}

		var teamid int64
		var err error

		if teamid, err = strconv.ParseInt(spath[1], 10, 64); err != nil {
			if lnk, err := os.Readlink(path.Join(TeamsDir, spath[1])); err != nil {
				log.Printf("[ERR] Unable to readlink %q: %s\n", path.Join(TeamsDir, spath[1]), err)
				return
			} else if teamid, err = strconv.ParseInt(lnk, 10, 64); err != nil {
				log.Printf("[ERR] Error during ParseInt team %q: %s\n", lnk, err)
				return
			}
		}

		var team *fic.Team
		if team, err = fic.GetTeam(teamid); err != nil {
			log.Printf("[ERR] Unable to retrieve team %d: %s\n", teamid, err)
			return
		}

		switch spath[2] {
		case "name":
			treatRename(raw_path, team)
		case "issue":
			treatIssue(raw_path, team)
		case "hint":
			treatOpeningHint(raw_path, team)
		case "choices":
			treatWantChoices(raw_path, team)
		case "reset_progress":
			treatResetProgress(raw_path, team)
		case ".locked":
			treatLocked(raw_path, team)
		default:
			treatSubmission(raw_path, team, spath[2])
		}
	} else {
		log.Println("Invalid new file:", raw_path)
	}
}
