// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpcaddyfile

import (
	"encoding/json"
	"fmt"
	"slices"
	"strconv"

	"github.com/dustin/go-humanize"

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

// serverOptions collects server config overrides parsed from Caddyfile global options
type serverOptions struct {
	// If set, will only apply these options to servers that contain a
	// listener address that matches exactly. If empty, will apply to all
	// servers that were not already matched by another serverOptions.
	ListenerAddress string

	// These will all map 1:1 to the caddyhttp.Server struct
	Name                  string
	ListenerWrappersRaw   []json.RawMessage
	PacketConnWrappersRaw []json.RawMessage
	ReadTimeout           caddy.Duration
	ReadHeaderTimeout     caddy.Duration
	WriteTimeout          caddy.Duration
	IdleTimeout           caddy.Duration
	KeepAliveInterval     caddy.Duration
	KeepAliveIdle         caddy.Duration
	KeepAliveCount        int
	MaxHeaderBytes        int
	EnableFullDuplex      bool
	Protocols             []string
	StrictSNIHost         *bool
	TrustedProxiesRaw     json.RawMessage
	TrustedProxiesStrict  int
	TrustedProxiesUnix    bool
	ClientIPHeaders       []string
	ShouldLogCredentials  bool
	Metrics               *caddyhttp.Metrics
	Trace                 bool // TODO: EXPERIMENTAL
	// If set, overrides whether QUIC listeners allow 0-RTT (early data).
	// If nil, the default behavior is used (currently allowed).
	Allow0RTT *bool
}

func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
	d.Next() // consume option name

	serverOpts := serverOptions{}
	if d.NextArg() {
		serverOpts.ListenerAddress = d.Val()
		if d.NextArg() {
			return nil, d.ArgErr()
		}
	}
	for d.NextBlock(0) {
		switch d.Val() {
		case "name":
			if serverOpts.ListenerAddress == "" {
				return nil, d.Errf("cannot set a name for a server without a listener address")
			}
			if !d.NextArg() {
				return nil, d.ArgErr()
			}
			serverOpts.Name = d.Val()

		case "listener_wrappers":
			for nesting := d.Nesting(); d.NextBlock(nesting); {
				modID := "caddy.listeners." + d.Val()
				unm, err := caddyfile.UnmarshalModule(d, modID)
				if err != nil {
					return nil, err
				}
				listenerWrapper, ok := unm.(caddy.ListenerWrapper)
				if !ok {
					return nil, fmt.Errorf("module %s (%T) is not a listener wrapper", modID, unm)
				}
				jsonListenerWrapper := caddyconfig.JSONModuleObject(
					listenerWrapper,
					"wrapper",
					listenerWrapper.(caddy.Module).CaddyModule().ID.Name(),
					nil,
				)
				serverOpts.ListenerWrappersRaw = append(serverOpts.ListenerWrappersRaw, jsonListenerWrapper)
			}

		case "packet_conn_wrappers":
			for nesting := d.Nesting(); d.NextBlock(nesting); {
				modID := "caddy.packetconns." + d.Val()
				unm, err := caddyfile.UnmarshalModule(d, modID)
				if err != nil {
					return nil, err
				}
				packetConnWrapper, ok := unm.(caddy.PacketConnWrapper)
				if !ok {
					return nil, fmt.Errorf("module %s (%T) is not a packet conn wrapper", modID, unm)
				}
				jsonPacketConnWrapper := caddyconfig.JSONModuleObject(
					packetConnWrapper,
					"wrapper",
					packetConnWrapper.(caddy.Module).CaddyModule().ID.Name(),
					nil,
				)
				serverOpts.PacketConnWrappersRaw = append(serverOpts.PacketConnWrappersRaw, jsonPacketConnWrapper)
			}

		case "timeouts":
			for nesting := d.Nesting(); d.NextBlock(nesting); {
				switch d.Val() {
				case "read_body":
					if !d.NextArg() {
						return nil, d.ArgErr()
					}
					dur, err := caddy.ParseDuration(d.Val())
					if err != nil {
						return nil, d.Errf("parsing read_body timeout duration: %v", err)
					}
					serverOpts.ReadTimeout = caddy.Duration(dur)

				case "read_header":
					if !d.NextArg() {
						return nil, d.ArgErr()
					}
					dur, err := caddy.ParseDuration(d.Val())
					if err != nil {
						return nil, d.Errf("parsing read_header timeout duration: %v", err)
					}
					serverOpts.ReadHeaderTimeout = caddy.Duration(dur)

				case "write":
					if !d.NextArg() {
						return nil, d.ArgErr()
					}
					dur, err := caddy.ParseDuration(d.Val())
					if err != nil {
						return nil, d.Errf("parsing write timeout duration: %v", err)
					}
					serverOpts.WriteTimeout = caddy.Duration(dur)

				case "idle":
					if !d.NextArg() {
						return nil, d.ArgErr()
					}
					dur, err := caddy.ParseDuration(d.Val())
					if err != nil {
						return nil, d.Errf("parsing idle timeout duration: %v", err)
					}
					serverOpts.IdleTimeout = caddy.Duration(dur)

				default:
					return nil, d.Errf("unrecognized timeouts option '%s'", d.Val())
				}
			}

		case "keepalive_interval":
			if !d.NextArg() {
				return nil, d.ArgErr()
			}
			dur, err := caddy.ParseDuration(d.Val())
			if err != nil {
				return nil, d.Errf("parsing keepalive interval duration: %v", err)
			}
			serverOpts.KeepAliveInterval = caddy.Duration(dur)

		case "keepalive_idle":
			if !d.NextArg() {
				return nil, d.ArgErr()
			}
			dur, err := caddy.ParseDuration(d.Val())
			if err != nil {
				return nil, d.Errf("parsing keepalive idle duration: %v", err)
			}
			serverOpts.KeepAliveIdle = caddy.Duration(dur)

		case "keepalive_count":
			if !d.NextArg() {
				return nil, d.ArgErr()
			}
			cnt, err := strconv.ParseInt(d.Val(), 10, 32)
			if err != nil {
				return nil, d.Errf("parsing keepalive count int: %v", err)
			}
			serverOpts.KeepAliveCount = int(cnt)

		case "max_header_size":
			var sizeStr string
			if !d.AllArgs(&sizeStr) {
				return nil, d.ArgErr()
			}
			size, err := humanize.ParseBytes(sizeStr)
			if err != nil {
				return nil, d.Errf("parsing max_header_size: %v", err)
			}
			serverOpts.MaxHeaderBytes = int(size)

		case "enable_full_duplex":
			if d.NextArg() {
				return nil, d.ArgErr()
			}
			serverOpts.EnableFullDuplex = true

		case "log_credentials":
			if d.NextArg() {
				return nil, d.ArgErr()
			}
			serverOpts.ShouldLogCredentials = true

		case "protocols":
			protos := d.RemainingArgs()
			for _, proto := range protos {
				if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
					return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
				}
				if slices.Contains(serverOpts.Protocols, proto) {
					return nil, d.Errf("protocol %s specified more than once", proto)
				}
				serverOpts.Protocols = append(serverOpts.Protocols, proto)
			}
			if nesting := d.Nesting(); d.NextBlock(nesting) {
				return nil, d.ArgErr()
			}

		case "strict_sni_host":
			if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
				return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
			}
			boolVal := true
			if d.Val() == "insecure_off" {
				boolVal = false
			}
			serverOpts.StrictSNIHost = &boolVal

		case "trusted_proxies":
			if !d.NextArg() {
				return nil, d.Err("trusted_proxies expects an IP range source module name as its first argument")
			}
			modID := "http.ip_sources." + d.Val()
			unm, err := caddyfile.UnmarshalModule(d, modID)
			if err != nil {
				return nil, err
			}
			source, ok := unm.(caddyhttp.IPRangeSource)
			if !ok {
				return nil, fmt.Errorf("module %s (%T) is not an IP range source", modID, unm)
			}
			jsonSource := caddyconfig.JSONModuleObject(
				source,
				"source",
				source.(caddy.Module).CaddyModule().ID.Name(),
				nil,
			)
			serverOpts.TrustedProxiesRaw = jsonSource

		case "trusted_proxies_strict":
			if d.NextArg() {
				return nil, d.ArgErr()
			}
			serverOpts.TrustedProxiesStrict = 1

		case "trusted_proxies_unix":
			if d.NextArg() {
				return nil, d.ArgErr()
			}
			serverOpts.TrustedProxiesUnix = true

		case "client_ip_headers":
			headers := d.RemainingArgs()
			for _, header := range headers {
				if slices.Contains(serverOpts.ClientIPHeaders, header) {
					return nil, d.Errf("client IP header %s specified more than once", header)
				}
				serverOpts.ClientIPHeaders = append(serverOpts.ClientIPHeaders, header)
			}
			if nesting := d.Nesting(); d.NextBlock(nesting) {
				return nil, d.ArgErr()
			}

		case "metrics":
			caddy.Log().Warn("The nested 'metrics' option inside `servers` is deprecated and will be removed in the next major version. Use the global 'metrics' option instead.")
			serverOpts.Metrics = new(caddyhttp.Metrics)
			for nesting := d.Nesting(); d.NextBlock(nesting); {
				switch d.Val() {
				case "per_host":
					serverOpts.Metrics.PerHost = true
				default:
					return nil, d.Errf("unrecognized metrics option '%s'", d.Val())
				}
			}

		case "trace":
			if d.NextArg() {
				return nil, d.ArgErr()
			}
			serverOpts.Trace = true

		case "0rtt":
			// only supports "off" for now
			if !d.NextArg() {
				return nil, d.ArgErr()
			}
			if d.Val() != "off" {
				return nil, d.Errf("unsupported 0rtt argument '%s' (only 'off' is supported)", d.Val())
			}
			boolVal := false
			serverOpts.Allow0RTT = &boolVal

		default:
			return nil, d.Errf("unrecognized servers option '%s'", d.Val())
		}
	}
	return serverOpts, nil
}

// applyServerOptions sets the server options on the appropriate servers
func applyServerOptions(
	servers map[string]*caddyhttp.Server,
	options map[string]any,
	_ *[]caddyconfig.Warning,
) error {
	serverOpts, ok := options["servers"].([]serverOptions)
	if !ok {
		return nil
	}

	// check for duplicate names, which would clobber the config
	existingNames := map[string]bool{}
	for _, opts := range serverOpts {
		if opts.Name == "" {
			continue
		}
		if existingNames[opts.Name] {
			return fmt.Errorf("cannot use duplicate server name '%s'", opts.Name)
		}
		existingNames[opts.Name] = true
	}

	// collect the server name overrides
	nameReplacements := map[string]string{}

	for key, server := range servers {
		// find the options that apply to this server
		optsIndex := slices.IndexFunc(serverOpts, func(s serverOptions) bool {
			return s.ListenerAddress == "" || slices.Contains(server.Listen, s.ListenerAddress)
		})

		// if none apply, then move to the next server
		if optsIndex == -1 {
			continue
		}
		opts := serverOpts[optsIndex]

		// set all the options
		server.ListenerWrappersRaw = opts.ListenerWrappersRaw
		server.PacketConnWrappersRaw = opts.PacketConnWrappersRaw
		server.ReadTimeout = opts.ReadTimeout
		server.ReadHeaderTimeout = opts.ReadHeaderTimeout
		server.WriteTimeout = opts.WriteTimeout
		server.IdleTimeout = opts.IdleTimeout
		server.KeepAliveInterval = opts.KeepAliveInterval
		server.KeepAliveIdle = opts.KeepAliveIdle
		server.KeepAliveCount = opts.KeepAliveCount
		server.MaxHeaderBytes = opts.MaxHeaderBytes
		server.EnableFullDuplex = opts.EnableFullDuplex
		server.Protocols = opts.Protocols
		server.StrictSNIHost = opts.StrictSNIHost
		server.TrustedProxiesRaw = opts.TrustedProxiesRaw
		server.ClientIPHeaders = opts.ClientIPHeaders
		server.TrustedProxiesStrict = opts.TrustedProxiesStrict
		server.TrustedProxiesUnix = opts.TrustedProxiesUnix
		server.Metrics = opts.Metrics
		server.Allow0RTT = opts.Allow0RTT
		if opts.ShouldLogCredentials {
			if server.Logs == nil {
				server.Logs = new(caddyhttp.ServerLogConfig)
			}
			server.Logs.ShouldLogCredentials = opts.ShouldLogCredentials
		}
		if opts.Trace {
			// TODO: THIS IS EXPERIMENTAL (MAY 2024)
			if server.Logs == nil {
				server.Logs = new(caddyhttp.ServerLogConfig)
			}
			server.Logs.Trace = opts.Trace
		}

		if opts.Name != "" {
			nameReplacements[key] = opts.Name
		}
	}

	// rename the servers if marked to do so
	for old, new := range nameReplacements {
		servers[new] = servers[old]
		delete(servers, old)
	}

	return nil
}
