// Package syslog provides a slog handler for transmitting logs locally
// or over the network using the syslog protocol described in RFC 5424.
package syslog

import (
	"context"
	"crypto/tls"
	"fmt"
	"log/slog"
	"math"
	"os"
	"slices"
	"strings"
	"time"

	"github.com/leodido/go-syslog/v4/rfc5424"
)

const defaultBufferSize = 64

// Handler transmits [slog.Record] values to a syslog daemon. A new value may
// be constructed using [NewHandler].
type Handler interface {
	slog.Handler

	// Flush waits for all buffered messages to be sent until the passed
	// duration has passed.
	Flush(timeout time.Duration)

	// Close terminates the connection and renders the handler (including ones
	// created using WithAttrs and WithGroup) a no-op.
	Close()
}

type handler struct {
	transport *transport
	opts      *HandlerOptions
	data      [][2]string
	prefix    string
}

var _ Handler = (*handler)(nil)

// HandlerOptions specifies connection parameters and modifies the processing
// of log records by the handler.
type HandlerOptions struct {
	// Address specifies the destination for the log handler. It is specified
	// as the address family and the address itself, joined by a forward slash.
	// "tcp+tls" is a special case family: it instructs the handler to connect
	// to the specified address using TLS over TCP.
	//
	// If the field is empty, the default value of "unixgram//dev/log" is used.
	Address string

	// TLSConfig optionally specifies the TLS configuration to be used by the
	// Go crypto/tls package for establishing a TLS connection.
	TLSConfig *tls.Config

	// Level specifies the minimum level below which log records are dropped.
	//
	// If the field is empty, no filtering is done.
	Level slog.Leveler

	// BufferSize specifies how many syslog messages should be kept in memory
	// in case delivery is lagging behind. If the buffer is full, all incoming
	// log records are dropped.
	//
	// If the field is empty, the default value of 64 is used.
	BufferSize int

	// Facility specifies the numerical facility value that is joined together
	// with a message's severity into the priority value.
	//
	// https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.1
	//
	// If the field is empty, the default value of 3 (corresponding to "system
	// daemon") is used.
	Facility int

	// LevelToSeverity specifies a function for mapping slog.Level to the
	// numerical severity value that is joined together with the global
	// facility value into the message's priority value.
	//
	// https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.1
	//
	// If the field is empty, the following mapping is used:
	//
	//   - slog.LevelDebug is mapped to 7;
	//   - slog.LevelInfo is mapped to 6;
	//   - slog.LevelWarn is mapped to 4;
	//   - slog.LevelError is mapped to 3;
	//   - any other value is mapped to 0 (emergency).
	LevelToSeverity func(slog.Level) int

	// Hostname specifies the HOSTNAME part of every syslog message produced by
	// this handler.
	//
	// https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.4
	//
	// If the field is nil, the machine hostname is used as the value,
	// otherwise the part is left blank.
	Hostname *string

	// AppName specifies the APP-NAME part of every syslog message produced by
	// this handler. The specification places no restriction on the contents of
	// this field.
	//
	// https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.5
	//
	// If the field is empty, the part is left blank.
	AppName string

	// ProcID specifies the PROCID part of every syslog message produced by
	// this handler. The specification places no restriction on the contents of
	// this field.
	//
	// https://www.rfc-editor.org/rfc/rfc5424.html#section-6.2.6
	//
	// If the field is nil, the ID of the current process is used as the value,
	// otherwise the part is left blank.
	ProcID *string

	// StructuredDataID specifies the ID to use for every structured data
	// element produced by this handler. The specification places certain
	// restrictions on the contents of this field.
	//
	// https://www.rfc-editor.org/rfc/rfc5424.html#section-6.3.2
	//
	// If the field is empty, all attributes are dropped and no structured data
	// is ever transmitted, otherwise the value is used with no validation.
	StructuredDataID string
}

// NewHandler creates a syslog handler connected to a daemon. If the attempt to
// connect fails, an error is returned. If a connection is lost during the life
// of the handler, an automatic reconnection attempt is performed in the
// background. No guarantee is provided as to reliable delivery of every
// message should network errors occur.
func NewHandler(opts *HandlerOptions) (Handler, error) {
	h := &handler{opts: &HandlerOptions{}}
	if opts != nil {
		*h.opts = *opts
	}
	if h.opts.Address == "" {
		h.opts.Address = "unixgram//dev/log"
	}
	if h.opts.Level == nil {
		h.opts.Level = slog.Level(math.MinInt)
	}
	if h.opts.BufferSize == 0 {
		h.opts.BufferSize = defaultBufferSize
	}
	if h.opts.Facility < 0 || h.opts.Facility > 23 {
		return nil, fmt.Errorf("facility value must be in the range of 0 to 23 inclusive")
	}
	if h.opts.Hostname == nil {
		hostname, err := os.Hostname()
		if err == nil && hostname != "" {
			h.opts.Hostname = &hostname
		}
	}
	if h.opts.ProcID == nil {
		procID := fmt.Sprint(os.Getpid())
		h.opts.ProcID = &procID
	}

	family, addr, found := strings.Cut(h.opts.Address, "/")
	if !found {
		return nil, fmt.Errorf("address must be of format \"family/address\"")
	}

	var err error
	h.transport, err = dial(family, addr, h.opts.BufferSize, h.opts.TLSConfig)
	if err != nil {
		return nil, err
	}

	return h, nil
}

func (h *handler) Enabled(ctx context.Context, level slog.Level) bool {
	return level >= h.opts.Level.Level()
}

func (h *handler) Handle(ctx context.Context, r slog.Record) error {
	var msg rfc5424.SyslogMessage
	msg.SetVersion(1)
	msg.SetMessage(r.Message)
	msg.Timestamp = &r.Time

	var facility, severity int
	facility = h.opts.Facility
	if h.opts.LevelToSeverity != nil {
		severity = h.opts.LevelToSeverity(r.Level)
	} else {
		severity = mapSlogLevelToSeverity(r.Level)
	}
	if severity < 0 || severity > 7 {
		severity = 0
	}
	msg.SetPriority(uint8(facility*8 + severity))

	if h.opts.Hostname != nil && *h.opts.Hostname != "" {
		msg.SetHostname(*h.opts.Hostname)
	}
	if h.opts.ProcID != nil && *h.opts.ProcID != "" {
		msg.SetProcID(*h.opts.ProcID)
	}
	if h.opts.AppName != "" {
		msg.SetAppname(h.opts.AppName)
	}
	if h.opts.StructuredDataID != "" {
		attrs := make([]slog.Attr, 0, r.NumAttrs())
		r.Attrs(func(attr slog.Attr) bool {
			attrs = append(attrs, attr)
			return true
		})
		prefixSlogAttrs(h.prefix, attrs)
		data := append(h.data, serializeSlogAttrs(attrs)...)
		for _, datum := range data {
			msg.SetParameter(h.opts.StructuredDataID, datum[0], datum[1])
		}
	}

	h.transport.send(&msg)

	return nil
}

func (h *handler) WithAttrs(attrs []slog.Attr) slog.Handler {
	if len(attrs) == 0 {
		return h
	}
	h = shallowCopy(h)
	prefixSlogAttrs(h.prefix, attrs)
	data := serializeSlogAttrs(attrs)
	h.data = slices.Clip(append(h.data, data...))
	return h
}

func (h *handler) WithGroup(name string) slog.Handler {
	if name == "" {
		return h
	}
	h = shallowCopy(h)
	if h.prefix != "" {
		h.prefix += "." + name
	} else {
		h.prefix = name
	}
	return h
}

func (h *handler) Flush(timeout time.Duration) {
	h.transport.flush(timeout)
}

func (h *handler) Close() {
	h.transport.close()
}

// mapSlogLevelToSeverity maps log levels defined in the slog package to
// equivalent syslog severity values. Unknown levels are mapped to syslog
// severity 0 (emergency).
func mapSlogLevelToSeverity(level slog.Level) (severity int) {
	switch level {
	case slog.LevelDebug:
		severity = 7
	case slog.LevelInfo:
		severity = 6
	case slog.LevelWarn:
		severity = 4
	case slog.LevelError:
		severity = 3
	default:
		severity = 0
	}
	return
}

// flattenSlogAttrs removes groups from the attrs slice. Nested attributes are
// inlined, with their keys modified to start with the group's key.
func flattenSlogAttrs(attrs []slog.Attr) []slog.Attr {
	hasGroups := false
outer:
	for _, attr := range attrs {
		switch attr.Value.Kind() {
		case slog.KindGroup:
			hasGroups = true
			break outer
		}
	}
	if !hasGroups {
		return attrs
	}

	result := make([]slog.Attr, 0, len(attrs))
	for _, attr := range attrs {
		switch attr.Value.Kind() {
		case slog.KindGroup:
			nested := slices.Clone(attr.Value.Group())
			prefixSlogAttrs(attr.Key, nested)
			nested = flattenSlogAttrs(nested)
			result = append(result, nested...)
		default:
			result = append(result, attr)
		}
	}
	return result
}

// prefixSlogAttrs adds a prefix to every element in the attrs slice.
func prefixSlogAttrs(prefix string, attrs []slog.Attr) {
	if prefix == "" {
		return
	}
	for idx := range attrs {
		attrs[idx].Key = prefix + "." + attrs[idx].Key
	}
}

// serializeSlogAttrs stringifies attributes.
func serializeSlogAttrs(attrs []slog.Attr) [][2]string {
	attrs = flattenSlogAttrs(attrs)
	result := make([][2]string, 0, len(attrs))
	for _, attr := range attrs {
		if attr.Equal(slog.Attr{}) {
			continue
		}
		value := attr.Value.Resolve()
		switch value.Kind() {
		case slog.KindGroup:
			panic("flattened slog.Value must not be a Group")
		case slog.KindLogValuer:
			panic("resolved slog.Value must not be a LogValuer")
		default:
			result = append(result, [2]string{attr.Key, value.String()})
		}
	}
	return result
}

func shallowCopy[T any](val *T) *T {
	copy := new(T)
	*copy = *val
	return copy
}
