/*
** Copyright (C) 2001-2026 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** 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 Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

package modbus

import (
	"encoding/binary"
	"fmt"
	"net"
	"net/url"
	"runtime"
	"strconv"
	"strings"

	"golang.zabbix.com/sdk/errs"
)

// main parsing
func parseParams(params *[]string) (*mbParams, error) {
	var p mbParams
	var err error

	if p.ReqType, err = getReqType((*params)[0]); err != nil {
		return nil, err
	}

	switch p.ReqType {
	case RTU, ASCII:
		if p.Serial, err = getSerial((*params)[0]); err != nil {
			return nil, err
		}
	case TCP:
		if p.NetAddr, err = getNetAddr((*params)[0]); err != nil {
			return nil, err
		}
	default:
		return nil, fmt.Errorf("Unsupported modbus protocol")
	}

	if p.SlaveID, err = getSlaveID(params, 1, p.ReqType); err != nil {
		return nil, err
	}

	if p.FuncID, err = getFuncID(params, 2); err != nil {
		return nil, err
	}

	if p.MemAddr, p.FuncID, err = getMemAddr(params, 3, p.FuncID); err != nil {
		return nil, err
	}

	if p.RetType, err = getRetType(params, 5, p.FuncID); err != nil {
		return nil, err
	}

	if p.Count, p.RetCount, err = getCount(params, 4, p.RetType); err != nil {
		return nil, err
	}

	if p.Endianness, err = getEndianness(params, 6, p.RetType); err != nil {
		return nil, err
	}

	if p.Count, p.Offset, err = getOffset(params, 7, p.Count); err != nil {
		return nil, err
	}

	return &p, nil
}

func getReqType(v string) (reqType bits8, err error) {
	pos := strings.Index(v, "://")
	if pos < 0 {
		return 0, fmt.Errorf("Unsupported endpoint format")
	}
	switch v[:pos] {
	case "rtu":
		reqType = RTU
	case "ascii":
		reqType = ASCII
	case "tcp":
		reqType = TCP
	default:
		reqType = 0
	}
	return reqType, nil
}

func getSerial(v string) (addr *Serial, err error) {
	a := Serial{Speed: 115200, DataBits: 8, Parity: "N", StopBit: 1}

	val := v[strings.Index(v, "://")+3:]
	inx := strings.Index(val, ":")
	if inx < 0 {
		a.PortName = val
		if runtime.GOOS != "windows" && strings.Index(a.PortName, "/") < 0 {
			a.PortName = "/dev/" + a.PortName
		}
		return &a, nil
	}
	a.PortName = val[:inx]
	if runtime.GOOS != "windows" && strings.Index(a.PortName, "/") < 0 {
		a.PortName = "/dev/" + a.PortName
	}

	var speed uint64
	val = val[inx+1:]
	if inx = strings.Index(val, ":"); inx < 0 {
		if speed, err = strconv.ParseUint(val, 10, 32); err != nil {
			return &a, fmt.Errorf("unsupported speed value: %w", err)
		}
		a.Speed = uint32(speed)
		return &a, nil
	} else if speed, err = strconv.ParseUint(val[:inx], 10, 32); err != nil {
		return nil, fmt.Errorf("unsupported speed value: %w", err)
	}
	a.Speed = uint32(speed)

	val = val[inx+1:]
	if len(val) != 3 {
		return nil, errs.New("unsupported params format of serial line")
	}

	switch val[0] {
	case '5':
		a.DataBits = 5
	case '6':
		a.DataBits = 6
	case '7':
		a.DataBits = 7
	case '8':
		a.DataBits = 8
	default:
		return nil, errs.Errorf("unsupported data bits value of serial line:%s", val[:1])
	}

	switch val[1] {
	case 'n':
		a.Parity = "N"
	case 'e':
		a.Parity = "E"
	case 'o':
		a.Parity = "O"
	default:
		return nil, errs.Errorf("unsupported parity value of serial line:%s", val[1:2])
	}

	switch val[2] {
	case '1':
		a.StopBit = 1
	case '2':
		a.StopBit = 2
	default:
		return nil, errs.Errorf("Unsupported stop bit value of serial line:%s", val[2:])
	}

	return &a, nil
}

func getNetAddr(v string) (netAddr string, err error) {
	val := v[strings.Index(v, "://")+3:]

	u := url.URL{Host: val}
	ip := net.ParseIP(val)
	if nil == ip && 0 == len(strings.TrimSpace(u.Hostname())) {
		return "", fmt.Errorf("address \"%s\": empty value", val)
	}

	var checkAddr string
	if nil != ip {
		checkAddr = net.JoinHostPort(val, "502")
	} else if 0 == len(u.Port()) {
		checkAddr = net.JoinHostPort(u.Hostname(), "502")
	} else {
		checkAddr = val
	}

	var (
		h string
		p string
	)
	if h, p, err = net.SplitHostPort(checkAddr); err != nil {
		return "", fmt.Errorf("address \"%s\": %s", val, err)
	}
	netAddr = net.JoinHostPort(strings.TrimSpace(h), strings.TrimSpace(p))

	return netAddr, nil
}

func getSlaveID(p *[]string, n int, reqType bits8) (slaveID uint8, err error) {
	v := ""

	if len(*p) > n {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		if reqType == TCP {
			return 255, nil
		}
		return 1, nil
	}
	var val uint64
	if val, err = strconv.ParseUint(v, 10, 8); err != nil {
		return 0, fmt.Errorf("Unsupported slave id for serial line: %w", err)
	}
	slaveID = uint8(val)

	if 0 != (reqType&(RTU|ASCII)) && (slaveID == 0 || slaveID > 247) {
		return 0, fmt.Errorf("Unsupported slave id value for serial line:%d", slaveID)
	}

	return slaveID, nil
}

func getFuncID(p *[]string, n int) (funcID uint8, err error) {
	v := ""

	if len(*p) > n {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		return 0, nil
	}

	var val uint64
	if val, err = strconv.ParseUint(v, 10, 8); err != nil {
		return 0, fmt.Errorf("Operation id: %w", err)
	}
	funcID = uint8(val)

	if funcID == 0 || funcID > 4 {
		return 0, fmt.Errorf("Unsupported modbus operation:%d", funcID)
	}

	return funcID, nil
}

func getMemAddr(p *[]string, n int, fid uint8) (memAddr uint16, funcID uint8, err error) {
	var v string

	if len(*p) > n && strings.TrimSpace((*p)[n]) != "" {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		switch fid {
		case 0:
			v = "00001"
		default:
			v = "0"
		}
	}

	if fid == 0 && len(v) != 5 {
		return 0, fid, fmt.Errorf("Unsupported modbus address length for empty function:%d", len(v))
	}

	var val uint64
	if val, err = strconv.ParseUint(v, 10, 16); err != nil {
		return 0, fid, fmt.Errorf("Unsupported modbus address: %w", err)
	}
	memAddr = uint16(val)

	if fid != 0 {
		return memAddr, fid, nil
	}

	if memAddr >= 50000 || memAddr == 0 {
		return 0, fid, fmt.Errorf("Unsupported modbus address for empty function:%d", memAddr)
	}
	funcID = uint8(memAddr / 10000)

	switch funcID {
	case 0:
		funcID = 1
	case 1:
		funcID = 2
		memAddr = memAddr - 10000
	case 3:
		funcID = 4
		memAddr = memAddr - 30000
	case 4:
		funcID = 3
		memAddr = memAddr - 40000
	default:
		return 0, fid, fmt.Errorf("Unsupported modbus function for address:%d", memAddr)
	}

	return memAddr - 1, funcID, nil
}

func getRetType(p *[]string, n int, funcID uint8) (retType bits16, err error) {
	v := ""

	if len(*p) > n {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		if funcID == ReadCoil || funcID == ReadDiscrete {
			return Bit, nil
		}
		return Uint16, nil
	}
	switch v {
	case "bit":
		retType = Bit
	case "int8":
		retType = Int8
	case "uint8":
		retType = Uint8
	case "int16":
		retType = Int16
	case "uint16":
		retType = Uint16
	case "int32":
		retType = Int32
	case "uint32":
		retType = Uint32
	case "float":
		retType = Float
	case "uint64":
		retType = Uint64
	case "double":
		retType = Double
	default:
		return 0, fmt.Errorf("Unsupported type:%s", v)
	}
	if (funcID == ReadCoil || funcID == ReadDiscrete) && retType != Bit {
		return 0, fmt.Errorf("Unsupported type for Read Coil and Read Discrete Input:%s", v)
	}
	if (funcID == ReadHolding || funcID == ReadInput) && retType == Bit {
		return 0, fmt.Errorf("Unsupported type for Read Holding and Read Input registers:%s", v)
	}
	return retType, nil
}

func getCount(p *[]string, n int, retType bits16) (count uint16, retCount uint, err error) {
	v := "1"

	if len(*p) > n {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		v = "1"
	}

	var val uint64
	if val, err = strconv.ParseUint(v, 10, 32); err != nil {
		return 0, 0, fmt.Errorf("Unsupported data length: %w", err)
	} else if val == 0 {
		return 0, 0, fmt.Errorf("Unsupported zero as data length")
	}
	retCount = uint(val)

	var realCount uint
	switch retType {
	case Int8, Uint8:
		realCount = uint((retCount + 1) / 2)
	case Int32, Uint32, Float:
		realCount = uint(retCount * 2)
	case Double, Uint64:
		realCount = retCount * 4
	default:
		realCount = retCount
	}
	if realCount > 65535 {
		return 0, 0, fmt.Errorf("Unsupported data type with required data count:%d", retCount)
	}
	count = uint16(realCount)
	return count, retCount, nil
}

func getEndianness(p *[]string, n int, retType bits16) (endianness Endianness, err error) {
	v := "be"

	if len(*p) > n {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		v = "be"
	}
	switch v {
	case "be":
		endianness.order = binary.BigEndian
		endianness.middle = 0
	case "le":
		endianness.order = binary.LittleEndian
		endianness.middle = 0
	case "mbe":
		endianness.order = binary.BigEndian
		endianness.middle = Mbe
	case "mle":
		endianness.order = binary.LittleEndian
		endianness.middle = Mle
	default:
		return endianness, fmt.Errorf("Unsupported endianness of data:%s", v)
	}

	if retType == Bit && (endianness.order != binary.BigEndian || endianness.middle != 0) {
		return endianness, fmt.Errorf("Unsupported endianness with required data type:%s", v)
	}

	if 0 != (retType&(Uint8|Int8|Uint16|Int16)) && endianness.middle != 0 {
		return endianness, fmt.Errorf("Unsupported middle-endian with required data type:%s", v)
	}

	return endianness, nil
}

func getOffset(p *[]string, n int, c uint16) (count uint16, offset uint16, err error) {
	v := ""

	if len(*p) > n {
		v = strings.TrimSpace((*p)[n])
	}
	if len(v) == 0 {
		return c, 0, nil
	}
	var val uint64
	if val, err = strconv.ParseUint(v, 10, 16); err != nil {
		return c, 0, fmt.Errorf("Unsupported offset: %w", err)
	}
	offset = uint16(val)
	if (offset + c) > 65535 {
		return c, 0, fmt.Errorf("Unsupported common data length and offset:%d", offset+c)
	}
	count = c + offset
	return count, offset, nil
}
