package xlog

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"

	"github.com/google/safeopen"
	"go.uber.org/multierr"

	"github.com/benz9527/xboot/lib/infra"
)

var _ io.WriteCloser = (*singleLog)(nil)

// singleLog is not thread-safe.
type singleLog struct {
	ctx         context.Context
	filePath    string
	filename    string
	wroteSize   uint64
	mkdirOnce   sync.Once
	currentFile atomic.Pointer[os.File]
}

func (log *singleLog) Write(p []byte) (n int, err error) {
	select {
	case <-log.ctx.Done():
		return 0, io.EOF
	default:
	}

	if log.currentFile.Load() == nil {
		if err := log.openOrCreate(); err != nil {
			return 0, err
		}
	}
	n, err = log.currentFile.Load().Write(p)
	log.wroteSize += uint64(n)
	return
}

func (log *singleLog) Close() error {
	cur := log.currentFile.Load()
	if cur == nil {
		return nil
	}
	if err := cur.Close(); err != nil {
		return err
	}
	log.currentFile.Store(nil)
	return nil
}

func (log *singleLog) openOrCreate() error {
	if err := log.mkdir(); err != nil {
		return err
	}

	pathToLog := filepath.Join(log.filePath, log.filename)
	info, err := os.Stat(pathToLog)
	if os.IsNotExist(err) {
		var merr error
		merr = multierr.Append(merr, err)
		if err = log.create(); err != nil {
			return multierr.Append(merr, err)
		}
		return nil
	} else if err != nil {
		log.currentFile.Store(nil)
		return infra.WrapErrorStack(err)
	}

	if info.IsDir() {
		log.currentFile.Store(nil)
		return infra.NewErrorStack("log file <" + pathToLog + "> is a dir")
	}

	var f *os.File
	if f, err = safeopen.OpenFileBeneath(log.filePath, log.filename, os.O_WRONLY|os.O_APPEND, 0o644); err != nil {
		return infra.WrapErrorStackWithMessage(err, "failed to open an exists log file: "+pathToLog)
	}
	log.currentFile.Store(f)
	log.wroteSize = uint64(info.Size())
	return nil
}

func (log *singleLog) mkdir() error {
	var err error = nil
	log.mkdirOnce.Do(func() {
		if log.filePath == "" {
			log.filePath = os.TempDir()
		}
		if log.filePath == os.TempDir() {
			return
		}
		err = os.MkdirAll(log.filePath, 0o644)
	})
	return infra.WrapErrorStack(err)
}

func (log *singleLog) create() error {
	if err := log.mkdir(); err != nil {
		return err
	}

	f, err := safeopen.OpenFileBeneath(log.filePath, log.filename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o644)
	if err != nil {
		return infra.WrapErrorStackWithMessage(err, "unable to create new log file: "+filepath.Join(log.filePath, log.filename))
	}
	log.currentFile.Store(f)
	log.wroteSize = 0
	return nil
}

func (log *singleLog) initialize() error {
	go func() {
		select {
		case <-log.ctx.Done():
			_ = log.Close()
			return
		}
	}()
	return nil
}

func SingleLog(ctx context.Context, cfg *FileCoreConfig) io.WriteCloser {
	if cfg == nil || ctx == nil {
		return nil
	}
	log := &singleLog{
		ctx:      ctx,
		filePath: cfg.FilePath,
		filename: cfg.Filename,
	}
	_ = log.initialize()
	return log
}
