package file

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"github.com/antitbone/ja4/correlator/internal/domain"
)

const (
	// DefaultFilePermissions for output files
	DefaultFilePermissions os.FileMode = 0644
	// DefaultDirPermissions for output directories
	DefaultDirPermissions os.FileMode = 0750
)

// Config holds the file sink configuration.
type Config struct {
	Path string
}

// FileSink writes correlated logs to a file as JSON lines.
type FileSink struct {
	config Config
	mu     sync.Mutex
	file   *os.File
}

// NewFileSink creates a new file sink.
func NewFileSink(config Config) (*FileSink, error) {
	// Validate path
	if err := validateFilePath(config.Path); err != nil {
		return nil, fmt.Errorf("invalid file path: %w", err)
	}

	s := &FileSink{
		config: config,
	}

	// Open file on creation
	if err := s.openFile(); err != nil {
		return nil, err
	}

	return s, nil
}

// Name returns the sink name.
func (s *FileSink) Name() string {
	return "file"
}

// Reopen closes and reopens the file (for log rotation on SIGHUP).
func (s *FileSink) Reopen() error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.file != nil {
		if err := s.file.Close(); err != nil {
			return fmt.Errorf("failed to close file: %w", err)
		}
	}

	return s.openFile()
}

// Write writes a correlated log to the file.
func (s *FileSink) Write(ctx context.Context, log domain.CorrelatedLog) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.file == nil {
		if err := s.openFile(); err != nil {
			return err
		}
	}

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

	line := append(data, '\n')
	if _, err := s.file.Write(line); err != nil {
		return fmt.Errorf("failed to write log line: %w", err)
	}
	if err := s.file.Sync(); err != nil {
		return fmt.Errorf("failed to sync log line: %w", err)
	}

	return nil
}

// Flush flushes any buffered data.
func (s *FileSink) Flush(ctx context.Context) error {
	s.mu.Lock()
	defer s.mu.Unlock()

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

// Close closes the sink.
func (s *FileSink) Close() error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.file != nil {
		err := s.file.Close()
		s.file = nil
		return err
	}
	return nil
}

func (s *FileSink) openFile() error {
	// Validate path again before opening
	if err := validateFilePath(s.config.Path); err != nil {
		return fmt.Errorf("invalid file path: %w", err)
	}

	// Ensure directory exists
	dir := filepath.Dir(s.config.Path)
	if err := os.MkdirAll(dir, DefaultDirPermissions); err != nil {
		return fmt.Errorf("failed to create directory: %w", err)
	}

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

	s.file = file
	return nil
}

// validateFilePath validates that the file path is safe and allowed.
func validateFilePath(path string) error {
	if strings.TrimSpace(path) == "" {
		return fmt.Errorf("path cannot be empty")
	}

	cleanPath := filepath.Clean(path)

	// Allow relative paths for testing/dev
	if !filepath.IsAbs(cleanPath) {
		return nil
	}

	absPath, err := filepath.Abs(cleanPath)
	if err != nil {
		return fmt.Errorf("failed to resolve absolute path: %w", err)
	}

	allowedRoots := []string{
		"/var/log/logcorrelator",
		"/var/log",
		"/tmp",
	}

	for _, root := range allowedRoots {
		absRoot, err := filepath.Abs(filepath.Clean(root))
		if err != nil {
			continue
		}

		rel, err := filepath.Rel(absRoot, absPath)
		if err != nil {
			continue
		}

		if rel == "." {
			return nil
		}
		if rel == ".." {
			continue
		}
		if !strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
			return nil
		}
	}

	return fmt.Errorf("path must be under allowed directories: %v", allowedRoots)
}
