package journal

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"time"

	logging "github.com/ipfs/go-log"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/lotus/build"
)

func InitializeSystemJournal(dir string) error {
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}
	j, err := OpenFSJournal(dir)
	if err != nil {
		return err
	}
	currentJournal = j
	return nil
}

func Add(sys string, val interface{}) {
	if currentJournal == nil {
		log.Warn("no journal configured")
		return
	}
	currentJournal.AddEntry(sys, val)
}

var log = logging.Logger("journal")

var currentJournal Journal

type Journal interface {
	AddEntry(system string, obj interface{})
	Close() error
}

// fsJournal is a basic journal backed by files on a filesystem
type fsJournal struct {
	fi    *os.File
	fSize int64

	journalDir string

	incoming         chan *JournalEntry
	journalSizeLimit int64

	closing chan struct{}
}

func OpenFSJournal(dir string) (Journal, error) {
	fsj := &fsJournal{
		journalDir:       dir,
		incoming:         make(chan *JournalEntry, 32),
		journalSizeLimit: 1 << 30,
		closing:          make(chan struct{}),
	}

	if err := fsj.rollJournalFile(); err != nil {
		return nil, err
	}

	go fsj.runLoop()

	return fsj, nil
}

type JournalEntry struct {
	System    string
	Timestamp time.Time
	Val       interface{}
}

func (fsj *fsJournal) putEntry(je *JournalEntry) error {
	b, err := json.Marshal(je)
	if err != nil {
		return err
	}
	n, err := fsj.fi.Write(append(b, '\n'))
	if err != nil {
		return err
	}

	fsj.fSize += int64(n)

	if fsj.fSize >= fsj.journalSizeLimit {
		return fsj.rollJournalFile()
	}

	return nil
}

const RFC3339nocolon = "2006-01-02T150405Z0700"

func (fsj *fsJournal) rollJournalFile() error {
	if fsj.fi != nil {
		err := fsj.fi.Close()
		if err != nil {
			return err
		}
	}

	nfi, err := os.Create(filepath.Join(fsj.journalDir, fmt.Sprintf("lotus-journal-%s.ndjson", build.Clock.Now().Format(RFC3339nocolon))))
	if err != nil {
		return xerrors.Errorf("failed to open journal file: %w", err)
	}

	fsj.fi = nfi
	fsj.fSize = 0
	return nil
}

func (fsj *fsJournal) runLoop() {
	for {
		select {
		case je := <-fsj.incoming:
			if err := fsj.putEntry(je); err != nil {
				log.Errorw("failed to write out journal entry", "entry", je, "err", err)
			}
		case <-fsj.closing:
			if err := fsj.fi.Close(); err != nil {
				log.Errorw("failed to close journal", "err", err)
			}
			return
		}
	}
}

func (fsj *fsJournal) AddEntry(system string, obj interface{}) {
	je := &JournalEntry{
		System:    system,
		Timestamp: build.Clock.Now(),
		Val:       obj,
	}
	select {
	case fsj.incoming <- je:
	case <-fsj.closing:
		log.Warnw("journal closed but tried to log event", "entry", je)
	}
}

func (fsj *fsJournal) Close() error {
	close(fsj.closing)
	return nil
}