package replacer

import (
	"context"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/coredns/coredns/plugin/metadata"
	"github.com/coredns/coredns/plugin/pkg/dnstest"
	"github.com/coredns/coredns/request"

	"github.com/miekg/dns"
)

// Replacer replaces labels for values in strings.
type Replacer struct{}

// New makes a new replacer. This only needs to be called once in the setup and
// then call Replace for each incoming message. A replacer is safe for concurrent use.
func New() Replacer {
	return Replacer{}
}

// Replace performs a replacement of values on s and returns the string with the replaced values.
func (r Replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder, s string) string {
	return loadFormat(s).Replace(ctx, state, rr)
}

const (
	headerReplacer = "{>"
	// EmptyValue is the default empty value.
	EmptyValue = "-"
)

// labels are all supported labels that can be used in the default Replacer.
var labels = map[string]struct{}{
	"{type}":   {},
	"{name}":   {},
	"{class}":  {},
	"{proto}":  {},
	"{size}":   {},
	"{remote}": {},
	"{port}":   {},
	"{local}":  {},
	// Header values.
	headerReplacer + "id}":      {},
	headerReplacer + "opcode}":  {},
	headerReplacer + "do}":      {},
	headerReplacer + "bufsize}": {},
	// Recorded replacements.
	"{rcode}":                  {},
	"{rsize}":                  {},
	"{duration}":               {},
	headerReplacer + "rflags}": {},
}

// appendValue appends the current value of label.
func appendValue(b []byte, state request.Request, rr *dnstest.Recorder, label string) []byte {
	switch label {
	case "{type}":
		return append(b, state.Type()...)
	case "{name}":
		return append(b, state.Name()...)
	case "{class}":
		return append(b, state.Class()...)
	case "{proto}":
		return append(b, state.Proto()...)
	case "{size}":
		return strconv.AppendInt(b, int64(state.Req.Len()), 10)
	case "{remote}":
		return appendAddrToRFC3986(b, state.IP())
	case "{port}":
		return append(b, state.Port()...)
	case "{local}":
		return appendAddrToRFC3986(b, state.LocalIP())
	// Header placeholders (case-insensitive).
	case headerReplacer + "id}":
		return strconv.AppendInt(b, int64(state.Req.Id), 10)
	case headerReplacer + "opcode}":
		return strconv.AppendInt(b, int64(state.Req.Opcode), 10)
	case headerReplacer + "do}":
		return strconv.AppendBool(b, state.Do())
	case headerReplacer + "bufsize}":
		return strconv.AppendInt(b, int64(state.Size()), 10)
	// Recorded replacements.
	case "{rcode}":
		if rr == nil || rr.Msg == nil {
			return append(b, EmptyValue...)
		}
		if rcode := dns.RcodeToString[rr.Rcode]; rcode != "" {
			return append(b, rcode...)
		}
		return strconv.AppendInt(b, int64(rr.Rcode), 10)
	case "{rsize}":
		if rr == nil {
			return append(b, EmptyValue...)
		}
		return strconv.AppendInt(b, int64(rr.Len), 10)
	case "{duration}":
		if rr == nil {
			return append(b, EmptyValue...)
		}
		secs := time.Since(rr.Start).Seconds()
		return append(strconv.AppendFloat(b, secs, 'f', -1, 64), 's')
	case headerReplacer + "rflags}":
		if rr != nil && rr.Msg != nil {
			return appendFlags(b, rr.Msg.MsgHdr)
		}
		return append(b, EmptyValue...)
	default:
		return append(b, EmptyValue...)
	}
}

// appendFlags checks all header flags and appends those
// that are set as a string separated with commas
func appendFlags(b []byte, h dns.MsgHdr) []byte {
	origLen := len(b)
	if h.Response {
		b = append(b, "qr,"...)
	}
	if h.Authoritative {
		b = append(b, "aa,"...)
	}
	if h.Truncated {
		b = append(b, "tc,"...)
	}
	if h.RecursionDesired {
		b = append(b, "rd,"...)
	}
	if h.RecursionAvailable {
		b = append(b, "ra,"...)
	}
	if h.Zero {
		b = append(b, "z,"...)
	}
	if h.AuthenticatedData {
		b = append(b, "ad,"...)
	}
	if h.CheckingDisabled {
		b = append(b, "cd,"...)
	}
	if n := len(b); n > origLen {
		return b[:n-1] // trim trailing ','
	}
	return b
}

// appendAddrToRFC3986 will add brackets to the address if it is an IPv6 address.
func appendAddrToRFC3986(b []byte, addr string) []byte {
	if strings.IndexByte(addr, ':') != -1 {
		b = append(b, '[')
		b = append(b, addr...)
		b = append(b, ']')
	} else {
		b = append(b, addr...)
	}
	return b
}

type nodeType int

const (
	typeLabel    nodeType = iota // "{type}"
	typeLiteral                  // "foo"
	typeMetadata                 // "{/metadata}"
)

// A node represents a segment of a parsed format.  For example: "A {type}"
// contains two nodes: "A " (literal); and "{type}" (label).
type node struct {
	value string // Literal value, label or metadata label
	typ   nodeType
}

// A replacer is an ordered list of all the nodes in a format.
type replacer []node

func parseFormat(s string) replacer {
	// Assume there is a literal between each label - its cheaper to over
	// allocate once than allocate twice.
	rep := make(replacer, 0, strings.Count(s, "{")*2)
	for {
		// We find the right bracket then backtrack to find the left bracket.
		// This allows us to handle formats like: "{ {foo} }".
		j := strings.IndexByte(s, '}')
		if j < 0 {
			break
		}
		i := strings.LastIndexByte(s[:j], '{')
		if i < 0 {
			// Handle: "A } {foo}" by treating "A }" as a literal
			rep = append(rep, node{
				value: s[:j+1],
				typ:   typeLiteral,
			})
			s = s[j+1:]
			continue
		}

		val := s[i : j+1]
		var typ nodeType
		switch _, ok := labels[val]; {
		case ok:
			typ = typeLabel
		case strings.HasPrefix(val, "{/"):
			// Strip "{/}" from metadata labels
			val = val[2 : len(val)-1]
			typ = typeMetadata
		default:
			// Given: "A {X}" val is "{X}" expand it to the whole literal.
			val = s[:j+1]
			typ = typeLiteral
		}

		// Append any leading literal.  Given "A {type}" the literal is "A "
		if i != 0 && typ != typeLiteral {
			rep = append(rep, node{
				value: s[:i],
				typ:   typeLiteral,
			})
		}
		rep = append(rep, node{
			value: val,
			typ:   typ,
		})
		s = s[j+1:]
	}
	if len(s) != 0 {
		rep = append(rep, node{
			value: s,
			typ:   typeLiteral,
		})
	}
	return rep
}

var replacerCache sync.Map // map[string]replacer

func loadFormat(s string) replacer {
	if v, ok := replacerCache.Load(s); ok {
		return v.(replacer)
	}
	v, _ := replacerCache.LoadOrStore(s, parseFormat(s))
	return v.(replacer)
}

// bufPool stores pointers to scratch buffers.
var bufPool = sync.Pool{
	New: func() interface{} {
		return make([]byte, 0, 256)
	},
}

func (r replacer) Replace(ctx context.Context, state request.Request, rr *dnstest.Recorder) string {
	b := bufPool.Get().([]byte)
	for _, s := range r {
		switch s.typ {
		case typeLabel:
			b = appendValue(b, state, rr, s.value)
		case typeLiteral:
			b = append(b, s.value...)
		case typeMetadata:
			if fm := metadata.ValueFunc(ctx, s.value); fm != nil {
				b = append(b, fm()...)
			} else {
				b = append(b, EmptyValue...)
			}
		}
	}
	s := string(b)
	//nolint:staticcheck
	bufPool.Put(b[:0])
	return s
}
