package backend

import (
	"encoding/json"
	"errors"
	"time"

	"github.com/grafana/grafana-plugin-sdk-go/backend/useragent"
	"github.com/grafana/grafana-plugin-sdk-go/config"
	"github.com/grafana/grafana-plugin-sdk-go/data"
	"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

// ConvertFromProtobuf has a collection of methods for converting from the autogenerated
// protobuf go code to our SDK objects. This object exists to attach a collection
// of conversion methods to.
//
// This is used internally by the SDK and inside Grafana-server, plugin authors should not
// need this functionality.
type ConvertFromProtobuf struct{}

// FromProto returns a new ConvertFromProtobuf.
func FromProto() ConvertFromProtobuf {
	return ConvertFromProtobuf{}
}

// User converts protobuf version of a User to the SDK version.
func (f ConvertFromProtobuf) User(user *pluginv2.User) *User {
	if user == nil {
		return nil
	}

	return &User{
		Login: user.Login,
		Name:  user.Name,
		Email: user.Email,
		Role:  user.Role,
	}
}

// AppInstanceSettings converts protobuf version of an AppInstanceSettings to the SDK version.
func (f ConvertFromProtobuf) AppInstanceSettings(proto *pluginv2.AppInstanceSettings) *AppInstanceSettings {
	if proto == nil {
		return nil
	}

	return &AppInstanceSettings{
		JSONData:                proto.JsonData,
		DecryptedSecureJSONData: proto.DecryptedSecureJsonData,
		Updated:                 time.Unix(0, proto.LastUpdatedMS*int64(time.Millisecond)),
		APIVersion:              proto.ApiVersion,
	}
}

// DataSourceInstanceSettings converts protobuf version of a DataSourceInstanceSettings to the SDK version.
func (f ConvertFromProtobuf) DataSourceInstanceSettings(proto *pluginv2.DataSourceInstanceSettings, pluginID string) *DataSourceInstanceSettings {
	if proto == nil {
		return nil
	}

	return &DataSourceInstanceSettings{
		ID:                      proto.Id, // nolint:staticcheck
		UID:                     proto.Uid,
		Type:                    pluginID,
		Name:                    proto.Name,
		URL:                     proto.Url,
		User:                    proto.User,
		Database:                proto.Database,
		BasicAuthEnabled:        proto.BasicAuthEnabled,
		BasicAuthUser:           proto.BasicAuthUser,
		JSONData:                proto.JsonData,
		DecryptedSecureJSONData: proto.DecryptedSecureJsonData,
		Updated:                 time.Unix(0, proto.LastUpdatedMS*int64(time.Millisecond)),
		APIVersion:              proto.ApiVersion,
	}
}

// UserAgent converts protobuf version of a UserAgent to the SDK version.
func (f ConvertFromProtobuf) UserAgent(u string) *useragent.UserAgent {
	if len(u) == 0 {
		return nil
	}
	ua, err := useragent.Parse(u)
	if err != nil {
		return nil
	}
	return ua
}

// PluginContext converts protobuf version of a PluginContext to the SDK version.
func (f ConvertFromProtobuf) PluginContext(proto *pluginv2.PluginContext) PluginContext {
	return PluginContext{
		OrgID:                      proto.OrgId, // nolint:staticcheck
		PluginID:                   proto.PluginId,
		PluginVersion:              proto.PluginVersion,
		APIVersion:                 proto.ApiVersion,
		Namespace:                  proto.Namespace,
		User:                       f.User(proto.User),
		AppInstanceSettings:        f.AppInstanceSettings(proto.AppInstanceSettings),
		DataSourceInstanceSettings: f.DataSourceInstanceSettings(proto.DataSourceInstanceSettings, proto.PluginId),
		GrafanaConfig:              f.GrafanaConfig(proto.GrafanaConfig),
		UserAgent:                  f.UserAgent(proto.UserAgent),
	}
}

// TimeRange converts protobuf version of a TimeRange to the SDK version.
func (f ConvertFromProtobuf) TimeRange(proto *pluginv2.TimeRange) TimeRange {
	return TimeRange{
		From: time.Unix(0, proto.FromEpochMS*int64(time.Millisecond)),
		To:   time.Unix(0, proto.ToEpochMS*int64(time.Millisecond)),
	}
}

// DataQuery converts protobuf version of a DataQuery to the SDK version.
func (f ConvertFromProtobuf) DataQuery(proto *pluginv2.DataQuery) *DataQuery {
	if proto == nil {
		return nil
	}
	return &DataQuery{
		RefID:         proto.RefId,
		QueryType:     proto.QueryType,
		MaxDataPoints: proto.MaxDataPoints,
		TimeRange:     f.TimeRange(proto.TimeRange),
		Interval:      time.Duration(proto.IntervalMS) * time.Millisecond,
		JSON:          proto.Json,
	}
}

// QueryDataRequest converts protobuf version of a QueryDataRequest to the SDK version.
func (f ConvertFromProtobuf) QueryDataRequest(protoReq *pluginv2.QueryDataRequest) *QueryDataRequest {
	queries := make([]DataQuery, len(protoReq.Queries))
	for i, q := range protoReq.Queries {
		queries[i] = *f.DataQuery(q)
	}

	return &QueryDataRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Headers:       protoReq.Headers,
		Queries:       queries,
		Format:        DataFrameFormat(protoReq.Format),
	}
}

// QueryDataResponse converts protobuf version of a QueryDataResponse to the SDK version.
func (f ConvertFromProtobuf) QueryDataResponse(protoRes *pluginv2.QueryDataResponse) (*QueryDataResponse, error) {
	qdr := &QueryDataResponse{
		Responses: make(Responses, len(protoRes.Responses)),
	}
	var err error
	for refID, res := range protoRes.Responses {
		status := Status(res.Status)
		if !status.IsValid() {
			status = StatusUnknown
		}

		dr := DataResponse{Status: status}

		switch res.Format {
		case pluginv2.DataFrameFormat_ARROW:
			dr.Frames, err = data.UnmarshalArrowFrames(res.Frames)
			if err != nil {
				return nil, err
			}
		case pluginv2.DataFrameFormat_JSON:
			dr.Frames = make([]*data.Frame, len(res.Frames))
			for i, b := range res.Frames {
				var v *data.Frame
				err = json.Unmarshal(b, &v)
				if err != nil {
					return nil, err
				}
				dr.Frames[i] = v
			}
		default:
			return nil, errors.New("unknown data frame format: " + res.Format.String())
		}

		if res.Error != "" {
			dr.Error = errors.New(res.Error)
			dr.ErrorSource = ErrorSource(res.ErrorSource)
		}
		qdr.Responses[refID] = dr
	}
	return qdr, nil
}

// Experimental: QueryChunkedDataRequest converts a protobuf QueryChunkedDataRequest to the SDK version.
// This handles the translation between wire format and SDK objects.
func (f ConvertFromProtobuf) QueryChunkedDataRequest(protoReq *pluginv2.QueryChunkedDataRequest) *QueryChunkedDataRequest {
	queries := make([]DataQuery, len(protoReq.Queries))
	for i, q := range protoReq.Queries {
		queries[i] = *f.DataQuery(q)
	}

	return &QueryChunkedDataRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Headers:       protoReq.Headers,
		Queries:       queries,
		Format:        DataFrameFormat(protoReq.Format),
	}
}

// CallResourceRequest converts protobuf version of a CallResourceRequest to the SDK version.
func (f ConvertFromProtobuf) CallResourceRequest(protoReq *pluginv2.CallResourceRequest) *CallResourceRequest {
	headers := map[string][]string{}
	for k, values := range protoReq.Headers {
		headers[k] = values.Values
	}

	return &CallResourceRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Path:          protoReq.Path,
		Method:        protoReq.Method,
		URL:           protoReq.Url,
		Headers:       headers,
		Body:          protoReq.Body,
	}
}

// CallResourceResponse converts protobuf version of a CallResourceResponse to the SDK version.
func (f ConvertFromProtobuf) CallResourceResponse(protoResp *pluginv2.CallResourceResponse) *CallResourceResponse {
	headers := map[string][]string{}
	for k, values := range protoResp.Headers {
		headers[k] = values.Values
	}

	return &CallResourceResponse{
		Status:  int(protoResp.Code),
		Body:    protoResp.Body,
		Headers: headers,
	}
}

// CheckHealthRequest converts protobuf version of a CheckHealthRequest to the SDK version.
func (f ConvertFromProtobuf) CheckHealthRequest(protoReq *pluginv2.CheckHealthRequest) *CheckHealthRequest {
	if protoReq.Headers == nil {
		protoReq.Headers = map[string]string{}
	}

	return &CheckHealthRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Headers:       protoReq.Headers,
	}
}

// CheckHealthResponse converts protobuf version of a HealthCheckResponse to the SDK version.
func (f ConvertFromProtobuf) CheckHealthResponse(protoResp *pluginv2.CheckHealthResponse) *CheckHealthResult {
	status := HealthStatusUnknown
	switch protoResp.Status {
	case pluginv2.CheckHealthResponse_ERROR:
		status = HealthStatusError
	case pluginv2.CheckHealthResponse_OK:
		status = HealthStatusOk
	}

	return &CheckHealthResult{
		Status:      status,
		Message:     protoResp.Message,
		JSONDetails: protoResp.JsonDetails,
	}
}

// CollectMetricsRequest converts protobuf version of a CollectMetricsRequest to the SDK version.
func (f ConvertFromProtobuf) CollectMetricsRequest(protoReq *pluginv2.CollectMetricsRequest) *CollectMetricsRequest {
	return &CollectMetricsRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
	}
}

// CollectMetricsResponse converts protobuf version of a CollectMetricsResponse to the SDK version.
func (f ConvertFromProtobuf) CollectMetricsResponse(protoResp *pluginv2.CollectMetricsResponse) *CollectMetricsResult {
	var prometheusMetrics []byte

	if protoResp.Metrics != nil {
		prometheusMetrics = protoResp.Metrics.Prometheus
	}

	return &CollectMetricsResult{
		PrometheusMetrics: prometheusMetrics,
	}
}

// SubscribeStreamRequest ...
func (f ConvertFromProtobuf) SubscribeStreamRequest(protoReq *pluginv2.SubscribeStreamRequest) *SubscribeStreamRequest {
	if protoReq.Headers == nil {
		protoReq.Headers = map[string]string{}
	}
	return &SubscribeStreamRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Path:          protoReq.GetPath(),
		Data:          protoReq.GetData(),
		Headers:       protoReq.Headers,
	}
}

// SubscribeStreamResponse ...
func (f ConvertFromProtobuf) SubscribeStreamResponse(protoReq *pluginv2.SubscribeStreamResponse) *SubscribeStreamResponse {
	return &SubscribeStreamResponse{
		Status: SubscribeStreamStatus(protoReq.GetStatus()),
		InitialData: &InitialData{
			data: protoReq.Data,
		},
	}
}

// PublishStreamRequest ...
func (f ConvertFromProtobuf) PublishStreamRequest(protoReq *pluginv2.PublishStreamRequest) *PublishStreamRequest {
	if protoReq.Headers == nil {
		protoReq.Headers = map[string]string{}
	}
	return &PublishStreamRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Path:          protoReq.GetPath(),
		Data:          protoReq.GetData(),
		Headers:       protoReq.Headers,
	}
}

// PublishStreamResponse ...
func (f ConvertFromProtobuf) PublishStreamResponse(protoReq *pluginv2.PublishStreamResponse) *PublishStreamResponse {
	return &PublishStreamResponse{
		Status: PublishStreamStatus(protoReq.GetStatus()),
		Data:   protoReq.GetData(),
	}
}

// RunStreamRequest ...
func (f ConvertFromProtobuf) RunStreamRequest(protoReq *pluginv2.RunStreamRequest) *RunStreamRequest {
	if protoReq.Headers == nil {
		protoReq.Headers = map[string]string{}
	}
	return &RunStreamRequest{
		PluginContext: f.PluginContext(protoReq.PluginContext),
		Path:          protoReq.GetPath(),
		Data:          protoReq.GetData(),
		Headers:       protoReq.Headers,
	}
}

// StreamPacket ...
func (f ConvertFromProtobuf) StreamPacket(protoReq *pluginv2.StreamPacket) *StreamPacket {
	return &StreamPacket{
		Data: protoReq.GetData(),
	}
}

// StatusResult ...
func (f ConvertFromProtobuf) StatusResult(s *pluginv2.StatusResult) *StatusResult {
	if s == nil {
		return nil
	}
	return &StatusResult{
		Status:  s.Status,
		Message: s.Message,
		Reason:  s.Reason,
		Code:    s.Code,
	}
}

// GroupVersionKind ...
func (f ConvertFromProtobuf) GroupVersionKind(req *pluginv2.GroupVersionKind) GroupVersionKind {
	return GroupVersionKind{
		Group:   req.Group,
		Version: req.Version,
		Kind:    req.Kind,
	}
}

// GroupVersion ...
func (f ConvertFromProtobuf) GroupVersion(req *pluginv2.GroupVersion) GroupVersion {
	return GroupVersion{
		Group:   req.Group,
		Version: req.Version,
	}
}

// RawObject ...
func (f ConvertFromProtobuf) RawObject(req *pluginv2.RawObject) RawObject {
	return RawObject{
		Raw:         req.Raw,
		ContentType: req.ContentType,
	}
}

// RawObjects ...
func (f ConvertFromProtobuf) RawObjects(req []*pluginv2.RawObject) []RawObject {
	rawObjects := make([]RawObject, len(req))
	for i, r := range req {
		rawObjects[i] = f.RawObject(r)
	}
	return rawObjects
}

// AdmissionRequest ...
func (f ConvertFromProtobuf) AdmissionRequest(req *pluginv2.AdmissionRequest) *AdmissionRequest {
	return &AdmissionRequest{
		PluginContext:  f.PluginContext(req.PluginContext),
		Operation:      AdmissionRequestOperation(req.Operation),
		Kind:           f.GroupVersionKind(req.Kind),
		ObjectBytes:    req.ObjectBytes,
		OldObjectBytes: req.OldObjectBytes,
	}
}

// ConversionRequest ...
func (f ConvertFromProtobuf) ConversionRequest(req *pluginv2.ConversionRequest) *ConversionRequest {
	return &ConversionRequest{
		PluginContext: f.PluginContext(req.PluginContext),
		UID:           req.Uid,
		Objects:       f.RawObjects(req.Objects),
		TargetVersion: f.GroupVersion(req.TargetVersion),
	}
}

// MutationResponse ...
func (f ConvertFromProtobuf) MutationResponse(rsp *pluginv2.MutationResponse) *MutationResponse {
	return &MutationResponse{
		Allowed:     rsp.Allowed,
		Result:      f.StatusResult(rsp.Result),
		Warnings:    rsp.Warnings,
		ObjectBytes: rsp.ObjectBytes,
	}
}

// ValidationResponse ...
func (f ConvertFromProtobuf) ValidationResponse(rsp *pluginv2.ValidationResponse) *ValidationResponse {
	return &ValidationResponse{
		Allowed:  rsp.Allowed,
		Result:   f.StatusResult(rsp.Result),
		Warnings: rsp.Warnings,
	}
}

// ConversionResponse ...
func (f ConvertFromProtobuf) ConversionResponse(rsp *pluginv2.ConversionResponse) *ConversionResponse {
	return &ConversionResponse{
		UID:     rsp.Uid,
		Result:  f.StatusResult(rsp.Result),
		Objects: f.RawObjects(rsp.Objects),
	}
}

func (f ConvertFromProtobuf) GrafanaConfig(cfg map[string]string) *config.GrafanaCfg {
	return config.NewGrafanaCfg(cfg)
}
