// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package jmx

import (
	"encoding/json"
	"strings"

	"github.com/joeshaw/multierror"
	"github.com/pkg/errors"

	"github.com/elastic/beats/libbeat/common"
	"github.com/elastic/beats/libbeat/logp"
)

const (
	mbeanEventKey = "mbean"
)

type Entry struct {
	Request struct {
		Mbean string `json:"mbean"`
	}
	Value map[string]interface{}
}

// Map responseBody to common.MapStr
//
// A response has the following structure
//  [
//    {
//        "request": {
//            "mbean": "java.lang:type=Memory",
//            "attribute": [
//                "HeapMemoryUsage",
//                "NonHeapMemoryUsage"
//            ],
//            "type": "read"
//        },
//        "value": {
//            "HeapMemoryUsage": {
//                "init": 1073741824,
//                "committed": 1037959168,
//                "max": 1037959168,
//                "used": 227420472
//            },
//            "NonHeapMemoryUsage": {
//                "init": 2555904,
//                "committed": 53477376,
//                "max": -1,
//                "used": 50519768
//            }
//        },
//        "timestamp": 1472298687,
//        "status": 200
//     }
//  ]
//
// With wildcards there is an additional nesting level:
//
//  [
//     {
//        "request": {
//           "type": "read",
//           "attribute": "maxConnections",
//           "mbean": "Catalina:name=*,type=ThreadPool"
//        },
//        "value": {
//           "Catalina:name=\"http-bio-8080\",type=ThreadPool": {
//              "maxConnections": 200
//           },
//           "Catalina:name=\"ajp-bio-8009\",type=ThreadPool": {
//              "maxConnections": 200
//           }
//        },
//        "timestamp": 1519409583
//        "status": 200,
//     }
//  }
type eventKey struct {
	mbean, event string
}

func eventMapping(content []byte, mapping AttributeMapping) ([]common.MapStr, error) {
	var entries []Entry
	if err := json.Unmarshal(content, &entries); err != nil {
		return nil, errors.Wrapf(err, "failed to unmarshal jolokia JSON response '%v'", string(content))
	}

	// Generate a different event for each wildcard mbean, and and additional one
	// for non-wildcard requested mbeans, group them by event name if defined
	mbeanEvents := make(map[eventKey]common.MapStr)
	var errs multierror.Errors

	for _, v := range entries {
		hasWildcard := strings.Contains(v.Request.Mbean, "*")
		for attribute, value := range v.Value {
			if !hasWildcard {
				err := parseResponseEntry(v.Request.Mbean, v.Request.Mbean, attribute, value, mbeanEvents, mapping)
				if err != nil {
					errs = append(errs, err)
				}
				continue
			}

			// If there was a wildcard, we are going to have an additional
			// nesting level in response values, and attribute here is going
			// to be actually the matching mbean name
			values, ok := value.(map[string]interface{})
			if !ok {
				errs = append(errs, errors.Errorf("expected map of values for %s", v.Request.Mbean))
				continue
			}

			responseMbean := attribute
			for attribute, value := range values {
				err := parseResponseEntry(v.Request.Mbean, responseMbean, attribute, value, mbeanEvents, mapping)
				if err != nil {
					errs = append(errs, err)
				}
			}
		}
	}

	var events []common.MapStr
	for _, event := range mbeanEvents {
		events = append(events, event)
	}

	return events, errs.Err()
}

func selectEvent(events map[eventKey]common.MapStr, key eventKey) common.MapStr {
	event, found := events[key]
	if !found {
		event = common.MapStr{}
		if key.mbean != "" {
			event.Put(mbeanEventKey, key.mbean)
		}
		events[key] = event
	}
	return event
}

func parseResponseEntry(
	requestMbeanName string,
	responseMbeanName string,
	attributeName string,
	attributeValue interface{},
	events map[eventKey]common.MapStr,
	mapping AttributeMapping,
) error {
	field, exists := mapping.Get(requestMbeanName, attributeName)
	if !exists {
		// This shouldn't ever happen, if it does it is probably that some of our
		// assumptions when building the request and the mapping is wrong.
		logp.Debug("jolokia.jmx", "mapping: %+v", mapping)
		return errors.Errorf("metric key '%v' for mbean '%s' not found in mapping", attributeName, requestMbeanName)
	}

	var key eventKey
	key.event = field.Event
	if responseMbeanName != requestMbeanName {
		key.mbean = responseMbeanName
	}
	event := selectEvent(events, key)

	// In case the attributeValue is a map the keys are dedotted
	data := attributeValue
	c, ok := data.(map[string]interface{})
	if ok {
		newData := map[string]interface{}{}
		for k, v := range c {
			newData[common.DeDot(k)] = v
		}
		data = newData
	}
	_, err := event.Put(field.Field, data)
	return err
}
