package main

import (
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path"
	"runtime"
	"sync"
	"time"

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

var parallelJobs = runtime.NumCPU()
var genQueue chan *fic.GenStruct
var inQueueMutex sync.RWMutex
var inGenQueue map[fic.GenerateType]bool

func init() {
	genQueue = make(chan *fic.GenStruct)
	inGenQueue = map[fic.GenerateType]bool{}
}

func launchWorkers() {
	log.Println("Running with", parallelJobs, "worker(s)")
	for i := parallelJobs; i > 0; i-- {
		go consumer()
	}
}

func enqueueHandler(w http.ResponseWriter, r *http.Request) {
	var gs fic.GenStruct

	dec := json.NewDecoder(r.Body)
	err := dec.Decode(&gs)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	log.Printf("POST /enqueue | %v", gs)

	appendGenQueue(gs)
	http.Error(w, "OK", http.StatusOK)
}

func performHandler(w http.ResponseWriter, r *http.Request) {
	var gs fic.GenStruct

	dec := json.NewDecoder(r.Body)
	err := dec.Decode(&gs)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	log.Printf("POST /perform | %v", gs)

	err = <-appendGenQueue(gs).GenEnded()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	} else {
		http.Error(w, "OK", http.StatusOK)
	}
}

func performFullResyncHandler(w http.ResponseWriter, r *http.Request) {
	log.Printf("POST /full-resync started")

	waitList := genAll()
	var errs string
	for _, e := range waitList {
		err := <-e
		if err != nil {
			errs += err.Error() + "\n"
		}
	}

	if errs != "" {
		lastRegeneration = time.Now()
		http.Error(w, errs, http.StatusInternalServerError)
	} else {
		http.Error(w, "done", http.StatusOK)
	}

	log.Printf("POST /full-resync done")
}

func appendGenQueue(gs fic.GenStruct) *fic.GenStruct {
	if gs.Type == fic.GenTeam || gs.Type == fic.GenTeamIssues {
		genQueue <- &gs
		return &gs
	}

	// Append only if not already in queue
	inQueueMutex.RLock()
	if v, ok := inGenQueue[gs.Type]; !ok || !v {
		inQueueMutex.RUnlock()

		inQueueMutex.Lock()
		inGenQueue[gs.Type] = true
		inQueueMutex.Unlock()

		genQueue <- &gs
	} else {
		inQueueMutex.RUnlock()
	}

	return &gs
}

func consumer() {
	var id string
	var err error

	for {
		gs := <-genQueue
		id = gs.Id

		inQueueMutex.Lock()
		inGenQueue[gs.Type] = false
		inQueueMutex.Unlock()

		switch gs.Type {
		case fic.GenPublic:
			err = genMyPublicFile()
		case fic.GenEvents:
			err = genEventsFile()
		case fic.GenTeam:
			err = genTeamMyFile(gs.TeamId)
		case fic.GenTeams:
			err = genTeamsFile()
		case fic.GenThemes:
			err = genThemesFile()
		case fic.GenTeamIssues:
			err = genTeamIssuesFile(gs.TeamId)
		}

		if err != nil {
			log.Println(id, "[ERR] Unable to generate:", err)
		}
		gs.End(err)
	}
}

// Generate issues.json for a given team
func genTeamIssuesFile(teamid int64) error {
	team, err := fic.GetTeam(teamid)
	if err != nil {
		return fmt.Errorf("Unable to GetTeam: %w", err)
	}

	dirPath := path.Join(TeamsDir, fmt.Sprintf("%d", team.Id))

	my, err := team.MyIssueFile()
	if err != nil {
		return err
	}

	if my == nil {
		if _, err := os.Stat(path.Join(dirPath, "issues.json")); !os.IsNotExist(err) {
			err = os.Remove(path.Join(dirPath, "issues.json"))
			if err != nil {
				log.Printf("Unable to remove empty issues file: %s", path.Join(dirPath, "issues.json"))
			}
		}
		return nil
	}

	if s, err := os.Stat(dirPath); os.IsNotExist(err) {
		os.MkdirAll(dirPath, 0751)
	} else if !s.IsDir() {
		return fmt.Errorf("%s is not a directory", dirPath)
	}

	if j, err := json.Marshal(my); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(dirPath, "issues.json"), j, 0644); err != nil {
		return err
	}

	return nil
}

// Generate my.json, wait.json and scores.json for a given team
func genTeamMyFile(teamid int64) error {
	team, err := fic.GetTeam(teamid)
	if err != nil {
		return fmt.Errorf("Unable to GetTeam: %w", err)
	}

	dirPath := path.Join(TeamsDir, fmt.Sprintf("%d", team.Id))

	if s, err := os.Stat(dirPath); os.IsNotExist(err) {
		os.MkdirAll(dirPath, 0751)
	} else if !s.IsDir() {
		return fmt.Errorf("%s is not a directory", dirPath)
	}

	if my, err := fic.MyJSONTeam(team, true); err != nil {
		return err
	} else if j, err := json.Marshal(my); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(dirPath, "my.json"), j, 0666); err != nil {
		return err
	}

	// Speed up generation when challenge is started
	if !ChStarted {
		if my, err := fic.MyJSONTeam(team, false); err != nil {
			return err
		} else if j, err := json.Marshal(my); err != nil {
			return err
		} else if err = ioutil.WriteFile(path.Join(dirPath, "wait.json"), j, 0666); err != nil {
			return err
		}
	} else {
		if scores, err := team.ScoreGrid(); err != nil {
			return err
		} else if j, err := json.Marshal(scores); err != nil {
			return err
		} else if err = ioutil.WriteFile(path.Join(dirPath, "scores.json"), j, 0666); err != nil {
			return err
		}
	}

	return nil
}

// Generate public my.json file
func genMyPublicFile() error {
	dirPath := path.Join(TeamsDir, "public")

	if s, err := os.Stat(dirPath); os.IsNotExist(err) {
		os.MkdirAll(dirPath, 0751)
	} else if !s.IsDir() {
		return fmt.Errorf("%s is not a directory", dirPath)
	}

	if my, err := fic.MyJSONTeam(nil, true); err != nil {
		return err
	} else if j, err := json.Marshal(my); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(dirPath, "my.json"), j, 0666); err != nil {
		return err
	}

	os.Symlink("my.json", path.Join(dirPath, "wait.json"))

	if teams, err := fic.ExportTeams(true); err != nil {
		return err
	} else if j, err := json.Marshal(teams); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(dirPath, "teams.json"), j, 0666); err != nil {
		return err
	}

	return nil
}

// Generate general evemts.json file
func genEventsFile() error {
	if evts, err := fic.GetLastEvents(); err != nil {
		return err
	} else if j, err := json.Marshal(evts); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(TeamsDir, "events.json"), j, 0666); err != nil {
		return err
	}

	return nil
}

// Generate general teams.json file
func genTeamsFile() error {
	if teams, err := fic.ExportTeams(false); err != nil {
		return err
	} else if j, err := json.Marshal(teams); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(TeamsDir, "teams.json"), j, 0666); err != nil {
		return err
	}

	if teams, err := fic.ExportTeams(true); err != nil {
		return err
	} else if j, err := json.Marshal(teams); err != nil {
		return err
	} else if err = ioutil.WriteFile(path.Join(TeamsDir, "public", "teams.json"), j, 0666); err != nil {
		return err
	}

	return nil
}

// Generate general themes.json file
func genThemesFile() error {
	themes, err := fic.ExportThemes()
	if err != nil {
		return fmt.Errorf("unable to generate themes: %w", err)
	}

	var wr io.Writer

	themesfd, err := os.Create(path.Join(TeamsDir, "themes.json"))
	if err != nil {
		return fmt.Errorf("unable to re-create themes.json: %w", err)
	}
	defer themesfd.Close()

	themeswait, err := os.Create(path.Join(TeamsDir, "themes-wait.json"))
	if err != nil {
		return fmt.Errorf("unable to re-create themes-wait.json: %w", err)
	}
	defer themeswait.Close()

	if allowRegistration {
		wr = io.MultiWriter(themesfd, themeswait)
	} else {
		wr = themesfd
	}

	enc := json.NewEncoder(wr)
	err = enc.Encode(themes)
	if err != nil {
		return fmt.Errorf("unable to encode themes.json: %w", err)
	}

	if !allowRegistration {
		enc = json.NewEncoder(themeswait)
		err = enc.Encode(map[string]string{})
		if err != nil {
			return fmt.Errorf("unable to encode themes-wait.json: %w", err)
		}
	}

	return nil
}

func genAll() (waitList []chan error) {
	waitList = append(
		waitList,
		appendGenQueue(fic.GenStruct{Type: fic.GenThemes}).GenEnded(),
		appendGenQueue(fic.GenStruct{Type: fic.GenTeams}).GenEnded(),
		appendGenQueue(fic.GenStruct{Type: fic.GenEvents}).GenEnded(),
		appendGenQueue(fic.GenStruct{Type: fic.GenPublic}).GenEnded(),
	)

	if teams, err := fic.GetActiveTeams(); err != nil {
		log.Println("Team retrieval error: ", err)
	} else {
		for _, team := range teams {
			waitList = append(
				waitList,
				appendGenQueue(fic.GenStruct{Type: fic.GenTeam, TeamId: team.Id}).GenEnded(),
				appendGenQueue(fic.GenStruct{Type: fic.GenTeamIssues, TeamId: team.Id}).GenEnded(),
			)
		}
	}

	return
}
