package jaeger

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/go-logfmt/logfmt"
	"github.com/grafana/grafana-plugin-sdk-go/backend"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana/pkg/tsdb/jaeger/types"
	"github.com/grafana/grafana/pkg/tsdb/jaeger/utils"
)

func (j *JaegerClient) GrpcServices() ([]string, error) {
	var response types.GrpcServicesResponse
	services := []string{}

	u, err := url.JoinPath(j.url, "/api/v3/services")
	if err != nil {
		return services, backend.DownstreamErrorf("failed to join url: %w", err)
	}

	res, err := j.httpClient.Get(u)
	if err != nil {
		if backend.IsDownstreamHTTPError(err) {
			return services, backend.DownstreamError(err)
		}
		return services, err
	}

	defer func() {
		if err = res.Body.Close(); err != nil {
			j.logger.Error("Failed to close response body", "error", err)
		}
	}()

	if res != nil && res.StatusCode != http.StatusOK {
		err := fmt.Errorf("request failed: %s", res.Status)
		if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream {
			return services, backend.DownstreamError(err)
		}
		return services, err
	}

	if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
		return services, backend.DownstreamError(err)
	}

	services = response.Services
	return services, nil
}

func (j *JaegerClient) GrpcOperations(s string) ([]string, error) {
	var response types.GrpcOperationsResponse
	operations := []string{}

	u, err := url.JoinPath(j.url, "/api/v3/operations")
	if err != nil {
		return operations, backend.DownstreamErrorf("failed to join url: %w", err)
	}

	jaegerURL, err := url.Parse(u)
	if err != nil {
		return operations, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err)
	}

	urlQuery := jaegerURL.Query()
	urlQuery.Set("service", s)
	jaegerURL.RawQuery = urlQuery.Encode()

	res, err := j.httpClient.Get(jaegerURL.String())
	if err != nil {
		if backend.IsDownstreamHTTPError(err) {
			return operations, backend.DownstreamError(err)
		}
		return operations, err
	}

	defer func() {
		if err = res.Body.Close(); err != nil {
			j.logger.Error("Failed to close response body", "error", err)
		}
	}()

	if res != nil && res.StatusCode != http.StatusOK {
		err := fmt.Errorf("request failed: %s", res.Status)
		if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream {
			return operations, backend.DownstreamError(err)
		}
		return operations, err
	}

	if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
		return operations, backend.DownstreamError(err)
	}

	// extract name from operations response
	for _, op := range response.Operations {
		operations = append(operations, op.Name)
	}

	return operations, nil
}

// Note that this and all functionality around search is not yet being used. Once Jaeger adds support for attributes and limit parameters
// we will be able to start using this and routing traffic to the new API based on the feature flag.
func (j *JaegerClient) GrpcSearch(query *JaegerQuery, start, end time.Time) (*data.Frame, error) {
	u, err := url.JoinPath(j.url, "/api/v3/traces")
	if err != nil {
		return nil, backend.DownstreamErrorf("failed to join url path: %w", err)
	}

	jaegerURL, err := url.Parse(u)
	if err != nil {
		return nil, backend.DownstreamErrorf("failed to parse Jaeger URL: %w", err)
	}

	var queryTags string
	if query.Tags != "" {
		tagMap := make(map[string]string)
		decoder := logfmt.NewDecoder(strings.NewReader(query.Tags))
		for decoder.ScanRecord() {
			for decoder.ScanKeyval() {
				key := decoder.Key()
				value := decoder.Value()
				tagMap[string(key)] = string(value)
			}
		}

		marshaledTags, err := json.Marshal(tagMap)
		if err != nil {
			return nil, backend.DownstreamErrorf("failed to convert tags to JSON: %w", err)
		}

		queryTags = string(marshaledTags)
	}

	queryParams := map[string]string{
		"query.service_name":   query.Service,
		"query.operation_name": query.Operation,
		"query.attributes":     queryTags, // TODO: no native support of attributes/tags figure out if we want to do it in post processing.
		"query.duration_min":   query.MinDuration,
		"query.duration_max":   query.MaxDuration,
	}

	urlQuery := jaegerURL.Query()

	for key, value := range queryParams {
		if value != "" {
			urlQuery.Set(key, value)
		}
	}

	jaegerURL.RawQuery = urlQuery.Encode()
	// jaeger will not be able to process the request if the time is encoded, all other parameters are encoded except for the start and end time
	jaegerURL.RawQuery += fmt.Sprintf("&query.start_time_min=%s&query.start_time_max=%s", start.Format(time.RFC3339Nano), end.Format(time.RFC3339Nano))
	resp, err := j.httpClient.Get(jaegerURL.String())
	if err != nil {
		if backend.IsDownstreamHTTPError(err) {
			return nil, backend.DownstreamError(err)
		}
		return nil, err
	}

	defer func() {
		if err = resp.Body.Close(); err != nil {
			j.logger.Error("Failed to close response body", "error", err)
		}
	}()

	if resp != nil && resp.StatusCode != http.StatusOK {
		err := fmt.Errorf("request failed: %s", resp.Status)
		if backend.ErrorSourceFromHTTPStatus(resp.StatusCode) == backend.ErrorSourceDownstream {
			return nil, backend.DownstreamError(err)
		}
		return nil, err
	}

	var response types.GrpcTracesResponse
	if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
		return nil, backend.DownstreamErrorf("failed to decode Jaeger response: %w", err)
	}

	// for search call, an unsuccessful response is exposed through the error attribute
	// see https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L77C17-L79C18
	if response.Error.HttpCode != 0 && response.Error.HttpCode != http.StatusOK {
		err := fmt.Errorf("request failed %s", response.Error.Message)
		if backend.ErrorSourceFromHTTPStatus(response.Error.HttpCode) == backend.ErrorSourceDownstream {
			return nil, backend.DownstreamError(err)
		}
		return nil, err
	}
	frames := utils.TransformGrpcSearchResponse(response.Result, j.settings.UID, j.settings.Name, query.Limit)
	return frames, nil
}

func (j *JaegerClient) GrpcTrace(ctx context.Context, traceID string, start, end time.Time, refID string) (*data.Frame, error) {
	logger := j.logger.FromContext(ctx)
	var response types.GrpcTracesResponse

	if traceID == "" {
		return nil, backend.DownstreamErrorf("traceID is empty")
	}

	traceUrl, err := url.JoinPath(j.url, "/api/v3/traces", url.QueryEscape(traceID))
	if err != nil {
		return nil, backend.DownstreamErrorf("failed to join url: %w", err)
	}

	var jsonData types.SettingsJSONData
	if err := json.Unmarshal(j.settings.JSONData, &jsonData); err != nil {
		return nil, backend.DownstreamErrorf("failed to parse settings JSON data: %w", err)
	}

	// Add time parameters if trace ID time is enabled and time range is provided
	if jsonData.TraceIdTimeParams.Enabled {
		if start.UnixMicro() > 0 || end.UnixMicro() > 0 {
			parsedURL, err := url.Parse(traceUrl)
			if err != nil {
				return nil, backend.DownstreamErrorf("failed to parse url: %w", err)
			}

			// jaeger will not be able to process the request if the time is encoded, all other parameters are encoded except for the start and end time
			parsedURL.RawQuery += fmt.Sprintf("start_time=%s&end_time=%s", start.Format(time.RFC3339Nano), end.Format(time.RFC3339Nano))
			traceUrl = parsedURL.String()
		}
	}

	res, err := j.httpClient.Get(traceUrl)
	if err != nil {
		if backend.IsDownstreamHTTPError(err) {
			return nil, backend.DownstreamError(err)
		}
		return nil, err
	}

	defer func() {
		if err = res.Body.Close(); err != nil {
			logger.Error("Failed to close response body", "error", err)
		}
	}()

	if res != nil && res.StatusCode != http.StatusOK {
		err := fmt.Errorf("request failed: %s", res.Status)
		if backend.ErrorSourceFromHTTPStatus(res.StatusCode) == backend.ErrorSourceDownstream {
			return nil, backend.DownstreamError(err)
		}
		return nil, err
	}

	if err := json.NewDecoder(res.Body).Decode(&response); err != nil {
		return nil, err
	}

	// for trace search call, an unsuccessful response is exposed through the error attribute
	// see https://github.com/jaegertracing/jaeger-idl/blob/7c7460fc400325ae69435c0aa65697f4cc1ab581/swagger/api_v3/query_service.swagger.json#L77C17-L79C18
	if response.Error.HttpCode != 0 && response.Error.HttpCode != http.StatusOK {
		err := fmt.Errorf("request failed %s", response.Error.Message)
		if backend.ErrorSourceFromHTTPStatus(response.Error.HttpCode) == backend.ErrorSourceDownstream {
			return nil, backend.DownstreamError(err)
		}
		return nil, err
	}

	frame := utils.TransformGrpcTraceResponse(response.Result.ResourceSpans, refID)
	return frame, nil
}
