package config

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/mattermost/logr/v2"
	"github.com/mattermost/logr/v2/formatters"
	"github.com/mattermost/logr/v2/targets"
)

type TargetCfg struct {
	Type          string          `json:"type"` // one of "console", "file", "tcp", "syslog", "none".
	Options       json.RawMessage `json:"options,omitempty"`
	Format        string          `json:"format"` // one of "json", "plain", "gelf"
	FormatOptions json.RawMessage `json:"format_options,omitempty"`
	Levels        []logr.Level    `json:"levels"`
	MaxQueueSize  int             `json:"maxqueuesize,omitempty"`
}

type ConsoleOptions struct {
	Out string `json:"out"` // one of "stdout", "stderr"
}

type TargetFactory func(targetType string, options json.RawMessage) (logr.Target, error)
type FormatterFactory func(format string, options json.RawMessage) (logr.Formatter, error)

type Factories struct {
	TargetFactory    TargetFactory    // can be nil
	FormatterFactory FormatterFactory // can be nil
}

var removeAll = func(ti logr.TargetInfo) bool { return true }

// ConfigureTargets replaces the current list of log targets with a new one based on a map
// of name->TargetCfg. The map of TargetCfg's would typically be serialized from a JSON
// source or can be programmatically created.
//
// An optional set of factories can be provided which will be called to create any target
// types or formatters not built-in.
//
// To append log targets to an existing config, use `(*Logr).AddTarget` or
// `(*Logr).AddTargetFromConfig` instead.
func ConfigureTargets(lgr *logr.Logr, config map[string]TargetCfg, factories *Factories) error {
	if err := lgr.RemoveTargets(context.Background(), removeAll); err != nil {
		return fmt.Errorf("error removing existing log targets: %w", err)
	}

	if factories == nil {
		factories = &Factories{nil, nil}
	}

	for name, tcfg := range config {
		target, err := newTarget(tcfg.Type, tcfg.Options, factories.TargetFactory)
		if err != nil {
			return fmt.Errorf("error creating log target %s: %w", name, err)
		}

		if target == nil {
			continue
		}

		formatter, err := newFormatter(tcfg.Format, tcfg.FormatOptions, factories.FormatterFactory)
		if err != nil {
			return fmt.Errorf("error creating formatter for log target %s: %w", name, err)
		}

		filter := newFilter(tcfg.Levels)
		qSize := tcfg.MaxQueueSize
		if qSize == 0 {
			qSize = logr.DefaultMaxQueueSize
		}

		if err = lgr.AddTarget(target, name, filter, formatter, qSize); err != nil {
			return fmt.Errorf("error adding log target %s: %w", name, err)
		}
	}
	return nil
}

func newFilter(levels []logr.Level) logr.Filter {
	filter := &logr.CustomFilter{}
	for _, lvl := range levels {
		filter.Add(lvl)
	}
	return filter
}

func newTarget(targetType string, options json.RawMessage, factory TargetFactory) (logr.Target, error) {
	switch strings.ToLower(targetType) {
	case "console":
		c := ConsoleOptions{}
		if len(options) != 0 {
			if err := json.Unmarshal(options, &c); err != nil {
				return nil, fmt.Errorf("error decoding console target options: %w", err)
			}
		}
		var w io.Writer
		switch c.Out {
		case "stderr":
			w = os.Stderr
		case "stdout", "":
			w = os.Stdout
		default:
			return nil, fmt.Errorf("invalid console target option '%s'", c.Out)
		}
		return targets.NewWriterTarget(w), nil
	case "file":
		fo := targets.FileOptions{}
		if len(options) == 0 {
			return nil, errors.New("missing file target options")
		}
		if err := json.Unmarshal(options, &fo); err != nil {
			return nil, fmt.Errorf("error decoding file target options: %w", err)
		}
		if err := fo.CheckValid(); err != nil {
			return nil, fmt.Errorf("invalid file target options: %w", err)
		}
		return targets.NewFileTarget(fo), nil
	case "tcp":
		to := targets.TcpOptions{}
		if len(options) == 0 {
			return nil, errors.New("missing TCP target options")
		}
		if err := json.Unmarshal(options, &to); err != nil {
			return nil, fmt.Errorf("error decoding TCP target options: %w", err)
		}
		if err := to.CheckValid(); err != nil {
			return nil, fmt.Errorf("invalid TCP target options: %w", err)
		}
		return targets.NewTcpTarget(&to), nil
	case "syslog":
		so := targets.SyslogOptions{}
		if len(options) == 0 {
			return nil, errors.New("missing SysLog target options")
		}
		if err := json.Unmarshal(options, &so); err != nil {
			return nil, fmt.Errorf("error decoding Syslog target options: %w", err)
		}
		if err := so.CheckValid(); err != nil {
			return nil, fmt.Errorf("invalid SysLog target options: %w", err)
		}
		return targets.NewSyslogTarget(&so)
	case "none":
		return nil, nil
	default:
		if factory != nil {
			t, err := factory(targetType, options)
			if err != nil || t == nil {
				return nil, fmt.Errorf("error from target factory: %w", err)
			}
			return t, nil
		}
	}
	return nil, fmt.Errorf("target type '%s' is unrecognized", targetType)
}

func newFormatter(format string, options json.RawMessage, factory FormatterFactory) (logr.Formatter, error) {
	switch strings.ToLower(format) {
	case "json":
		j := formatters.JSON{}
		if len(options) != 0 {
			if err := json.Unmarshal(options, &j); err != nil {
				return nil, fmt.Errorf("error decoding JSON formatter options: %w", err)
			}
			if err := j.CheckValid(); err != nil {
				return nil, fmt.Errorf("invalid JSON formatter options: %w", err)
			}
		}
		return &j, nil
	case "plain":
		p := formatters.Plain{}
		if len(options) != 0 {
			if err := json.Unmarshal(options, &p); err != nil {
				return nil, fmt.Errorf("error decoding Plain formatter options: %w", err)
			}
			if err := p.CheckValid(); err != nil {
				return nil, fmt.Errorf("invalid plain formatter options: %w", err)
			}
		}
		return &p, nil
	case "gelf":
		g := formatters.Gelf{}
		if len(options) != 0 {
			if err := json.Unmarshal(options, &g); err != nil {
				return nil, fmt.Errorf("error decoding Gelf formatter options: %w", err)
			}
			if err := g.CheckValid(); err != nil {
				return nil, fmt.Errorf("invalid GELF formatter options: %w", err)
			}
		}
		return &g, nil

	default:
		if factory != nil {
			f, err := factory(format, options)
			if err != nil || f == nil {
				return nil, fmt.Errorf("error from formatter factory: %w", err)
			}
			return f, nil
		}
	}
	return nil, fmt.Errorf("format '%s' is unrecognized", format)
}
