package server

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"syscall"
	"time"

	"gopkg.in/natefinch/lumberjack.v2"
)

// RotationStrategy represents the log rotation strategy type.
type RotationStrategy string

const (
	// RotationStrategyRename uses lumberjack's rename method (default).
	// The original file is renamed to a backup file, and a new file is created.
	// This requires the application to reopen the log file after rotation.
	RotationStrategyRename RotationStrategy = "rename"

	// RotationStrategyCopyTruncate copies the file content to a backup file,
	// then truncates the original file to zero size.
	// This is suitable for applications that don't support log file reopening.
	//
	// WARNING: This strategy has an inherent race condition. Data written between
	// the copy and truncate operations may be lost. This implementation minimizes
	// the race window by keeping the file descriptor open throughout the operation,
	// but cannot eliminate it entirely.
	RotationStrategyCopyTruncate RotationStrategy = "copytruncate"
)

// Rotator defines the interface for log rotation strategies.
type Rotator interface {
	// Rotate performs the rotation operation on the specified file.
	// It returns the backup filename (without path) if rotation was performed,
	// or an empty string if no rotation was needed (e.g., empty file).
	Rotate(dir, filename string) (backupName string, err error)
}

// RenameRotator implements Rotator using lumberjack's rename strategy.
type RenameRotator struct{}

// Rotate renames the original file to a timestamped backup file.
func (r *RenameRotator) Rotate(dir, filename string) (string, error) {
	fpath := filepath.Join(dir, filename)
	stat, err := os.Stat(fpath)
	if err != nil {
		return "", err
	}
	if stat.Size() == 0 {
		return "", nil // empty file, no rotation needed
	}

	lg := lumberjack.Logger{
		Filename: fpath,
	}
	defer lg.Close()

	err = lg.Rotate()
	if err != nil {
		return "", err
	}

	// Return the expected backup filename based on lumberjack's naming convention.
	// NOTE: This name may differ slightly (by milliseconds) from the actual filename
	// created by lumberjack, since lumberjack captures time.Now() internally before
	// the rename operation. This is acceptable because:
	// 1. This name is only used for informational logging
	// 2. Actual file operations (upload, cleanup) use directory scanning via listLog()
	return generateBackupName(filename, time.Now()), nil
}

// CopyTruncateRotator implements Rotator using copy-then-truncate strategy.
type CopyTruncateRotator struct{}

// Rotate copies the file content to a backup file, then truncates the original.
//
// This implementation minimizes the race window by:
// 1. Opening the source file with O_RDWR to avoid reopening for truncation
// 2. Keeping the file descriptor open throughout the entire operation
// 3. Using Truncate on the same fd instead of os.Truncate (which reopens the file)
// 4. Using CopyN with a snapshot size to avoid reading partial data written during copy
//
// Despite these optimizations, there is still a small race window between
// CopyN completing and Truncate being called. Any data written during this
// window will be lost.
func (c *CopyTruncateRotator) Rotate(dir, filename string) (string, error) {
	srcPath := filepath.Join(dir, filename)

	// Open source file with O_RDWR to perform both read and truncate on the same fd.
	// This minimizes the race window compared to reopening the file for truncation.
	src, err := os.OpenFile(srcPath, os.O_RDWR, 0)
	if err != nil {
		return "", fmt.Errorf("failed to open source file: %w", err)
	}
	defer src.Close()

	stat, err := src.Stat()
	if err != nil {
		return "", fmt.Errorf("failed to stat source file: %w", err)
	}

	// Record the current size as snapshot point for copying.
	// This ensures we only copy complete data that existed at this moment,
	// avoiding partial lines that might be written during the copy operation.
	currentSize := stat.Size()
	if currentSize == 0 {
		return "", nil // empty file, no rotation needed
	}

	// Generate backup filename consistent with lumberjack's naming convention
	now := time.Now()
	backupName := generateBackupName(filename, now)
	dstPath := filepath.Join(dir, backupName)

	// Create backup file with the same permissions as the source file
	dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, stat.Mode().Perm())
	if err != nil {
		return "", fmt.Errorf("failed to create backup file: %w", err)
	}
	defer dst.Close()

	// Copy only the snapshot size to the backup.
	// Using CopyN instead of Copy ensures we don't read partial data
	// that might be written during the copy operation.
	_, err = io.CopyN(dst, src, currentSize)
	if err != nil && !errors.Is(err, io.EOF) {
		os.Remove(dstPath) // cleanup on failure
		return "", fmt.Errorf("failed to copy file content: %w", err)
	}

	// Sync to ensure data is written to disk before truncating
	if err := dst.Sync(); isRealError(err) {
		os.Remove(dstPath) // cleanup on failure
		return "", fmt.Errorf("failed to sync backup file: %w", err)
	}

	// Truncate the original file using the same file descriptor.
	// This is faster and has a smaller race window than os.Truncate(srcPath, 0)
	// which would need to reopen the file.
	if err := src.Truncate(0); isRealError(err) {
		return "", fmt.Errorf("failed to truncate source file: %w", err)
	}

	// Seek to the beginning so subsequent writes start from offset 0.
	// This is important for applications that keep the fd open.
	if _, err := src.Seek(0, io.SeekStart); isRealError(err) {
		return "", fmt.Errorf("failed to seek source file: %w", err)
	}

	// Sync to ensure the truncation is persisted to disk.
	// Without this, a crash could result in duplicate data if the file
	// system recovers to pre-truncate state while backup already exists.
	if err := src.Sync(); isRealError(err) {
		return "", fmt.Errorf("failed to sync source file after truncation: %w", err)
	}

	return backupName, nil
}

// generateBackupName generates a backup filename consistent with lumberjack's naming.
// Format: basename-YYYY-MM-DDTHH-MM-SS.sss.ext
// Examples:
//   - tidb-audit.log -> tidb-audit-2025-08-01T09-42-10.442.log
//   - app.log -> app-2025-08-01T09-42-10.442.log
func generateBackupName(filename string, t time.Time) string {
	ext := filepath.Ext(filename)
	base := filename[:len(filename)-len(ext)]
	timestamp := t.Format("2006-01-02T15-04-05.000")
	return fmt.Sprintf("%s-%s%s", base, timestamp, ext)
}

// NewRotator creates a Rotator based on the specified strategy.
// If an unknown strategy is provided, it defaults to RenameRotator.
func NewRotator(strategy RotationStrategy) Rotator {
	switch strategy {
	case RotationStrategyCopyTruncate:
		return &CopyTruncateRotator{}
	default:
		return &RenameRotator{}
	}
}

// isRealError checks if an error is a real error.
// errno 0 is not a real error (it means success in POSIX).
// This can happen when using tools like mirrord that proxy syscalls
// and return syscall.Errno(0) for successful operations.
func isRealError(err error) bool {
	if err == nil {
		return false
	}
	// Check if it's syscall.Errno(0) which means success
	var errno syscall.Errno
	if errors.As(err, &errno) && errno == 0 {
		return false
	}
	return true
}
