package audit

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"sync"
	"sync/atomic"
	"time"

	"github.com/pingcap/log"
	"go.uber.org/zap"
)

const (
	defaultBufferSize    = 4096
	defaultChannelSize   = 1000
	defaultFlushInterval = 5 * time.Second

	// maxBufferSize is the maximum allowed buffer size (10MB).
	maxBufferSize = 10 * 1024 * 1024
	// maxChannelSize is the maximum allowed channel size.
	maxChannelSize   = 100000
	minFlushInterval = 100 * time.Millisecond
	maxFlushInterval = time.Minute
)

// Config holds audit logging configuration.
type Config struct {
	Enabled              bool          `yaml:"enabled"`
	FilePath             string        `yaml:"filePath"`
	BufferSize           int           `yaml:"bufferSize"`           // Default: 4096
	ChannelSize          int           `yaml:"channelSize"`          // Default: 1000
	FlushInterval        time.Duration `yaml:"flushInterval"`        // Default: 5s
	SensitiveQueryParams []string      `yaml:"sensitiveQueryParams"` // Additional query params to redact (merged with defaults)
	IncludeGetRequests   bool          `yaml:"includeGetRequests"`   // Default: false, exclude GET requests from audit logs
	MaxBodySize          int           `yaml:"maxBodySize"`          // Maximum request body size to log; <= 0 disables request body logging.
	SensitiveBodyFields  []string      `yaml:"sensitiveBodyFields"`  // Additional body fields to redact (merged with defaults)
}

func validateAudit(cfg *Config) error {
	if cfg.FilePath == "" {
		return errors.New("filePath is required when audit enabled")
	}
	if cfg.BufferSize > maxBufferSize {
		return fmt.Errorf("bufferSize must not exceed %d bytes, got %d", maxBufferSize, cfg.BufferSize)
	}
	if cfg.ChannelSize > maxChannelSize {
		return fmt.Errorf("channelSize must not exceed %d, got %d", maxChannelSize, cfg.ChannelSize)
	}
	// Validate FlushInterval bounds (100ms - 1 minute)
	if cfg.FlushInterval > 0 {
		if cfg.FlushInterval < minFlushInterval {
			return fmt.Errorf("flushInterval must be at least %v, got %v", minFlushInterval, cfg.FlushInterval)
		}
		if cfg.FlushInterval > maxFlushInterval {
			return fmt.Errorf("flushInterval must not exceed %v, got %v", maxFlushInterval, cfg.FlushInterval)
		}
	}
	return nil
}

// ValidateConfig validates audit configuration without performing file I/O.
// This is exported for use by the config package's Validate function.
func ValidateConfig(cfg *Config) error {
	if !cfg.Enabled {
		return nil
	}
	return validateAudit(cfg)
}

// Logger is the audit logger that writes audit entries to a file.
type Logger struct {
	file    *os.File
	buffer  *bufio.Writer
	entryCh chan *Entry
	done    chan struct{} // signals shutdown to writer and Log()
	wg      sync.WaitGroup
	closed  atomic.Bool

	// hot holds hot-reloadable configuration
	hot *hotConfig
}

// NewLogger creates a new audit logger.
func NewLogger(cfg *Config) (*Logger, error) {
	if err := validateAudit(cfg); err != nil {
		return nil, fmt.Errorf("invalid audit config: %w", err)
	}

	// Open file with append mode (0600 = owner read/write only for security)
	file, err := os.OpenFile(cfg.FilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
	if err != nil {
		return nil, fmt.Errorf("open audit log file: %w", err)
	}

	bufferSize := cfg.BufferSize
	if bufferSize <= 0 {
		bufferSize = defaultBufferSize
	}
	channelSize := cfg.ChannelSize
	if channelSize <= 0 {
		channelSize = defaultChannelSize
	}

	l := &Logger{
		file:    file,
		buffer:  bufio.NewWriterSize(file, bufferSize),
		entryCh: make(chan *Entry, channelSize),
		done:    make(chan struct{}),
		hot:     newHotConfig(cfg),
	}

	l.wg.Add(1)
	go l.writer()

	log.Info("audit logger started",
		zap.String("file", cfg.FilePath),
		zap.Int("buffer_size", bufferSize),
		zap.Int("channel_size", channelSize),
		zap.Duration("flush_interval", l.hot.getFlushInterval()),
	)

	return l, nil
}

// Log submits an audit entry for asynchronous writing.
// It uses a done channel to detect shutdown, avoiding the need for
// panic recovery on closed channel sends.
func (l *Logger) Log(entry *Entry) {
	// Fast path: check if we're shutting down
	select {
	case <-l.done:
		log.Debug("audit entry dropped: logger is closing")
		return
	default:
	}

	// Try to send entry to channel
	select {
	case l.entryCh <- entry:
		// Entry sent successfully
	case <-l.done:
		// Shutdown happened between the two selects
		log.Debug("audit entry dropped during shutdown")
	default:
		// Channel is full
		log.Warn("audit log channel full, dropping entry",
			zap.String("uri", entry.RequestURI),
			zap.String("method", entry.RequestMethod),
		)
	}
}

// Close gracefully shuts down the audit logger.
// It signals the writer to stop, waits for it to drain remaining entries,
// flushes the buffer, syncs to disk, and closes the file.
func (l *Logger) Close() error {
	if !l.closed.CompareAndSwap(false, true) {
		log.Warn("audit logger already closed")
		return nil
	}

	log.Info("closing audit logger, draining remaining entries...")

	// Signal writer and Log() to stop
	close(l.done)

	// Wait for writer goroutine to finish draining
	l.wg.Wait()

	// Flush buffer to ensure all data is written
	if err := l.buffer.Flush(); err != nil {
		log.Error("failed to flush audit buffer", zap.Error(err))
		return fmt.Errorf("flush audit buffer: %w", err)
	}

	// Sync file to ensure data is persisted to disk
	if err := l.file.Sync(); err != nil {
		log.Error("failed to sync audit file", zap.Error(err))
		return fmt.Errorf("sync audit file: %w", err)
	}

	// Close file handle
	if err := l.file.Close(); err != nil {
		log.Error("failed to close audit file", zap.Error(err))
		return fmt.Errorf("close audit file: %w", err)
	}

	log.Info("audit logger closed successfully")
	return nil
}

// writer is the background goroutine that writes entries to file.
func (l *Logger) writer() {
	defer l.wg.Done()

	currentInterval := l.hot.getFlushInterval()
	ticker := time.NewTicker(currentInterval)
	defer ticker.Stop()

	encoder := json.NewEncoder(l.buffer)

	for {
		select {
		case entry := <-l.entryCh:
			if err := encoder.Encode(entry); err != nil {
				log.Error("failed to encode audit entry", zap.Error(err))
			}
		case <-l.done:
			// Shutdown signaled, drain remaining entries and return
			l.drainRemaining(encoder)
			return
		case <-ticker.C:
			if err := l.buffer.Flush(); err != nil {
				log.Error("failed to flush audit buffer", zap.Error(err))
			}

			// Check if flush interval changed (hot-reload)
			newInterval := l.hot.getFlushInterval()
			if newInterval != currentInterval {
				ticker.Reset(newInterval)
				currentInterval = newInterval
				log.Info("flush interval updated", zap.Duration("interval", newInterval))
			}
		}
	}
}

// drainRemaining processes any remaining entries in the channel after shutdown is signaled.
// Since entryCh is never closed, we use a non-blocking select to drain.
func (l *Logger) drainRemaining(encoder *json.Encoder) {
	count := 0
	for {
		select {
		case entry := <-l.entryCh:
			if err := encoder.Encode(entry); err != nil {
				log.Error("failed to encode audit entry during drain", zap.Error(err))
			}
			count++
		default:
			// Channel is empty, done draining
			// Explicitly flush after draining to ensure all data is written to buffer
			if err := l.buffer.Flush(); err != nil {
				log.Error("failed to flush buffer during drain", zap.Error(err))
			}
			if count > 0 {
				log.Info("drained remaining audit entries", zap.Int("count", count))
			}
			return
		}
	}
}

// updateHotConfig updates the hot-reloadable configuration.
// This method is called by Manager when config changes.
func (l *Logger) updateHotConfig(cfg *Config) {
	l.hot.update(cfg)
}
