package ofrep

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/http/httputil"
	"os"
	"path"
	"strconv"

	"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
	"github.com/grafana/grafana/pkg/infra/tracing"
	"github.com/grafana/grafana/pkg/util/proxyutil"

	goffmodel "github.com/thomaspoignant/go-feature-flag/cmd/relayproxy/model"
)

func (b *APIBuilder) proxyAllFlagReq(ctx context.Context, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.Start(ctx, "ofrep.proxy.evalAllFlags")
	defer span.End()

	r = r.WithContext(ctx)

	proxy, err := b.newProxy(ofrepPath)
	if err != nil {
		err = tracing.Error(span, err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	proxy.ModifyResponse = func(resp *http.Response) error {
		if resp.StatusCode == http.StatusOK && !isAuthedUser {
			var result goffmodel.OFREPBulkEvaluateSuccessResponse
			if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
				return err
			}
			_ = resp.Body.Close()

			var filteredFlags []goffmodel.OFREPFlagBulkEvaluateSuccessResponse
			for _, f := range result.Flags {
				if isPublicFlag(f.Key) {
					filteredFlags = append(filteredFlags, f)
				}
			}

			result.Flags = filteredFlags
			newBodyBytes, err := json.Marshal(result)
			if err != nil {
				logger.Error("Failed to encode filtered result", "error", err)
				return err
			}

			// Replace the body
			resp.Body = io.NopCloser(bytes.NewReader(newBodyBytes))
			resp.ContentLength = int64(len(newBodyBytes))
			resp.Header.Set("Content-Length", strconv.Itoa(len(newBodyBytes)))
			resp.Header.Set("Content-Type", "application/json")
		}

		return nil
	}

	proxy.ServeHTTP(w, r)
}

func (b *APIBuilder) proxyFlagReq(ctx context.Context, flagKey string, isAuthedUser bool, w http.ResponseWriter, r *http.Request) {
	ctx, span := tracing.Start(ctx, "ofrep.proxy.evalFlag")
	defer span.End()

	r = r.WithContext(ctx)

	proxy, err := b.newProxy(path.Join(ofrepPath, flagKey))
	if err != nil {
		err = tracing.Error(span, err)
		b.logger.Error("Failed to create proxy", "key", flagKey, "error", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	proxy.ModifyResponse = func(resp *http.Response) error {
		if resp.StatusCode == http.StatusOK && !isAuthedUser && !isPublicFlag(flagKey) {
			writeResponse(http.StatusUnauthorized, struct{}{}, b.logger, w)
		}
		return nil
	}

	proxy.ServeHTTP(w, r)
}

func (b *APIBuilder) newProxy(proxyPath string) (*httputil.ReverseProxy, error) {
	if proxyPath == "" {
		return nil, fmt.Errorf("proxy path is required")
	}

	if b.url == nil {
		return nil, fmt.Errorf("OpenFeatureService provider URL is not set")
	}

	var caRoot *x509.CertPool
	if b.caFile != "" {
		var err error
		caRoot, err = getCARoot(b.caFile)
		if err != nil {
			return nil, err
		}
	}

	director := func(req *http.Request) {
		req.URL.Scheme = b.url.Scheme
		req.URL.Host = b.url.Host
		req.URL.Path = proxyPath
	}

	proxy := proxyutil.NewReverseProxy(b.logger, director)
	proxy.Transport = &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: b.insecure,
			RootCAs:            caRoot,
		},
	}
	return proxy, nil
}

func getCARoot(caFile string) (*x509.CertPool, error) {
	// It should be safe to ignore since caFile is passed as --internal.root-ca-file flag of apiserver
	// nolint:gosec
	caCert, err := os.ReadFile(caFile)
	if err != nil {
		return nil, err
	}
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)
	return caCertPool, nil
}
