// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package client

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"time"

	httptransport "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
	"golang.org/x/oauth2"

	hcptelemetry "github.com/hashicorp/hcp-sdk-go/clients/cloud-consul-telemetry-gateway/preview/2023-04-14/client/consul_telemetry_service"
	hcpgnm "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/client/global_network_manager_service"
	gnmmod "github.com/hashicorp/hcp-sdk-go/clients/cloud-global-network-manager-service/preview/2022-02-15/models"
	"github.com/hashicorp/hcp-sdk-go/httpclient"
	"github.com/hashicorp/hcp-sdk-go/resource"

	"github.com/hashicorp/consul/agent/hcp/config"
	"github.com/hashicorp/consul/version"
)

// metricsGatewayPath is the default path for metrics export request on the Telemetry Gateway.
const metricsGatewayPath = "/v1/metrics"

// Client interface exposes HCP operations that can be invoked by Consul
//
//go:generate mockery --name Client --with-expecter --inpackage
type Client interface {
	FetchBootstrap(ctx context.Context) (*BootstrapConfig, error)
	FetchTelemetryConfig(ctx context.Context) (*TelemetryConfig, error)
	GetObservabilitySecret(ctx context.Context) (clientID, clientSecret string, err error)
	PushServerStatus(ctx context.Context, status *ServerStatus) error
	DiscoverServers(ctx context.Context) ([]string, error)
	GetCluster(ctx context.Context) (*Cluster, error)
}

type BootstrapConfig struct {
	Name            string
	BootstrapExpect int
	GossipKey       string
	TLSCert         string
	TLSCertKey      string
	TLSCAs          []string
	ConsulConfig    string
	ManagementToken string
}

type Cluster struct {
	Name         string
	HCPPortalURL string
	AccessLevel  *gnmmod.HashicorpCloudGlobalNetworkManager20220215ClusterConsulAccessLevel
}

type hcpClient struct {
	hc       *httptransport.Runtime
	cfg      config.CloudConfig
	gnm      hcpgnm.ClientService
	tgw      hcptelemetry.ClientService
	resource resource.Resource
}

func NewClient(cfg config.CloudConfig) (Client, error) {
	client := &hcpClient{
		cfg: cfg,
	}

	var err error
	client.resource, err = resource.FromString(cfg.ResourceID)
	if err != nil {
		return nil, err
	}

	client.hc, err = httpClient(cfg)
	if err != nil {
		return nil, err
	}

	client.gnm = hcpgnm.New(client.hc, nil)
	client.tgw = hcptelemetry.New(client.hc, nil)

	return client, nil
}

func httpClient(c config.CloudConfig) (*httptransport.Runtime, error) {
	cfg, err := c.HCPConfig()
	if err != nil {
		return nil, err
	}

	return httpclient.New(httpclient.Config{
		HCPConfig:     cfg,
		SourceChannel: "consul " + version.GetHumanVersion(),
	})
}

// FetchTelemetryConfig obtains telemetry configuration from the Telemetry Gateway.
func (c *hcpClient) FetchTelemetryConfig(ctx context.Context) (*TelemetryConfig, error) {
	params := hcptelemetry.NewAgentTelemetryConfigParamsWithContext(ctx).
		WithLocationOrganizationID(c.resource.Organization).
		WithLocationProjectID(c.resource.Project).
		WithClusterID(c.resource.ID)

	resp, err := c.tgw.AgentTelemetryConfig(params, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch from HCP: %w", err)
	}

	if err := validateAgentTelemetryConfigPayload(resp); err != nil {
		return nil, fmt.Errorf("invalid response payload: %w", err)
	}

	return convertAgentTelemetryResponse(ctx, resp, c.cfg)
}

func (c *hcpClient) FetchBootstrap(ctx context.Context) (*BootstrapConfig, error) {
	version := version.GetHumanVersion()
	params := hcpgnm.NewAgentBootstrapConfigParamsWithContext(ctx).
		WithID(c.resource.ID).
		WithLocationOrganizationID(c.resource.Organization).
		WithLocationProjectID(c.resource.Project).
		WithConsulVersion(&version)

	resp, err := c.gnm.AgentBootstrapConfig(params, nil)
	if err != nil {
		return nil, decodeError(err)
	}
	return bootstrapConfigFromHCP(resp.Payload), nil
}

func bootstrapConfigFromHCP(res *gnmmod.HashicorpCloudGlobalNetworkManager20220215AgentBootstrapResponse) *BootstrapConfig {
	var serverTLS gnmmod.HashicorpCloudGlobalNetworkManager20220215ServerTLS
	if res.Bootstrap.ServerTLS != nil {
		serverTLS = *res.Bootstrap.ServerTLS
	}

	return &BootstrapConfig{
		Name:            res.Bootstrap.ID,
		BootstrapExpect: int(res.Bootstrap.BootstrapExpect),
		GossipKey:       res.Bootstrap.GossipKey,
		TLSCert:         serverTLS.Cert,
		TLSCertKey:      serverTLS.PrivateKey,
		TLSCAs:          serverTLS.CertificateAuthorities,
		ConsulConfig:    res.Bootstrap.ConsulConfig,
		ManagementToken: res.Bootstrap.ManagementToken,
	}
}

func (c *hcpClient) PushServerStatus(ctx context.Context, s *ServerStatus) error {
	params := hcpgnm.NewAgentPushServerStateParamsWithContext(ctx).
		WithID(c.resource.ID).
		WithLocationOrganizationID(c.resource.Organization).
		WithLocationProjectID(c.resource.Project)

	params.SetBody(hcpgnm.AgentPushServerStateBody{
		ServerState: serverStatusToHCP(s),
	})

	_, err := c.gnm.AgentPushServerState(params, nil)
	return err
}

// ServerStatus is used to collect server status information in order to push
// to HCP. Fields should mirror HashicorpCloudGlobalNetworkManager20220215ServerState
type ServerStatus struct {
	ID         string
	Name       string
	Version    string
	LanAddress string
	GossipPort int
	RPCPort    int
	Datacenter string

	Autopilot         ServerAutopilot
	Raft              ServerRaft
	ACL               ServerACLInfo
	ServerTLSMetadata ServerTLSMetadata

	// TODO: TLS will be deprecated in favor of ServerTLSInfo in GNM. Handle
	// removal in a subsequent PR
	// https://hashicorp.atlassian.net/browse/CC-7015
	TLS ServerTLSInfo

	ScadaStatus string
}

type ServerAutopilot struct {
	FailureTolerance int
	Healthy          bool
	MinQuorum        int
	NumServers       int
	NumVoters        int
}

type ServerRaft struct {
	IsLeader             bool
	KnownLeader          bool
	AppliedIndex         uint64
	TimeSinceLastContact time.Duration
}

type ServerACLInfo struct {
	Enabled bool
}

// ServerTLSInfo mirrors HashicorpCloudGlobalNetworkManager20220215TLSInfo
type ServerTLSInfo struct {
	Enabled                bool
	CertExpiry             time.Time
	CertIssuer             string
	CertName               string
	CertSerial             string
	CertificateAuthorities []CertificateMetadata
	VerifyIncoming         bool
	VerifyOutgoing         bool
	VerifyServerHostname   bool
}

// ServerTLSMetadata mirrors HashicorpCloudGlobalNetworkManager20220215ServerTLSMetadata
type ServerTLSMetadata struct {
	InternalRPC ServerTLSInfo
}

// CertificateMetadata mirrors HashicorpCloudGlobalNetworkManager20220215CertificateMetadata
type CertificateMetadata struct {
	CertExpiry time.Time
	CertName   string
	CertSerial string
}

func serverStatusToHCP(s *ServerStatus) *gnmmod.HashicorpCloudGlobalNetworkManager20220215ServerState {
	if s == nil {
		return nil
	}

	// Convert CA metadata
	caCerts := make([]*gnmmod.HashicorpCloudGlobalNetworkManager20220215CertificateMetadata,
		len(s.ServerTLSMetadata.InternalRPC.CertificateAuthorities))
	for ix, ca := range s.ServerTLSMetadata.InternalRPC.CertificateAuthorities {
		caCerts[ix] = &gnmmod.HashicorpCloudGlobalNetworkManager20220215CertificateMetadata{
			CertExpiry: strfmt.DateTime(ca.CertExpiry),
			CertName:   ca.CertName,
			CertSerial: ca.CertSerial,
		}
	}

	return &gnmmod.HashicorpCloudGlobalNetworkManager20220215ServerState{
		Autopilot: &gnmmod.HashicorpCloudGlobalNetworkManager20220215AutoPilotInfo{
			FailureTolerance: int32(s.Autopilot.FailureTolerance),
			Healthy:          s.Autopilot.Healthy,
			MinQuorum:        int32(s.Autopilot.MinQuorum),
			NumServers:       int32(s.Autopilot.NumServers),
			NumVoters:        int32(s.Autopilot.NumVoters),
		},
		GossipPort: int32(s.GossipPort),
		ID:         s.ID,
		LanAddress: s.LanAddress,
		Name:       s.Name,
		Raft: &gnmmod.HashicorpCloudGlobalNetworkManager20220215RaftInfo{
			AppliedIndex:         strconv.FormatUint(s.Raft.AppliedIndex, 10),
			IsLeader:             s.Raft.IsLeader,
			KnownLeader:          s.Raft.KnownLeader,
			TimeSinceLastContact: s.Raft.TimeSinceLastContact.String(),
		},
		RPCPort: int32(s.RPCPort),
		TLS: &gnmmod.HashicorpCloudGlobalNetworkManager20220215TLSInfo{
			// TODO: remove TLS in preference for ServerTLSMetadata.InternalRPC
			// when deprecation path is ready
			// https://hashicorp.atlassian.net/browse/CC-7015
			CertExpiry:           strfmt.DateTime(s.TLS.CertExpiry),
			CertName:             s.TLS.CertName,
			CertSerial:           s.TLS.CertSerial,
			Enabled:              s.TLS.Enabled,
			VerifyIncoming:       s.TLS.VerifyIncoming,
			VerifyOutgoing:       s.TLS.VerifyOutgoing,
			VerifyServerHostname: s.TLS.VerifyServerHostname,
		},
		ServerTLS: &gnmmod.HashicorpCloudGlobalNetworkManager20220215ServerTLSMetadata{
			InternalRPC: &gnmmod.HashicorpCloudGlobalNetworkManager20220215TLSInfo{
				CertExpiry:             strfmt.DateTime(s.ServerTLSMetadata.InternalRPC.CertExpiry),
				CertIssuer:             s.ServerTLSMetadata.InternalRPC.CertIssuer,
				CertName:               s.ServerTLSMetadata.InternalRPC.CertName,
				CertSerial:             s.ServerTLSMetadata.InternalRPC.CertSerial,
				Enabled:                s.ServerTLSMetadata.InternalRPC.Enabled,
				VerifyIncoming:         s.ServerTLSMetadata.InternalRPC.VerifyIncoming,
				VerifyOutgoing:         s.ServerTLSMetadata.InternalRPC.VerifyOutgoing,
				VerifyServerHostname:   s.ServerTLSMetadata.InternalRPC.VerifyServerHostname,
				CertificateAuthorities: caCerts,
			},
		},
		Version:     s.Version,
		ScadaStatus: s.ScadaStatus,
		ACL: &gnmmod.HashicorpCloudGlobalNetworkManager20220215ACLInfo{
			Enabled: s.ACL.Enabled,
		},
		Datacenter: s.Datacenter,
	}
}

func (c *hcpClient) DiscoverServers(ctx context.Context) ([]string, error) {
	params := hcpgnm.NewAgentDiscoverParamsWithContext(ctx).
		WithID(c.resource.ID).
		WithLocationOrganizationID(c.resource.Organization).
		WithLocationProjectID(c.resource.Project)

	resp, err := c.gnm.AgentDiscover(params, nil)
	if err != nil {
		return nil, err
	}
	var servers []string
	for _, srv := range resp.Payload.Servers {
		if srv != nil {
			servers = append(servers, fmt.Sprintf("%s:%d", srv.LanAddress, srv.GossipPort))
		}
	}

	return servers, nil
}

func (c *hcpClient) GetCluster(ctx context.Context) (*Cluster, error) {
	params := hcpgnm.NewGetClusterParamsWithContext(ctx).
		WithID(c.resource.ID).
		WithLocationOrganizationID(c.resource.Organization).
		WithLocationProjectID(c.resource.Project)

	resp, err := c.gnm.GetCluster(params, nil)
	if err != nil {
		return nil, decodeError(err)
	}

	return clusterFromHCP(resp.Payload), nil
}

func clusterFromHCP(payload *gnmmod.HashicorpCloudGlobalNetworkManager20220215GetClusterResponse) *Cluster {
	return &Cluster{
		Name:         payload.Cluster.ID,
		AccessLevel:  payload.Cluster.ConsulAccessLevel,
		HCPPortalURL: payload.Cluster.HcpPortalURL,
	}
}

func decodeError(err error) error {
	// Determine the code from the type of error
	var code int
	switch e := err.(type) {
	case *url.Error:
		oauthErr, ok := errors.Unwrap(e.Err).(*oauth2.RetrieveError)
		if ok {
			code = oauthErr.Response.StatusCode
		}
	case *hcpgnm.AgentBootstrapConfigDefault:
		code = e.Code()
	case *hcpgnm.GetClusterDefault:
		code = e.Code()
	}

	// Return specific error for codes if relevant
	switch code {
	case http.StatusUnauthorized:
		return ErrUnauthorized
	case http.StatusForbidden:
		return ErrForbidden
	}

	return err
}

func (c *hcpClient) GetObservabilitySecret(ctx context.Context) (string, string, error) {
	params := hcpgnm.NewGetObservabilitySecretParamsWithContext(ctx).
		WithID(c.resource.ID).
		WithLocationOrganizationID(c.resource.Organization).
		WithLocationProjectID(c.resource.Project)

	resp, err := c.gnm.GetObservabilitySecret(params, nil)
	if err != nil {
		return "", "", err
	}

	if len(resp.GetPayload().Keys) == 0 {
		return "", "", fmt.Errorf("no observability keys returned for cluster")
	}

	key := resp.GetPayload().Keys[len(resp.GetPayload().Keys)-1]
	return key.ClientID, key.ClientSecret, nil
}
