package main

import (
	"bytes"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"

	"github.com/prymitive/karma/internal/alertmanager"
	"github.com/prymitive/karma/internal/config"
	"github.com/prymitive/karma/internal/mapper"
)

func proxyPathPrefix(name string) string {
	maybeSlash := ""
	if !strings.HasSuffix(config.Config.Listen.Prefix, "/") {
		maybeSlash = "/"
	}
	return fmt.Sprintf("%s%sproxy/alertmanager/%s", config.Config.Listen.Prefix, maybeSlash, name)
}

func proxyPath(name, path string) string {
	return fmt.Sprintf("%s%s", proxyPathPrefix(name), path)
}

// NewAlertmanagerProxy creates a proxy instance for given alertmanager instance
func NewAlertmanagerProxy(alertmanager *alertmanager.Alertmanager) *httputil.ReverseProxy {
	upstreamURL, _ := url.Parse(alertmanager.URI)
	proxy := httputil.ReverseProxy{
		Director: func(req *http.Request) {
			req.URL.Scheme = upstreamURL.Scheme
			req.URL.Host = upstreamURL.Host

			if upstreamURL.User.Username() != "" {
				username := upstreamURL.User.Username()
				password, _ := upstreamURL.User.Password()
				req.SetBasicAuth(username, password)
			}

			for key, val := range alertmanager.HTTPHeaders {
				req.Header.Set(key, val)
			}

			// drop Accept-Encoding header so we always get uncompressed responses from
			// upstream, there's a gzip middleware that's global so we don't want it
			// to gzip twice
			req.Header.Del("Accept-Encoding")

			// set hostname of proxied target
			req.Host = upstreamURL.Host

			// Prepend with upstream URL path if exists
			if len(upstreamURL.Path) > 0 {
				req.URL.Path = strings.TrimSuffix(upstreamURL.Path, "/") + req.URL.Path
			}

			slog.Debug(
				"Forwarding request",
				slog.String("alertmanager", alertmanager.Name),
				slog.String("uri", req.RequestURI),
				slog.String("forwardedURI", req.URL.String()),
			)
		},
		Transport: alertmanager.HTTPTransport,
		ModifyResponse: func(resp *http.Response) error {
			// drop Content-Length header from upstream responses, gzip middleware
			// will compress those and that could cause a mismatch
			resp.Header.Del("Content-Length")
			return nil
		},
	}
	return &proxy
}

func handlePostRequest(alertmanager *alertmanager.Alertmanager, h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		slog.Debug("Proxy request", slog.String("alertmanager", alertmanager.Name), slog.String("uri", r.RequestURI))

		defer r.Body.Close()
		body, err := io.ReadAll(r.Body)
		if err != nil {
			slog.Error(
				"Failed to read proxied request",
				slog.Any("error", err),
				slog.String("alertmanager", alertmanager.Name),
				slog.String("method", r.Method),
				slog.String("uri", r.RequestURI),
			)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		ver := alertmanager.Version()
		m, err := mapper.GetSilenceMapper(ver)
		if err != nil {
			slog.Error(
				"Failed to proxy a request",
				slog.Any("error", err),
				slog.String("alertmanager", alertmanager.Name),
				slog.String("method", r.Method),
				slog.String("uri", r.RequestURI),
			)
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		if len(silenceACLs) > 0 {
			silence, err := m.Unmarshal(body)
			if err != nil {
				slog.Error(
					"Failed to unmarshal silence body",
					slog.Any("error", err),
					slog.String("alertmanager", alertmanager.Name),
					slog.String("method", r.Method),
					slog.String("uri", r.RequestURI),
				)
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			for i, acl := range silenceACLs {
				groups := getGroupsFromContext(r)
				isAllowed, err := acl.isAllowed(alertmanager.Name, silence, groups)
				slog.Debug("ACL rule check", slog.Int("index", i), slog.Bool("allowed", isAllowed), slog.Any("error", err))
				if err != nil {
					slog.Warn(
						"Proxy request was blocked by ACL rule",
						slog.Any("error", err),
						slog.String("alertmanager", alertmanager.Name),
						slog.String("method", r.Method),
						slog.String("uri", r.RequestURI),
					)
					http.Error(w, err.Error(), http.StatusBadRequest)
					return
				}
				if isAllowed {
					break
				}
			}
		}

		if config.Config.Authentication.Enabled {
			username := getUserFromContext(r)
			newBody, err := m.RewriteUsername(body, username)
			if err != nil {
				slog.Error(
					"Failed to rewrite silence body",
					slog.Any("error", err),
					slog.String("alertmanager", alertmanager.Name),
					slog.String("method", r.Method),
					slog.String("uri", r.RequestURI),
				)
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			r.Body = io.NopCloser(bytes.NewBuffer(newBody))
			r.ContentLength = int64(len(newBody))
			r.Header.Set("Content-Length", strconv.FormatInt(r.ContentLength, 10))
		} else {
			r.Body = io.NopCloser(bytes.NewBuffer(body))
		}

		h.ServeHTTP(w, r)
	}
}

func setupRouterProxyHandlers(router *chi.Mux, alertmanager *alertmanager.Alertmanager) {
	proxy := NewAlertmanagerProxy(alertmanager)
	router.Post(
		proxyPath(alertmanager.Name, "/api/v2/silences"),
		handlePostRequest(alertmanager, http.StripPrefix(proxyPathPrefix(alertmanager.Name), proxy)),
	)
	router.Delete(
		proxyPath(alertmanager.Name, "/api/v2/silence/{id}"),
		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			h := http.StripPrefix(proxyPathPrefix(alertmanager.Name), proxy)
			h.ServeHTTP(w, r)
		}),
	)
}

// this fixes a problem with go-chi usage of RawPath
// see https://github.com/prymitive/karma/issues/4674
// and https://github.com/go-chi/chi/issues/641
func proxyPathFixMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		r.URL.RawPath = ""
		ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
		next.ServeHTTP(ww, r)
	})
}
