package cmd

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"sync"

	"github.com/packwiz/packwiz/core"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var refreshMutex sync.RWMutex

// serveCmd represents the serve command
var serveCmd = &cobra.Command{
	Use:     "serve",
	Short:   "Run a local development server",
	Long:    `Run a local HTTP server for development, automatically refreshing the index when it is queried`,
	Aliases: []string{"server"},
	Args:    cobra.NoArgs,
	Run: func(cmd *cobra.Command, args []string) {
		if viper.GetBool("serve.basic") {
			http.Handle("/", http.FileServer(http.Dir(".")))
		} else {
			fmt.Println("Loading modpack...")
			pack, err := core.LoadPack()
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
			index, err := pack.LoadIndex()
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}
			indexPath := filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))
			indexDir := filepath.Dir(indexPath)

			http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
				urlPath := strings.TrimPrefix(path.Clean("/"+strings.TrimPrefix(req.URL.Path, "/")), "/")
				indexRelPath, err := filepath.Rel(indexDir, filepath.FromSlash(urlPath))
				if err != nil {
					fmt.Println(err)
					return
				}
				indexRelPathSlash := path.Clean(filepath.ToSlash(indexRelPath))
				var destPath string

				found := false
				if urlPath == filepath.ToSlash(indexPath) {
					found = true
					destPath = indexPath
					// Must be done here, to ensure all paths gain the lock at some point
					refreshMutex.RLock()
				} else if urlPath == filepath.ToSlash(viper.GetString("pack-file")) {
					found = true
					if viper.GetBool("serve.refresh") {
						// Get write lock, to do a refresh
						refreshMutex.Lock()
						// Reload pack and index (might have changed on disk)
						pack, err = core.LoadPack()
						if err != nil {
							fmt.Println(err)
							return
						}
						index, err = pack.LoadIndex()
						if err != nil {
							fmt.Println(err)
							return
						}
						err = index.Refresh()
						if err != nil {
							fmt.Println(err)
							return
						}
						err = index.Write()
						if err != nil {
							fmt.Println(err)
							return
						}
						err = pack.UpdateIndexHash()
						if err != nil {
							fmt.Println(err)
							return
						}
						err = pack.Write()
						if err != nil {
							fmt.Println(err)
							return
						}
						fmt.Println("Index refreshed!")

						// Downgrade to a read lock
						refreshMutex.Unlock()
					}
					refreshMutex.RLock()
					destPath = viper.GetString("pack-file")
				} else {
					refreshMutex.RLock()
					// Only allow indexed files
					for _, v := range index.Files {
						if indexRelPathSlash == v.File {
							found = true
							break
						}
					}
					if found {
						destPath = filepath.FromSlash(urlPath)
					}
				}
				defer refreshMutex.RUnlock()
				if found {
					f, err := os.Open(destPath)
					if err != nil {
						fmt.Printf("Error reading file \"%s\": %s\n", destPath, err)
						w.WriteHeader(404)
						_, _ = w.Write([]byte("File not found"))
						return
					}
					_, err = io.Copy(w, f)
					err2 := f.Close()
					if err == nil {
						err = err2
					}
					if err != nil {
						fmt.Printf("Error reading file \"%s\": %s\n", destPath, err)
						w.WriteHeader(500)
						_, _ = w.Write([]byte("Failed to read file"))
						return
					}
				} else {
					fmt.Printf("File not found: %s\n", destPath)
					w.WriteHeader(404)
					_, _ = w.Write([]byte("File not found"))
					return
				}
			})
		}

		port := strconv.Itoa(viper.GetInt("serve.port"))
		fmt.Println("Running on port " + port)
		err := http.ListenAndServe(":"+port, nil)
		if err != nil {
			fmt.Printf("Error running server: %s\n", err)
			os.Exit(1)
		}
	},
}

func init() {
	rootCmd.AddCommand(serveCmd)

	serveCmd.Flags().IntP("port", "p", 8080, "The port to run the server on")
	_ = viper.BindPFlag("serve.port", serveCmd.Flags().Lookup("port"))
	serveCmd.Flags().BoolP("refresh", "r", true, "Automatically refresh the index file")
	_ = viper.BindPFlag("serve.refresh", serveCmd.Flags().Lookup("refresh"))
	serveCmd.Flags().Bool("basic", false, "Disable refreshing and allow all files in the directory, rather than just files listed in the index")
	_ = viper.BindPFlag("serve.basic", serveCmd.Flags().Lookup("basic"))
}