package wavefront

import (
	"bufio"
	"bytes"
	"io"
	"maps"
	"strconv"
	"time"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/metric"
)

type point struct {
	Name      string
	Value     string
	Timestamp int64
	Source    string
	Tags      map[string]string
}

// pointParser is a thread-unsafe parser and must be kept in a pool.
type pointParser struct {
	s   *PointScanner
	buf struct {
		tok []Token  // last read n tokens
		lit []string // last read n literals
		n   int      // unscanned buffer size (max=2)
	}
	scanBuf  bytes.Buffer // buffer reused for scanning tokens
	writeBuf bytes.Buffer // buffer reused for parsing elements
	elements []elementParser
	parent   *Parser
}

func newPointParserFactory(p *Parser) func() any {
	return func() any {
		wsParser := whiteSpaceParser{}
		wsParserNextOpt := whiteSpaceParser{nextOptional: true}
		repeatParser := loopedParser{wrappedParser: &tagParser{}, wsParser: &wsParser}
		elements := []elementParser{
			&nameParser{},
			&wsParser,
			&valueParser{},
			&wsParserNextOpt,
			&timestampParser{optional: true, timeFunc: p.timeFunc},
			&wsParserNextOpt,
			&repeatParser,
		}

		return &pointParser{elements: elements, parent: p}
	}
}

func (p *pointParser) Parse(buf []byte) ([]telegraf.Metric, error) {
	// parse even if the buffer begins with a newline
	buf = bytes.TrimPrefix(buf, []byte("\n"))
	// add newline to end if not exists:
	if len(buf) > 0 && !bytes.HasSuffix(buf, []byte("\n")) {
		buf = append(buf, []byte("\n")...)
	}

	points := make([]point, 0)

	buffer := bytes.NewBuffer(buf)
	reader := bufio.NewReader(buffer)
	for {
		// Read up to the next newline.
		buf, err := reader.ReadBytes('\n')
		if err == io.EOF {
			break
		}

		p.reset(buf)
		point := point{}
		for _, element := range p.elements {
			err := element.parse(p, &point)
			if err != nil {
				return nil, err
			}
		}

		points = append(points, point)
	}

	metrics, err := p.convertPointToTelegrafMetric(points)
	if err != nil {
		return nil, err
	}
	return metrics, nil
}

func (p *pointParser) convertPointToTelegrafMetric(points []point) ([]telegraf.Metric, error) {
	metrics := make([]telegraf.Metric, 0, len(points))

	for _, point := range points {
		tags := make(map[string]string, len(point.Tags)+len(p.parent.DefaultTags))
		maps.Copy(tags, point.Tags)
		// apply default tags after parsed tags
		maps.Copy(tags, p.parent.DefaultTags)

		// single field for value
		fields := make(map[string]any)
		v, err := strconv.ParseFloat(point.Value, 64)
		if err != nil {
			return nil, err
		}
		fields["value"] = v

		m := metric.New(point.Name, tags, fields, time.Unix(point.Timestamp, 0))

		metrics = append(metrics, m)
	}

	return metrics, nil
}

func (p *pointParser) scan() (Token, string) {
	// If we have a token on the buffer, then return it.
	if p.buf.n != 0 {
		idx := p.buf.n % maxBufferSize
		tok, lit := p.buf.tok[idx], p.buf.lit[idx]
		p.buf.n--
		return tok, lit
	}

	// Otherwise read the next token from the scanner.
	tok, lit := p.s.Scan()

	// Save it to the buffer in case we unscan later.
	p.buffer(tok, lit)

	return tok, lit
}

func (p *pointParser) buffer(tok Token, lit string) {
	// create the buffer if it is empty
	if len(p.buf.tok) == 0 {
		p.buf.tok = make([]Token, maxBufferSize)
		p.buf.lit = make([]string, maxBufferSize)
	}

	// for now assume a simple circular buffer of length two
	p.buf.tok[0], p.buf.lit[0] = p.buf.tok[1], p.buf.lit[1]
	p.buf.tok[1], p.buf.lit[1] = tok, lit
}

func (p *pointParser) unscan() {
	p.unscanTokens(1)
}

func (p *pointParser) unscanTokens(n int) {
	if n > maxBufferSize {
		// just log for now
		p.parent.Log.Infof("Cannot unscan more than %d tokens", maxBufferSize)
	}
	p.buf.n += n
}

func (p *pointParser) reset(buf []byte) {
	// reset the scan buffer and write new byte
	p.scanBuf.Reset()
	p.scanBuf.Write(buf)

	if p.s == nil {
		p.s = NewScanner(&p.scanBuf)
	} else {
		// reset p.s.r passing in the buffer as the reader
		p.s.r.Reset(&p.scanBuf)
	}
	p.buf.n = 0
}
