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

// +build windows

package eventlog

import (
	"fmt"
	"syscall"
	"time"

	"github.com/joeshaw/multierror"

	"github.com/elastic/beats/libbeat/common"
	"github.com/elastic/beats/libbeat/logp"
	"github.com/elastic/beats/winlogbeat/checkpoint"
	"github.com/elastic/beats/winlogbeat/sys"
	win "github.com/elastic/beats/winlogbeat/sys/eventlogging"
)

const (
	// eventLoggingAPIName is the name used to identify the Event Logging API
	// as both an event type and an API.
	eventLoggingAPIName = "eventlogging"
)

var eventLoggingConfigKeys = append(commonConfigKeys, "ignore_older",
	"read_buffer_size", "format_buffer_size")

type eventLoggingConfig struct {
	ConfigCommon     `config:",inline"`
	IgnoreOlder      time.Duration `config:"ignore_older"`
	ReadBufferSize   uint          `config:"read_buffer_size"   validate:"min=1"`
	FormatBufferSize uint          `config:"format_buffer_size" validate:"min=1"`
}

// Validate validates the eventLoggingConfig data and returns an error
// describing any problems or nil.
func (c *eventLoggingConfig) Validate() error {
	var errs multierror.Errors
	if c.Name == "" {
		errs = append(errs, fmt.Errorf("event log is missing a 'name'"))
	}

	if c.ReadBufferSize > win.MaxEventBufferSize {
		errs = append(errs, fmt.Errorf("'read_buffer_size' must be less than "+
			"%d bytes", win.MaxEventBufferSize))
	}

	if c.FormatBufferSize > win.MaxFormatMessageBufferSize {
		errs = append(errs, fmt.Errorf("'format_buffer_size' must be less than "+
			"%d bytes", win.MaxFormatMessageBufferSize))
	}

	return errs.Err()
}

// Validate that eventLogging implements the EventLog interface.
var _ EventLog = &eventLogging{}

// eventLogging implements the EventLog interface for reading from the Event
// Logging API.
type eventLogging struct {
	config    eventLoggingConfig
	name      string             // Name of the log that is opened.
	handle    win.Handle         // Handle to the event log.
	readBuf   []byte             // Buffer for reading in events.
	formatBuf []byte             // Buffer for formatting messages.
	insertBuf win.StringInserts  // Buffer for parsing insert strings.
	handles   *messageFilesCache // Cached mapping of source name to event message file handles.
	logPrefix string             // Prefix to add to all log entries.

	recordNumber uint32 // First record number to read.
	seek         bool   // Read should use seek.
	ignoreFirst  bool   // Ignore first message returned from a read.
}

// Name returns the name of the event log (i.e. Application, Security, etc.).
func (l eventLogging) Name() string {
	return l.name
}

func (l *eventLogging) Open(state checkpoint.EventLogState) error {
	detailf("%s Open(recordNumber=%d) calling OpenEventLog(uncServerPath=, "+
		"providerName=%s)", l.logPrefix, state.RecordNumber, l.name)
	handle, err := win.OpenEventLog("", l.name)
	if err != nil {
		return err
	}

	numRecords, err := win.GetNumberOfEventLogRecords(handle)
	if err != nil {
		return err
	}

	var oldestRecord, newestRecord uint32
	if numRecords > 0 {
		l.recordNumber = uint32(state.RecordNumber)
		l.seek = true
		l.ignoreFirst = true

		oldestRecord, err = win.GetOldestEventLogRecord(handle)
		if err != nil {
			return err
		}
		newestRecord = oldestRecord + numRecords - 1

		if l.recordNumber < oldestRecord || l.recordNumber > newestRecord {
			l.recordNumber = oldestRecord
			l.ignoreFirst = false
		}
	} else {
		l.recordNumber = 0
		l.seek = false
		l.ignoreFirst = false
	}

	logp.Info("%s contains %d records. Record number range [%d, %d]. Starting "+
		"at %d (ignoringFirst=%t)", l.logPrefix, numRecords, oldestRecord,
		newestRecord, l.recordNumber, l.ignoreFirst)

	l.handle = handle
	return nil
}

func (l *eventLogging) Read() ([]Record, error) {
	flags := win.EVENTLOG_SEQUENTIAL_READ | win.EVENTLOG_FORWARDS_READ
	if l.seek {
		flags = win.EVENTLOG_SEEK_READ | win.EVENTLOG_FORWARDS_READ
		l.seek = false
	}

	var numBytesRead int
	err := retry(
		func() error {
			l.readBuf = l.readBuf[0:cap(l.readBuf)]
			// TODO: Use number of bytes to grow the buffer size as needed.
			var err error
			numBytesRead, err = win.ReadEventLog(
				l.handle,
				flags,
				l.recordNumber,
				l.readBuf)
			return err
		},
		l.readRetryErrorHandler)
	if err != nil {
		debugf("%s ReadEventLog returned error %v", l.logPrefix, err)
		return readErrorHandler(err)
	}
	detailf("%s ReadEventLog read %d bytes", l.logPrefix, numBytesRead)

	l.readBuf = l.readBuf[0:numBytesRead]
	events, _, err := win.RenderEvents(
		l.readBuf[:numBytesRead], 0, l.formatBuf, &l.insertBuf, l.handles.get)
	if err != nil {
		return nil, err
	}
	detailf("%s RenderEvents returned %d events", l.logPrefix, len(events))

	records := make([]Record, 0, len(events))
	for _, e := range events {
		// The events do not contain the name of the event log so we must add
		// the name of the log from which we are reading.
		e.Channel = l.name

		err = sys.PopulateAccount(&e.User)
		if err != nil {
			debugf("%s SID %s account lookup failed. %v", l.logPrefix,
				e.User.Identifier, err)
		}

		records = append(records, Record{
			API:   eventLoggingAPIName,
			Event: e,
			Offset: checkpoint.EventLogState{
				Name:         l.name,
				RecordNumber: e.RecordID,
				Timestamp:    e.TimeCreated.SystemTime,
			},
		})
	}

	if l.ignoreFirst && len(records) > 0 {
		debugf("%s Ignoring first event with record ID %d", l.logPrefix,
			records[0].RecordID)
		records = records[1:]
		l.ignoreFirst = false
	}

	records = filter(records, l.ignoreOlder)
	debugf("%s Read() is returning %d records", l.logPrefix, len(records))
	return records, nil
}

func (l *eventLogging) Close() error {
	debugf("%s Closing handle", l.logPrefix)
	return win.CloseEventLog(l.handle)
}

// readRetryErrorHandler handles errors returned from the readEventLog function
// by attempting to correct the error through closing and reopening the event
// log.
func (l *eventLogging) readRetryErrorHandler(err error) error {
	incrementMetric(readErrors, err)
	if errno, ok := err.(syscall.Errno); ok {
		var reopen bool

		switch errno {
		case win.ERROR_EVENTLOG_FILE_CHANGED:
			debugf("Re-opening event log because event log file was changed")
			reopen = true
		case win.ERROR_EVENTLOG_FILE_CORRUPT:
			debugf("Re-opening event log because event log file is corrupt")
			reopen = true
		}

		if reopen {
			l.Close()
			return l.Open(checkpoint.EventLogState{
				Name:         l.name,
				RecordNumber: uint64(l.recordNumber),
			})
		}
	}
	return err
}

// readErrorHandler handles errors returned by the readEventLog function.
func readErrorHandler(err error) ([]Record, error) {
	switch err {
	case syscall.ERROR_HANDLE_EOF,
		win.ERROR_EVENTLOG_FILE_CHANGED,
		win.ERROR_EVENTLOG_FILE_CORRUPT:
		return []Record{}, nil
	}
	return nil, err
}

// Filter returns a new slice holding only the elements of s that satisfy the
// predicate fn().
func filter(in []Record, fn func(*Record) bool) []Record {
	var out []Record
	for _, r := range in {
		if fn(&r) {
			out = append(out, r)
		}
	}
	return out
}

// ignoreOlder is a filter predicate that checks the record timestamp and
// returns true if the event was not matched by the filter.
func (l *eventLogging) ignoreOlder(r *Record) bool {
	if l.config.IgnoreOlder != 0 && time.Since(r.TimeCreated.SystemTime) > l.config.IgnoreOlder {
		return false
	}

	return true
}

// newEventLogging creates and returns a new EventLog for reading event logs
// using the Event Logging API.
func newEventLogging(options *common.Config) (EventLog, error) {
	c := eventLoggingConfig{
		ReadBufferSize:   win.MaxEventBufferSize,
		FormatBufferSize: win.MaxFormatMessageBufferSize,
	}
	if err := readConfig(options, &c, eventLoggingConfigKeys); err != nil {
		return nil, err
	}

	return &eventLogging{
		config: c,
		name:   c.Name,
		handles: newMessageFilesCache(c.Name, win.QueryEventMessageFiles,
			win.FreeLibrary),
		logPrefix: fmt.Sprintf("EventLogging[%s]", c.Name),
		readBuf:   make([]byte, 0, c.ReadBufferSize),
		formatBuf: make([]byte, c.FormatBufferSize),
	}, nil
}

func init() {
	// Register eventlogging API if it is available.
	available, _ := win.IsAvailable()
	if available {
		Register(eventLoggingAPIName, 1, newEventLogging, nil)
	}
}
