package client

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	portainer "github.com/portainer/portainer/api"

	"github.com/rs/zerolog/log"
	"github.com/segmentio/encoding/json"
)

var errInvalidResponseStatus = errors.New("invalid response status (expecting 200)")

const defaultHTTPTimeout = 5

// HTTPClient represents a client to send HTTP requests.
type HTTPClient struct {
	*http.Client
}

// NewHTTPClient is used to build a new HTTPClient.
func NewHTTPClient() *HTTPClient {
	return &HTTPClient{
		&http.Client{
			Timeout: time.Second * time.Duration(defaultHTTPTimeout),
		},
	}
}

// AzureAuthenticationResponse represents an Azure API authentication response.
type AzureAuthenticationResponse struct {
	AccessToken string `json:"access_token"`
	ExpiresOn   string `json:"expires_on"`
}

// ExecuteAzureAuthenticationRequest is used to execute an authentication request
// against the Azure API. It re-uses the same http.Client.
func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portainer.AzureCredentials) (*AzureAuthenticationResponse, error) {
	loginURL := fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", credentials.TenantID)
	params := url.Values{
		"grant_type":    {"client_credentials"},
		"client_id":     {credentials.ApplicationID},
		"client_secret": {credentials.AuthenticationKey},
		"resource":      {"https://management.azure.com/"},
	}

	resp, err := client.PostForm(loginURL, params)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, errors.New("invalid Azure credentials")
	}

	var token AzureAuthenticationResponse
	err = json.NewDecoder(resp.Body).Decode(&token)
	if err != nil {
		return nil, err
	}

	return &token, nil
}

// Get executes a simple HTTP GET to the specified URL and returns
// the content of the response body. Timeout can be specified via the timeout parameter,
// will default to defaultHTTPTimeout if set to 0.
func Get(url string, timeout int) ([]byte, error) {
	if timeout == 0 {
		timeout = defaultHTTPTimeout
	}

	client := &http.Client{
		Timeout: time.Second * time.Duration(timeout),
	}

	response, err := client.Get(url)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()

	if response.StatusCode != http.StatusOK {
		log.Error().Int("status_code", response.StatusCode).Msg("unexpected status code")

		return nil, errInvalidResponseStatus
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}

	return body, nil
}

// ExecutePingOperation will send a SystemPing operation HTTP request to a Docker environment(endpoint)
// using the specified host and optional TLS configuration.
// It uses a new Http.Client for each operation.
func ExecutePingOperation(host string, tlsConfig *tls.Config) (bool, error) {
	transport := &http.Transport{}

	scheme := "http"
	if tlsConfig != nil {
		transport.TLSClientConfig = tlsConfig
		scheme = "https"
	}

	client := &http.Client{
		Timeout:   time.Second * 3,
		Transport: transport,
	}

	target := strings.Replace(host, "tcp://", scheme+"://", 1)
	return pingOperation(client, target)
}

func pingOperation(client *http.Client, target string) (bool, error) {
	pingOperationURL := target + "/_ping"

	resp, err := client.Get(pingOperationURL)
	if err != nil {
		return false, err
	}

	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()

	agentOnDockerEnvironment := false
	if resp.Header.Get(portainer.PortainerAgentHeader) != "" {
		agentOnDockerEnvironment = true
	}

	return agentOnDockerEnvironment, nil
}
