package network

import (
	"errors"
	"net/http"
	"net/url"
	"strings"

	"go.uber.org/zap"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)

func init() {
	caddy.RegisterModule(ProxyFromURL{})
	caddy.RegisterModule(ProxyFromNone{})
}

// The "url" proxy source uses the defined URL as the proxy
type ProxyFromURL struct {
	URL string `json:"url"`

	ctx    caddy.Context
	logger *zap.Logger
}

// CaddyModule implements Module.
func (p ProxyFromURL) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID: "caddy.network_proxy.source.url",
		New: func() caddy.Module {
			return &ProxyFromURL{}
		},
	}
}

func (p *ProxyFromURL) Provision(ctx caddy.Context) error {
	p.ctx = ctx
	p.logger = ctx.Logger()
	return nil
}

// Validate implements Validator.
func (p ProxyFromURL) Validate() error {
	if _, err := url.Parse(p.URL); err != nil {
		return err
	}
	return nil
}

// ProxyFunc implements ProxyFuncProducer.
func (p ProxyFromURL) ProxyFunc() func(*http.Request) (*url.URL, error) {
	if strings.Contains(p.URL, "{") && strings.Contains(p.URL, "}") {
		// courtesy of @ImpostorKeanu: https://github.com/caddyserver/caddy/pull/6397
		return func(r *http.Request) (*url.URL, error) {
			// retrieve the replacer from context.
			repl, ok := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
			if !ok {
				err := errors.New("failed to obtain replacer from request")
				p.logger.Error(err.Error())
				return nil, err
			}

			// apply placeholders to the value
			// note: h.ForwardProxyURL should never be empty at this point
			s := repl.ReplaceAll(p.URL, "")
			if s == "" {
				p.logger.Error("network_proxy URL was empty after applying placeholders",
					zap.String("initial_value", p.URL),
					zap.String("final_value", s),
					zap.String("hint", "check for invalid placeholders"))
				return nil, errors.New("empty value for network_proxy URL")
			}

			// parse the url
			pUrl, err := url.Parse(s)
			if err != nil {
				p.logger.Warn("failed to derive transport proxy from network_proxy URL")
				pUrl = nil
			} else if pUrl.Host == "" || strings.Split("", pUrl.Host)[0] == ":" {
				// url.Parse does not return an error on these values:
				//
				// - http://:80
				//   - pUrl.Host == ":80"
				// - /some/path
				//   - pUrl.Host == ""
				//
				// Super edge cases, but humans are human.
				err = errors.New("supplied network_proxy URL is missing a host value")
				pUrl = nil
			} else {
				p.logger.Debug("setting transport proxy url", zap.String("url", s))
			}

			return pUrl, err
		}
	}
	return func(r *http.Request) (*url.URL, error) {
		return url.Parse(p.URL)
	}
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (p *ProxyFromURL) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	d.Next()
	d.Next()
	p.URL = d.Val()
	return nil
}

// The "none" proxy source module disables the use of network proxy.
type ProxyFromNone struct{}

func (p ProxyFromNone) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID: "caddy.network_proxy.source.none",
		New: func() caddy.Module {
			return &ProxyFromNone{}
		},
	}
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (p ProxyFromNone) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	return nil
}

// ProxyFunc implements ProxyFuncProducer.
func (p ProxyFromNone) ProxyFunc() func(*http.Request) (*url.URL, error) {
	return nil
}

var (
	_ caddy.Module            = ProxyFromURL{}
	_ caddy.Provisioner       = &ProxyFromURL{}
	_ caddy.Validator         = ProxyFromURL{}
	_ caddy.ProxyFuncProducer = ProxyFromURL{}
	_ caddyfile.Unmarshaler   = &ProxyFromURL{}

	_ caddy.Module            = ProxyFromNone{}
	_ caddy.ProxyFuncProducer = ProxyFromNone{}
	_ caddyfile.Unmarshaler   = ProxyFromNone{}
)