// Package settings is shared across multiple services for easy parsing and
// retrieval of the challenge settings.
package settings

import (
	"encoding/json"
	"log"
	"os"
	"os/signal"
	"path"
	"syscall"
	"time"

	"gopkg.in/fsnotify.v1"
)

// SettingsFile is the expected name of the file containing the settings.
const SettingsFile = "settings.json"

// SettingsDir is the relative location where the SettingsFile lies.
var SettingsDir string = "./SETTINGS"

// Settings represents the settings panel.
type Settings struct {
	// WorkInProgress indicates if the current challenge is under development or if it is in production.
	WorkInProgress bool `json:"wip,omitempty"`

	// Start is the departure time (expected or effective).
	Start time.Time `json:"start"`
	// End is the expected end time (if empty their is no end-date).
	End *time.Time `json:"end,omitempty"`
	// ActivateTime is the time when the current file should be proceed.
	ActivateTime time.Time `json:"activateTime"`

	// FirstBlood is the coefficient applied to each first team who solve a challenge.
	FirstBlood float64 `json:"firstBlood"`
	// SubmissionCostBase is a complex number representing the cost of each attempts.
	SubmissionCostBase float64 `json:"submissionCostBase"`
	// ExerciceCurrentCoefficient is the current coefficient applied globaly to exercices.
	ExerciceCurCoefficient float64 `json:"exerciceCurrentCoefficient"`
	// HintCurrentCoefficient is the current coefficient applied to hint discovery.
	HintCurCoefficient float64 `json:"hintCurrentCoefficient"`
	// WChoiceCurCoefficient is the current coefficient applied to wanted choices.
	WChoiceCurCoefficient float64 `json:"wchoiceCurrentCoefficient"`
	// GlobalScoreCoefficient is a coefficient to apply on display scores, not considered bonus.
	GlobalScoreCoefficient float64 `json:"globalScoreCoefficient"`
	// DiscountedFactor stores the percentage of the exercice's gain lost on each validation.
	DiscountedFactor float64 `json:"discountedFactor,omitempty"`

	// AllowRegistration permits unregistered Team to register themselves.
	AllowRegistration bool `json:"allowRegistration,omitempty"`
	// CanJoinTeam permits unregistered account to join an already existing team.
	CanJoinTeam bool `json:"canJoinTeam,omitempty"`
	// DenyTeamCreation forces unregistered account to join a team, it's not possible to create a new team.
	DenyTeamCreation bool `json:"denyTeamCreation,omitempty"`
	// DenyNameChange disallow Team to change their name.
	DenyNameChange bool `json:"denyNameChange,omitempty"`
	// IgnoreTeamMembers don't ask team to have known members.
	IgnoreTeamMembers bool `json:"ignoreTeamMembers,omitempty"`
	// AcceptNewIssue enables the reporting system.
	AcceptNewIssue bool `json:"acceptNewIssue,omitempty"`
	// QAenabled enables links to QA interface.
	QAenabled bool `json:"QAenabled,omitempty"`
	// EnableResolutionRoute activates the route displaying resolution movies.
	EnableResolutionRoute bool `json:"enableResolutionRoute,omitempty"`
	// PartialValidation validates each correct given answers, don't expect Team to give all correct answer in a try.
	PartialValidation bool `json:"partialValidation,omitempty"`
	// UnlockedChallengeDepth don't show (or permit to solve) to team challenges they are not unlocked through dependancies.
	UnlockedChallengeDepth int `json:"unlockedChallengeDepth"`
	// UnlockedChallengeUpTo unlock challenge up to a given level of deps.
	UnlockedChallengeUpTo int `json:"unlockedChallengeUpTo"`
	// SubmissionUniqueness don't count multiple times identical tries.
	SubmissionUniqueness bool `json:"submissionUniqueness,omitempty"`
	// CountOnlyNotGoodTries don't count as a try when one good response is given at least.
	CountOnlyNotGoodTries bool `json:"countOnlyNotGoodTries,omitempty"`
	// DisplayAllFlags doesn't respect the predefined constraint existing between flags.
	DisplayAllFlags bool `json:"displayAllFlags,omitempty"`
	// DisplayMCQBadCount activates the report of MCQ bad responses counter.
	DisplayMCQBadCount bool `json:"displayMCQBadCount,omitempty"`
	// EventKindness will ask browsers to delay notification interval.
	EventKindness bool `json:"eventKindness,omitempty"`
	// DisableSubmitButton replace button by this text (eg. scheduled updates, ...).
	DisableSubmitButton string `json:"disablesubmitbutton,omitempty"`
	// GlobalTopMessage display a message on top of each pages.
	GlobalTopMessage string `json:"globaltopmessage,omitempty"`
	// GlobalTopMessageVariant control the variant/color of the previous message.
	GlobalTopMessageVariant string `json:"globaltopmessagevariant,omitempty"`
	// HideHeader will hide the countdown and partners block on front pages.
	HideHeader bool `json:"hide_header,omitempty"`

	// DelegatedQA contains the users allowed to perform administrative tasks on the QA platform.
	DelegatedQA []string `json:"delegated_qa,omitempty"`
}

// ExistsSettings checks if the settings file can by found at the given path.
func ExistsSettings(settingsPath string) bool {
	_, err := os.Stat(settingsPath)
	return !os.IsNotExist(err)
}

// ReadSettings parses the file at the given location.
func ReadSettings(path string) (*Settings, error) {
	var s Settings
	if fd, err := os.Open(path); err != nil {
		return nil, err
	} else {
		defer fd.Close()
		jdec := json.NewDecoder(fd)

		if err := jdec.Decode(&s); err != nil {
			return &s, err
		}

		return &s, nil
	}
}

// SaveSettings saves settings at the given location.
func SaveSettings(path string, s interface{}) error {
	if fd, err := os.Create(path); err != nil {
		return err
	} else {
		defer fd.Close()
		jenc := json.NewEncoder(fd)

		if err := jenc.Encode(s); err != nil {
			return err
		}

		return nil
	}
}

// LoadAndWatchSettings is the function you are looking for!
// Giving the location and a callback, this function will first call your reload function
// before returning (if the file can be parsed); then it starts watching modifications made to
// this file. Your callback is then run each time the file is modified.
func LoadAndWatchSettings(settingsPath string, reload func(*Settings)) {
	// First load of configuration if it exists
	if _, err := os.Stat(settingsPath); !os.IsNotExist(err) {
		if config, err := ReadSettings(settingsPath); err != nil {
			log.Println("ERROR: Unable to read challenge settings:", err)
		} else {
			reload(config)
		}
	}

	// Register SIGHUP
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGHUP)
	go func() {
		for range c {
			log.Println("SIGHUP received, reloading settings...")
			go tryReload(settingsPath, reload)
		}
	}()

	// Watch the configuration file
	if watcher, err := fsnotify.NewWatcher(); err != nil {
		log.Fatal(err)
	} else {
		if err := watcher.Add(path.Dir(settingsPath)); err != nil {
			log.Fatal("Unable to watch: ", path.Dir(settingsPath), ": ", err)
		}

		go func() {
			defer watcher.Close()
			for {
				select {
				case ev := <-watcher.Events:
					if path.Base(ev.Name) == SettingsFile && ev.Op&(fsnotify.Write|fsnotify.Create) != 0 {
						log.Println("Settings file changes, reloading it!")
						go tryReload(settingsPath, reload)
					}
				case err := <-watcher.Errors:
					log.Println("watcher error:", err)
				}
			}
		}()
	}
}

func tryReload(settingsPath string, reload func(*Settings)) {
	if config, err := ReadSettings(settingsPath); err != nil {
		log.Println("ERROR: Unable to read challenge settings:", err)
	} else if time.Until(config.ActivateTime) > 0 {
		log.Println("Configuration reloading postponed, activating at:", config.ActivateTime)
		time.Sleep(time.Until(config.ActivateTime))
		log.Println("Time to activate configuration...")
		tryReload(settingsPath, reload)
	} else {
		reload(config)
	}
}
