package internal

import (
	"compress/gzip"
	"context"
	"fmt"
	"github.com/gogf/gf/v2/util/gconv"
	"io"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"
)

// Cutter 实现 io.Writer 接口
// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator)
type Cutter struct {
	level        string   // 日志级别(debug, info, warn, error, dpanic, panic, fatal)
	layout       string   // 时间格式 2006-01-02 15:04:05
	formats      []string // 自定义参数([]string{Director,time.DateOnly, "business"(此参数可不写), level+".log"}
	director     string   // 日志文件夹
	retentionDay int      // 日志保留天数
	// 新增以下字段
	maxSize       int64         // 单文件最大大小(字节)
	compress      bool          // 是否压缩旧文件
	currentSize   int64         // 当前文件已写入大小
	fileIndex     int           // 同级别下的文件序号(用于大小切割)
	lastLayout    string        // 上一次的时间字符串(判断是否跨天)
	file          *os.File      // 文件句柄
	mutex         *sync.RWMutex // 读写锁
	compressLevel int
	// 新增：定时任务相关
	checkInterval time.Duration // 检查间隔（转换后的时间）
	ticker        *time.Ticker  // 定时触发器
	ctx           context.Context
	cancel        context.CancelFunc // 用于停止定时任务

}

type CutterOption func(*Cutter)

// 新增选项：设置单文件最大大小(MB转换为字节)
func CutterWithMaxSize(sizeStr string) CutterOption {
	return func(c *Cutter) {
		if sizeStr == "" {
			c.maxSize = 0 // 不限制大小
			return
		}
		// 解析字符串为字节数
		bytes, err := parseSize(sizeStr)
		if err != nil {
			// 解析失败时，默认不限制大小，并打印警告
			fmt.Printf("日志大小配置解析失败: %v，将不限制文件大小\n", err)
			c.maxSize = 0
			return
		}
		c.maxSize = bytes
	}
}

// 新增选项：设置定时检查间隔
func CutterWithCheckInterval(intervalStr string) CutterOption {
	return func(c *Cutter) {
		interval, err := parseDuration(intervalStr)
		if err != nil {
			fmt.Printf("时间间隔解析失败: %v，使用默认30秒\n", err)
			interval = 30 * time.Second
		}
		c.checkInterval = interval
	}
}

// 初始化Cutter时启动定时任务
func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter {
	ctx, cancel := context.WithCancel(context.Background())
	rotate := &Cutter{
		level:         level,
		director:      director,
		retentionDay:  retentionDay,
		mutex:         new(sync.RWMutex),
		ctx:           ctx,
		cancel:        cancel,
		checkInterval: 30 * time.Second, // 默认30秒
	}
	for _, opt := range options {
		opt(rotate)
	}

	// 启动定时检查任务
	rotate.startTicker()

	// 关键：启动时扫描历史文件，获取最大序号，设置初始fileIndex
	rotate.lastLayout = time.Now().Format(rotate.layout) // 先初始化当前时间布局
	rotate.fileIndex = rotate.getMaxFileIndex()          // 从最大序号开始
	return rotate
}

// 启动定时检查的ticker
func (c *Cutter) startTicker() {
	c.ticker = time.NewTicker(c.checkInterval)
	go func() {
		for {
			select {
			case <-c.ctx.Done():
				c.ticker.Stop()
				return
			case <-c.ticker.C:

				c.checkAndRotate() // 定时执行检查和切割

			}
		}
	}()
}

// 定时检查是否需要切割和压缩
func (c *Cutter) checkAndRotate() {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	// 定时删除过期文件
	defer removeNDaysFolders(c.director, c.retentionDay)

	// 1. 检查是否跨天（时间切割）
	currentLayout := time.Now().Format(c.layout)
	if currentLayout != c.lastLayout {
		c.rotateFile(currentLayout) // 跨天切割
		return
	}

	// 2. 检查文件大小是否超限（大小切割）
	if c.maxSize > 0 && c.currentSize > c.maxSize {
		c.rotateFile(currentLayout) // 大小超限切割
	}
}

// 执行切割逻辑（创建新文件，压缩旧文件）
func (c *Cutter) rotateFile(currentLayout string) {
	// 关闭当前文件（如果存在）
	if c.file != nil {
		_ = c.file.Close()
		c.file = nil
	}

	// 压缩旧文件（如果开启压缩）
	oldFilename := c.getFilename(c.lastLayout, c.fileIndex)

	if c.currentSize > 0 && c.compress { // 避免压缩刚创建的文件
		// 异步压缩，不阻塞定时任务
		go c.compressFile(oldFilename)
	}

	// 更新状态，准备新文件
	// 更新状态：跨天切割重置序号，大小切割累加序号
	if currentLayout != c.lastLayout {
		// 跨天：序号从0开始
		c.fileIndex = 0
	} else {
		// 大小超限：序号+1
		c.fileIndex++
	}
	c.lastLayout = currentLayout
	if c.currentSize == 0 {
		return
	}
	c.currentSize = 0
	// 预创建新文件（可选，避免首次写入时创建的开销）
	newFilename := c.getFilename(currentLayout, c.fileIndex)
	if err := os.MkdirAll(filepath.Dir(newFilename), os.ModePerm); err != nil {
		fmt.Printf("创建新文件目录失败: %v\n", err)
		return
	}
	// 打开新文件（追加模式，确保文件存在）
	if f, err := os.OpenFile(newFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err == nil {
		c.file = f
	} else {
		fmt.Printf("预创建新文件失败: %v\n", err)
	}
}

// 新增选项：设置是否压缩旧文件
func CutterWithCompress(compress bool) CutterOption {
	return func(c *Cutter) {
		c.compress = compress
	}
}

// CutterWithLayout 时间格式
func CutterWithLayout(layout string) CutterOption {
	return func(c *Cutter) {
		c.layout = layout
	}
}

// CutterWithFormats 格式化参数
func CutterWithFormats(format ...string) CutterOption {
	return func(c *Cutter) {
		if len(format) > 0 {
			c.formats = format
		}
	}
}

// 新增选项：设置压缩等级
func CutterWithCompressLevel(level int) CutterOption {
	return func(c *Cutter) {
		// 验证等级有效性（1-9），无效则用默认值6
		if level < 1 || level > 9 {
			//fmt.Printf("无效的压缩等级 %d（范围1-9），使用默认等级6\n", level)
			c.compressLevel = 6
			return
		}
		c.compressLevel = level
	}
}

// 辅助函数：扫描当前日期目录下的日志文件，返回最大序号
func (c *Cutter) getMaxFileIndex() int {
	// 1. 构建当前日期的目录路径（与日志写入的目录一致）
	currentLayout := time.Now().Format(c.layout)
	dirParts := []string{c.director}
	if currentLayout != "" {
		dirParts = append(dirParts, currentLayout)
	}
	dirParts = append(dirParts, c.formats...)
	currentDir := filepath.Join(dirParts...)

	// 2. 匹配文件名格式：级别.序号.log 或 级别.序号.log.gz（如 info.3.log、error.2.log.gz）
	// 正则表达式：匹配 "级别.数字.log" 或 "级别.数字.log.gz"
	pattern := filepath.Join(currentDir, fmt.Sprintf("%s.*.log", c.level))
	files, err := filepath.Glob(pattern)
	if err != nil || len(files) == 0 {
		return 0 // 目录不存在或无文件，从0开始
	}
	maxIndex := 0
	isMaxFileGz := false // 标记最大序号文件是否为.gz
	re := regexp.MustCompile(fmt.Sprintf(`%s.(\d+).log`, c.level))

	for _, file := range files {
		filename := filepath.Base(file)
		matches := re.FindStringSubmatch(filename)
		if len(matches) < 2 {
			continue
		}

		index := gconv.Int(matches[1])

		// 关键：更新最大序号时，同步记录该文件是否为.gz
		if index > maxIndex {
			maxIndex = index
			// 判断文件是否以.gz结尾（注意：用完整路径判断，避免文件名包含.gz但不是后缀）
			isMaxFileGz = strings.HasSuffix(file, ".gz")
		} else if index == maxIndex {
			// 极端情况：同一序号存在.log和.log.gz（如info.2.log和info.2.log.gz）
			// 优先以「非.gz」为准（说明是最新的未压缩文件）
			if !strings.HasSuffix(file, ".gz") {
				isMaxFileGz = false
			}
		}
	}
	if isMaxFileGz {
		return maxIndex + 1
	}
	return maxIndex
}

// Write satisfies the io.Writer interface. It writes to the
// appropriate file handle that is currently being used.
// If we have reached rotation time, the target file gets
// automatically rotated, and also purged if necessary.
func (c *Cutter) Write(bytes []byte) (n int, err error) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	// 确保文件句柄有效（如果未打开则创建）

	// 新增：跨天轻量检查（仅在写入时判断，发现跨天立即切割）
	currentLayout := time.Now().Format(c.layout)
	if currentLayout != c.lastLayout {
		// 调用切割逻辑，切换到新日期目录
		// 切割后重新生成新文件名（此时c.lastLayout已更新）
		c.rotateFile(currentLayout)
	}
	filename := c.getFilename(currentLayout, c.fileIndex)
	if c.file == nil {
		if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
			return 0, err
		}
		f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
		if err != nil {
			return 0, err
		}
		c.file = f
		// 初始化文件大小（如果是新打开的文件）
		if c.currentSize == 0 {
			if fi, err := os.Stat(filename); err == nil {
				c.currentSize = fi.Size()
			}
		}
	}

	// 写入日志并更新大小
	n, err = c.file.Write(bytes)
	if err == nil {
		c.currentSize += int64(n)
	}
	return n, err
}

// 辅助函数：生成带序号的文件名
func (c *Cutter) getFilename(layout string, index int) string {
	values := []string{c.director}
	if layout != "" {
		values = append(values, layout)
	}
	values = append(values, c.formats...)
	// 文件名格式：级别.序号.log（如 error.0.log）
	values = append(values, fmt.Sprintf("%s.%d.log", c.level, index))
	return filepath.Join(values...)
}

func (c *Cutter) Sync() error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if c.file != nil {
		return c.file.Sync()
	}
	return nil
}

// 压缩文件为.gz（应用指定压缩等级）
func (c *Cutter) compressFile(src string) error { // 改为成员方法，方便使用c.compressLevel

	if _, err := os.Stat(src); os.IsNotExist(err) {
		return nil // 原文件不存在，无需压缩
	}
	// 打开原文件
	srcFile, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("打开原文件失败: %w", err)
	}
	defer srcFile.Close()

	// 创建压缩文件（如 info.0.log.gz）
	dst := src + ".gz"
	dstFile, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("创建压缩文件失败: %w", err)
	}
	defer dstFile.Close()

	// 关键：使用指定压缩等级创建gzip写入器
	gzWriter, err := gzip.NewWriterLevel(dstFile, c.compressLevel)
	if err != nil {
		return fmt.Errorf("初始化gzip写入器失败: %w", err)
	}
	defer gzWriter.Close()

	// 执行压缩（将原文件内容写入gzip）
	if _, err := io.Copy(gzWriter, srcFile); err != nil {
		return fmt.Errorf("压缩数据失败: %w", err)
	}

	// 压缩成功后删除原文件
	if err := os.Remove(src); err != nil {
		return fmt.Errorf("删除原文件失败: %w", err)
	}

	return nil
}

// 增加日志目录文件清理 小于等于零的值默认忽略不再处理
func removeNDaysFolders(dir string, days int) error {
	if days <= 0 {
		return nil
	}
	cutoff := time.Now().AddDate(0, 0, -days)
	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() && info.ModTime().Before(cutoff) && path != dir {
			err = os.RemoveAll(path)
			if err != nil {
				return err
			}
		}
		return nil
	})
}
