// 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 caddyhttp

import (
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"net"
	"net/http"
	"strings"
	"sync"

	"go.uber.org/zap"
	"go.uber.org/zap/exp/zapslog"
	"go.uber.org/zap/zapcore"

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

func init() {
	caddy.RegisterSlogHandlerFactory(func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler {
		return &extraFieldsSlogHandler{defaultHandler: handler, core: core, moduleID: moduleID}
	})
}

// ServerLogConfig describes a server's logging configuration. If
// enabled without customization, all requests to this server are
// logged to the default logger; logger destinations may be
// customized per-request-host.
type ServerLogConfig struct {
	// The default logger name for all logs emitted by this server for
	// hostnames that are not in the logger_names map.
	DefaultLoggerName string `json:"default_logger_name,omitempty"`

	// LoggerNames maps request hostnames to one or more custom logger
	// names. For example, a mapping of `"example.com": ["example"]` would
	// cause access logs from requests with a Host of example.com to be
	// emitted by a logger named "http.log.access.example". If there are
	// multiple logger names, then the log will be emitted to all of them.
	// If the logger name is an empty, the default logger is used, i.e.
	// the logger "http.log.access".
	//
	// Keys must be hostnames (without ports), and may contain wildcards
	// to match subdomains. The value is an array of logger names.
	//
	// For backwards compatibility, if the value is a string, it is treated
	// as a single-element array.
	LoggerNames map[string]StringArray `json:"logger_names,omitempty"`

	// By default, all requests to this server will be logged if
	// access logging is enabled. This field lists the request
	// hosts for which access logging should be disabled.
	SkipHosts []string `json:"skip_hosts,omitempty"`

	// If true, requests to any host not appearing in the
	// logger_names map will not be logged.
	SkipUnmappedHosts bool `json:"skip_unmapped_hosts,omitempty"`

	// If true, credentials that are otherwise omitted, will be logged.
	// The definition of credentials is defined by https://fetch.spec.whatwg.org/#credentials,
	// and this includes some request and response headers, i.e `Cookie`,
	// `Set-Cookie`, `Authorization`, and `Proxy-Authorization`.
	ShouldLogCredentials bool `json:"should_log_credentials,omitempty"`

	// Log each individual handler that is invoked.
	// Requires that the log emit at DEBUG level.
	//
	// NOTE: This may log the configuration of your
	// HTTP handler modules; do not enable this in
	// insecure contexts when there is sensitive
	// data in the configuration.
	//
	// EXPERIMENTAL: Subject to change or removal.
	Trace bool `json:"trace,omitempty"`
}

// wrapLogger wraps logger in one or more logger named
// according to user preferences for the given host.
func (slc ServerLogConfig) wrapLogger(logger *zap.Logger, req *http.Request) []*zap.Logger {
	// using the `log_name` directive or the `access_logger_names` variable,
	// the logger names can be overridden for the current request
	if names := GetVar(req.Context(), AccessLoggerNameVarKey); names != nil {
		if namesSlice, ok := names.([]any); ok {
			loggers := make([]*zap.Logger, 0, len(namesSlice))
			for _, loggerName := range namesSlice {
				// no name, use the default logger
				if loggerName == "" {
					loggers = append(loggers, logger)
					continue
				}
				// make a logger with the given name
				loggers = append(loggers, logger.Named(loggerName.(string)))
			}
			return loggers
		}
	}

	// get the hostname from the request, with the port number stripped
	host, _, err := net.SplitHostPort(req.Host)
	if err != nil {
		host = req.Host
	}

	// get the logger names for this host from the config
	hosts := slc.getLoggerHosts(host)

	// make a list of named loggers, or the default logger
	loggers := make([]*zap.Logger, 0, len(hosts))
	for _, loggerName := range hosts {
		// no name, use the default logger
		if loggerName == "" {
			loggers = append(loggers, logger)
			continue
		}
		// make a logger with the given name
		loggers = append(loggers, logger.Named(loggerName))
	}
	return loggers
}

func (slc ServerLogConfig) getLoggerHosts(host string) []string {
	// try the exact hostname first
	if hosts, ok := slc.LoggerNames[host]; ok {
		return hosts
	}

	// try matching wildcard domains if other non-specific loggers exist
	labels := strings.Split(host, ".")
	for i := range labels {
		if labels[i] == "" {
			continue
		}
		labels[i] = "*"
		wildcardHost := strings.Join(labels, ".")
		if hosts, ok := slc.LoggerNames[wildcardHost]; ok {
			return hosts
		}
	}

	return []string{slc.DefaultLoggerName}
}

func (slc *ServerLogConfig) clone() *ServerLogConfig {
	clone := &ServerLogConfig{
		DefaultLoggerName:    slc.DefaultLoggerName,
		LoggerNames:          make(map[string]StringArray),
		SkipHosts:            append([]string{}, slc.SkipHosts...),
		SkipUnmappedHosts:    slc.SkipUnmappedHosts,
		ShouldLogCredentials: slc.ShouldLogCredentials,
	}
	for k, v := range slc.LoggerNames {
		clone.LoggerNames[k] = append([]string{}, v...)
	}
	return clone
}

// StringArray is a slices of strings, but also accepts
// a single string as a value when JSON unmarshaling,
// converting it to a slice of one string.
type StringArray []string

// UnmarshalJSON satisfies json.Unmarshaler.
func (sa *StringArray) UnmarshalJSON(b []byte) error {
	var jsonObj any
	err := json.Unmarshal(b, &jsonObj)
	if err != nil {
		return err
	}
	switch obj := jsonObj.(type) {
	case string:
		*sa = StringArray([]string{obj})
		return nil
	case []any:
		s := make([]string, 0, len(obj))
		for _, v := range obj {
			value, ok := v.(string)
			if !ok {
				return errors.New("unsupported type")
			}
			s = append(s, value)
		}
		*sa = StringArray(s)
		return nil
	}
	return errors.New("unsupported type")
}

// errLogValues inspects err and returns the status code
// to use, the error log message, and any extra fields.
// If err is a HandlerError, the returned values will
// have richer information.
func errLogValues(err error) (status int, msg string, fields func() []zapcore.Field) {
	var handlerErr HandlerError
	if errors.As(err, &handlerErr) {
		status = handlerErr.StatusCode
		if handlerErr.Err == nil {
			msg = err.Error()
		} else {
			msg = handlerErr.Err.Error()
		}
		fields = func() []zapcore.Field {
			return []zapcore.Field{
				zap.Int("status", handlerErr.StatusCode),
				zap.String("err_id", handlerErr.ID),
				zap.String("err_trace", handlerErr.Trace),
			}
		}
		return status, msg, fields
	}
	fields = func() []zapcore.Field {
		return []zapcore.Field{
			zap.Error(err),
		}
	}
	status = http.StatusInternalServerError
	msg = err.Error()
	return status, msg, fields
}

// ExtraLogFields is a list of extra fields to log with every request.
type ExtraLogFields struct {
	fields   []zapcore.Field
	handlers sync.Map
}

// Add adds a field to the list of extra fields to log.
func (e *ExtraLogFields) Add(field zap.Field) {
	e.handlers.Clear()
	e.fields = append(e.fields, field)
}

// Set sets a field in the list of extra fields to log.
// If the field already exists, it is replaced.
func (e *ExtraLogFields) Set(field zap.Field) {
	e.handlers.Clear()

	for i := range e.fields {
		if e.fields[i].Key == field.Key {
			e.fields[i] = field
			return
		}
	}
	e.fields = append(e.fields, field)
}

func (e *ExtraLogFields) getSloggerHandler(handler *extraFieldsSlogHandler) (h slog.Handler) {
	if existing, ok := e.handlers.Load(handler); ok {
		return existing.(slog.Handler)
	}

	if handler.moduleID == "" {
		h = zapslog.NewHandler(handler.core.With(e.fields))
	} else {
		h = zapslog.NewHandler(handler.core.With(e.fields), zapslog.WithName(handler.moduleID))
	}

	if handler.group != "" {
		h = h.WithGroup(handler.group)
	}
	if handler.attrs != nil {
		h = h.WithAttrs(handler.attrs)
	}

	e.handlers.Store(handler, h)

	return h
}

const (
	// Variable name used to indicate that this request
	// should be omitted from the access logs
	LogSkipVar string = "log_skip"

	// For adding additional fields to the access logs
	ExtraLogFieldsCtxKey caddy.CtxKey = "extra_log_fields"

	// Variable name used to indicate the logger to be used
	AccessLoggerNameVarKey string = "access_logger_names"
)

type extraFieldsSlogHandler struct {
	defaultHandler slog.Handler
	core           zapcore.Core
	moduleID       string
	group          string
	attrs          []slog.Attr
}

func (e *extraFieldsSlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
	return e.defaultHandler.Enabled(ctx, level)
}

func (e *extraFieldsSlogHandler) Handle(ctx context.Context, record slog.Record) error {
	if elf, ok := ctx.Value(ExtraLogFieldsCtxKey).(*ExtraLogFields); ok {
		return elf.getSloggerHandler(e).Handle(ctx, record)
	}

	return e.defaultHandler.Handle(ctx, record)
}

func (e *extraFieldsSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	return &extraFieldsSlogHandler{
		e.defaultHandler.WithAttrs(attrs),
		e.core,
		e.moduleID,
		e.group,
		append(e.attrs, attrs...),
	}
}

func (e *extraFieldsSlogHandler) WithGroup(name string) slog.Handler {
	return &extraFieldsSlogHandler{
		e.defaultHandler.WithGroup(name),
		e.core,
		e.moduleID,
		name,
		e.attrs,
	}
}
