// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package logif

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/PaesslerAG/gval"
	"github.com/buger/jsonparser"
	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
	"github.com/caddyserver/caddy/v2/modules/logging"
	jsonselect "github.com/gitpod-io/gitpod/proxy/plugins/jsonselect"
	"github.com/gitpod-io/gitpod/proxy/plugins/logif/lang"
	"go.uber.org/zap"
	"go.uber.org/zap/buffer"
	"go.uber.org/zap/zapcore"
	"golang.org/x/term"
)

const (
	moduleName = "if"
	moduleID   = "caddy.logging.encoders." + moduleName
)

func init() {
	caddy.RegisterModule(ConditionalEncoder{})
}

type ConditionalEncoder struct {
	zapcore.Encoder       `json:"-"`
	zapcore.EncoderConfig `json:"-"`

	EncRaw    json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
	Eval      gval.Evaluable  `json:"-"`
	Expr      string
	Logger    func(...caddy.Module) *zap.Logger `json:"-"`
	Formatter string
}

func (ce ConditionalEncoder) Clone() zapcore.Encoder {
	ret := ConditionalEncoder{
		Encoder:       ce.Encoder.Clone(),
		EncoderConfig: ce.EncoderConfig,
		Eval:          ce.Eval,
		Logger:        ce.Logger,
		Formatter:     ce.Formatter,
	}
	return ret
}

func (ce ConditionalEncoder) EncodeEntry(e zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
	// Clone the original encoder to be sure we don't mess up it
	enc := ce.Encoder.Clone()

	if ce.Formatter == "console" {
		// Add the zap entries to the console encoder
		// todo > Set the values according to line_ending, time_format, level_format
		// todo > Investigate duration_format too?
		enc.AddString(ce.LevelKey, e.Level.String())
		enc.AddTime(ce.TimeKey, e.Time)
		enc.AddString(ce.NameKey, e.LoggerName)
		enc.AddString(ce.MessageKey, e.Message)
		// todo > caller, stack
	} else if ce.Formatter == "jsonselect" {
		// Use the JSON encoder that JSONSelect wraps
		jsonEncoder, ok := ce.Encoder.(jsonselect.JSONSelectEncoder)
		if !ok {
			return nil, fmt.Errorf("unexpected encoder type %T", ce.Encoder)
		}
		enc = jsonEncoder.Encoder
	}

	// Store the logging encoder's buffer
	buf, err := enc.EncodeEntry(e, fields)
	if err != nil {
		return buf, err
	}
	data := buf.Bytes()

	// Strip non JSON-like prefix from the data buffer when it comes from a non JSON encoder
	if pos := bytes.Index(data, []byte(`{"`)); ce.Formatter == "console" && pos != -1 {
		data = data[pos:]
	}

	// Extract values
	values := make(map[string]interface{})
	for _, key := range lang.Fields {
		path := strings.Split(key, ">")
		val, typ, _, err := jsonparser.Get(data, path...)
		if err != nil {
			// Field not found, ignore the current expression
			ce.Logger(&ce).Warn("field not found: please fix or remove it", zap.String("field", key))
			continue
		}
		switch typ {
		case jsonparser.NotExist:
			// todo > try to reproduce
		case jsonparser.Number, jsonparser.String, jsonparser.Boolean:
			values[key] = string(val)
		default:
			// Advice to remove it from the expression
			ce.Logger(&ce).Warn("field has an unsupported value type: please fix or remove it", zap.String("field", key), zap.String("type", typ.String()))
		}
	}

	// Evaluate the expression against values
	res, err := lang.Execute(ce.Eval, values)
	emit, ok := res.(bool)
	if !ok {
		ce.Logger(&ce).Error("expecting a boolean expression", zap.String("return", fmt.Sprintf("%T", res)))
		goto emitNothing
	}

	if emit {
		// Using the original (wrapped) encoder for output
		return ce.Encoder.EncodeEntry(e, fields)
	}

emitNothing:
	buf.Reset()
	return buf, nil
}

func (ConditionalEncoder) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID: moduleID, // see https://github.com/caddyserver/caddy/blob/ef7f15f3a42474319e2db0dff6720d91c153f0bf/caddyconfig/httpcaddyfile/builtins.go#L720
		New: func() caddy.Module {
			return new(ConditionalEncoder)
		},
	}
}

func (ce *ConditionalEncoder) Provision(ctx caddy.Context) error {
	// Store the logger
	ce.Logger = ctx.Logger

	if len(ce.Expr) == 0 {
		ctx.Logger(ce).Error("must provide an expression")
		return nil
	}

	if ce.EncRaw == nil {
		ce.Encoder, ce.Formatter = newDefaultProductionLogEncoder(true)

		ctx.Logger(ce).Warn("fallback to a default production logging encoder")
		return nil
	}

	val, err := ctx.LoadModule(ce, "EncRaw")
	if err != nil {
		return fmt.Errorf("loading fallback encoder module: %v", err)
	}
	switch v := val.(type) {
	case *logging.JSONEncoder:
		ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()
	case *logging.ConsoleEncoder:
		ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()
	case *jsonselect.JSONSelectEncoder:
		ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()
	default:
		return fmt.Errorf("unsupported encoder type %T", v)
	}
	ce.Encoder = val.(zapcore.Encoder)

	eval, err := lang.Compile(ce.Expr)
	if err != nil {
		return fmt.Errorf(err.Error())
	}
	ce.Eval = eval

	return nil
}

func newDefaultProductionLogEncoder(colorize bool) (zapcore.Encoder, string) {
	encCfg := zap.NewProductionEncoderConfig()
	if term.IsTerminal(int(os.Stdout.Fd())) {
		// if interactive terminal, make output more human-readable by default
		encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
			encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
		}
		if colorize {
			encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
		}
		return zapcore.NewConsoleEncoder(encCfg), "console"
	}
	return zapcore.NewJSONEncoder(encCfg), "json"
}

// Interface guards
var (
	_ zapcore.Encoder       = (*ConditionalEncoder)(nil)
	_ caddy.Provisioner     = (*ConditionalEncoder)(nil)
	_ caddyfile.Unmarshaler = (*ConditionalEncoder)(nil)
)
