// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.

package resource

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"gopkg.in/yaml.v3"
	"io"
	"net/url"
	"os/exec"
	"regexp"
_	"strconv"
	"strings"
	"gitea.rosskeen.house/rosskeen.house/machine"
)

func init() {
	ResourceTypes.Register("route", func(u *url.URL) Resource {
		n := NewNetworkRoute()
		return n
	})
}

/*

ROUTE := NODE_SPEC [ INFO_SPEC ]
NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ]
             [ table TABLE_ID ] [ proto RTPROTO ]
             [ scope SCOPE ] [ metric METRIC ]
             [ ttl-propagate { enabled | disabled } ]
INFO_SPEC := { NH | nhid ID } OPTIONS FLAGS [ nexthop NH ]...

NH := [ encap ENCAPTYPE ENCAPHDR ] [ via [ FAMILY ] ADDRESS ]
      [ dev STRING ] [ weight NUMBER ] NHFLAGS
FAMILY := [ inet | inet6 | mpls | bridge | link ]
OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ]
           [ rtt TIME ] [ rttvar TIME ] [ reordering NUMBER ]
           [ window NUMBER ] [ cwnd NUMBER ] [ initcwnd NUMBER ]
           [ ssthresh NUMBER ] [ realms REALM ] [ src ADDRESS ]
           [ rto_min TIME ] [ hoplimit NUMBER ] [ initrwnd NUMBER ]
           [ features FEATURES ] [ quickack BOOL ] [ congctl NAME ]
           [ pref PREF ] [ expires TIME ] [ fastopen_no_cookie BOOL ]
NHFLAGS := [ onlink | pervasive ]
PREF := [ low | medium | high ]
TIME := NUMBER[s|ms]
BOOL := [1|0]
FEATURES := ecn
ENCAPTYPE := [ mpls | ip | ip6 | seg6 | seg6local | rpl | ioam6 ]
ENCAPHDR := [ MPLSLABEL | SEG6HDR | SEG6LOCAL | IOAM6HDR ]
SEG6HDR := [ mode SEGMODE ] segs ADDR1,ADDRi,ADDRn [hmac HMACKEYID] [cleanup]
SEGMODE := [ encap | inline ]
SEG6LOCAL := action ACTION [ OPTIONS ] [ count ]
ACTION := { End | End.X | End.T | End.DX2 | End.DX6 | End.DX4 |
            End.DT6 | End.DT4 | End.DT46 | End.B6 | End.B6.Encaps |
            End.BM | End.S | End.AS | End.AM | End.BPF }
OPTIONS := OPTION [ OPTIONS ]
OPTION := { srh SEG6HDR | nh4 ADDR | nh6 ADDR | iif DEV | oif DEV |
            table TABLEID | vrftable TABLEID | endpoint PROGNAME }
IOAM6HDR := trace prealloc type IOAM6_TRACE_TYPE ns IOAM6_NAMESPACE size IOAM6_TRACE_SIZE
ROUTE_GET_FLAGS := [ fibmatch ]

*/

type NetworkRouteType string

const (
	NetworkRouteTypeUnicast     = "unicast"
	NetworkRouteTypeLocal       = "local"
	NetworkRouteTypeBroadcast   = "broadcast"
	NetworkRouteTypeMulticast   = "multicast"
	NetworkRouteTypeThrow       = "throw"
	NetworkRouteTypeUnreachable = "unreachable"
	NetworkRouteTypeProhibit    = "prohibit"
	NetworkRouteTypeBlackhole   = "blackhole"
	NetworkRouteTypeNat         = "nat"
)

type NetworkRouteTableId string

const (
	NetworkRouteTableLocal   = "local"
	NetworkRouteTableMain    = "main"
	NetworkRouteTableDefault = "default"
	NetworkRouteTableAll     = "all"
)

var NetworkRouteNumber = regexp.MustCompile(`^[0-9]+$`)

type NetworkRouteScope string

const (
	NetworkRouteScopeHost   = "host"
	NetworkRouteScopeLink   = "link"
	NetworkRouteScopeGlobal = "global"
)

type NetworkRouteProto string

const (
	NetworkRouteProtoKernel = "kernel"
	NetworkRouteProtoBoot   = "boot"
	NetworkRouteProtoStatic = "static"
)

// Manage the state of network routes
type NetworkRoute struct {
	Id        string
	To        string              `json:"to" yaml:"to"`
	Interface string              `json:"interface" yaml:"interface"`
	Gateway   string              `json:"gateway" yaml:"gateway"`
	Metric    uint                `json:"metric" yaml:"metric"`
	Rtid      NetworkRouteTableId `json:"rtid" yaml:"rtid"`
	RouteType NetworkRouteType    `json:"routetype" yaml:"routetype"`
	Scope     NetworkRouteScope   `json:"scope" yaml:"scope"`
	Proto     NetworkRouteProto   `json:"proto" yaml:"proto"`
	State     string              `json:"state" yaml:"state"`
}

func NewNetworkRoute() *NetworkRoute {
	return &NetworkRoute{Rtid: NetworkRouteTableMain}
}

func (n *NetworkRoute) Clone() Resource {
	return &NetworkRoute {
		Id: n.Id,
		To: n.To,
		Interface: n.Interface,
		Gateway: n.Gateway,
		Metric: n.Metric,
		Rtid: n.Rtid,
		RouteType: n.RouteType,
		Scope: n.Scope,
		Proto: n.Proto,
		State: n.State,
	}
}

func (n *NetworkRoute) StateMachine() machine.Stater {
	return StorageMachine()
}

func (n *NetworkRoute) URI() string {
	return fmt.Sprintf("route://%s", n.Id)
}

func (n *NetworkRoute) SetURI(uri string) error {
	return nil
}

func (n *NetworkRoute) Validate() error {
	return fmt.Errorf("failed")
}

func (n *NetworkRoute) Apply() error {

	switch n.State {
	case "absent":
	case "present":
	}
	return nil
}

func (n *NetworkRoute) Load(r io.Reader) error {
	c := NewYAMLDecoder(r)
	return c.Decode(n)
}

func (n *NetworkRoute) LoadDecl(yamlResourceDeclaration string) error {
	c := NewYAMLStringDecoder(yamlResourceDeclaration)
	return c.Decode(n)
}

func (n *NetworkRoute) ResolveId(ctx context.Context) string {
	uri := fmt.Sprintf("%s?gateway=%s&interface=%s&rtid=%s&metric=%d&type=%s&scope=%s",
		n.To, n.Gateway, n.Interface, n.Rtid, n.Metric, n.RouteType, n.Scope)
	fmt.Printf("%#v\n", uri)
	n.Id = hex.EncodeToString([]byte(uri))
	return n.Id
}

func (n *NetworkRoute) Read(ctx context.Context) ([]byte, error) {
	var cmdArgs []string = make([]string, 17)
	cmdArgs[0] = "route"
	cmdArgs[1] = "show"
	if len(n.To) > 0 {
		cmdArgs = append(cmdArgs, "to", n.To)
	}
	if len(n.Rtid) > 0 {
		cmdArgs = append(cmdArgs, "table", string(n.Rtid))
	}
	if len(n.Gateway) > 0 {
		cmdArgs = append(cmdArgs, "via", n.Gateway)
	}
	if len(n.Proto) > 0 {
		cmdArgs = append(cmdArgs, "protocol", string(n.Proto))
	}
	if len(n.Scope) > 0 {
		cmdArgs = append(cmdArgs, "scope", string(n.Scope))
	}
	if len(n.RouteType) > 0 {
		cmdArgs = append(cmdArgs, "type", string(n.RouteType))
	}
	if len(n.Interface) > 0 {
		cmdArgs = append(cmdArgs, "dev", n.Interface)
	}

	readRouteTable, errRouteList := exec.Command("ip", cmdArgs...).Output()
	if errRouteList != nil {
		return nil, errRouteList
	}
	routes := strings.Split(string(readRouteTable), "\n")
	if len(routes) == 1 {
		fields := strings.Split(routes[0], " ")
		numberOfFields := len(fields)
		if numberOfFields > 1 {
			n.To = fields[0]
			for i := 1; i < numberOfFields; i += 2 {
				n.SetField(fields[i], fields[i+1])
			}
		}
	}
	n.ResolveId(ctx)
	return yaml.Marshal(n)
}

func (n *NetworkRoute) SetField(name string, value string) {
	switch name {
	case "to":
		n.To = value
	case "table":
		n.Rtid = NetworkRouteTableId(value)
	case "via":
		n.Gateway = value
	case "protocol":
		n.Proto = NetworkRouteProto(value)
	case "scope":
		n.Scope = NetworkRouteScope(value)
	case "type":
		n.RouteType = NetworkRouteType(value)
	case "dev":
		n.Interface = value
	}
}

func (n *NetworkRoute) GetField(name string) string {
	switch name {
	case "to":
		return n.To
	case "table":
		return string(n.Rtid)
	case "via":
		return n.Gateway
	case "protocol":
		return string(n.Proto)
	case "scope":
		return string(n.Scope)
	case "type":
		return string(n.RouteType)
	case "dev":
		return n.Interface
	}
	return ""
}

func (n *NetworkRoute) Type() string { return "route" }

func (n *NetworkRouteType) UnmarshalValue(value string) error {
	switch value {
	case string(NetworkRouteTypeUnicast), string(NetworkRouteTypeLocal), string(NetworkRouteTypeBroadcast), string(NetworkRouteTypeMulticast), string(NetworkRouteTypeThrow), string(NetworkRouteTypeUnreachable), string(NetworkRouteTypeProhibit), string(NetworkRouteTypeBlackhole), string(NetworkRouteTypeNat):
		*n = NetworkRouteType(value)
		return nil
	default:
		return errors.New("invalid NetworkRouteType value")
	}
}

func (n *NetworkRouteType) UnmarshalJSON(data []byte) error {
	var s string
	if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
		return unmarshalRouteTypeErr
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteType) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteTableId) UnmarshalValue(value string) error {
	switch value {
	case NetworkRouteNumber.FindString(value):
		fallthrough
	case string(NetworkRouteTableLocal), string(NetworkRouteTableMain), string(NetworkRouteTableDefault), string(NetworkRouteTableAll):
		*n = NetworkRouteTableId(value)
		return nil
	default:
		return errors.New("invalid NetworkRouteTableId value")
	}
}

func (n *NetworkRouteTableId) UnmarshalJSON(data []byte) error {
	var s string
	if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
		return unmarshalRouteTypeErr
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteTableId) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteScope) UnmarshalValue(value string) error {
	switch value {
	case NetworkRouteNumber.FindString(value):
		fallthrough
	case string(NetworkRouteScopeHost), string(NetworkRouteScopeLink), string(NetworkRouteScopeGlobal):
		*n = NetworkRouteScope(value)
		return nil
	default:
		return errors.New("invalid NetworkRouteScope value")
	}
}

func (n *NetworkRouteScope) UnmarshalJSON(data []byte) error {
	var s string
	if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
		return unmarshalRouteTypeErr
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteScope) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteProto) UnmarshalValue(value string) error {
	switch value {
	case NetworkRouteNumber.FindString(value):
		fallthrough
	case string(NetworkRouteProtoKernel), string(NetworkRouteProtoBoot), string(NetworkRouteProtoStatic):
		*n = NetworkRouteProto(value)
		return nil
	default:
		return errors.New("invalid NetworkRouteProto value")
	}
}

func (n *NetworkRouteProto) UnmarshalJSON(data []byte) error {
	var s string
	if unmarshalRouteTypeErr := json.Unmarshal(data, &s); unmarshalRouteTypeErr != nil {
		return unmarshalRouteTypeErr
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRouteProto) UnmarshalYAML(value *yaml.Node) error {
	var s string
	if err := value.Decode(&s); err != nil {
		return err
	}
	return n.UnmarshalValue(s)
}

func (n *NetworkRoute) UnmarshalJSON(data []byte) error {
	if err := json.Unmarshal(data, n); err != nil {
		return err
	}
	return nil
}
