package gce

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/promscrape/discoveryutils"
	"golang.org/x/oauth2/google"
)

type apiConfig struct {
	client       *http.Client
	zones        []string
	project      string
	filter       string
	tagSeparator string
	port         int
}

var configMap = discoveryutils.NewConfigMap()

func getAPIConfig(sdc *SDConfig) (*apiConfig, error) {
	v, err := configMap.Get(sdc, func() (interface{}, error) { return newAPIConfig(sdc) })
	if err != nil {
		return nil, err
	}
	return v.(*apiConfig), nil
}

func newAPIConfig(sdc *SDConfig) (*apiConfig, error) {
	ctx := context.Background()
	client, err := google.DefaultClient(ctx, "https://www.googleapis.com/auth/compute.readonly")
	if err != nil {
		return nil, fmt.Errorf("cannot create oauth2 client for gce: %w", err)
	}
	project := sdc.Project
	if len(project) == 0 {
		proj, err := getCurrentProject()
		if err != nil {
			client.CloseIdleConnections()
			return nil, fmt.Errorf("cannot determine the current project; make sure `vmagent` runs inside GCE; error: %w", err)
		}
		project = proj
		logger.Infof("autodetected the current GCE project: %q", project)
	}
	zones := sdc.Zone.Zones
	if len(zones) == 0 {
		// Autodetect the current zone.
		zone, err := getCurrentZone()
		if err != nil {
			client.CloseIdleConnections()
			return nil, fmt.Errorf("cannot determine the current zone; make sure `vmagent` runs inside GCE; error: %w", err)
		}
		zones = append(zones, zone)
		logger.Infof("autodetected the current GCE zone: %q", zone)
	} else if len(zones) == 1 && zones[0] == "*" {
		// Autodetect zones for project.
		// Do not pass sdc.Filter when discovering zones, since GCE doesn't support it.
		// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/3202
		zs, err := getZonesForProject(client, project)
		if err != nil {
			client.CloseIdleConnections()
			return nil, fmt.Errorf("cannot obtain zones for project %q: %w", project, err)
		}
		zones = zs
		logger.Infof("autodetected all the zones for the GCE project %q: %q", project, zones)
	}
	tagSeparator := ","
	if sdc.TagSeparator != nil {
		tagSeparator = *sdc.TagSeparator
	}
	port := 80
	if sdc.Port != nil {
		port = *sdc.Port
	}
	return &apiConfig{
		client:       client,
		zones:        zones,
		project:      project,
		filter:       sdc.Filter,
		tagSeparator: tagSeparator,
		port:         port,
	}, nil
}

func getAPIResponse(client *http.Client, apiURL, filter, pageToken string) ([]byte, error) {
	apiURL = appendNonEmptyQueryArg(apiURL, "filter", filter)
	apiURL = appendNonEmptyQueryArg(apiURL, "pageToken", pageToken)
	resp, err := client.Get(apiURL)
	if err != nil {
		return nil, fmt.Errorf("cannot query %q: %w", apiURL, err)
	}
	return readResponseBody(resp, apiURL)
}

func readResponseBody(resp *http.Response, apiURL string) ([]byte, error) {
	data, err := io.ReadAll(resp.Body)
	_ = resp.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("cannot read response from %q: %w", apiURL, err)
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status code for %q; got %d; want %d; response body: %q",
			apiURL, resp.StatusCode, http.StatusOK, data)
	}
	return data, nil
}

func appendNonEmptyQueryArg(apiURL, argName, argValue string) string {
	if len(argValue) == 0 {
		return apiURL
	}
	prefix := "?"
	if strings.Contains(apiURL, "?") {
		prefix = "&"
	}
	return apiURL + fmt.Sprintf("%s%s=%s", prefix, url.QueryEscape(argName), url.QueryEscape(argValue))
}

func getCurrentZone() (string, error) {
	// See https://cloud.google.com/compute/docs/storing-retrieving-metadata#default
	data, err := getGCEMetadata("instance/zone")
	if err != nil {
		return "", err
	}
	parts := strings.Split(string(data), "/")
	if len(parts) != 4 {
		return "", fmt.Errorf("unexpected data returned from GCE; it must contain something like `projects/projectnum/zones/zone`; data: %q", data)
	}
	return parts[3], nil
}

func getCurrentProject() (string, error) {
	// See https://cloud.google.com/compute/docs/storing-retrieving-metadata#default
	data, err := getGCEMetadata("project/project-id")
	if err != nil {
		return "", err
	}
	return string(data), nil
}

func getGCEMetadata(path string) ([]byte, error) {
	// See https://cloud.google.com/compute/docs/storing-retrieving-metadata#default
	metadataURL := "http://metadata.google.internal/computeMetadata/v1/" + path
	req, err := http.NewRequest(http.MethodGet, metadataURL, nil)
	if err != nil {
		return nil, fmt.Errorf("cannot create http request for %q: %w", metadataURL, err)
	}
	req.Header.Set("Metadata-Flavor", "Google")
	resp, err := discoveryutils.GetHTTPClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("cannot obtain response to %q: %w", metadataURL, err)
	}
	return readResponseBody(resp, metadataURL)
}