package parser

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"mime"
	"net/http"
	"net/url"
	"os"
	"path"
	"strings"
	"time"

	"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 {
	// CfgFiles is the list of YAML or JSON config sources: file paths,
	// HTTP(S) URLs, "-" for stdin, or an inline JSON string.
	// Multiple sources are merged left-to-right.
	CfgFiles []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{}

	for _, cfgFile := range p.args.CfgFiles {
		cfgFile = strings.TrimSpace(cfgFile)
		if cfgFile == "" {
			continue
		}
		fcfg, err := readConfig(cfgFile)
		if err != nil {
			return nil, err
		}
		cfg = mergeConfig(cfg, fcfg)
	}

	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 &&
		len(p.args.CfgFiles) == 0 {
		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
}

// readConfig reads a single configuration source, which may be "-" (stdin),
// an HTTP(S) URL, an inline JSON string starting with "{", or a file path.
func readConfig(cfgFile string) (*config.Config, error) {
	cfg := &config.Config{}

	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 isHTTPURL(cfgFile) { // URL
		if err := readConfigFromURL(cfgFile, cfg); err != nil {
			return nil, err
		}
	} else {
		if err := cfg.ReadFile(cfgFile); err != nil { // file
			return nil, err
		}
	}

	return cfg, nil
}

// isHTTPURL reports whether s is an HTTP or HTTPS URL.
func isHTTPURL(s string) bool {
	u, err := url.Parse(s)
	return err == nil && u.Scheme != "" && u.Host != "" &&
		(u.Scheme == "http" || u.Scheme == "https")
}

// maxConfigSize is the maximum response body size for remote config files.
const maxConfigSize = 10 << 20 // 10 MiB

// sharedHTTPClient is reused across config URL fetches to benefit from
// connection pooling.
var sharedHTTPClient = &http.Client{
	Timeout: 30 * time.Second,
}

// readConfigFromURL fetches a configuration from an HTTP(S) URL and reads it
// into cfg. The format (yaml or json) is detected from the Content-Type header
// or the URL path extension.
func readConfigFromURL(urlStr string, cfg *config.Config) error {
	resp, err := sharedHTTPClient.Get(urlStr)
	if err != nil {
		return fmt.Errorf("fetch config from %s: %w", sanitizeURL(urlStr), err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		io.Copy(io.Discard, resp.Body) // drain body for connection reuse; ignore error since we are already failing
		return fmt.Errorf("fetch config from %s: %s", sanitizeURL(urlStr), resp.Status)
	}

	data, err := io.ReadAll(io.LimitReader(resp.Body, maxConfigSize+1))
	if err != nil {
		return fmt.Errorf("read config from %s: %w", sanitizeURL(urlStr), err)
	}
	if len(data) > maxConfigSize {
		return fmt.Errorf("config from %s exceeds maximum size %d", sanitizeURL(urlStr), maxConfigSize)
	}

	format := detectFormat(resp.Header.Get("Content-Type"), urlStr)

	if err := cfg.Read(bytes.NewReader(data), format); err != nil {
		return fmt.Errorf("parse config from %s: %w", sanitizeURL(urlStr), err)
	}
	return nil
}

// sanitizeURL returns the URL with any embedded userinfo stripped for safe
// use in error messages.
func sanitizeURL(rawURL string) string {
	u, err := url.Parse(rawURL)
	if err != nil {
		return rawURL
	}
	u.User = nil
	return u.String()
}

// detectFormat determines the config format from the HTTP Content-Type header
// or the URL path extension. Returns "yaml" or "json".
func detectFormat(contentType, urlStr string) string {
	if mediatype, _, err := mime.ParseMediaType(contentType); err == nil {
		switch mediatype {
		case "application/json":
			return "json"
		case "application/yaml", "application/x-yaml",
			"text/yaml", "text/x-yaml":
			return "yaml"
		}
	}

	if u, err := url.Parse(urlStr); err == nil {
		switch strings.ToLower(strings.TrimPrefix(path.Ext(u.Path), ".")) {
		case "json":
			return "json"
		case "yaml", "yml":
			return "yaml"
		}
	}

	return "yaml"
}

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...),
		Quotas:     append(cfg1.Quotas, cfg2.Quotas...),
		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
}
