package starlog

import (
	"errors"
	"os"
	"path/filepath"
	"time"

	"b612.me/staros"

	"b612.me/starmap"
)

var archMap starmap.StarMapKV

func init() {
	archMap = starmap.NewStarMap()
}

type Archive interface {
	ShouldArchiveNow(string, os.FileInfo) bool
	NextLogFilePath(string, os.FileInfo) string
	Interval() int64
	HookBeforArchive() func(string, os.FileInfo) error
	HookAfterArchive() func(string, string, os.FileInfo) error
}

type logfileinfo struct {
	fullpath string
	pointer  *os.File
}

func SetLogFile(path string, logger *StarLogger, appendMode bool) error {
	var fileMode int
	if appendMode {
		fileMode = os.O_APPEND | os.O_CREATE | os.O_WRONLY
	} else {
		fileMode = os.O_CREATE | os.O_WRONLY | os.O_TRUNC
	}
	fullpath, err := filepath.Abs(path)
	if err != nil {
		return err
	}
	if !appendMode && staros.Exists(fullpath) {
		os.Remove(fullpath)
	}
	fp, err := os.OpenFile(fullpath, fileMode, 0644)
	if err != nil {
		return err
	}
	if archMap.MustGet(logger.logcore.id) != nil {
		logger.SetSwitching(true)
		err := archMap.MustGet(logger.logcore.id).(logfileinfo).pointer.Close()
		if err != nil {
			logger.logcore.output = nil
			logger.SetSwitching(false)
			return err
		}
		err = archMap.Delete(logger.logcore.id)
		if err != nil {
			logger.logcore.output = nil
			logger.SetSwitching(false)
			return err
		}
	}

	err = archMap.Store(logger.logcore.id, logfileinfo{
		fullpath: fullpath,
		pointer:  fp,
	})
	if err != nil {
		fp.Close()
		logger.logcore.output = nil
		logger.SetSwitching(false)
		return err
	}
	logger.SetSwitching(true)
	logger.logcore.output = fp
	logger.SetSwitching(false)
	return nil
}

func CloseWithSwitching(logger *StarLogger) error {
	if archMap.MustGet(logger.logcore.id) != nil {
		logger.SetSwitching(true)
		err := archMap.MustGet(logger.logcore.id).(logfileinfo).pointer.Close()
		if err != nil {
			logger.logcore.output = nil
			return err
		}
		err = archMap.Delete(logger.logcore.id)
		if err != nil {
			return err
		}
	}
	return nil
}

func Close(logger *StarLogger) error {
	defer logger.SetSwitching(false)
	return CloseWithSwitching(logger)
}

func GetLogFileInfo(logger *StarLogger) (os.FileInfo, error) {
	if archMap.MustGet(logger.logcore.id) != nil {
		return archMap.MustGet(logger.logcore.id).(logfileinfo).pointer.Stat()
	}
	return nil, errors.New("logger don't have a register logfile")
}

func StartArchive(logger *StarLogger, arch Archive) error {
	if archMap.MustGet("arch"+logger.logcore.id) != nil {
		return errors.New("already running")
	}
	stopChan := make(chan int)
	archMap.Store("arch"+logger.logcore.id, stopChan)
	go func(stopChan chan int, arch Archive, logger *StarLogger) {
		for {
			select {
			case <-stopChan:
				return
			case <-time.After(time.Second * time.Duration(arch.Interval())):
			}
			fileinfo, err := GetLogFileInfo(logger)
			if err != nil {
				logger.Errorf("cannot get log file info,reason is %v\n", err)
				continue
			}
			if archMap.MustGet(logger.logcore.id) == nil {
				logger.Errorf("cannot get log core info from the map:no such keys\n")
				continue
			}
			fullpath := archMap.MustGet(logger.logcore.id).(logfileinfo).fullpath
			if !arch.ShouldArchiveNow(fullpath, fileinfo) {
				continue
			}
			newLogPath := arch.NextLogFilePath(fullpath, fileinfo)
			if arch.HookBeforArchive() != nil {
				if err := arch.HookBeforArchive()(fullpath, fileinfo); err != nil {
					logger.Errorf("error occur while executing hook before archive,detail is %v\n", err)
					continue
				}
			}
			if err := SetLogFile(newLogPath, logger, false); err != nil {
				logger.Errorf("error occur while executing coverting new log file,detail is %v\n", err)
				continue
			} else {
				logger.Debugln("Set Log Success")
			}
			fileinfo, err = GetLogFileInfo(logger)
			if err != nil {
				logger.Errorf("cannot get new log core info from the map:no such keys\n")
				continue
			}
			if arch.HookAfterArchive() != nil {
				if err := arch.HookAfterArchive()(fullpath, newLogPath, fileinfo); err != nil {
					logger.Errorf("error occur while executing hook after archive,detail is %v\n", err)
					continue
				}
			}
		}
	}(stopChan, arch, logger)
	return nil
}

func IsArchiveRun(logger *StarLogger) bool {
	if archMap.MustGet("arch"+logger.logcore.id) == nil {
		return false
	}
	return true
}

func StopArchive(logger *StarLogger) {
	if archMap.MustGet("arch"+logger.logcore.id) == nil {
		return
	}
	archMap.MustGet("arch" + logger.logcore.id).(chan int) <- 1
}

type ArchiveByDate struct {
	interval         int64
	checkInterval    int64
	newFileNameStyle string
	hookBefor        func(string, os.FileInfo) error
	hookAfter        func(string, string, os.FileInfo) error
}

func (abd *ArchiveByDate) ShouldArchiveNow(fullpath string, info os.FileInfo) bool {
	if time.Now().Unix()-staros.GetFileCreationTime(info).Unix() > abd.interval {
		return true
	}
	return false
}

func (abd *ArchiveByDate) NextLogFilePath(oldpath string, info os.FileInfo) string {
	dir := filepath.Dir(oldpath)
	newName := time.Now().Format(abd.newFileNameStyle)
	return filepath.Join(dir, newName)
}

func (abd *ArchiveByDate) Interval() int64 {
	return abd.checkInterval
}

func (abd *ArchiveByDate) HookBeforArchive() func(string, os.FileInfo) error {

	return abd.hookBefor
}

func (abd *ArchiveByDate) HookAfterArchive() func(string, string, os.FileInfo) error {
	return abd.hookAfter
}

func (abd *ArchiveByDate) SetHookBeforArchive(f func(string, os.FileInfo) error) {

	abd.hookBefor = f
}

func (abd *ArchiveByDate) SetHookAfterArchive(f func(string, string, os.FileInfo) error) {
	abd.hookAfter = f
}

func NewArchiveByDate(archInterval int64, checkInterval int64, fileStyle string, hookbefor func(string, os.FileInfo) error, hookafter func(string, string, os.FileInfo) error) *ArchiveByDate {
	return &ArchiveByDate{
		interval:         archInterval,
		checkInterval:    checkInterval,
		newFileNameStyle: fileStyle,
		hookBefor:        hookbefor,
		hookAfter:        hookafter,
	}
}

type ArchiveBySize struct {
	size             int64
	checkInterval    int64
	newFileNameStyle string
	hookBefor        func(string, os.FileInfo) error
	hookAfter        func(string, string, os.FileInfo) error
}

func (abd *ArchiveBySize) ShouldArchiveNow(fullpath string, info os.FileInfo) bool {
	if info.Size() > abd.size {
		return true
	}
	return false
}

func (abd *ArchiveBySize) NextLogFilePath(oldpath string, info os.FileInfo) string {
	dir := filepath.Dir(oldpath)
	newName := time.Now().Format(abd.newFileNameStyle)
	return filepath.Join(dir, newName)
}

func (abd *ArchiveBySize) Interval() int64 {
	return abd.checkInterval
}

func (abd *ArchiveBySize) HookBeforArchive() func(string, os.FileInfo) error {

	return abd.hookBefor
}

func (abd *ArchiveBySize) HookAfterArchive() func(string, string, os.FileInfo) error {
	return abd.hookAfter
}

func (abd *ArchiveBySize) SetHookBeforArchive(f func(string, os.FileInfo) error) {

	abd.hookBefor = f
}

func (abd *ArchiveBySize) SetHookAfterArchive(f func(string, string, os.FileInfo) error) {
	abd.hookAfter = f
}

func NewArchiveBySize(size int64, checkInterval int64, fileStyle string, hookbefor func(string, os.FileInfo) error, hookafter func(string, string, os.FileInfo) error) *ArchiveBySize {
	return &ArchiveBySize{
		size:             size,
		checkInterval:    checkInterval,
		newFileNameStyle: fileStyle,
		hookBefor:        hookbefor,
		hookAfter:        hookafter,
	}
}
