package logging

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

	"github.com/fatih/color"
	"github.com/go-logfmt/logfmt"
	"github.com/pkg/errors"

	"github.com/zrepl/zrepl/logger"
)

const (
	FieldLevel   = "level"
	FieldMessage = "msg"
	FieldTime    = "time"
)

const (
	JobField    string = "job"
	SubsysField string = "subsystem"
	SpanField   string = "span"
)

type MetadataFlags int64

const (
	MetadataTime MetadataFlags = 1 << iota
	MetadataLevel
	MetadataColor

	MetadataNone MetadataFlags = 0
	MetadataAll  MetadataFlags = ^0
)

type NoFormatter struct{}

func (f NoFormatter) SetMetadataFlags(flags MetadataFlags) {}

func (f NoFormatter) Format(e *logger.Entry) ([]byte, error) {
	return []byte(e.Message), nil
}

type HumanFormatter struct {
	metadataFlags MetadataFlags
	ignoreFields  map[string]bool
}

const HumanFormatterDateFormat = time.RFC3339

func (f *HumanFormatter) SetMetadataFlags(flags MetadataFlags) {
	f.metadataFlags = flags
}

func (f *HumanFormatter) SetIgnoreFields(ignore []string) {
	if ignore == nil {
		f.ignoreFields = nil
		return
	}
	f.ignoreFields = make(map[string]bool, len(ignore))

	for _, field := range ignore {
		f.ignoreFields[field] = true
	}
}

func (f *HumanFormatter) ignored(field string) bool {
	return f.ignoreFields != nil && f.ignoreFields[field]
}

func (f *HumanFormatter) Format(e *logger.Entry) (out []byte, err error) {

	var line bytes.Buffer
	col := color.New()
	if f.metadataFlags&MetadataColor != 0 {
		col = e.Color()
	}

	if f.metadataFlags&MetadataTime != 0 {
		fmt.Fprintf(&line, "%s ", e.Time.Format(HumanFormatterDateFormat))
	}
	if f.metadataFlags&MetadataLevel != 0 {
		fmt.Fprintf(&line, "[%s]", col.Sprint(e.Level.Short()))
	}

	prefixFields := []string{JobField, SubsysField, SpanField}
	prefixed := make(map[string]bool, len(prefixFields)+2)
	for _, field := range prefixFields {
		val, ok := e.Fields[field]
		if !ok {
			continue
		}
		if !f.ignored(field) {
			fmt.Fprintf(&line, "[%s]", col.Sprint(val))
			prefixed[field] = true
		}
	}

	if line.Len() > 0 {
		fmt.Fprint(&line, ": ")
	}
	fmt.Fprint(&line, e.Message)

	if len(e.Fields)-len(prefixed) > 0 {
		for field, value := range e.Fields {
			if prefixed[field] || f.ignored(field) {
				continue
			}
			fmt.Fprintf(&line, " %s=%q", col.Sprint(field), fmt.Sprint(value))
		}
	}

	return line.Bytes(), nil
}

type JSONFormatter struct {
	metadataFlags MetadataFlags
}

func (f *JSONFormatter) SetMetadataFlags(flags MetadataFlags) {
	f.metadataFlags = flags
}

func (f *JSONFormatter) Format(e *logger.Entry) ([]byte, error) {
	data := make(logger.Fields, len(e.Fields)+3)
	for k, v := range e.Fields {
		switch v := v.(type) {
		case error:
			// Otherwise errors are ignored by `encoding/json`
			// https://github.com/sirupsen/logrus/issues/137
			data[k] = v.Error()
		default:
			_, err := json.Marshal(v)
			if err != nil {
				return nil, errors.Errorf("field is not JSON encodable: %s", k)
			}
			data[k] = v
		}
	}

	data[FieldMessage] = e.Message
	data[FieldTime] = e.Time.Format(time.RFC3339)
	data[FieldLevel] = e.Level

	return json.Marshal(data)

}

type LogfmtFormatter struct {
	metadataFlags MetadataFlags
}

func (f *LogfmtFormatter) SetMetadataFlags(flags MetadataFlags) {
	f.metadataFlags = flags
}

func (f *LogfmtFormatter) Format(e *logger.Entry) ([]byte, error) {
	var buf bytes.Buffer
	enc := logfmt.NewEncoder(&buf)

	if f.metadataFlags&MetadataTime != 0 {
		err := enc.EncodeKeyval(FieldTime, e.Time)
		if err != nil {
			return nil, errors.Wrap(err, "logfmt: encode time")
		}
	}
	if f.metadataFlags&MetadataLevel != 0 {
		err := enc.EncodeKeyval(FieldLevel, e.Level)
		if err != nil {
			return nil, errors.Wrap(err, "logfmt: encode level")
		}
	}

	// at least try and put job and task in front
	prefixed := make(map[string]bool, 3)
	prefix := []string{JobField, SubsysField, SpanField}
	for _, pf := range prefix {
		v, ok := e.Fields[pf]
		if !ok {
			break
		}
		if err := logfmtTryEncodeKeyval(enc, pf, v); err != nil {
			return nil, err // unlikely
		}
		prefixed[pf] = true
	}

	err := enc.EncodeKeyval(FieldMessage, e.Message)
	if err != nil {
		return nil, errors.Wrap(err, "logfmt: encode message")
	}
	for k, v := range e.Fields {
		if !prefixed[k] {
			if err := logfmtTryEncodeKeyval(enc, k, v); err != nil {
				return nil, err
			}
		}
	}

	return buf.Bytes(), nil
}

func logfmtTryEncodeKeyval(enc *logfmt.Encoder, field, value interface{}) error {

	err := enc.EncodeKeyval(field, value)
	switch err {
	case nil: // ok
		return nil
	case logfmt.ErrUnsupportedValueType:
		err := enc.EncodeKeyval(field, fmt.Sprintf("<%T>", value))
		if err != nil {
			return errors.Wrap(err, "cannot encode unsupported value type Go type")
		}
		return nil
	}
	return errors.Wrapf(err, "cannot encode field '%s'", field)

}