package eventlog

import (
	"encoding/json"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/safedep/dry/log"
	"github.com/safedep/pmg/config"
)

// Event is the on-disk DTO for a logged security event. The event vocabulary is
// owned by the audit package; here event_type is just a JSON string column.
type Event struct {
	Timestamp   time.Time              `json:"timestamp"`
	EventType   string                 `json:"event_type"`
	Message     string                 `json:"message"`
	PackageName string                 `json:"package_name,omitempty"`
	Version     string                 `json:"version,omitempty"`
	Ecosystem   string                 `json:"ecosystem,omitempty"`
	Details     map[string]interface{} `json:"details,omitempty"`
}

// Logger defines the contract for implementing event loggers.
type Logger interface {
	// Log writes an event to the log file
	Log(event Event) error

	// Close closes the logger
	Close() error

	// IsActive returns whether the logger is active
	IsActive() bool
}

// fileWithRotationLogger represents an event logger that writes to a file and rotates the
// file when it reaches a certain age
type fileWithRotationLogger struct {
	file   *os.File
	writer io.Writer
	mu     sync.Mutex
	active bool
}

// fileWithRotationLogger implements the Logger interface. This is the default logger
// that will be used. Future enhancements will introduce additional and optional loggers.
var _ Logger = &fileWithRotationLogger{}

var (
	globalLogger Logger
	once         sync.Once
)

// GetDefaultLogDir returns the default log directory based on the OS
func GetDefaultLogDir() (string, error) {
	return config.Get().EventLogDir(), nil
}

// Initialize sets up the global event logger with the default log directory
func Initialize() error {
	if config.Get().Config.SkipEventLogging {
		return nil
	}

	logDir, err := GetDefaultLogDir()
	if err != nil {
		return fmt.Errorf("failed to get default log directory: %w", err)
	}

	return InitializeWithDir(logDir)
}

// InitializeWithFile sets up the global event logger with a specific file path
func InitializeWithFile(filePath string) error {
	if config.Get().Config.SkipEventLogging {
		return nil
	}

	var initErr error
	once.Do(func() {
		fwrl := &fileWithRotationLogger{}
		initErr = fwrl.initWithFile(filePath)
		globalLogger = fwrl
	})

	return initErr
}

// InitializeWithDir sets up the global event logger with a custom log directory
func InitializeWithDir(logDir string) error {
	if config.Get().Config.SkipEventLogging {
		return nil
	}

	var initErr error
	once.Do(func() {
		fwrl := &fileWithRotationLogger{}
		initErr = fwrl.init(logDir)
		globalLogger = fwrl
	})

	return initErr
}

// reinitializeForTest resets and reinitializes the logger for testing purposes
// This should only be used in tests
func reinitializeForTest(logDir string) error {
	// Close existing logger if any
	if globalLogger != nil {
		if err := globalLogger.Close(); err != nil {
			log.Warnf("failed to close existing logger: %v", err)
		}
	}

	// Reset once
	once = sync.Once{}

	// Initialize new logger
	return InitializeWithDir(logDir)
}

// init initializes the logger with the specified directory
func (l *fileWithRotationLogger) init(logDir string) error {
	// Create log directory if it doesn't exist
	if err := os.MkdirAll(logDir, 0755); err != nil {
		return fmt.Errorf("failed to create log directory: %w", err)
	}

	// Create log file with timestamp-based naming
	logFileName := time.Now().Format("20060102") + "-pmg.log"
	logFilePath := filepath.Join(logDir, logFileName)

	file, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return fmt.Errorf("failed to open log file: %w", err)
	}

	l.file = file
	l.writer = file
	l.active = true

	// Clean up old logs in background
	go l.cleanupOldLogs(logDir)

	return nil
}

// initWithFile initializes the logger with a specific file path
func (l *fileWithRotationLogger) initWithFile(filePath string) error {
	// Create directory if it doesn't exist
	dir := filepath.Dir(filePath)
	if dir != "" && dir != "." {
		if err := os.MkdirAll(dir, 0755); err != nil {
			return fmt.Errorf("failed to create log directory: %w", err)
		}
	}

	file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil {
		return fmt.Errorf("failed to open log file: %w", err)
	}

	l.file = file
	l.writer = file
	l.active = true

	// No cleanup needed for custom log files (user manages them)

	return nil
}

// cleanupOldLogs removes log files older than 7 days
func (l *fileWithRotationLogger) cleanupOldLogs(logDir string) {
	cutoff := time.Now().AddDate(0, 0, -1*config.Get().Config.EventLogRetentionDays)

	entries, err := os.ReadDir(logDir)
	if err != nil {
		log.Warnf("Failed to read log directory for cleanup: %v", err)
		return
	}

	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}

		// Only process *-pmg.log files
		name := entry.Name()
		matched, err := filepath.Match("*-pmg.log", name)
		if err != nil || !matched {
			continue
		}

		filePath := filepath.Join(logDir, name)
		info, err := entry.Info()
		if err != nil {
			log.Warnf("Failed to get info for log file: %v", err)
			continue
		}

		if info.ModTime().Before(cutoff) {
			if err := os.Remove(filePath); err != nil {
				log.Warnf("Failed to remove old log file: %v", err)
			}
		}
	}
}

// Log writes an event to the log file
func (l *fileWithRotationLogger) Log(event Event) error {
	if !l.active {
		return nil
	}

	l.mu.Lock()
	defer l.mu.Unlock()

	// Set timestamp if not already set
	if event.Timestamp.IsZero() {
		event.Timestamp = time.Now()
	}

	// Marshal event to JSON
	data, err := json.Marshal(event)
	if err != nil {
		return fmt.Errorf("failed to marshal event: %w", err)
	}

	// Write to file
	_, err = l.writer.Write(append(data, '\n'))
	if err != nil {
		return fmt.Errorf("failed to write event: %w", err)
	}

	// Flush to ensure data is written
	if l.file != nil {
		if err := l.file.Sync(); err != nil {
			return fmt.Errorf("failed to sync file: %w", err)
		}
	}

	return nil
}

// Close closes the logger
func (l *fileWithRotationLogger) Close() error {
	if !l.active {
		return nil
	}

	l.mu.Lock()
	defer l.mu.Unlock()

	l.active = false
	if l.file != nil {
		return l.file.Close()
	}

	return nil
}

// IsActive returns whether the logger is active
func (l *fileWithRotationLogger) IsActive() bool {
	return l.active
}

// LogEvent logs an event using the global logger
func LogEvent(event Event) error {
	// If logger is not initialized or not active, silently fail
	if globalLogger == nil || !globalLogger.IsActive() {
		return nil
	}

	return globalLogger.Log(event)
}

// Close closes the global logger
func Close() error {
	if globalLogger != nil {
		return globalLogger.Close()
	}

	return nil
}
