package hop

import (
	"crypto/tls"
	"strings"

	"github.com/go-gost/core/chain"
	"github.com/go-gost/core/hop"
	"github.com/go-gost/core/logger"
	xbypass "github.com/go-gost/x/bypass"
	"github.com/go-gost/x/config"
	"github.com/go-gost/x/config/parsing"
	bypass_parser "github.com/go-gost/x/config/parsing/bypass"
	node_parser "github.com/go-gost/x/config/parsing/node"
	selector_parser "github.com/go-gost/x/config/parsing/selector"
	xhop "github.com/go-gost/x/hop"
	hop_plugin "github.com/go-gost/x/hop/plugin"
	"github.com/go-gost/x/internal/loader"
	"github.com/go-gost/x/internal/plugin"
	"github.com/go-gost/x/metadata"
	mdutil "github.com/go-gost/x/metadata/util"
)

// ParseHop converts a HopConfig into a hop.Hop. It resolves plugin backends
// (HTTP or gRPC), parses inline nodes with metadata inheritance from the parent
// hop, wires up a node selector, and configures optional file, Redis, and HTTP
// hot-reload sources. Returns nil with no error when cfg is nil.
func ParseHop(cfg *config.HopConfig, log logger.Logger) (hop.Hop, error) {
	if cfg == nil {
		return nil, nil
	}

	if cfg.Plugin != nil {
		var tlsCfg *tls.Config
		if cfg.Plugin.TLS != nil {
			tlsCfg = &tls.Config{
				ServerName:         cfg.Plugin.TLS.ServerName,
				InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
			}
		}
		switch strings.ToLower(cfg.Plugin.Type) {
		case plugin.HTTP:
			return hop_plugin.NewHTTPPlugin(
				cfg.Name, cfg.Plugin.Addr,
				plugin.TokenOption(cfg.Plugin.Token),
				plugin.TLSConfigOption(tlsCfg),
				plugin.TimeoutOption(cfg.Plugin.Timeout),
			), nil
		default:
			return hop_plugin.NewGRPCPlugin(
				cfg.Name, cfg.Plugin.Addr,
				plugin.TokenOption(cfg.Plugin.Token),
				plugin.TLSConfigOption(tlsCfg),
			), nil
		}
	}

	var ppv int
	var soMark int
	ifce := cfg.Interface
	var netns string

	if cfg.SockOpts != nil {
		soMark = cfg.SockOpts.Mark
	}

	if cfg.Metadata != nil {
		md := metadata.NewMetadata(cfg.Metadata)
		if v := mdutil.GetString(md, parsing.MDKeyInterface); v != "" {
			ifce = v
		}

		if v := mdutil.GetInt(md, parsing.MDKeySoMark); v > 0 {
			soMark = v
		}
		ppv = mdutil.GetInt(md, parsing.MDKeyProxyProtocol)
		netns = mdutil.GetString(md, parsing.MDKeyNetns)
	}

	var nodes []*chain.Node
	for _, v := range cfg.Nodes {
		if v == nil {
			continue
		}

		// Build a merged metadata map for inheritance without mutating
		// the original node config.
		merged := make(map[string]any)
		for k, val := range v.Metadata {
			merged[k] = val
		}
		md := metadata.NewMetadata(merged)

		if v.Resolver == "" {
			v.Resolver = cfg.Resolver
		}
		if v.Hosts == "" {
			v.Hosts = cfg.Hosts
		}

		if !md.IsExists(parsing.MDKeyInterface) {
			if ifce != "" {
				merged[parsing.MDKeyInterface] = ifce
			}
			if v.Interface != "" {
				merged[parsing.MDKeyInterface] = v.Interface
			}
		}
		if !md.IsExists(parsing.MDKeySoMark) {
			if soMark != 0 {
				merged[parsing.MDKeySoMark] = soMark
			}
			if v.SockOpts != nil && v.SockOpts.Mark != 0 {
				merged[parsing.MDKeySoMark] = v.SockOpts.Mark
			}
		}
		if !md.IsExists(parsing.MDKeyProxyProtocol) && ppv > 0 {
			merged[parsing.MDKeyProxyProtocol] = ppv
		}
		if !md.IsExists(parsing.MDKeyNetns) {
			if netns != "" {
				merged[parsing.MDKeyNetns] = netns
			}
			if v.Netns != "" {
				merged[parsing.MDKeyNetns] = v.Netns
			}
		}

		if v.Connector == nil {
			v.Connector = &config.ConnectorConfig{}
		}
		if strings.TrimSpace(v.Connector.Type) == "" {
			v.Connector.Type = "http"
		}

		if v.Dialer == nil {
			v.Dialer = &config.DialerConfig{}
		}
		if strings.TrimSpace(v.Dialer.Type) == "" {
			v.Dialer.Type = "tcp"
		}

		// Temporarily swap merged metadata so ParseNode sees inherited values.
		origMeta := v.Metadata
		v.Metadata = merged
		node, err := node_parser.ParseNode(cfg.Name, v, log)
		v.Metadata = origMeta
		if err != nil {
			return nil, err
		}
		if node != nil {
			nodes = append(nodes, node)
		}
	}

	sel := selector_parser.ParseNodeSelector(cfg.Selector)
	if sel == nil {
		sel = selector_parser.DefaultNodeSelector()
	}

	opts := []xhop.Option{
		xhop.NameOption(cfg.Name),
		xhop.NodeOption(nodes...),
		xhop.SelectorOption(sel),
		xhop.BypassOption(xbypass.BypassGroup(bypass_parser.List(cfg.Bypass, cfg.Bypasses...)...)),
		xhop.ReloadPeriodOption(cfg.Reload),
		xhop.LoggerOption(log.WithFields(map[string]any{
			"kind": "hop",
			"hop":  cfg.Name,
		})),
	}

	if cfg.File != nil && cfg.File.Path != "" {
		opts = append(opts, xhop.FileLoaderOption(loader.FileLoader(cfg.File.Path)))
	}
	if cfg.Redis != nil && cfg.Redis.Addr != "" {
		opts = append(opts, xhop.RedisLoaderOption(loader.RedisStringLoader(
			cfg.Redis.Addr,
			loader.DBRedisLoaderOption(cfg.Redis.DB),
			loader.UsernameRedisLoaderOption(cfg.Redis.Username),
			loader.PasswordRedisLoaderOption(cfg.Redis.Password),
			loader.KeyRedisLoaderOption(cfg.Redis.Key),
		)))
	}
	if cfg.HTTP != nil && cfg.HTTP.URL != "" {
		opts = append(opts, xhop.HTTPLoaderOption(loader.HTTPLoader(
			cfg.HTTP.URL,
			loader.TimeoutHTTPLoaderOption(cfg.HTTP.Timeout),
		)))
	}
	return xhop.NewHop(opts...), nil
}
