package parser

import (
	"bufio"
	"encoding/json"
	"os"
	"strings"

	"github.com/go-gost/core/logger"
	"github.com/go-gost/x/config"
	"github.com/go-gost/x/config/cmd"
	xmd "github.com/go-gost/x/metadata"
	mdutil "github.com/go-gost/x/metadata/util"
)

var (
	defaultParser = &parser{}
)

// Init stores the parsed CLI arguments so that subsequent calls to Parse can
// merge them with config-file and environment-variable sources.
func Init(args Args) {
	defaultParser = &parser{
		args: args,
	}
}

// Parse reads configuration from the sources specified during Init (config
// file, CLI services/nodes, and environment variables) and returns a merged
// Config ready for loading.
func Parse() (*config.Config, error) {
	return defaultParser.Parse()
}

// Args holds the raw CLI flags and positional arguments that Init will merge
// into the final configuration.
type Args struct {
	// CfgFile is the path to a YAML or JSON config file, "-" for stdin, or
	// an inline JSON string.
	CfgFile string

	// Services is the list of service definitions from -L flags.
	Services []string

	// Nodes is the list of chain-node definitions from -F flags.
	Nodes []string

	// Debug toggles debug-level logging.
	Debug bool

	// Trace toggles trace-level logging (supersedes Debug).
	Trace bool

	// ApiAddr is the address for the HTTP API server.
	ApiAddr string

	// MetricsAddr is the address for the Prometheus metrics endpoint.
	MetricsAddr string
}

type parser struct {
	args Args
}

func (p *parser) Parse() (*config.Config, error) {
	cfg := &config.Config{}

	if cfgFile := strings.TrimSpace(p.args.CfgFile); cfgFile != "" {
		if cfgFile == "-" { // stdin
			br := bufio.NewReader(os.Stdin)
			b, err := br.Peek(1)
			if err != nil {
				return nil, err
			}
			if b[0] == '{' {
				if err := cfg.Read(br, "json"); err != nil {
					return nil, err
				}
			} else {
				if err := cfg.Read(br, "yaml"); err != nil {
					return nil, err
				}
			}
		} else if strings.HasPrefix(cfgFile, "{") && strings.HasSuffix(cfgFile, "}") { // inline
			if err := json.Unmarshal([]byte(cfgFile), cfg); err != nil {
				return nil, err
			}
		} else {
			if err := cfg.ReadFile(cfgFile); err != nil { // file
				return nil, err
			}
		}
	}

	cmdCfg, err := cmd.BuildConfigFromCmd(p.args.Services, p.args.Nodes)
	if err != nil {
		return nil, err
	}
	cfg = mergeConfig(cfg, cmdCfg)

	if len(cfg.Services) == 0 && p.args.ApiAddr == "" && cfg.API == nil {
		if err := cfg.Load(); err != nil {
			return nil, err
		}
	}

	if v := os.Getenv("GOST_LOGGER_LEVEL"); v != "" {
		if cfg.Log == nil {
			cfg.Log = &config.LogConfig{}
		}
		cfg.Log.Level = v
	}
	if v := os.Getenv("GOST_API"); v != "" {
		if cfg.API == nil {
			cfg.API = &config.APIConfig{}
		}
		cfg.API.Addr = v
	}
	if v := os.Getenv("GOST_METRICS"); v != "" {
		if cfg.Metrics == nil {
			cfg.Metrics = &config.MetricsConfig{}
		}
		cfg.Metrics.Addr = v
	}
	if v := os.Getenv("GOST_PROFILING"); v != "" {
		if cfg.Profiling == nil {
			cfg.Profiling = &config.ProfilingConfig{}
		}
		cfg.Profiling.Addr = v
	}

	if p.args.Debug || p.args.Trace {
		if cfg.Log == nil {
			cfg.Log = &config.LogConfig{}
		}

		cfg.Log.Level = string(logger.DebugLevel)
		if p.args.Trace {
			cfg.Log.Level = string(logger.TraceLevel)
		}
	}

	if p.args.ApiAddr != "" {
		cfg.API = &config.APIConfig{
			Addr: p.args.ApiAddr,
		}
		if url, _ := cmd.Norm(p.args.ApiAddr); url != nil {
			cfg.API.Addr = url.Host
			if url.User != nil {
				username := url.User.Username()
				password, _ := url.User.Password()
				cfg.API.Auth = &config.AuthConfig{
					Username: username,
					Password: password,
				}
			}
			m := map[string]any{}
			for k, v := range url.Query() {
				if len(v) > 0 {
					m[k] = v[0]
				}
			}
			md := xmd.NewMetadata(m)
			cfg.API.PathPrefix = mdutil.GetString(md, "pathPrefix")
			cfg.API.AccessLog = mdutil.GetBool(md, "accesslog")
		}
	}
	if p.args.MetricsAddr != "" {
		cfg.Metrics = &config.MetricsConfig{
			Addr: p.args.MetricsAddr,
		}
		if url, _ := cmd.Norm(p.args.MetricsAddr); url != nil {
			cfg.Metrics.Addr = url.Host
			if url.User != nil {
				username := url.User.Username()
				password, _ := url.User.Password()
				cfg.Metrics.Auth = &config.AuthConfig{
					Username: username,
					Password: password,
				}
			}
			m := map[string]any{}
			for k, v := range url.Query() {
				if len(v) > 0 {
					m[k] = v[0]
				}
			}
			md := xmd.NewMetadata(m)
			cfg.Metrics.Path = mdutil.GetString(md, "path")
		}
	}

	return cfg, nil
}

func mergeConfig(cfg1, cfg2 *config.Config) *config.Config {
	if cfg1 == nil {
		return cfg2
	}
	if cfg2 == nil {
		return cfg1
	}

	cfg := &config.Config{
		Services:   append(cfg1.Services, cfg2.Services...),
		Chains:     append(cfg1.Chains, cfg2.Chains...),
		Hops:       append(cfg1.Hops, cfg2.Hops...),
		Authers:    append(cfg1.Authers, cfg2.Authers...),
		Admissions: append(cfg1.Admissions, cfg2.Admissions...),
		Bypasses:   append(cfg1.Bypasses, cfg2.Bypasses...),
		Resolvers:  append(cfg1.Resolvers, cfg2.Resolvers...),
		Hosts:      append(cfg1.Hosts, cfg2.Hosts...),
		Ingresses:  append(cfg1.Ingresses, cfg2.Ingresses...),
		SDs:        append(cfg1.SDs, cfg2.SDs...),
		Recorders:  append(cfg1.Recorders, cfg2.Recorders...),
		Limiters:   append(cfg1.Limiters, cfg2.Limiters...),
		CLimiters:  append(cfg1.CLimiters, cfg2.CLimiters...),
		RLimiters:  append(cfg1.RLimiters, cfg2.RLimiters...),
		Loggers:    append(cfg1.Loggers, cfg2.Loggers...),
		Routers:    append(cfg1.Routers, cfg2.Routers...),
		Observers:  append(cfg1.Observers, cfg2.Observers...),
		TLS:        cfg1.TLS,
		Log:        cfg1.Log,
		API:        cfg1.API,
		Metrics:    cfg1.Metrics,
		Profiling:  cfg1.Profiling,
	}
	if cfg2.TLS != nil {
		cfg.TLS = cfg2.TLS
	}
	if cfg2.Log != nil {
		cfg.Log = cfg2.Log
	}
	if cfg2.API != nil {
		cfg.API = cfg2.API
	}
	if cfg2.Metrics != nil {
		cfg.Metrics = cfg2.Metrics
	}
	if cfg2.Profiling != nil {
		cfg.Profiling = cfg2.Profiling
	}

	return cfg
}
