package main

import (
	"embed"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"
	"website/internal/config"

	"github.com/ansrivas/fiberprometheus/v2"
	"github.com/getsentry/sentry-go"
	"github.com/gofiber/contrib/fibersentry"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/middleware/cache"
	"github.com/gofiber/fiber/v2/middleware/compress"
	"github.com/gofiber/fiber/v2/middleware/etag"
	"github.com/gofiber/fiber/v2/middleware/filesystem"
	"github.com/gofiber/fiber/v2/middleware/healthcheck"
	"github.com/gofiber/fiber/v2/middleware/logger"
	"github.com/gofiber/fiber/v2/middleware/recover"
	"github.com/gofiber/fiber/v2/middleware/skip"

	"github.com/shengyanli1982/law"
)

// TODO purge CSS
// TODO HTTP2 https://github.com/dgrr/http2

type Host struct {
	Fiber *fiber.App
}

//go:embed all:public/*
var fs embed.FS

func main() {
	err := sentry.Init(sentry.ClientOptions{
		Dsn:         os.Getenv("SENTRY_DSN"),
		Release:     os.Getenv("FLY_MACHINE_VERSION"),
		Environment: os.Getenv("ENV"),
	})
	if err != nil {
		log.Panic("could not set up sentry")
	}
	defer sentry.Flush(2 * time.Second)

	metricServer := fiber.New(fiber.Config{
		GETOnly:                  true,
		StrictRouting:            true,
		DisableDefaultDate:       true,
		DisableHeaderNormalizing: true,
		DisableStartupMessage:    true,
	})
	prometheus := fiberprometheus.New("homestead")
	prometheus.RegisterAt(metricServer, "/metrics")

	hosts := map[string]*Host{}
	config, err := config.GetConfig()
	if err != nil {
		log.Panic("config error", err)
	}

	internal := fiber.New(fiber.Config{
		GETOnly:       true,
		StrictRouting: true,
	})
	internal.Use(healthcheck.New(healthcheck.Config{}))
	hosts["fly-internal"] = &Host{internal}

	website := fiber.New(fiber.Config{
		EnableTrustedProxyCheck: true,
		TrustedProxies:          []string{"172.16.0.0/16"},
		ProxyHeader:             "Fly-Client-IP",
		GETOnly:                 true,
		ReadTimeout:             5 * time.Minute,
		WriteTimeout:            5 * time.Minute,
		ServerHeader:            "Fiber",
		StrictRouting:           true,
		UnescapePath:            true,
	})

	website.Use(prometheus.Middleware)
	website.Use(fibersentry.New(fibersentry.Config{}))

	website.Use(compress.New())
	website.Use(cache.New(cache.Config{
		CacheControl:         true,
		Expiration:           24 * time.Hour,
		StoreResponseHeaders: true,
	}))
	// must be after compress to be encoding-independent
	website.Use(etag.New(etag.Config{
		Weak: true,
	}))

	website.Use(recover.New(recover.Config{}))

	files := http.FS(fs)
	notFoundHandler := func(c *fiber.Ctx) error {
		c.Status(fiber.StatusNotFound).Type("html", "utf-8")
		content, err := fs.ReadFile("public/404.html")
		if err != nil {
			content = []byte("404 Not Found")
			c.Type("txt")
		}
		return c.Send(content)
	}
	website.Get("/404.html", notFoundHandler)
	website.Use("/", filesystem.New(filesystem.Config{
		Root:               files,
		PathPrefix:         "public",
		ContentTypeCharset: "utf-8",
		MaxAge:             int((24 * time.Hour).Seconds()),
	}))
	website.Use(notFoundHandler)
	hosts[config.BaseURL.Host] = &Host{website}

	toplevel := fiber.New(fiber.Config{
		DisableStartupMessage: config.Production,
	})
	toplevel.Get("/health", func(c *fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})
	toplevel.Use(skip.New(logger.New(logger.Config{
		Output: law.NewWriteAsyncer(os.Stdout, nil),
		Format: "${protocol} ${method} ${status} ${host} ${url} ${respHeader:Location}\n",
	}), func(c *fiber.Ctx) bool {
		return c.Hostname() != "fly-internal"
	}))
	toplevel.Use(func(c *fiber.Ctx) error {
		host := hosts[c.Hostname()]
		if host == nil {
			if config.RedirectOtherHostnames {
				return c.Redirect(config.BaseURL.JoinPath(c.OriginalURL()).String())
			} else {
				hosts[config.BaseURL.Host].Fiber.Handler()(c.Context())
				return nil
			}
		} else {
			host.Fiber.Handler()(c.Context())
			return nil
		}
	})

	go func() {
		err := metricServer.Listen(":9091")
		log.Printf("failed to start metrics server: %v", err)
	}()
	log.Fatal(toplevel.Listen(fmt.Sprintf("%s:%d", "", config.Port)))
}