package cmd

import (
	"bytes"
	_ "embed"
	"fmt"
	"html/template"
	"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

//go:embed serve-templates/index.html
var indexPage string

// 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) {
		port := strconv.Itoa(viper.GetInt("serve.port"))

		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)
			}
			packServeDir := filepath.Dir(viper.GetString("pack-file"))
			packFileName := filepath.Base(viper.GetString("pack-file"))

			t, err := template.New("index-page").Parse(indexPage)
			if err != nil {
				fmt.Println(err)
				os.Exit(1)
			}

			indexPageBuf := new(bytes.Buffer)
			err = t.Execute(indexPageBuf, struct{ Port string }{Port: port})
			if err != nil {
				panic(fmt.Errorf("failed to compile index page template: %w", err))
			}

			// Force-disable no-internal-hashes mode (equiv to --build flag in refresh) for serving over HTTP
			if viper.GetBool("no-internal-hashes") {
				fmt.Println("Note: no-internal-hashes mode is set; still writing hashes for use with packwiz-installer - run packwiz refresh to remove them.")
				viper.Set("no-internal-hashes", false)
			}

			http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
				if req.URL.Path == "/" {
					_, _ = w.Write(indexPageBuf.Bytes())
					return
				}

				// Relative to pack.toml
				urlPath := strings.TrimPrefix(path.Clean("/"+strings.TrimPrefix(req.URL.Path, "/")), "/")
				// Convert to absolute
				destPath := filepath.Join(packServeDir, filepath.FromSlash(urlPath))
				// Relativisation needs to be done using filepath, as path doesn't have Rel!
				// (now using index util function)
				// Relative to index.toml ("pack root")
				indexRelPath, err := index.RelIndexPath(destPath)
				if err != nil {
					fmt.Println("Failed to parse path", err)
					return
				}

				if urlPath == path.Clean(pack.Index.File) {
					// Must be done here, to ensure all paths gain the lock at some point
					refreshMutex.RLock()
				} else if urlPath == packFileName { // Only need to compare name - already relative to pack.toml
					if viper.GetBool("serve.refresh") {
						// Get write lock, to do a refresh
						refreshMutex.Lock()
						// Reload pack and index (might have changed on disk)
						err = doServeRefresh(&pack, &index)
						if err != nil {
							fmt.Println("Failed to refresh pack", err)
							return
						}

						// Downgrade to a read lock
						refreshMutex.Unlock()
					}
					refreshMutex.RLock()
				} else {
					refreshMutex.RLock()
					// Only allow indexed files
					if _, found := index.Files[indexRelPath]; !found {
						fmt.Printf("File not found: %s\n", destPath)
						refreshMutex.RUnlock()
						w.WriteHeader(404)
						_, _ = w.Write([]byte("File not found"))
						return
					}
				}
				defer refreshMutex.RUnlock()

				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
				}
			})
		}

		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 doServeRefresh(pack *core.Pack, index *core.Index) error {
	var err error
	*pack, err = core.LoadPack()
	if err != nil {
		return err
	}
	*index, err = pack.LoadIndex()
	if err != nil {
		return err
	}
	err = index.Refresh()
	if err != nil {
		return err
	}
	err = index.Write()
	if err != nil {
		return err
	}
	err = pack.UpdateIndexHash()
	if err != nil {
		return err
	}
	err = pack.Write()
	if err != nil {
		return err
	}
	fmt.Println("Index refreshed!")

	return nil
}

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"))
}