package logging

import (
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/internal/config"
)

type rotatingFileConfig struct {
	path        string
	maxBytes    int64
	maxDays     int
	maxBackups  int
	compression string
}

// rotatingFile performs rotate, optional compression, and retention
// synchronously. Its caller serializes Write and Close, so no post-processing
// goroutine can outlive Pipeline.Close.
type rotatingFile struct {
	cfg  rotatingFileConfig
	file *os.File
	size int64
	now  func() time.Time
}

func newRotatingFile(cfg rotatingFileConfig) (*rotatingFile, error) {
	writer := &rotatingFile{cfg: cfg, now: time.Now}
	if err := writer.open(); err != nil {
		return nil, err
	}
	if err := writer.prune(); err != nil {
		_ = writer.file.Close()
		return nil, err
	}
	return writer, nil
}

func (writer *rotatingFile) Write(data []byte) (int, error) {
	if writer.file == nil {
		return 0, os.ErrClosed
	}
	if writer.size > 0 && writer.size+int64(len(data)) > writer.cfg.maxBytes {
		if err := writer.rotate(); err != nil {
			return 0, err
		}
	}
	written, err := writer.file.Write(data)
	writer.size += int64(written)
	return written, err
}

func (writer *rotatingFile) Close() error {
	if writer.file == nil {
		return nil
	}
	err := writer.file.Close()
	writer.file = nil
	return err
}

func (writer *rotatingFile) open() error {
	file, err := os.OpenFile(writer.cfg.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
	if err != nil {
		return err
	}
	info, err := file.Stat()
	if err != nil {
		_ = file.Close()
		return err
	}
	writer.file = file
	writer.size = info.Size()
	return nil
}

func (writer *rotatingFile) rotate() error {
	if err := writer.file.Close(); err != nil {
		return fmt.Errorf("close active log for rotation: %w", err)
	}
	writer.file = nil
	backupPath, err := writer.nextBackupPath()
	if err != nil {
		return err
	}
	if err := os.Rename(writer.cfg.path, backupPath); err != nil {
		_ = writer.open()
		return fmt.Errorf("rename active log for rotation: %w", err)
	}
	if writer.cfg.compression == config.LogCompressionGzip {
		if err := gzipFile(backupPath); err != nil {
			_ = writer.open()
			return fmt.Errorf("compress rotated log: %w", err)
		}
	}
	if err := writer.prune(); err != nil {
		_ = writer.open()
		return err
	}
	if err := writer.open(); err != nil {
		return fmt.Errorf("open active log after rotation: %w", err)
	}
	return nil
}

func (writer *rotatingFile) nextBackupPath() (string, error) {
	dir := filepath.Dir(writer.cfg.path)
	name := filepath.Base(writer.cfg.path)
	extension := filepath.Ext(name)
	stem := strings.TrimSuffix(name, extension)
	timestamp := writer.now().Format("20060102-150405.000000000")
	for sequence := 0; sequence < 1000; sequence++ {
		suffix := ""
		if sequence > 0 {
			suffix = fmt.Sprintf("-%03d", sequence)
		}
		candidate := filepath.Join(dir, stem+"-"+timestamp+suffix+extension)
		available, err := backupPathAvailable(candidate)
		if err != nil {
			return "", err
		}
		if available {
			return candidate, nil
		}
	}
	return "", fmt.Errorf("allocate rotated log path: too many collisions")
}

func backupPathAvailable(path string) (bool, error) {
	for _, candidate := range []string{path, path + ".gz"} {
		if _, err := os.Lstat(candidate); err == nil {
			return false, nil
		} else if !errors.Is(err, os.ErrNotExist) {
			return false, fmt.Errorf("inspect rotated log path: %w", err)
		}
	}
	return true, nil
}

type backupFile struct {
	path    string
	modTime time.Time
}

func (writer *rotatingFile) prune() error {
	dir := filepath.Dir(writer.cfg.path)
	name := filepath.Base(writer.cfg.path)
	extension := filepath.Ext(name)
	stem := strings.TrimSuffix(name, extension)
	entries, err := os.ReadDir(dir)
	if err != nil {
		return fmt.Errorf("list rotated logs: %w", err)
	}
	backups := make([]backupFile, 0)
	for _, entry := range entries {
		entryName := entry.Name()
		if entry.IsDir() || !isBackupName(entryName, stem, extension) {
			continue
		}
		info, err := entry.Info()
		if err != nil {
			return fmt.Errorf("inspect rotated log: %w", err)
		}
		backups = append(backups, backupFile{path: filepath.Join(dir, entryName), modTime: info.ModTime()})
	}
	sort.Slice(backups, func(i, j int) bool { return backups[i].modTime.After(backups[j].modTime) })
	cutoff := time.Time{}
	if writer.cfg.maxDays > 0 {
		cutoff = writer.now().Add(-time.Duration(writer.cfg.maxDays) * 24 * time.Hour)
	}
	retained := 0
	for _, backup := range backups {
		removeForAge := !cutoff.IsZero() && backup.modTime.Before(cutoff)
		removeForCount := writer.cfg.maxBackups > 0 && retained >= writer.cfg.maxBackups
		if removeForAge || removeForCount {
			if err := os.Remove(backup.path); err != nil && !errors.Is(err, os.ErrNotExist) {
				return fmt.Errorf("remove rotated log: %w", err)
			}
			continue
		}
		retained++
	}
	return nil
}

func isBackupName(name, stem, extension string) bool {
	withoutCompression := name
	if extension == "" {
		withoutCompression = strings.TrimSuffix(name, ".gz")
	} else if strings.HasSuffix(name, extension+".gz") {
		withoutCompression = strings.TrimSuffix(name, ".gz")
	} else if !strings.HasSuffix(name, extension) {
		return false
	}
	withoutExtension := strings.TrimSuffix(withoutCompression, extension)
	timestampAndSequence, found := strings.CutPrefix(withoutExtension, stem+"-")
	if !found || len(timestampAndSequence) < len("20060102-150405.000000000") {
		return false
	}
	_, err := time.Parse("20060102-150405.000000000", timestampAndSequence[:25])
	if err != nil {
		return false
	}
	remainder := timestampAndSequence[25:]
	if remainder == "" {
		return true
	}
	if len(remainder) != 4 || remainder[0] != '-' {
		return false
	}
	for _, character := range remainder[1:] {
		if character < '0' || character > '9' {
			return false
		}
	}
	return true
}

func gzipFile(path string) (returnErr error) {
	source, err := os.Open(path)
	if err != nil {
		return err
	}
	sourceClosed := false
	defer func() {
		if !sourceClosed {
			returnErr = errors.Join(returnErr, source.Close())
		}
	}()
	temporaryPath := path + ".gz.tmp"
	targetPath := path + ".gz"
	target, err := os.OpenFile(temporaryPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
	if err != nil {
		return err
	}
	completed := false
	targetClosed := false
	defer func() {
		if !targetClosed {
			returnErr = errors.Join(returnErr, target.Close())
		}
		if !completed {
			_ = os.Remove(temporaryPath)
		}
	}()
	compressed := gzip.NewWriter(target)
	if _, err := io.Copy(compressed, source); err != nil {
		_ = compressed.Close()
		return err
	}
	if err := compressed.Close(); err != nil {
		return err
	}
	if err := source.Close(); err != nil {
		return err
	}
	sourceClosed = true
	if err := target.Sync(); err != nil {
		return err
	}
	if err := target.Close(); err != nil {
		return err
	}
	targetClosed = true
	if err := os.Rename(temporaryPath, targetPath); err != nil {
		return err
	}
	if err := os.Remove(path); err != nil {
		return err
	}
	completed = true
	return nil
}
