package config

import (
	"fmt"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"syscall"

	"github.com/pkg/errors"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/template/html"
	"github.com/mattn/go-colorable"
	"github.com/mattn/go-isatty"
	"github.com/oarkflow/log"
)

type ServerConfig struct {
	*fiber.App
	TemplateEngine *html.Engine
	Name           string `mapstructure:"APP_NAME" yaml:"name" env:"APP_NAME" env-default:"iSend.to"`
	Version        string `mapstructure:"APP_VERSION" yaml:"version" env:"APP_VERSION" env-default:"dev"`
	Mode           string `mapstructure:"APP_MODE" yaml:"mode" env:"APP_MODE" env-default:"app"`
	Env            string `mapstructure:"APP_ENV" yaml:"env" env:"APP_ENV" env-default:"dev"`
	Key            string `mapstructure:"APP_KEY" yaml:"key" env:"APP_KEY" env-default:"1894cde6c936a294a478cff0a9227fd276d86df6573b51af5dc59c9064edf426"`
	Url            string `mapstructure:"APP_URL" yaml:"url" env:"APP_URL" env-default:"http://localhost"`
	PublicUrl      string `mapstructure:"APP_PUBLIC_URL" yaml:"public_url" env:"APP_PUBLIC_URL"`
	Host           string `mapstructure:"APP_HOST" yaml:"host" env:"APP_HOST" env-default:"localhost"`
	Port           string `mapstructure:"APP_PORT" yaml:"port" env:"APP_PORT" env-default:"8080"`
	Path           string `mapstructure:"APP_PATH" yaml:"path" env:"APP_PATH"`
	ProxyHeader    string `mapstructure:"PROXY_HEADER" yaml:"PROXY_HEADER" env:"PROXY_HEADER" env-default:"*"`
	AssetPath      string `mapstructure:"ASSET_PATH" yaml:"asset_path" env:"ASSET_PATH" env-default:"assets"`
	PublicPath     string `mapstructure:"PUBLIC_PATH" yaml:"public_path" env:"PUBLIC_PATH" env-default:"public"`
	UploadPath     string `mapstructure:"UPLOAD_PATH" yaml:"upload_path" env:"UPLOAD_PATH" env-default:"uploads"`
	StoragePath    string `mapstructure:"STORAGE_PATH" yaml:"storage_path" env:"STORAGE_PATH" env-default:"storage"`
	LogPath        string `mapstructure:"LOG_PATH" yaml:"log_path" env:"LOG_PATH" env-default:"storage/logs"`
	ExecPath       bool   `mapstructure:"EXEC_PATH" yaml:"exec_path" env:"EXEC_PATH" env-default:"false"`
	Debug          bool   `mapstructure:"APP_DEBUG" yaml:"debug" env:"APP_DEBUG" env-default:"true"`
	UploadSize     int    `mapstructure:"UPLOAD_SIZE" yaml:"upload_size" env:"UPLOAD_SIZE" env-default:"400"`
	Prefork        bool   `mapstructure:"APP_PREFORK" yaml:"prefork" env:"APP_PREFORK" env-default:"false"`
}

func (s *ServerConfig) LoadPath() {
	if s.Url == "" {
		s.Url = fmt.Sprintf("http://localhost:%s", s.Port)
	}
	path, _ := os.Getwd()
	if s.ExecPath {
		path = getPath()
	}
	s.Path = path
	s.UploadPath = MakeDir(filepath.Join(path, s.UploadPath))
	s.AssetPath = MakeDir(filepath.Join(path, s.AssetPath))
	s.StoragePath = MakeDir(filepath.Join(path, s.StoragePath))
	s.LogPath = MakeDir(filepath.Join(path, s.LogPath))
	s.UploadSize = s.UploadSize * 1024 * 1024
}

func (s *ServerConfig) Setup() {

	s.App = fiber.New(fiber.Config{
		Views:                 s.TemplateEngine,
		Concurrency:           256 * 1024 * 1024,
		ServerHeader:          s.Name,
		BodyLimit:             s.UploadSize,
		ReduceMemoryUsage:     true,
		ErrorHandler:          CustomErrorHandler,
		DisableStartupMessage: true,
		ProxyHeader:           s.ProxyHeader,
		Prefork:               s.Prefork,
	})
}

func (s *ServerConfig) Serve(addr ...string) error {
	a := s.Host + ":" + s.Port
	if len(addr) != 0 {
		a = addr[0]
	}
	s.startupMessage(a, false, "")
	return s.Listen(a)
}

func (s *ServerConfig) ServeWithGraceFullShutdown(addr ...string) error {
	a := s.Host + ":" + s.Port
	if len(addr) != 0 {
		a = addr[0]
	}
	s.startupMessage(a, false, "")
	// Listen from a different goroutine
	go func() {
		if err := s.Listen(a); err != nil {
			log.Fatal().Err(err)
		}
	}()

	c := make(chan os.Signal, 1) // Create channel to signify a signal being sent
	signal.Notify(c,
		syscall.SIGINT,
		syscall.SIGTERM,
		syscall.SIGABRT,
		syscall.SIGQUIT,
	) // When an interrupt is sent, notify the channel
	<-c // This blocks the main thread until an interrupt is received
	fmt.Println("I'm shutting down")
	return s.Shutdown()
}

func (s *ServerConfig) startupMessage(addr string, tls bool, processIds string) {
	// ignore child processes
	if fiber.IsChild() {
		return
	}

	var logo string
	logo += "%s"
	logo += " ┌─────────────────────────────────────────────────────┐\n"
	logo += " │ %s   │\n"
	logo += " │ %s   │\n"
	logo += " │                                                     │\n"
	logo += " │ Handlers %s  Processes %s   │\n"
	logo += " │ Prefork .%s  PID ....%s   │\n"
	logo += " └─────────────────────────────────────────────────────┘"
	logo += "%s"

	const (
		cBlack = "\u001b[90m"
		// cRed   = "\u001b[91m"
		cCyan = "\u001b[96m"
		// cGreen = "\u001b[92m"
		// cYellow  = "\u001b[93m"
		// cBlue    = "\u001b[94m"
		// cMagenta = "\u001b[95m"
		// cWhite   = "\u001b[97m"
		cReset = "\u001b[0m"
	)

	value := func(s string, width int) string {
		pad := width - len(s)
		str := ""
		for i := 0; i < pad; i++ {
			str += "."
		}
		if s == "Disabled" {
			str += " " + s
		} else {
			str += fmt.Sprintf(" %s%s%s", cCyan, s, cBlack)
		}
		return str
	}

	center := func(s string, width int) string {
		pad := strconv.Itoa((width - len(s)) / 2)
		str := fmt.Sprintf("%"+pad+"s", " ")
		str += s
		str += fmt.Sprintf("%"+pad+"s", " ")
		if len(str) < width {
			str += " "
		}
		return str
	}

	centerValue := func(s string, width int) string {
		pad := strconv.Itoa((width - len(s)) / 2)
		str := fmt.Sprintf("%"+pad+"s", " ")
		str += fmt.Sprintf("%s%s%s", cCyan, s, cBlack)
		str += fmt.Sprintf("%"+pad+"s", " ")
		if len(str)-10 < width {
			str += " "
		}
		return str
	}

	pad := func(s string, width int) (str string) {
		toAdd := width - len(s)
		str += s
		for i := 0; i < toAdd; i++ {
			str += " "
		}
		return
	}

	host, port := parseAddr(addr)
	if host == "" || host == "0.0.0.0" {
		host = "127.0.0.1"
	}
	addr = "http://" + host + ":" + port
	if tls {
		addr = "https://" + host + ":" + port
	}

	isPrefork := "Disabled"
	if s.Config().Prefork {
		isPrefork = "Enabled"
	}

	procs := strconv.Itoa(runtime.GOMAXPROCS(0))
	if !s.Config().Prefork {
		procs = "1"
	}
	routeCount := 0
	for _, route := range s.Stack() {
		routeCount += len(route)
	}
	mainLogo := fmt.Sprintf(logo,
		cBlack,
		centerValue(s.Name+" "+s.Version, 49),
		center(addr, 49),
		value(strconv.Itoa(routeCount), 14), value(procs, 12),
		value(isPrefork, 14), value(strconv.Itoa(os.Getpid()), 14),
		cReset,
	)

	var childPidsLogo string
	if s.Config().Prefork {
		var childPidsTemplate string
		childPidsTemplate += "%s"
		childPidsTemplate += " ┌───────────────────────────────────────────────────┐\n%s"
		childPidsTemplate += " └───────────────────────────────────────────────────┘"
		childPidsTemplate += "%s"

		newLine := " │ %s%s%s │"

		// Turn the `processIds` variable (in the form ",a,b,c,d,e,f,etc") into a slice of PIDs
		var pidSlice []string
		for _, v := range strings.Split(processIds, ",") {
			if v != "" {
				pidSlice = append(pidSlice, v)
			}
		}

		var lines []string
		thisLine := "Child PIDs ... "
		var itemsOnThisLine []string

		addLine := func() {
			lines = append(lines,
				fmt.Sprintf(
					newLine,
					cBlack,
					thisLine+cCyan+pad(strings.Join(itemsOnThisLine, ", "), 49-len(thisLine)),
					cBlack,
				),
			)
		}

		for _, pid := range pidSlice {
			if len(thisLine+strings.Join(append(itemsOnThisLine, pid), ", ")) > 49 {
				addLine()
				thisLine = ""
				itemsOnThisLine = []string{pid}
			} else {
				itemsOnThisLine = append(itemsOnThisLine, pid)
			}
		}

		// Add left over items to their own line
		if len(itemsOnThisLine) != 0 {
			addLine()
		}

		// Form logo
		childPidsLogo = fmt.Sprintf(childPidsTemplate,
			cBlack,
			strings.Join(lines, "\n")+"\n",
			cReset,
		)
	}

	// Combine both the child PID logo and the main Fiber logo

	// Pad the shorter logo to the length of the longer one
	splitMainLogo := strings.Split(mainLogo, "\n")
	splitChildPidsLogo := strings.Split(childPidsLogo, "\n")

	mainLen := len(splitMainLogo)
	childLen := len(splitChildPidsLogo)

	if mainLen > childLen {
		diff := mainLen - childLen
		for i := 0; i < diff; i++ {
			splitChildPidsLogo = append(splitChildPidsLogo, "")
		}
	} else {
		diff := childLen - mainLen
		for i := 0; i < diff; i++ {
			splitMainLogo = append(splitMainLogo, "")
		}
	}

	// Combine the two logos, line by line
	output := "\n"
	for i := range splitMainLogo {
		output += cBlack + splitMainLogo[i] + " " + splitChildPidsLogo[i] + "\n"
	}

	out := colorable.NewColorableStdout()
	if os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) {
		out = colorable.NewNonColorable(os.Stdout)
	}

	_, _ = fmt.Fprintln(out, output)
}

func (s *ServerConfig) Stop() {
	_ = s.Shutdown()
}

func getPath() string {
	ex, err := os.Executable()
	if err != nil {
		panic(err)
	}
	exPath := filepath.Dir(ex)
	return exPath
}

func MakeDir(path string) string {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		_ = os.MkdirAll(path, os.ModePerm)
	}
	return path
}

func CustomErrorHandler(c *fiber.Ctx, err error) error {
	// StatusCode defaults to 500
	code := fiber.StatusInternalServerError
	//nolint:misspell    // Retrieve the custom statuscode if it's an fiber.*Error
	if e, ok := err.(*fiber.Error); ok {
		code = e.Code
	} //nolint:gofmt,wsl
	er := errors.WithStack(err)
	fmt.Printf("%+v", er)
	fmt.Printf("%+v", err)
	if c.Is("json") {
		return c.Status(code).JSON(err)
	}
	return c.Status(code).Render(fmt.Sprintf("errors/%d", code), fiber.Map{ //nolint:nolintlint,errcheck
		"error": err,
	})
}

func parseAddr(raw string) (host, port string) {
	if i := strings.LastIndex(raw, ":"); i != -1 {
		return raw[:i], raw[i+1:]
	}
	return raw, ""
}
