package main

import (
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"time"

	cfg "website/internal/config"

	"github.com/ansrivas/fiberprometheus/v2"
	"github.com/ardanlabs/conf/v3"
	"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"
)

type Config struct {
	Production             bool    `conf:"default:false"`
	ListenAddress          string  `conf:"default:localhost"`
	Port                   uint16  `conf:"default:3000,short:p"`
	BaseURL                cfg.URL `conf:"default:http://localhost:3000,short:b"`
	RedirectOtherHostnames bool    `conf:"default:false"`
}

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

type Host struct {
	Fiber *fiber.App
}

var Commit string

func main() {
	runtimeConfig := Config{}
	if help, err := conf.Parse("", &runtimeConfig); err != nil {
		if errors.Is(err, conf.ErrHelpWanted) {
			fmt.Println(help)
			os.Exit(1)
		}
		log.Panicf("parsing runtime configuration: %v", err)
	}
	config, err := cfg.GetConfig()
	if err != nil {
		log.Panicf("parsing configuration file: %v", err)
	}

	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,
		Network:                  fiber.NetworkTCP,
	})
	prometheus := fiberprometheus.New("homestead")
	prometheus.RegisterAt(metricServer, "/metrics")

	hosts := map[string]*Host{}

	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,
		StrictRouting:           true,
		UnescapePath:            true,
	})

	website.Use(prometheus.Middleware)
	website.Use(fibersentry.New(fibersentry.Config{}))
	website.Use(func(c *fiber.Ctx) error {
		for k, v := range config.Extra.Headers {
			c.Set(k, v)
		}
		if c.Secure() {
			c.Set("Strict-Transport-Security", "max-age=31536000; includeSubdomains; preload")
		}
		return c.Next()
	})

	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.Dir("public")
	notFoundHandler := func(c *fiber.Ctx) error {
		c.Status(fiber.StatusNotFound).Type("html", "utf-8")
		content, err := files.Open("404.html")
		if err != nil {
			c.Type("txt")
			return c.SendString("404 Not Found")
		}
		return c.SendStream(content)
	}
	website.Get("/404.html", notFoundHandler)
	website.Use("/", filesystem.New(filesystem.Config{
		Root:               files,
		ContentTypeCharset: "utf-8",
		MaxAge:             int((24 * time.Hour).Seconds()),
	}))
	website.Use(notFoundHandler)
	hosts[runtimeConfig.BaseURL.Host] = &Host{website}

	toplevel := fiber.New(fiber.Config{
		DisableStartupMessage: runtimeConfig.Production,
		ServerHeader:          fmt.Sprintf("website (%s)", Commit),
		Network:               fiber.NetworkTCP,
	})
	toplevel.Get("/health", func(c *fiber.Ctx) error {
		return c.SendStatus(fiber.StatusOK)
	})
	var logWriter io.Writer
	if runtimeConfig.Production {
		logWriter = law.NewWriteAsyncer(os.Stdout, nil)
	} else {
		logWriter = os.Stdout
	}
	toplevel.Use(skip.New(logger.New(logger.Config{
		Output: logWriter,
		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 runtimeConfig.RedirectOtherHostnames {
				return c.Redirect(runtimeConfig.BaseURL.JoinPath(c.OriginalURL()).String())
			} else {
				hosts[runtimeConfig.BaseURL.Host].Fiber.Handler()(c.Context())
				return nil
			}
		} else {
			host.Fiber.Handler()(c.Context())
			return nil
		}
	})

	go func() {
		err := metricServer.Listen(net.JoinHostPort(runtimeConfig.ListenAddress, "9091"))
		log.Printf("failed to start metrics server: %v", err)
	}()
	log.Fatal(toplevel.Listen(net.JoinHostPort(runtimeConfig.ListenAddress, fmt.Sprint(runtimeConfig.Port))))
}