// Copyright 2013-2025 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package conf supports a configuration file format used by gnatsd. It is
// a flexible format that combines the best of traditional
// configuration formats and newer styles such as JSON and YAML.
package conf

// The format supported is less restrictive than today's formats.
// Supports mixed Arrays [], nested Maps {}, multiple comment types (# and //)
// Also supports key value assignments using '=' or ':' or whiteSpace()
//   e.g. foo = 2, foo : 2, foo 2
// maps can be assigned with no key separator as well
// semicolons as value terminators in key/value assignments are optional
//
// see parse_test.go for more examples.

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
	"unicode"
)

const _EMPTY_ = ""

type parser struct {
	mapping map[string]any
	lx      *lexer

	// The current scoped context, can be array or map
	ctx any

	// stack of contexts, either map or array/slice stack
	ctxs []any

	// Keys stack
	keys []string

	// Keys stack as items
	ikeys []item

	// The config file path, empty by default.
	fp string

	// pedantic reports error when configuration is not correct.
	pedantic bool

	// Tracks environment variable references, to avoid cycles
	envVarReferences map[string]bool
}

// Parse will return a map of keys to any, although concrete types
// underly them. The values supported are string, bool, int64, float64, DateTime.
// Arrays and nested Maps are also supported.
func Parse(data string) (map[string]any, error) {
	p, err := parse(data, "", false)
	if err != nil {
		return nil, err
	}
	return p.mapping, nil
}

// ParseWithChecks is equivalent to Parse but runs in pedantic mode.
func ParseWithChecks(data string) (map[string]any, error) {
	p, err := parse(data, "", true)
	if err != nil {
		return nil, err
	}
	return p.mapping, nil
}

// ParseFile is a helper to open file, etc. and parse the contents.
func ParseFile(fp string) (map[string]any, error) {
	data, err := os.ReadFile(fp)
	if err != nil {
		return nil, fmt.Errorf("error opening config file: %v", err)
	}

	p, err := parse(string(data), fp, false)
	if err != nil {
		return nil, err
	}
	return p.mapping, nil
}

// ParseFileWithChecks is equivalent to ParseFile but runs in pedantic mode.
func ParseFileWithChecks(fp string) (map[string]any, error) {
	data, err := os.ReadFile(fp)
	if err != nil {
		return nil, err
	}

	p, err := parse(string(data), fp, true)
	if err != nil {
		return nil, err
	}

	return p.mapping, nil
}

// configDigest returns a digest for the parsed config.
func configDigest(m map[string]any) (string, error) {
	digest := sha256.New()
	e := json.NewEncoder(digest)
	if err := e.Encode(m); err != nil {
		return _EMPTY_, err
	}
	return fmt.Sprintf("sha256:%x", digest.Sum(nil)), nil
}

// ParseFileWithChecksDigest returns the processed config and a digest
// that represents the configuration.
func ParseFileWithChecksDigest(fp string) (map[string]any, string, error) {
	m, err := ParseFileWithChecks(fp)
	if err != nil {
		return nil, _EMPTY_, err
	}
	digest, err := configDigest(m)
	if err != nil {
		return nil, _EMPTY_, err
	}
	return m, digest, nil
}

type token struct {
	item         item
	value        any
	usedVariable bool
	sourceFile   string
}

func (t *token) MarshalJSON() ([]byte, error) {
	return json.Marshal(t.value)
}

func (t *token) Value() any {
	return t.value
}

func (t *token) Line() int {
	return t.item.line
}

func (t *token) IsUsedVariable() bool {
	return t.usedVariable
}

func (t *token) SourceFile() string {
	return t.sourceFile
}

func (t *token) Position() int {
	return t.item.pos
}

func newParser(data, fp string, pedantic bool) *parser {
	return &parser{
		mapping:          make(map[string]any),
		lx:               lex(data),
		ctxs:             make([]any, 0, 4),
		keys:             make([]string, 0, 4),
		ikeys:            make([]item, 0, 4),
		fp:               filepath.Dir(fp),
		pedantic:         pedantic,
		envVarReferences: make(map[string]bool),
	}
}

func parse(data, fp string, pedantic bool) (*parser, error) {
	p := newParser(data, fp, pedantic)
	if err := p.parse(fp); err != nil {
		return nil, err
	}
	return p, nil
}

func parseEnv(data string, parent *parser) (*parser, error) {
	p := newParser(data, "", false)
	p.envVarReferences = parent.envVarReferences
	if err := p.parse(""); err != nil {
		return nil, err
	}
	return p, nil
}

func (p *parser) parse(fp string) error {
	p.pushContext(p.mapping)

	var prevItem item
	for {
		it := p.next()
		if it.typ == itemEOF {
			// Here we allow the final character to be a bracket '}'
			// in order to support JSON like configurations.
			if prevItem.typ == itemKey && prevItem.val != mapEndString {
				return fmt.Errorf("config is invalid (%s:%d:%d)", fp, it.line, it.pos)
			}
			break
		}
		prevItem = it
		if err := p.processItem(it, fp); err != nil {
			return err
		}
	}
	return nil
}

func (p *parser) next() item {
	return p.lx.nextItem()
}

func (p *parser) pushContext(ctx any) {
	p.ctxs = append(p.ctxs, ctx)
	p.ctx = ctx
}

func (p *parser) popContext() any {
	if len(p.ctxs) == 0 {
		panic("BUG in parser, context stack empty")
	}
	li := len(p.ctxs) - 1
	last := p.ctxs[li]
	p.ctxs = p.ctxs[0:li]
	p.ctx = p.ctxs[len(p.ctxs)-1]
	return last
}

func (p *parser) pushKey(key string) {
	p.keys = append(p.keys, key)
}

func (p *parser) popKey() string {
	if len(p.keys) == 0 {
		panic("BUG in parser, keys stack empty")
	}
	li := len(p.keys) - 1
	last := p.keys[li]
	p.keys = p.keys[0:li]
	return last
}

func (p *parser) pushItemKey(key item) {
	p.ikeys = append(p.ikeys, key)
}

func (p *parser) popItemKey() item {
	if len(p.ikeys) == 0 {
		panic("BUG in parser, item keys stack empty")
	}
	li := len(p.ikeys) - 1
	last := p.ikeys[li]
	p.ikeys = p.ikeys[0:li]
	return last
}

func (p *parser) processItem(it item, fp string) error {
	setValue := func(it item, v any) {
		if p.pedantic {
			p.setValue(&token{it, v, false, fp})
		} else {
			p.setValue(v)
		}
	}

	switch it.typ {
	case itemError:
		return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val)
	case itemKey:
		// Keep track of the keys as items and strings,
		// we do this in order to be able to still support
		// includes without many breaking changes.
		p.pushKey(it.val)

		if p.pedantic {
			p.pushItemKey(it)
		}
	case itemMapStart:
		newCtx := make(map[string]any)
		p.pushContext(newCtx)
	case itemMapEnd:
		setValue(it, p.popContext())
	case itemString:
		// FIXME(dlc) sanitize string?
		setValue(it, it.val)
	case itemInteger:
		lastDigit := 0
		for _, r := range it.val {
			if !unicode.IsDigit(r) && r != '-' {
				break
			}
			lastDigit++
		}
		numStr := it.val[:lastDigit]
		num, err := strconv.ParseInt(numStr, 10, 64)
		if err != nil {
			if e, ok := err.(*strconv.NumError); ok &&
				e.Err == strconv.ErrRange {
				return fmt.Errorf("integer '%s' is out of the range", it.val)
			}
			return fmt.Errorf("expected integer, but got '%s'", it.val)
		}
		// Process a suffix
		suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:]))

		switch suffix {
		case "":
			setValue(it, num)
		case "k":
			setValue(it, num*1000)
		case "kb", "ki", "kib":
			setValue(it, num*1024)
		case "m":
			setValue(it, num*1000*1000)
		case "mb", "mi", "mib":
			setValue(it, num*1024*1024)
		case "g":
			setValue(it, num*1000*1000*1000)
		case "gb", "gi", "gib":
			setValue(it, num*1024*1024*1024)
		case "t":
			setValue(it, num*1000*1000*1000*1000)
		case "tb", "ti", "tib":
			setValue(it, num*1024*1024*1024*1024)
		case "p":
			setValue(it, num*1000*1000*1000*1000*1000)
		case "pb", "pi", "pib":
			setValue(it, num*1024*1024*1024*1024*1024)
		case "e":
			setValue(it, num*1000*1000*1000*1000*1000*1000)
		case "eb", "ei", "eib":
			setValue(it, num*1024*1024*1024*1024*1024*1024)
		}
	case itemFloat:
		num, err := strconv.ParseFloat(it.val, 64)
		if err != nil {
			if e, ok := err.(*strconv.NumError); ok &&
				e.Err == strconv.ErrRange {
				return fmt.Errorf("float '%s' is out of the range", it.val)
			}
			return fmt.Errorf("expected float, but got '%s'", it.val)
		}
		setValue(it, num)
	case itemBool:
		switch strings.ToLower(it.val) {
		case "true", "yes", "on":
			setValue(it, true)
		case "false", "no", "off":
			setValue(it, false)
		default:
			return fmt.Errorf("expected boolean value, but got '%s'", it.val)
		}

	case itemDatetime:
		dt, err := time.Parse("2006-01-02T15:04:05Z", it.val)
		if err != nil {
			return fmt.Errorf(
				"expected Zulu formatted DateTime, but got '%s'", it.val)
		}
		setValue(it, dt)
	case itemArrayStart:
		var array = make([]any, 0)
		p.pushContext(array)
	case itemArrayEnd:
		array := p.ctx
		p.popContext()
		setValue(it, array)
	case itemVariable:
		value, found, err := p.lookupVariable(it.val)
		if err != nil {
			return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
				it.val, it.line, err)
		}
		if !found {
			return fmt.Errorf("variable reference for '%s' on line %d can not be found",
				it.val, it.line)
		}

		if p.pedantic {
			switch tk := value.(type) {
			case *token:
				// Mark the looked up variable as used, and make
				// the variable reference become handled as a token.
				tk.usedVariable = true
				p.setValue(&token{it, tk.Value(), false, fp})
			default:
				// Special case to add position context to bcrypt references.
				p.setValue(&token{it, value, false, fp})
			}
		} else {
			p.setValue(value)
		}
	case itemInclude:
		var (
			m   map[string]any
			err error
		)
		if p.pedantic {
			m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val))
		} else {
			m, err = ParseFile(filepath.Join(p.fp, it.val))
		}
		if err != nil {
			return fmt.Errorf("error parsing include file '%s', %v", it.val, err)
		}
		for k, v := range m {
			p.pushKey(k)

			if p.pedantic {
				switch tk := v.(type) {
				case *token:
					p.pushItemKey(tk.item)
				}
			}
			p.setValue(v)
		}
	}

	return nil
}

// Used to map an environment value into a temporary map to pass to secondary Parse call.
const pkey = "pk"

// We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
const bcryptPrefix = "2a$"

// lookupVariable will lookup a variable reference. It will use block scoping on keys
// it has seen before, with the top level scoping being the environment variables. We
// ignore array contexts and only process the map contexts..
//
// Returns true for ok if it finds something, similar to map.
func (p *parser) lookupVariable(varReference string) (any, bool, error) {
	// Do special check to see if it is a raw bcrypt string.
	if strings.HasPrefix(varReference, bcryptPrefix) {
		return "$" + varReference, true, nil
	}

	// Loop through contexts currently on the stack.
	for i := len(p.ctxs) - 1; i >= 0; i-- {
		ctx := p.ctxs[i]
		// Process if it is a map context
		if m, ok := ctx.(map[string]any); ok {
			if v, ok := m[varReference]; ok {
				return v, ok, nil
			}
		}
	}

	// If we are here, we have exhausted our context maps and still not found anything.
	// Detect reference cycles
	if p.envVarReferences[varReference] {
		return nil, false, fmt.Errorf("variable reference cycle for '%s'", varReference)
	}
	p.envVarReferences[varReference] = true
	defer delete(p.envVarReferences, varReference)

	// Parse from the environment
	if vStr, ok := os.LookupEnv(varReference); ok {
		// Everything we get here will be a string value, so we need to process as a parser would.
		if subp, err := parseEnv(fmt.Sprintf("%s=%s", pkey, vStr), p); err == nil {
			v, ok := subp.mapping[pkey]
			return v, ok, nil
		} else {
			return nil, false, err
		}
	}
	return nil, false, nil
}

func (p *parser) setValue(val any) {
	// Test to see if we are on an array or a map

	// Array processing
	if ctx, ok := p.ctx.([]any); ok {
		p.ctx = append(ctx, val)
		p.ctxs[len(p.ctxs)-1] = p.ctx
	}

	// Map processing
	if ctx, ok := p.ctx.(map[string]any); ok {
		key := p.popKey()

		if p.pedantic {
			// Change the position to the beginning of the key
			// since more useful when reporting errors.
			switch v := val.(type) {
			case *token:
				it := p.popItemKey()
				v.item.pos = it.pos
				v.item.line = it.line
				ctx[key] = v
			}
		} else {
			// FIXME(dlc), make sure to error if redefining same key?
			ctx[key] = val
		}
	}
}
