package opts

import (
	"encoding/csv"
	"errors"
	"fmt"
	"net/netip"
	"regexp"
	"strconv"
	"strings"
)

const (
	networkOptName        = "name"
	networkOptAlias       = "alias"
	networkOptIPv4Address = "ip"
	networkOptIPv6Address = "ip6"
	networkOptMacAddress  = "mac-address"
	networkOptLinkLocalIP = "link-local-ip"
	driverOpt             = "driver-opt"
	gwPriorityOpt         = "gw-priority"
)

// NetworkAttachmentOpts represents the network options for endpoint creation
type NetworkAttachmentOpts struct {
	Target       string
	Aliases      []string
	DriverOpts   map[string]string
	Links        []string // TODO add support for links in the csv notation of `--network`
	IPv4Address  netip.Addr
	IPv6Address  netip.Addr
	LinkLocalIPs []netip.Addr
	MacAddress   string
	GwPriority   int
}

// NetworkOpt represents a network config in swarm mode.
type NetworkOpt struct {
	options []NetworkAttachmentOpts
}

// Set networkopts value
func (n *NetworkOpt) Set(value string) error { //nolint:gocyclo
	longSyntax, err := regexp.MatchString(`\w+=\w+(,\w+=\w+)*`, value)
	if err != nil {
		return err
	}

	var netOpt NetworkAttachmentOpts
	if longSyntax {
		csvReader := csv.NewReader(strings.NewReader(value))
		fields, err := csvReader.Read()
		if err != nil {
			return err
		}

		netOpt.Aliases = []string{}
		for _, field := range fields {
			// TODO(thaJeztah): these options should not be case-insensitive.
			key, val, ok := strings.Cut(strings.ToLower(field), "=")
			if !ok || key == "" {
				return fmt.Errorf("invalid field %s", field)
			}

			key = strings.TrimSpace(key)
			val = strings.TrimSpace(val)

			switch key {
			case networkOptName:
				netOpt.Target = val
			case networkOptAlias:
				netOpt.Aliases = append(netOpt.Aliases, val)
			case networkOptIPv4Address:
				netOpt.IPv4Address, err = netip.ParseAddr(val)
				if err != nil {
					return err
				}
			case networkOptIPv6Address:
				netOpt.IPv6Address, err = netip.ParseAddr(val)
				if err != nil {
					return err
				}
			case networkOptMacAddress:
				netOpt.MacAddress = val
			case networkOptLinkLocalIP:
				a, err := netip.ParseAddr(val)
				if err != nil {
					return err
				}
				netOpt.LinkLocalIPs = append(netOpt.LinkLocalIPs, a)
			case driverOpt:
				key, val, err = parseDriverOpt(val)
				if err != nil {
					return err
				}
				if netOpt.DriverOpts == nil {
					netOpt.DriverOpts = make(map[string]string)
				}
				netOpt.DriverOpts[key] = val
			case gwPriorityOpt:
				netOpt.GwPriority, err = strconv.Atoi(val)
				if err != nil {
					var numErr *strconv.NumError
					if errors.As(err, &numErr) {
						err = numErr.Err
					}
					return fmt.Errorf("invalid gw-priority (%s): %w", val, err)
				}
			default:
				return errors.New("invalid field key " + key)
			}
		}
		if len(netOpt.Target) == 0 {
			return errors.New("network name/id is not specified")
		}
	} else {
		netOpt.Target = value
	}
	n.options = append(n.options, netOpt)
	return nil
}

// Type returns the type of this option
func (*NetworkOpt) Type() string {
	return "network"
}

// Value returns the networkopts
func (n *NetworkOpt) Value() []NetworkAttachmentOpts {
	return n.options
}

// String returns the network opts as a string
func (*NetworkOpt) String() string {
	return ""
}

// NetworkMode return the network mode for the network option
func (n *NetworkOpt) NetworkMode() string {
	networkIDOrName := "default"
	netOptVal := n.Value()
	if len(netOptVal) > 0 {
		networkIDOrName = netOptVal[0].Target
	}
	return networkIDOrName
}

func parseDriverOpt(driverOpt string) (string, string, error) {
	// TODO(thaJeztah): these options should not be case-insensitive.
	// TODO(thaJeztah): should value be converted to lowercase as well, or only the key?
	key, value, ok := strings.Cut(strings.ToLower(driverOpt), "=")
	if !ok || key == "" {
		return "", "", errors.New("invalid key value pair format in driver options")
	}
	key = strings.TrimSpace(key)
	value = strings.TrimSpace(value)
	return key, value, nil
}
