package metrics

import (
	"errors"
	"net/http"
	"net/url"
	"strings"

	"github.com/labstack/echo/v4"
	log "github.com/sirupsen/logrus"

	"github.com/cloudfoundry/stratos/src/jetstream/api"
)

var (
	cellQueryAllowList = []string{
		"firehose_value_metric_rep_unhealthy_cell",
		"firehose_value_metric_rep_garden_health_check_failed",
		"firehose_value_metric_rep_capacity_remaining_containers",
		"firehose_value_metric_rep_capacity_remaining_disk",
		"firehose_value_metric_rep_capacity_remaining_memory",
		"firehose_value_metric_rep_capacity_total_containers",
		"firehose_value_metric_rep_capacity_total_disk",
		"firehose_value_metric_rep_capacity_total_memory",
		"firehose_value_metric_rep_num_cpus",
	}
)

// Metrics endpoints - non-admin - for a Cloud Foundry Application
func (m *MetricsSpecification) getCloudFoundryAppMetrics(c echo.Context) error {
	// We need to go and fetch the CF App, to make sure that the user is permitted to access it
	// We'll do this synchronously here for now - this can be done optimistically in parallel in the future
	// Use the passthrough mechanism to get the App metadata from Cloud Foundry
	appID := c.Param("appId")
	prometheusOp := c.Param("op")
	appURL, _ := url.Parse("/v2/apps/" + appID)
	responses, err := m.portalProxy.ProxyRequest(c, appURL)
	if err != nil {
		return err
	}

	// For an application, we only support the query operation
	if prometheusOp != "query" && prometheusOp != "query_range" {
		return errors.New("Only 'query' or 'query_range' is supported for a Cloud Foundry application")
	}

	// Now make the metrics requests to the appropriate metrics endpoint
	var cnsiList []string
	for k, v := range responses {
		// Check Status Code was ok
		if v.StatusCode < 400 {
			cnsiList = append(cnsiList, k)
		}
	}

	return m.makePrometheusRequest(c, cnsiList, "application_id=\""+appID+"\"")
}

func makePrometheusRequestInfos(c echo.Context, userGUID string, metrics map[string]EndpointMetricsRelation, prometheusOp string, queries string, addJob bool) []api.ProxyRequestInfo {
	// Construct the metadata for proxying
	requests := make([]api.ProxyRequestInfo, 0)
	for _, metric := range metrics {
		req := api.ProxyRequestInfo{}
		req.UserGUID = userGUID
		req.ResultGUID = metric.endpoint.GUID
		req.EndpointGUID = metric.metrics.EndpointGUID
		req.Method = c.Request().Method

		addQueries := queries
		if len(addQueries) > 0 {
			addQueries = addQueries + ","
		}

		if addJob {
			if metric.metrics.Job != "" {
				// stratos-metrics configures the firehose exporter to tag metrics with `job`
				addQueries = addQueries + "job=\"" + metric.metrics.Job + "\""
			} else if metric.metrics.Environment != "" {
				// prometheus-boshrelease deployed firehose exporter tags metrics with `environment`
				addQueries = addQueries + "environment=\"" + metric.metrics.Environment + "\""
			}
		}

		req.URI = makePrometheusRequestURI(c, prometheusOp, addQueries)
		requests = append(requests, req)
	}
	return requests
}

func makePrometheusRequestURI(c echo.Context, prometheusOp string, modify string) *url.URL {
	uri := getEchoURL(c)
	uri.Path = "/api/v1/" + prometheusOp
	values := uri.Query()
	query := values.Get("query")
	if len(query) > 0 {
		parts := strings.SplitAfter(query, "{")
		if len(parts) <= 2 {
			modified := parts[0]
			if len(parts) == 1 {
				modified = modified + "{" + modify + "}"
			} else {
				end := parts[1]
				if end != "}" && len(modify) > 0 {
					end = "," + end
				}
				modified = modified + modify + end
			}
			values.Set("query", modified)
		}
	}
	uri.RawQuery = values.Encode()
	log.Debugf("Sending prometheus query: %+v", uri.String())
	return &uri
}

func getEchoURL(c echo.Context) url.URL {
	u := c.Request().URL
	return *u
}

// Metrics API endpoints - admin - for a Cloud Foundry deployment
func (m *MetricsSpecification) getCloudFoundryMetrics(c echo.Context) error {
	userGUID, err := m.portalProxy.GetSessionStringValue(c, "user_id")
	if err != nil {
		return errors.New("Could not find session user_id")
	}
	cnsiList := strings.Split(c.Request().Header.Get("x-cap-cnsi-list"), ",")
	// User must be an admin of the Cloud Foundry
	// Check each in the list and if any is not, then return an error
	canAccessMetrics := true
	for _, endpointID := range cnsiList {
		// Get token for the UserID and EndpointID
		token, exists := m.portalProxy.GetCNSITokenRecord(endpointID, userGUID)
		if !exists {
			// Could not get a token for the user
			canAccessMetrics = false
			break
		} else {
			userTokenInfo, err := m.portalProxy.GetUserTokenInfo(token.AuthToken)
			if err == nil {
				// Do they have they admin scope for Cloud Foundry?
				isAdmin := strings.Contains(strings.Join(userTokenInfo.Scope, ""), m.portalProxy.GetConfig().CFAdminIdentifier)
				if !isAdmin {
					canAccessMetrics = false
					break
				}
			} else {
				// Could not decode the user's token to determine if they are an admin, so default is that they are not
				canAccessMetrics = false
				break
			}
		}
	}

	// Only proceed if the user is an Cloud Foundry admin of all of the endpoints we are requesting metrics for
	if !canAccessMetrics {
		return api.NewHTTPShadowError(
			http.StatusUnauthorized,
			"You must be a Cloud Foundry admin to access CF-level metrics",
			"You must be a Cloud Foundry admin to access CF-level metrics")
	}

	return m.makePrometheusRequest(c, cnsiList, "")
}

func (m *MetricsSpecification) makePrometheusRequest(c echo.Context, cnsiList []string, queries string) error {
	prometheusOp := c.Param("op")

	// get the user
	userGUID, err := m.portalProxy.GetSessionStringValue(c, "user_id")
	if err != nil {
		return errors.New("Could not find session user_id")
	}

	// For each CNSI, find the metrics endpoint that we need to talk to
	metrics, err2 := m.getMetricsEndpoints(userGUID, cnsiList)
	if err2 != nil {
		return errors.New("Can not get metric endpoint metadata")
	}

	// Construct the metadata for proxying
	requests := makePrometheusRequestInfos(c, userGUID, metrics, prometheusOp, queries, true)
	responses, err := m.portalProxy.DoProxyRequest(requests)
	return m.portalProxy.SendProxiedResponse(c, responses)
}

func isAllowedCellMetricsQuery(query string) bool {
	for _, allowListQuery := range cellQueryAllowList {
		if strings.Index(query, allowListQuery) == 0 {
			return true
		}
	}
	return false
}

// Metrics endpoints - cells - with white list of cell prometheus query values
func (m *MetricsSpecification) getCloudFoundryCellMetrics(c echo.Context) error {

	uri := getEchoURL(c)
	values := uri.Query()
	query := values.Get("query")

	// Fail all queries that are not of type 'cell'
	if !isAllowedCellMetricsQuery(query) {
		return errors.New("Unsupported prometheus query")
	}

	cnsiList := strings.Split(c.Request().Header.Get("x-cap-cnsi-list"), ",")
	return m.makePrometheusRequest(c, cnsiList, "")
}
