// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package configcat

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
	"go.uber.org/zap"
	"golang.org/x/sync/singleflight"
)

const (
	configCatModule = "gitpod.configcat"
)

var (
	DefaultConfig = []byte("{}")
	pathRegex     = regexp.MustCompile(`^/configcat/configuration-files/gitpod/config_v\d+\.json$`)
)

func init() {
	caddy.RegisterModule(ConfigCat{})
	httpcaddyfile.RegisterHandlerDirective(configCatModule, parseCaddyfile)
}

type configCache struct {
	data []byte
	hash string
}

// ConfigCat implements an configcat config CDN
type ConfigCat struct {
	sdkKey string
	// baseUrl of configcat, default https://cdn-global.configcat.com
	baseUrl string
	// pollInterval sets after how much time a configuration is considered stale.
	pollInterval time.Duration

	configCache map[string]*configCache
	m           sync.RWMutex

	httpClient *http.Client
	logger     *zap.Logger
}

// CaddyModule returns the Caddy module information.
func (ConfigCat) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.handlers.gitpod_configcat",
		New: func() caddy.Module { return new(ConfigCat) },
	}
}

// ServeHTTP implements caddyhttp.MiddlewareHandler.
func (c *ConfigCat) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
	if !pathRegex.MatchString(r.URL.Path) {
		return next.ServeHTTP(w, r)
	}
	w.Header().Set("Content-Type", "application/json")
	if c.sdkKey == "" {
		w.Write(DefaultConfig)
		return nil
	}
	etag := r.Header.Get("If-None-Match")
	arr := strings.Split(r.URL.Path, "/")
	configVersion := arr[len(arr)-1]
	config := c.getConfigWithCache(configVersion)
	if etag != "" && config.hash == etag {
		w.WriteHeader(http.StatusNotModified)
		return nil
	}
	if config.hash != "" {
		w.Header().Set("ETag", config.hash)
	}
	w.Write(config.data)
	return nil
}

func (c *ConfigCat) Provision(ctx caddy.Context) error {
	c.logger = ctx.Logger(c)
	c.configCache = make(map[string]*configCache)

	c.sdkKey = os.Getenv("CONFIGCAT_SDK_KEY")
	if c.sdkKey == "" {
		return nil
	}

	c.httpClient = &http.Client{
		Timeout: 10 * time.Second,
	}
	c.baseUrl = os.Getenv("CONFIGCAT_BASE_URL")
	if c.baseUrl == "" {
		c.baseUrl = "https://cdn-global.configcat.com"
	}
	dur, err := time.ParseDuration(os.Getenv("CONFIGCAT_POLL_INTERVAL"))
	if err != nil {
		c.pollInterval = time.Minute
		c.logger.Warn("cannot parse poll interval of configcat, default to 1m")
	} else {
		c.pollInterval = dur
	}

	// poll config
	go func() {
		for range time.Tick(c.pollInterval) {
			for version, cache := range c.configCache {
				c.updateConfigCache(version, cache)
			}
		}
	}()
	return nil
}

func (c *ConfigCat) getConfigWithCache(configVersion string) *configCache {
	c.m.RLock()
	data := c.configCache[configVersion]
	c.m.RUnlock()
	if data != nil {
		return data
	}
	return c.updateConfigCache(configVersion, nil)
}

func (c *ConfigCat) updateConfigCache(version string, prevConfig *configCache) *configCache {
	t, err := c.fetchConfigCatConfig(version, prevConfig)
	if err != nil {
		return &configCache{
			data: DefaultConfig,
			hash: "",
		}
	}
	c.m.Lock()
	c.configCache[version] = t
	c.m.Unlock()
	return t
}

var sg = &singleflight.Group{}

// fetchConfigCatConfig with different config version. i.e. config_v5.json
func (c *ConfigCat) fetchConfigCatConfig(version string, prevConfig *configCache) (*configCache, error) {
	b, err, _ := sg.Do(fmt.Sprintf("fetch_%s", version), func() (interface{}, error) {
		url := fmt.Sprintf("%s/configuration-files/%s/%s", c.baseUrl, c.sdkKey, version)
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			c.logger.With(zap.Error(err)).Error("cannot create request")
			return nil, err
		}
		if prevConfig != nil && prevConfig.hash != "" {
			req.Header.Add("If-None-Match", prevConfig.hash)
		}
		resp, err := c.httpClient.Do(req)
		if err != nil {
			c.logger.With(zap.Error(err)).Error("cannot fetch configcat config")
			return nil, err
		}

		if resp.StatusCode == 304 {
			return prevConfig, nil
		}

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			b, err := ioutil.ReadAll(resp.Body)
			if err != nil {
				c.logger.With(zap.Error(err), zap.String("version", version)).Error("cannot read configcat config response")
				return nil, err
			}
			return &configCache{
				data: b,
				hash: resp.Header.Get("Etag"),
			}, nil
		}
		return nil, fmt.Errorf("received unexpected response %v", resp.Status)
	})
	if err != nil {
		return nil, err
	}
	return b.(*configCache), nil
}

// UnmarshalCaddyfile implements Caddyfile.Unmarshaler.
func (m *ConfigCat) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	return nil
}

func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
	m := new(ConfigCat)
	err := m.UnmarshalCaddyfile(h.Dispenser)
	if err != nil {
		return nil, err
	}

	return m, nil
}

// Interface guards
var (
	_ caddyhttp.MiddlewareHandler = (*ConfigCat)(nil)
	_ caddyfile.Unmarshaler       = (*ConfigCat)(nil)
)
