//go:build linux

package journald

import (
	"errors"
	"fmt"
	"maps"
	"strconv"
	"sync/atomic"
	"time"
	"unicode"

	"github.com/coreos/go-systemd/v22/journal"

	"github.com/moby/moby/v2/daemon/internal/stringid"
	"github.com/moby/moby/v2/daemon/logger"
	"github.com/moby/moby/v2/daemon/logger/loggerutils"
)

const name = "journald"

// Well-known user journal fields.
// https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html
const (
	fieldSyslogIdentifier = "SYSLOG_IDENTIFIER"
	fieldSyslogTimestamp  = "SYSLOG_TIMESTAMP"
)

// User journal fields used by the log driver.
const (
	fieldContainerID     = "CONTAINER_ID"
	fieldContainerIDFull = "CONTAINER_ID_FULL"
	fieldContainerName   = "CONTAINER_NAME"
	fieldContainerTag    = "CONTAINER_TAG"
	fieldImageName       = "IMAGE_NAME"

	// Fields used to serialize PLogMetaData.

	fieldPLogID         = "CONTAINER_PARTIAL_ID"
	fieldPLogOrdinal    = "CONTAINER_PARTIAL_ORDINAL"
	fieldPLogLast       = "CONTAINER_PARTIAL_LAST"
	fieldPartialMessage = "CONTAINER_PARTIAL_MESSAGE"

	fieldLogEpoch   = "CONTAINER_LOG_EPOCH"
	fieldLogOrdinal = "CONTAINER_LOG_ORDINAL"
)

// waitUntilFlushed is set if read support is enabled and a no-op otherwise.
var waitUntilFlushed func(*journald) error

type journald struct {
	// Epoch identifier to distinguish sequence numbers from this instance
	// vs. other instances.
	epoch string
	// Sequence number of the most recent message sent by this instance of
	// the log driver, starting from 1. Corollary: ordinal == 0 implies no
	// messages have been sent by this instance.
	ordinal atomic.Uint64

	vars map[string]string // additional variables and values to send to the journal along with the log message

	closed chan struct{}

	// Overrides for unit tests.

	sendToJournal   func(message string, priority journal.Priority, vars map[string]string) error
	journalReadDir  string        //nolint:unused // Referenced in read.go, which has more restrictive build constraints.
	readSyncTimeout time.Duration //nolint:unused // Referenced in read.go, which has more restrictive build constraints.
}

// sanitizeKeyMod returns the sanitized string so that it could be used in journald.
// In journald log, there are special requirements for fields.
// Fields must be composed of uppercase letters, numbers, and underscores, but must
// not start with an underscore.
func sanitizeKeyMod(s string) string {
	n := ""
	for _, v := range s {
		if 'a' <= v && v <= 'z' {
			v = unicode.ToUpper(v)
		} else if ('Z' < v || v < 'A') && ('9' < v || v < '0') {
			v = '_'
		}
		if n == "" && v == '_' {
			// skip leading underscores
			continue
		}
		n += string(v)
	}
	return n
}

// New creates a journald logger using the configuration passed in on
// the context.
func New(info logger.Info) (logger.Logger, error) {
	if !journal.Enabled() {
		return nil, errors.New("journald is not enabled on this host")
	}

	return newJournald(info)
}

func newJournald(info logger.Info) (*journald, error) {
	// parse log tag
	tag, err := loggerutils.ParseLogTag(info, loggerutils.DefaultTemplate)
	if err != nil {
		return nil, err
	}

	epoch := stringid.GenerateRandomID()

	vars := map[string]string{
		fieldContainerID:      info.ContainerID[:12],
		fieldContainerIDFull:  info.ContainerID,
		fieldContainerName:    info.Name(),
		fieldContainerTag:     tag,
		fieldImageName:        info.ImageName(),
		fieldSyslogIdentifier: tag,
		fieldLogEpoch:         epoch,
	}
	extraAttrs, err := info.ExtraAttributes(sanitizeKeyMod)
	if err != nil {
		return nil, err
	}
	maps.Copy(vars, extraAttrs)
	return &journald{
		epoch:         epoch,
		vars:          vars,
		closed:        make(chan struct{}),
		sendToJournal: journal.Send,
	}, nil
}

func validateLogOpt(cfg map[string]string) error {
	for key := range cfg {
		switch key {
		case logger.AttrEnv, logger.AttrEnvRegex, logger.AttrLabels, logger.AttrLabelsRegex, logger.AttrLogTag:
			// Common attributes handled through [logger.Info.ExtraAttributes] and [loggerutils.ParseLogTag].
			continue
		default:
			return fmt.Errorf("unknown log opt '%s' for journald log driver", key)
		}
	}
	return nil
}

func (s *journald) Log(msg *logger.Message) (err error) {
	defer func() {
		if err == nil {
			logger.PutMessage(msg)
		}
	}()

	vars := map[string]string{}
	maps.Copy(vars, s.vars)
	if !msg.Timestamp.IsZero() {
		vars[fieldSyslogTimestamp] = msg.Timestamp.Format(time.RFC3339Nano)
	}
	if msg.PLogMetaData != nil {
		vars[fieldPLogID] = msg.PLogMetaData.ID
		vars[fieldPLogOrdinal] = strconv.Itoa(msg.PLogMetaData.Ordinal)
		vars[fieldPLogLast] = strconv.FormatBool(msg.PLogMetaData.Last)
		if !msg.PLogMetaData.Last {
			vars[fieldPartialMessage] = "true"
		}
	}

	seq := s.ordinal.Add(1)
	vars[fieldLogOrdinal] = strconv.FormatUint(seq, 10)

	if msg.Source == "stderr" {
		return s.sendToJournal(string(msg.Line), journal.PriErr, vars)
	}
	return s.sendToJournal(string(msg.Line), journal.PriInfo, vars)
}

func (s *journald) Name() string {
	return name
}

func (s *journald) Close() error {
	close(s.closed)
	if waitUntilFlushed != nil {
		return waitUntilFlushed(s)
	}
	return nil
}
