package journald

import (
	"context"
	"fmt"
	"iter"
	"strings"
	"sync"
	"time"

	"github.com/coreos/go-systemd/v22/sdjournal"
)

func syslogMatchData(syslogIdentifier string) string {
	builder := strings.Builder{}

	builder.WriteString(sdjournal.SD_JOURNAL_FIELD_SYSLOG_IDENTIFIER)
	builder.WriteString("=")
	builder.WriteString(syslogIdentifier)

	return builder.String()
}

type Journald struct {
	journal *sdjournal.Journal
	lock    sync.Mutex
}

func NewJournald(syslogName string) (*Journald, error) {
	journal, err := sdjournal.NewJournal()
	if err != nil {
		return nil, fmt.Errorf("failed to initialize journal: %w", err)
	}

	if syslogName != "" {
		matchData := syslogMatchData(syslogName)
		err = journal.AddMatch(matchData)
		if err != nil {
			return nil, fmt.Errorf("failed to add match %s to journal: %w", matchData, err)
		}
	}

	err = journal.SeekTail()
	if err != nil {
		return nil, fmt.Errorf("failed to seek to tail: %w", err)
	}

	_, err = journal.Previous()
	if err != nil {
		return nil, fmt.Errorf("failed to go to tail message: %w", err)
	}

	return &Journald{
		journal: journal,
	}, nil
}

func (j *Journald) Close() error {
	err := j.journal.Close()
	if err != nil {
		return fmt.Errorf("failed to close journal: %w", err)
	}

	return nil
}

func (j *Journald) Next() (*sdjournal.JournalEntry, error) {
	j.lock.Lock()
	defer j.lock.Unlock()

	entryCount, err := j.journal.Next()
	if err != nil {
		return nil, fmt.Errorf("failed to get next journald entry: %w", err)
	}

	if entryCount == 0 {
		return nil, nil
	}

	entry, err := j.journal.GetEntry()
	if err != nil {
		return nil, fmt.Errorf("failed to get journald entry: %w", err)
	}

	return entry, nil
}

func (j *Journald) Iter(ctx context.Context) iter.Seq2[*sdjournal.JournalEntry, error] {
	return func(yield func(*sdjournal.JournalEntry, error) bool) {
		for {
			j.journal.Wait(time.Second)
			if ctx.Err() != nil {
				yield(nil, fmt.Errorf("failed to iterate on journald, context error: %w", ctx.Err()))
				return
			}

			entry, err := j.Next()
			if err != nil {
				yield(nil, err)
				return
			}

			// no more entries at the moment
			if entry == nil {
				continue
			}

			// we were told to stop
			if !yield(entry, nil) {
				return
			}
		}
	}
}