package client

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"path"
	"strings"

	alertingNotify "github.com/grafana/alerting/notify"
	"github.com/grafana/grafana/pkg/infra/log"
	"github.com/grafana/grafana/pkg/infra/tracing"
	apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
	"github.com/grafana/grafana/pkg/services/ngalert/client"
	"github.com/grafana/grafana/pkg/services/ngalert/metrics"
)

// MimirClient contains all the methods to query the migration critical endpoints of Mimir instance, it's an interface to allow multiple implementations.
type MimirClient interface {
	GetGrafanaAlertmanagerState(ctx context.Context) (*UserGrafanaState, error)
	CreateGrafanaAlertmanagerState(ctx context.Context, state string) error
	DeleteGrafanaAlertmanagerState(ctx context.Context) error

	GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error)
	CreateGrafanaAlertmanagerConfig(ctx context.Context, configuration *apimodels.PostableUserConfig, hash string, createdAt int64, isDefault bool) error
	DeleteGrafanaAlertmanagerConfig(ctx context.Context) error

	TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error)
	TestReceivers(ctx context.Context, c alertingNotify.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error)

	ShouldPromoteConfig() bool

	// Mimir implements an extended version of the receivers API under a different path.
	GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
}

type Mimir struct {
	client        client.Requester
	endpoint      *url.URL
	logger        log.Logger
	metrics       *metrics.RemoteAlertmanager
	promoteConfig bool
	externalURL   string
	staticHeaders map[string]string
}

type Config struct {
	URL      *url.URL
	TenantID string
	Password string

	Logger        log.Logger
	PromoteConfig bool
	ExternalURL   string
	StaticHeaders map[string]string
}

// successResponse represents a successful response from the Mimir API.
type successResponse struct {
	Status string `json:"status"`
	Data   any    `json:"data"`
}

// errorResponse represents an error from the Mimir API.
type errorResponse struct {
	Status string `json:"status"`
	Error1 string `json:"error"`
	Error2 string `json:"Error"`
}

func (e *errorResponse) Error() string {
	if e.Error1 != "" {
		return e.Error1
	}

	return e.Error2
}

func New(cfg *Config, metrics *metrics.RemoteAlertmanager, tracer tracing.Tracer) (*Mimir, error) {
	rt := &MimirAuthRoundTripper{
		TenantID: cfg.TenantID,
		Password: cfg.Password,
		Next:     http.DefaultTransport,
	}

	c := &http.Client{
		Transport: rt,
	}
	tc := client.NewTimedClient(c, metrics.RequestLatency)
	trc := client.NewTracedClient(tc, tracer, "remote.alertmanager.client")

	return &Mimir{
		endpoint:      cfg.URL,
		client:        trc,
		logger:        cfg.Logger,
		metrics:       metrics,
		promoteConfig: cfg.PromoteConfig,
		externalURL:   cfg.ExternalURL,
		staticHeaders: cfg.StaticHeaders,
	}, nil
}

// do execute an HTTP requests against the specified path and method using the specified payload.
// It returns the HTTP response.
func (mc *Mimir) do(ctx context.Context, p, method string, payload io.Reader, out any) (*http.Response, error) {
	pathURL, err := url.Parse(p)
	if err != nil {
		return nil, err
	}

	endpoint := *mc.endpoint
	endpoint.Path = path.Join(endpoint.Path, pathURL.Path)

	r, err := http.NewRequestWithContext(ctx, method, endpoint.String(), payload)
	if err != nil {
		return nil, err
	}

	r.Header.Set("Accept", "application/json")
	r.Header.Set("Content-Type", "application/json")

	resp, err := mc.client.Do(r)
	if err != nil {
		msg := "Unable to fulfill request to the Mimir API"
		mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method)
		return nil, fmt.Errorf("%s: %w", msg, err)
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			mc.logger.Error("Error closing HTTP body", "err", err, "url", r.URL.String(), "method", r.Method)
		}
	}()

	ct := resp.Header.Get("Content-Type")
	if !strings.HasPrefix(ct, "application/json") {
		msg := "Response content-type is not application/json"
		mc.logger.Error(msg, "content-type", "url", r.URL.String(), "method", r.Method, ct, "status", resp.StatusCode)
		return nil, fmt.Errorf("%s: %s", msg, ct)
	}

	if out == nil {
		return resp, nil
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		msg := "Failed to read the request body"
		mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
		return nil, fmt.Errorf("%s: %w", msg, err)
	}

	if resp.StatusCode/100 != 2 {
		errResponse := &errorResponse{}
		err = json.Unmarshal(body, errResponse)

		if err == nil && errResponse.Error() != "" {
			msg := "Error response from the Mimir API"
			mc.logger.Error(msg, "err", errResponse, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
			return nil, fmt.Errorf("%s: %w", msg, errResponse)
		}

		msg := "Failed to decode non-2xx JSON response"
		mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
		return nil, fmt.Errorf("%s: %w", msg, err)
	}

	if err = json.Unmarshal(body, out); err != nil {
		msg := "Failed to decode 2xx JSON response"
		mc.logger.Error(msg, "err", err, "url", r.URL.String(), "method", r.Method, "status", resp.StatusCode)
		return nil, fmt.Errorf("%s: %w", msg, err)
	}

	return resp, nil
}

func (mc *Mimir) doOK(ctx context.Context, p, method string, payload io.Reader) error {
	var sr successResponse
	resp, err := mc.do(ctx, p, method, payload, &sr)
	if err != nil {
		return err
	}
	defer func() {
		if err := resp.Body.Close(); err != nil {
			mc.logger.Error("Error closing HTTP body", "err", err)
		}
	}()

	switch sr.Status {
	case "success":
		return nil
	case "error":
		return errors.New("received an 2xx status code but the request body reflected an error")
	default:
		return fmt.Errorf("received an unknown status from the request body: %s", sr.Status)
	}
}

func (mc *Mimir) TestReceivers(ctx context.Context, c alertingNotify.TestReceiversConfigBodyParams) (*alertingNotify.TestReceiversResult, int, error) {
	payload, err := json.Marshal(c)
	if err != nil {
		return nil, 0, err
	}

	trResult := &alertingNotify.TestReceiversResult{}

	// nolint:bodyclose
	// closed within `do`
	_, err = mc.do(ctx, "api/v1/grafana/receivers/test", http.MethodPost, bytes.NewBuffer(payload), &trResult)
	if err != nil {
		return nil, 0, err
	}

	return trResult, http.StatusOK, nil
}

func (mc *Mimir) TestTemplate(ctx context.Context, c alertingNotify.TestTemplatesConfigBodyParams) (*alertingNotify.TestTemplatesResults, error) {
	payload, err := json.Marshal(c)
	if err != nil {
		return nil, err
	}

	ttResult := &alertingNotify.TestTemplatesResults{}

	// nolint:bodyclose
	// closed within `do`
	_, err = mc.do(ctx, "api/v1/grafana/templates/test", http.MethodPost, bytes.NewBuffer(payload), &ttResult)
	if err != nil {
		return nil, err
	}

	return ttResult, nil
}
