package galog

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

type fileLogWriter struct {
	sync.RWMutex

	Rotate bool `json:"rotate"`
	Hourly bool `json:"hourly"`

	// The opened file
	Filename   string `json:"filename"`
	fileWriter *os.File

	// Rotate hourly
	MaxHours       int64 `json:"maxhours"` // 默认不删除日志
	hourlyOpenDate int
	hourlyOpenTime time.Time

	// Permissions for log file
	Perm string `json:"perm"`
	// Permissions for directory if it is specified in FileName
	DirPerm string `json:"dirperm"`

	RotatePerm string `json:"rotateperm"`

	fileNameOnly, suffix string // like "project.log", project is fileNameOnly and .log is suffix
}

func newFileWriter() LogProvider {
	w := &fileLogWriter{
		Rotate:     true, // 开启日志轮转
		Hourly:     true, // 开启日志小时轮转
		RotatePerm: "0440",
		Perm:       "0660",
		DirPerm:    "0770",
	}
	return w
}

// Init file logger with json config.
// jsonConfig like:
//
//	{"filename":"logs/glog.log"}
func (w *fileLogWriter) Init(config string) error {
	err := json.Unmarshal([]byte(config), w)
	if err != nil {
		return err
	}
	if w.Filename == "" {
		return errors.New("jsonconfig must have filename")
	}
	w.suffix = filepath.Ext(w.Filename)
	w.fileNameOnly = strings.TrimSuffix(w.Filename, w.suffix)
	if w.suffix == "" {
		w.suffix = ".log"
	}

	err = w.startLogger()
	return err
}

func (w *fileLogWriter) startLogger() error {
	file, err := w.createLogFile()
	if err != nil {
		return err
	}
	if w.fileWriter != nil {
		w.fileWriter.Close()
	}
	w.fileWriter = file
	return w.initFd()
}

func (w *fileLogWriter) needRotateHourly(hour int) bool {
	return w.Hourly && hour != w.hourlyOpenDate
}

func (w *fileLogWriter) createLogFile() (*os.File, error) {
	perm, err := strconv.ParseInt(w.Perm, 8, 64)
	if err != nil {
		return nil, err
	}

	dirperm, err := strconv.ParseInt(w.DirPerm, 8, 64)
	if err != nil {
		return nil, err
	}

	filepath := path.Dir(w.Filename)
	os.MkdirAll(filepath, os.FileMode(dirperm))

	fd, err := os.OpenFile(w.Filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(perm))
	if err == nil {
		os.Chmod(w.Filename, os.FileMode(perm))
	}
	return fd, err
}

func (w *fileLogWriter) initFd() error {
	w.hourlyOpenTime = time.Now()
	w.hourlyOpenDate = w.hourlyOpenTime.Hour()
	if w.Hourly {
		go w.hourlyRotate(w.hourlyOpenTime)
	}
	return nil
}

func (w *fileLogWriter) hourlyRotate(openTime time.Time) {
	y, m, d := openTime.Add(1 * time.Hour).Date()
	h := openTime.Add(1 * time.Hour).Hour()
	nextHour := time.Date(y, m, d, h, 0, 0, 0, openTime.Location())
	tm := time.NewTimer(time.Duration(nextHour.UnixNano() - openTime.UnixNano() + 100))
	<-tm.C
	w.Lock()
	if w.needRotateHourly(time.Now().Hour()) {
		if err := w.doRotate(); err != nil {
			fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
		}
	}
	w.Unlock()
}

func (w *fileLogWriter) doRotate() error {
	fName := ""
	format := ""
	var openTime time.Time
	rotatePerm, err := strconv.ParseInt(w.RotatePerm, 8, 64)
	if err != nil {
		return err
	}

	_, err = os.Lstat(w.Filename)
	if err != nil {
		goto RESTART_LOGGER
	}

	if w.Hourly {
		format = "20060102_15"
		openTime = w.hourlyOpenTime
	}

	fName = w.fileNameOnly + fmt.Sprintf("_%s%s", openTime.Format(format), w.suffix)
	_, err = os.Lstat(fName)

	// return error if the last file checked still existed
	if err == nil {
		//return fmt.Errorf("rotate: cannot find free log number to rename %s", w.Filename)
	}

	w.fileWriter.Close()

	// Rename the file to its new found name
	// even if occurs error,we MUST guarantee to restart new logger
	err = os.Rename(w.Filename, fName)
	if err != nil {
		goto RESTART_LOGGER
	}

	err = os.Chmod(fName, os.FileMode(rotatePerm))

RESTART_LOGGER:

	startLoggerErr := w.startLogger()
	if w.MaxHours > 0 {
		go w.deleteOldLog()
	}

	if startLoggerErr != nil {
		return fmt.Errorf("rotate startLogger: %s", startLoggerErr)
	}
	if err != nil {
		return fmt.Errorf("rotate: %s", err)
	}
	return nil
}

func (w *fileLogWriter) deleteOldLog() {
	dir := filepath.Dir(w.Filename)
	absolutePath, err := filepath.EvalSymlinks(w.Filename)
	if err == nil {
		dir = filepath.Dir(absolutePath)
	}
	filepath.Walk(dir, func(path string, info os.FileInfo, err error) (returnErr error) {
		defer func() {
			if r := recover(); r != nil {
				fmt.Fprintf(os.Stderr, "Unable to delete old log '%s', error: %v\n", path, r)
			}
		}()

		if info == nil {
			return
		}
		if w.Hourly {
			if !info.IsDir() && info.ModTime().Add(1*time.Hour*time.Duration(w.MaxHours)).Before(time.Now()) {
				if strings.HasPrefix(filepath.Base(path), filepath.Base(w.fileNameOnly)) &&
					strings.HasSuffix(filepath.Base(path), w.suffix) {
					os.Remove(path)
				}
			}
		}
		return
	})
}

// EncodeMsg encode log msg
func (*fileLogWriter) EncodeMsg(lm *LogMsg) []byte {
	buf := defaultBufPool.Get()
	buf.AppendString(lm.Msg)
	buf.AppendString(_defaultLineEnding)
	msg := buf.Bytes()
	buf.Free()
	return msg
}

// WriteMsg write msg to log and rotate
func (w *fileLogWriter) WriteMsg(lm *LogMsg) error {
	h := lm.When.Hour()

	if w.Rotate {
		w.RLock()
		if w.needRotateHourly(h) {
			w.RUnlock()
			w.Lock()
			if w.needRotateHourly(h) {
				if err := w.doRotate(); err != nil {
					fmt.Fprintf(os.Stderr, "FileLogWriter(%q): %s\n", w.Filename, err)
				}
			}
			w.Unlock()
		} else {
			w.RUnlock()
		}
	}

	msg := w.EncodeMsg(lm)
	_, err := w.fileWriter.Write(msg)
	return err
}

// Destroy close the file description, close file writer.
func (w *fileLogWriter) Destroy() {
	w.fileWriter.Close()
}

// Flush flushes file logger.
func (w *fileLogWriter) Flush() {
	w.fileWriter.Sync()
}

func init() {
	Register(AdapterFile, newFileWriter)
}
