/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>
*/

package config

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/cgrates/cgrates/utils"
)

// NewRjReader creates a new rjReader from a io.Reader
func NewRjReader(rdr io.Reader) (r *RjReader, err error) {
	var b []byte
	b, err = io.ReadAll(rdr)
	if err != nil {
		return
	}
	return NewRjReaderFromBytes(b), nil
}

// NewRjReaderFromBytes creates a new rjReader from a slice of bytes
func NewRjReaderFromBytes(b []byte) *RjReader {
	return &RjReader{buf: b}
}

// isNewLine check if byte is new line
func isNewLine(c byte) bool {
	return c == '\n' || c == '\r'
}

// isWhiteSpace check if byte is white space
func isWhiteSpace(c byte) bool {
	return c == ' ' || c == '\t' || isNewLine(c) || c == 0
}

// ReadEnv reads the environment variable
func ReadEnv(key string) (string, error) { //it shod print a warning not a error
	if env := os.Getenv(key); env != "" {
		return env, nil
	}
	return "", utils.ErrEnvNotFound(key)
}

// isAlfanum check if byte is number or letter
func isAlfanum(bit byte) bool {
	return (bit >= 'a' && bit <= 'z') ||
		(bit >= 'A' && bit <= 'Z') ||
		(bit >= '0' && bit <= '9')
}

// RjReader structure that implements io.Reader to read json files ignoring C style comments and replacing *env:
type RjReader struct {
	buf        []byte
	isInString bool // ignore character in strings
	indx       int  // used to parse the buffer
	envOff     bool
}

// Read implementation
func (rjr *RjReader) Read(p []byte) (n int, err error) {
	for n = range p {
		p[n], err = rjr.ReadByte()
		if !rjr.envOff &&
			p[n] == '*' &&
			rjr.checkMeta() {
			if err = rjr.replaceEnv(rjr.indx - 1); err != nil {
				return
			}
			p[n] = rjr.buf[rjr.indx-1] // replace with first value
		}
		if err != nil {
			return
		}
	}
	n++ //because it starts from 0
	return
}

// Close implementation
func (rjr *RjReader) Close() error {
	rjr.buf = nil
	return nil
}

// ReadByte implementation
func (rjr *RjReader) ReadByte() (bit byte, err error) {
	if rjr.isInString { //ignore commas in strings
		return rjr.ReadByteWC()
	}
	bit, err = rjr.ReadByteWC()
	if err != nil {
		return
	}
	if bit == utils.CSVSep {
		var bit2 byte
		bit2, err = rjr.PeekByteWC()
		if err != nil {
			return
		}
		if bit2 == ']' || bit2 == '}' {
			return rjr.ReadByteWC()
		}
	}
	return
}

// UnreadByte implementation
func (rjr *RjReader) UnreadByte() (err error) {
	if rjr.indx <= 0 {
		return bufio.ErrInvalidUnreadByte
	}
	rjr.indx--
	return
}

// returns true if the file was parsed completly
func (rjr *RjReader) isEndOfFile() bool {
	return rjr.indx >= len(rjr.buf)
}

// consumeComent consumes the comment based on the peeked byte
func (rjr *RjReader) consumeComent(pkbit byte) (bool, error) {
	switch pkbit {
	case '/':
		for !rjr.isEndOfFile() {
			bit := rjr.buf[rjr.indx]
			rjr.indx++
			if isNewLine(bit) { //read until newline or EOF
				return true, nil
			}
		}
		return true, io.EOF
	case '*':
		rjr.indx += 3 // increase to ignore peeked bytes
		for !rjr.isEndOfFile() {
			if rjr.buf[rjr.indx] == '/' &&
				rjr.buf[rjr.indx-1] == '*' {
				rjr.indx++
				return true, nil
			}
			rjr.indx++
		}
		return true, utils.ErrJsonIncompleteComment
	}
	return false, nil
}

// readFirstNonWhiteSpace reads first non white space byte
func (rjr *RjReader) readFirstNonWhiteSpace() (bit byte, err error) {
	for !rjr.isEndOfFile() {
		bit = rjr.buf[rjr.indx]
		rjr.indx++
		if !isWhiteSpace(bit) {
			return
		}
	}
	return 0, io.EOF
}

// ReadByteWC reads next byte skiping the comments
func (rjr *RjReader) ReadByteWC() (bit byte, err error) {
	if rjr.isEndOfFile() {
		return 0, io.EOF
	}
	if rjr.isInString {
		bit = rjr.buf[rjr.indx]
		rjr.indx++
	} else if bit, err = rjr.readFirstNonWhiteSpace(); err != nil {
		return
	}
	if bit == '"' {
		rjr.isInString = !rjr.isInString
		return
	}
	if !rjr.isInString && !rjr.isEndOfFile() && bit == '/' {
		var pkbit byte
		var isComment bool
		pkbit = rjr.buf[rjr.indx]
		isComment, err = rjr.consumeComent(pkbit)
		if err != nil && err != io.EOF {
			return
		}
		if isComment {
			return rjr.ReadByteWC()
		}
	}
	return
}

// PeekByteWC peeks next byte skiping the comments
func (rjr *RjReader) PeekByteWC() (bit byte, err error) {
	for !rjr.isEndOfFile() {
		bit = rjr.buf[rjr.indx]
		if !rjr.isInString && rjr.indx+1 < len(rjr.buf) && bit == '/' { //try consume comment
			var pkbit byte
			var isComment bool
			pkbit = rjr.buf[rjr.indx+1]
			isComment, err = rjr.consumeComent(pkbit)
			if err != nil {
				return
			}
			if isComment {
				return rjr.PeekByteWC()
			}
			return
		}
		if !isWhiteSpace(bit) {
			return
		}
		rjr.indx++
	}
	return 0, io.EOF
}

// checkMeta check if char mach with next char from MetaEnv if not reset the counting
func (rjr *RjReader) checkMeta() bool {
	return rjr.indx-1+len(utils.MetaEnv) < len(rjr.buf) &&
		utils.MetaEnv == string(rjr.buf[rjr.indx-1:rjr.indx-1+len(utils.MetaEnv)])
}

// readEnvName reads the environment key
func (rjr *RjReader) readEnvName(indx int) (name []byte, endindx int) { //0 if not set
	for indx < len(rjr.buf) { //read byte by byte
		bit := rjr.buf[indx]
		if !isAlfanum(bit) && bit != '_' { //[a-zA-Z_]+[a-zA-Z0-9_]*
			return name, indx
		}
		name = append(name, bit)
		indx++
	}
	return name, indx
}

// replaceEnv replaces the EnvMeta and environment key with  environment variable value in specific buffer
func (rjr *RjReader) replaceEnv(startEnv int) error {
	midEnv := len(utils.MetaEnv)
	key, endEnv := rjr.readEnvName(startEnv + midEnv)
	value, err := ReadEnv(string(key))
	if err != nil {
		return err
	}
	rjr.buf = append(rjr.buf[:startEnv], append([]byte(value), rjr.buf[endEnv:]...)...) // replace *env:ENV_STUFF with ENV_VALUE
	return nil
}

// HandleJSONError warning: needs to read file again
func (rjr *RjReader) HandleJSONError(err error) error {
	var offset int64
	switch realErr := err.(type) {
	case nil:
		return nil
	case *json.SyntaxError:
		offset = realErr.Offset
	case *json.UnmarshalTypeError:
		offset = realErr.Offset
	default:
		return err
	}
	if offset == 0 {
		return fmt.Errorf("%s at line 0 around position 0", err.Error())
	}
	rjr.indx = 0

	line, character := rjr.getJSONOffsetLine(offset)
	return fmt.Errorf("%s around line %v and position %v\n line: %q", err.Error(), line, character,
		strings.Split(string(rjr.buf), "\n")[line-1])
}

func (rjr *RjReader) getJSONOffsetLine(offset int64) (line, character int64) {
	line = 1 // start line counting from 1
	var lastChar byte

	var i int64 = 0
	readString := func() error {
		for i < offset {
			if rjr.isEndOfFile() {
				return io.EOF
			}
			b := rjr.buf[rjr.indx]
			rjr.indx++
			i++
			if isNewLine(b) {
				line++
				character = 0
			} else {
				character++
			}
			if b == '"' {
				return nil
			}
		}
		return nil
	}
	readLineComment := func() error {
		for !rjr.isEndOfFile() {
			b := rjr.buf[rjr.indx]
			rjr.indx++
			if isNewLine(b) {
				line++
				character = 0
				return nil
			}
			character++
		}
		return io.EOF
	}

	readComment := func() error {
		for !rjr.isEndOfFile() {
			b := rjr.buf[rjr.indx]
			rjr.indx++
			if isNewLine(b) {
				line++
				character = 0
			} else {
				character++
			}
			if b == '*' {
				if rjr.isEndOfFile() {
					return io.EOF
				}
				b := rjr.buf[rjr.indx]
				rjr.indx++
				if b == '/' {
					character++
					return nil
				}
				// Unread byte return error only if rjr.indx is small or equal to 0
				// The value is greater than 0 so it's safe to not check the error
				rjr.UnreadByte()
			}
		}
		return io.EOF
	}
	for i < offset { // handle the parsing
		if rjr.isEndOfFile() {
			return
		}
		b := rjr.buf[rjr.indx]
		rjr.indx++
		character++
		if isNewLine(b) {
			line++
			character = 0
		}
		if (b == ']' || b == '}') && lastChar == utils.CSVSep {
			i-- //ignore utils.CSVSep if is followed by ] or }
		}
		if !isWhiteSpace(b) {
			i++
			lastChar = b
		}
		if b == '"' { // read "" value
			rerr := readString()
			if rerr != nil {
				break
			}
		}
		if b == '/' {
			if rjr.isEndOfFile() {
				return
			}
			b := rjr.buf[rjr.indx]
			rjr.indx++
			if b == '/' { // read //
				character++
				i--
				rerr := readLineComment()
				if rerr != nil {
					break
				}
			} else if b == '*' { // read /*
				character++
				i--
				rerr := readComment()
				if rerr != nil {
					break
				}
			} else {
				// Unread byte return error only if rjr.indx is small or equal to 0
				// The value is greater than 0 so it's safe to not check the error
				rjr.UnreadByte()
			}
		}
	}
	return
}

// Decode loads the json config out of rjReader
func (rjr *RjReader) Decode(cfg any) (err error) {
	if err = json.NewDecoder(rjr).Decode(cfg); err != nil {
		return rjr.HandleJSONError(err)
	}
	return
}
