package entry

import (
	"strconv"
	"strings"

	"github.com/docker/docker/api/types"
	"github.com/yusing/go-proxy/internal/common"
	"github.com/yusing/go-proxy/internal/docker"
	"github.com/yusing/go-proxy/internal/homepage"
	"github.com/yusing/go-proxy/internal/logging"
	"github.com/yusing/go-proxy/internal/net/http/loadbalancer"
	U "github.com/yusing/go-proxy/internal/utils"
	F "github.com/yusing/go-proxy/internal/utils/functional"
	"github.com/yusing/go-proxy/internal/utils/strutils"
	"github.com/yusing/go-proxy/internal/watcher/health"
)

type (
	RawEntry struct {
		_ U.NoCopy

		// raw entry object before validation
		// loaded from docker labels or yaml file
		Alias        string                    `json:"-" yaml:"-"`
		Scheme       string                    `json:"scheme,omitempty" yaml:"scheme"`
		Host         string                    `json:"host,omitempty" yaml:"host"`
		Port         string                    `json:"port,omitempty" yaml:"port"`
		NoTLSVerify  bool                      `json:"no_tls_verify,omitempty" yaml:"no_tls_verify"` // https proxy only
		PathPatterns []string                  `json:"path_patterns,omitempty" yaml:"path_patterns"` // http(s) proxy only
		HealthCheck  *health.HealthCheckConfig `json:"healthcheck,omitempty" yaml:"healthcheck"`
		LoadBalance  *loadbalancer.Config      `json:"load_balance,omitempty" yaml:"load_balance"`
		Middlewares  docker.NestedLabelMap     `json:"middlewares,omitempty" yaml:"middlewares"`
		Homepage     *homepage.Item            `json:"homepage,omitempty" yaml:"homepage"`

		/* Docker only */
		Container *docker.Container `json:"container,omitempty" yaml:"-"`
	}

	RawEntries = F.Map[string, *RawEntry]
)

var NewProxyEntries = F.NewMapOf[string, *RawEntry]

func (e *RawEntry) FillMissingFields() {
	isDocker := e.Container != nil
	cont := e.Container
	if !isDocker {
		cont = docker.DummyContainer
	}

	if e.Host == "" {
		switch {
		case cont.PrivateIP != "":
			e.Host = cont.PrivateIP
		case cont.PublicIP != "":
			e.Host = cont.PublicIP
		case !isDocker:
			e.Host = "localhost"
		}
	}

	lp, pp, extra := e.splitPorts()

	if port, ok := common.ServiceNamePortMapTCP[cont.ImageName]; ok {
		if pp == "" {
			pp = strconv.Itoa(port)
		}
		if e.Scheme == "" {
			e.Scheme = "tcp"
		}
	} else if port, ok := common.ImageNamePortMap[cont.ImageName]; ok {
		if pp == "" {
			pp = strconv.Itoa(port)
		}
		if e.Scheme == "" {
			e.Scheme = "http"
		}
	} else if pp == "" && e.Scheme == "https" {
		pp = "443"
	} else if pp == "" {
		if p := lowestPort(cont.PrivatePortMapping); p != "" {
			pp = p
		} else if p := lowestPort(cont.PublicPortMapping); p != "" {
			pp = p
		} else if !isDocker {
			pp = "80"
		} else {
			logging.Debug().Msg("no port found for " + e.Alias)
		}
	}

	// replace private port with public port if using public IP.
	if e.Host == cont.PublicIP {
		if p, ok := cont.PrivatePortMapping[pp]; ok {
			pp = strutils.PortString(p.PublicPort)
		}
	}
	// replace public port with private port if using private IP.
	if e.Host == cont.PrivateIP {
		if p, ok := cont.PublicPortMapping[pp]; ok {
			pp = strutils.PortString(p.PrivatePort)
		}
	}

	if e.Scheme == "" && isDocker {
		switch {
		case e.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":
			e.Scheme = "udp"
		case e.Host == cont.PrivateIP && cont.PrivatePortMapping[pp].Type == "udp":
			e.Scheme = "udp"
		}
	}

	if e.Scheme == "" {
		switch {
		case lp != "":
			e.Scheme = "tcp"
		case strings.HasSuffix(pp, "443"):
			e.Scheme = "https"
		default: // assume its http
			e.Scheme = "http"
		}
	}

	if e.HealthCheck == nil {
		e.HealthCheck = new(health.HealthCheckConfig)
	}

	if e.HealthCheck.Interval == 0 {
		e.HealthCheck.Interval = common.HealthCheckIntervalDefault
	}
	if e.HealthCheck.Timeout == 0 {
		e.HealthCheck.Timeout = common.HealthCheckTimeoutDefault
	}

	if e.HealthCheck.Disable {
		e.HealthCheck = nil
	}

	if cont.IdleTimeout != "" {
		if cont.WakeTimeout == "" {
			cont.WakeTimeout = common.WakeTimeoutDefault
		}
		if cont.StopTimeout == "" {
			cont.StopTimeout = common.StopTimeoutDefault
		}
		if cont.StopMethod == "" {
			cont.StopMethod = common.StopMethodDefault
		}
	}

	e.Port = joinPorts(lp, pp, extra)

	if e.Port == "" || e.Host == "" {
		if lp != "" {
			e.Port = lp + ":0"
		} else {
			e.Port = "0"
		}
	}
}

func (e *RawEntry) splitPorts() (lp string, pp string, extra string) {
	portSplit := strings.Split(e.Port, ":")
	if len(portSplit) == 1 {
		pp = portSplit[0]
	} else {
		lp = portSplit[0]
		pp = portSplit[1]
	}
	if len(portSplit) > 2 {
		extra = strings.Join(portSplit[2:], ":")
	}
	return
}

func joinPorts(lp string, pp string, extra string) string {
	s := make([]string, 0, 3)
	if lp != "" {
		s = append(s, lp)
	}
	if pp != "" {
		s = append(s, pp)
	}
	if extra != "" {
		s = append(s, extra)
	}
	return strings.Join(s, ":")
}

func lowestPort(ports map[string]types.Port) string {
	var cmp uint16
	var res string
	for port, v := range ports {
		if v.PrivatePort < cmp || cmp == 0 {
			cmp = v.PrivatePort
			res = port
		}
	}
	return res
}