// 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 sys

import (
	"encoding/xml"
	"fmt"
	"time"
)

// UnmarshalEventXML unmarshals the given XML into a new Event.
func UnmarshalEventXML(rawXML []byte) (Event, error) {
	var event Event
	err := xml.Unmarshal(rawXML, &event)
	return event, err
}

// Event holds the data from a log record.
type Event struct {
	// System
	Provider        Provider        `xml:"System>Provider"`
	EventIdentifier EventIdentifier `xml:"System>EventID"`
	Version         uint8           `xml:"System>Version"`
	LevelRaw        uint8           `xml:"System>Level"`
	TaskRaw         uint16          `xml:"System>Task"`
	OpcodeRaw       uint8           `xml:"System>Opcode"`
	TimeCreated     TimeCreated     `xml:"System>TimeCreated"`
	RecordID        uint64          `xml:"System>EventRecordID"`
	Correlation     Correlation     `xml:"System>Correlation"`
	Execution       Execution       `xml:"System>Execution"`
	Channel         string          `xml:"System>Channel"`
	Computer        string          `xml:"System>Computer"`
	User            SID             `xml:"System>Security"`

	EventData EventData `xml:"EventData"`
	UserData  UserData  `xml:"UserData"`

	// RenderingInfo
	Message  string   `xml:"RenderingInfo>Message"`
	Level    string   `xml:"RenderingInfo>Level"`
	Task     string   `xml:"RenderingInfo>Task"`
	Opcode   string   `xml:"RenderingInfo>Opcode"`
	Keywords []string `xml:"RenderingInfo>Keywords>Keyword"`

	// ProcessingErrorData
	RenderErrorCode         uint32 `xml:"ProcessingErrorData>ErrorCode"`
	RenderErrorDataItemName string `xml:"ProcessingErrorData>DataItemName"`
	RenderErr               string
}

// Provider identifies the provider that logged the event. The Name and GUID
// attributes are included if the provider used an instrumentation manifest to
// define its events; otherwise, the EventSourceName attribute is included if a
// legacy event provider (using the Event Logging API) logged the event.
type Provider struct {
	Name            string `xml:"Name,attr"`
	GUID            string `xml:"Guid,attr"`
	EventSourceName string `xml:"EventSourceName,attr"`
}

// Correlation contains activity identifiers that consumers can use to group
// related events together.
type Correlation struct {
	ActivityID        string `xml:"ActivityID,attr"`
	RelatedActivityID string `xml:"RelatedActivityID,attr"`
}

// Execution contains information about the process and thread that logged the
// event.
type Execution struct {
	ProcessID uint32 `xml:"ProcessID,attr"`
	ThreadID  uint32 `xml:"ThreadID,attr"`

	// Only available for events logged to an event tracing log file (.etl file).
	ProcessorID   uint32 `xml:"ProcessorID,attr"`
	SessionID     uint32 `xml:"SessionID,attr"`
	KernelTime    uint32 `xml:"KernelTime,attr"`
	UserTime      uint32 `xml:"UserTime,attr"`
	ProcessorTime uint32 `xml:"ProcessorTime,attr"`
}

// EventIdentifier is the identifer that the provider uses to identify a
// specific event type.
type EventIdentifier struct {
	Qualifiers uint16 `xml:"Qualifiers,attr"`
	ID         uint32 `xml:",chardata"`
}

// TimeCreated contains the system time of when the event was logged.
type TimeCreated struct {
	SystemTime time.Time
}

// UnmarshalXML unmarshals an XML dataTime string.
func (t *TimeCreated) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	attrs := struct {
		SystemTime string `xml:"SystemTime,attr"`
		RawTime    uint64 `xml:"RawTime,attr"`
	}{}

	err := d.DecodeElement(&attrs, &start)
	if err != nil {
		return err
	}

	if attrs.SystemTime != "" {
		// This works but XML dateTime is really ISO8601.
		t.SystemTime, err = time.Parse(time.RFC3339Nano, attrs.SystemTime)
	} else if attrs.RawTime != 0 {
		// The units for RawTime are not specified in the documentation. I think
		// it is only used in event tracing so this shouldn't be a problem.
		err = fmt.Errorf("failed to unmarshal TimeCreated RawTime='%d'", attrs.RawTime)
	}

	return err
}

// EventData contains the event data. The EventData section is used if the
// message provider template does not contain a UserData section.
type EventData struct {
	Pairs []KeyValue `xml:",any"`
}

// UserData contains the event data.
type UserData struct {
	Name  xml.Name
	Pairs []KeyValue
}

// UnmarshalXML unmarshals UserData XML.
func (u *UserData) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	// Assume that UserData has the same general key-value structure as
	// EventData does.
	in := struct {
		Pairs []KeyValue `xml:",any"`
	}{}

	// Read tokens until we find the first StartElement then unmarshal it.
	for {
		t, err := d.Token()
		if err != nil {
			return err
		}

		if se, ok := t.(xml.StartElement); ok {
			err = d.DecodeElement(&in, &se)
			if err != nil {
				return err
			}

			u.Name = se.Name
			u.Pairs = in.Pairs
			d.Skip()
			break
		}
	}

	return nil
}

// KeyValue is a key value pair of strings.
type KeyValue struct {
	Key   string
	Value string
}

// UnmarshalXML unmarshals an arbitrary XML element into a KeyValue. The key
// becomes the name of the element or value of the Name attribute if it exists.
// The value is the character data contained within the element.
func (kv *KeyValue) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	elem := struct {
		XMLName xml.Name
		Name    string `xml:"Name,attr"`
		Value   string `xml:",chardata"`
	}{}

	err := d.DecodeElement(&elem, &start)
	if err != nil {
		return err
	}

	kv.Key = elem.XMLName.Local
	if elem.Name != "" {
		kv.Key = elem.Name
	}
	kv.Value = elem.Value

	return nil
}
