package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"mime"
	"os"
	"os/signal"
	"path/filepath"
	"regexp"
	"strings"
	"syscall"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/sts"
	"github.com/fsnotify/fsnotify"
)

// Config holds the application configuration
type Config struct {
	WatchDir           string   `json:"watch_dir"`
	S3Bucket           string   `json:"s3_bucket"`
	S3Prefix           string   `json:"s3_prefix"`
	S3Region           string   `json:"s3_region"`
	Patterns           []string `json:"patterns"`
	AWSProfile         string   `json:"aws_profile"`
	AWSAccessKey       string   `json:"aws_access_key"`
	AWSSecretKey       string   `json:"aws_secret_key"`
	AWSRoleARN         string   `json:"aws_role_arn"`
	AWSRoleSessionName string   `json:"aws_role_session_name"`
	Recursive          bool     `json:"recursive"`
	DebounceMs         int      `json:"debounce_ms"`
	RetryAttempts      int      `json:"retry_attempts"`
	RetryDelayMs       int      `json:"retry_delay_ms"`
}

// FileWatcher manages file watching and S3 uploads
type FileWatcher struct {
	config     *Config
	s3Client   *s3.Client
	watcher    *fsnotify.Watcher
	patterns   []*regexp.Regexp
	debouncer  map[string]*time.Timer
	ctx        context.Context
	cancelFunc context.CancelFunc
}

func main() {
	// Command line flags
	configFile := flag.String("config", "", "Path to config.json file")
	watchDir := flag.String("watch-dir", "", "Directory to watch for file changes")
	s3Bucket := flag.String("s3-bucket", "", "S3 bucket name")
	s3Prefix := flag.String("s3-prefix", "", "S3 key prefix for uploaded files")
	s3Region := flag.String("s3-region", "us-east-1", "AWS region")
	patterns := flag.String("patterns", "", "Comma-separated list of file patterns (glob or regex)")
	awsProfile := flag.String("aws-profile", "", "AWS profile name")
	awsAccessKey := flag.String("aws-access-key", "", "AWS access key ID")
	awsSecretKey := flag.String("aws-secret-key", "", "AWS secret access key")
	awsRoleARN := flag.String("aws-role-arn", "", "AWS IAM role ARN to assume")
	awsRoleSessionName := flag.String("aws-role-session-name", "s3-file-watcher", "Session name for assumed role")
	recursive := flag.Bool("recursive", false, "Watch subdirectories recursively")
	debounceMs := flag.Int("debounce-ms", 500, "Debounce delay in milliseconds")
	retryAttempts := flag.Int("retry-attempts", 3, "Number of retry attempts for S3 uploads")
	retryDelayMs := flag.Int("retry-delay-ms", 1000, "Delay between retries in milliseconds")

	flag.Parse()

	// Load configuration
	cfg := &Config{
		S3Region:      *s3Region,
		Recursive:     *recursive,
		DebounceMs:    *debounceMs,
		RetryAttempts: *retryAttempts,
		RetryDelayMs:  *retryDelayMs,
	}

	// If config file specified, load it first
	if *configFile != "" {
		if err := loadConfigFromFile(*configFile, cfg); err != nil {
			log.Fatalf("Failed to load config file: %v", err)
		}
	}

	// Command line flags override config file values
	if *watchDir != "" {
		cfg.WatchDir = *watchDir
	}
	if *s3Bucket != "" {
		cfg.S3Bucket = *s3Bucket
	}
	if *s3Prefix != "" {
		cfg.S3Prefix = *s3Prefix
	}
	if *s3Region != "us-east-1" {
		cfg.S3Region = *s3Region
	}
	if *patterns != "" {
		cfg.Patterns = strings.Split(*patterns, ",")
	}
	if *awsProfile != "" {
		cfg.AWSProfile = *awsProfile
	}
	if *awsAccessKey != "" {
		cfg.AWSAccessKey = *awsAccessKey
	}
	if *awsSecretKey != "" {
		cfg.AWSSecretKey = *awsSecretKey
	}
	if *awsRoleARN != "" {
		cfg.AWSRoleARN = *awsRoleARN
	}
	if *awsRoleSessionName != "s3-file-watcher" {
		cfg.AWSRoleSessionName = *awsRoleSessionName
	}
	if cfg.AWSRoleSessionName == "" {
		cfg.AWSRoleSessionName = "s3-file-watcher"
	}
	if *recursive {
		cfg.Recursive = *recursive
	}
	if *debounceMs != 500 {
		cfg.DebounceMs = *debounceMs
	}
	if *retryAttempts != 3 {
		cfg.RetryAttempts = *retryAttempts
	}
	if *retryDelayMs != 1000 {
		cfg.RetryDelayMs = *retryDelayMs
	}

	// Validate required configuration
	if err := validateConfig(cfg); err != nil {
		log.Fatalf("Configuration error: %v", err)
	}

	// Create and start the file watcher
	fw, err := NewFileWatcher(cfg)
	if err != nil {
		log.Fatalf("Failed to create file watcher: %v", err)
	}

	// Handle graceful shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		<-sigChan
		log.Println("Received shutdown signal, stopping...")
		fw.Stop()
	}()

	log.Printf("Starting S3 File Watcher Daemon")
	log.Printf("Watching: %s", cfg.WatchDir)
	log.Printf("S3 Bucket: %s", cfg.S3Bucket)
	log.Printf("Patterns: %v", cfg.Patterns)

	if err := fw.Start(); err != nil {
		log.Fatalf("Watcher error: %v", err)
	}
}

func loadConfigFromFile(path string, cfg *Config) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("reading config file: %w", err)
	}

	if err := json.Unmarshal(data, cfg); err != nil {
		return fmt.Errorf("parsing config file: %w", err)
	}

	return nil
}

func validateConfig(cfg *Config) error {
	if cfg.WatchDir == "" {
		return fmt.Errorf("watch directory is required")
	}

	// Expand home directory if needed
	if strings.HasPrefix(cfg.WatchDir, "~") {
		home, err := os.UserHomeDir()
		if err != nil {
			return fmt.Errorf("expanding home directory: %w", err)
		}
		cfg.WatchDir = filepath.Join(home, cfg.WatchDir[1:])
	}

	// Verify watch directory exists
	if _, err := os.Stat(cfg.WatchDir); os.IsNotExist(err) {
		return fmt.Errorf("watch directory does not exist: %s", cfg.WatchDir)
	}

	if cfg.S3Bucket == "" {
		return fmt.Errorf("S3 bucket is required")
	}

	if len(cfg.Patterns) == 0 {
		return fmt.Errorf("at least one file pattern is required")
	}

	return nil
}

// NewFileWatcher creates a new FileWatcher instance
func NewFileWatcher(cfg *Config) (*FileWatcher, error) {
	ctx, cancel := context.WithCancel(context.Background())

	// Create S3 client
	s3Client, err := createS3Client(ctx, cfg)
	if err != nil {
		cancel()
		return nil, fmt.Errorf("creating S3 client: %w", err)
	}

	// Create fsnotify watcher
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		cancel()
		return nil, fmt.Errorf("creating file watcher: %w", err)
	}

	// Compile patterns
	patterns, err := compilePatterns(cfg.Patterns)
	if err != nil {
		cancel()
		watcher.Close()
		return nil, fmt.Errorf("compiling patterns: %w", err)
	}

	return &FileWatcher{
		config:     cfg,
		s3Client:   s3Client,
		watcher:    watcher,
		patterns:   patterns,
		debouncer:  make(map[string]*time.Timer),
		ctx:        ctx,
		cancelFunc: cancel,
	}, nil
}

func createS3Client(ctx context.Context, cfg *Config) (*s3.Client, error) {
	var awsCfg aws.Config
	var err error

	opts := []func(*config.LoadOptions) error{
		config.WithRegion(cfg.S3Region),
	}

	// Use explicit credentials if provided
	if cfg.AWSAccessKey != "" && cfg.AWSSecretKey != "" {
		opts = append(opts, config.WithCredentialsProvider(
			credentials.NewStaticCredentialsProvider(
				cfg.AWSAccessKey,
				cfg.AWSSecretKey,
				"",
			),
		))
	} else if cfg.AWSProfile != "" {
		// Use named profile
		opts = append(opts, config.WithSharedConfigProfile(cfg.AWSProfile))
	}

	awsCfg, err = config.LoadDefaultConfig(ctx, opts...)
	if err != nil {
		return nil, fmt.Errorf("loading AWS config: %w", err)
	}

	// If role ARN is specified, assume the role
	if cfg.AWSRoleARN != "" {
		log.Printf("Assuming IAM role: %s", cfg.AWSRoleARN)
		stsClient := sts.NewFromConfig(awsCfg)
		creds := stscreds.NewAssumeRoleProvider(stsClient, cfg.AWSRoleARN, func(o *stscreds.AssumeRoleOptions) {
			o.RoleSessionName = cfg.AWSRoleSessionName
		})
		awsCfg.Credentials = aws.NewCredentialsCache(creds)
	}

	return s3.NewFromConfig(awsCfg), nil
}

func compilePatterns(patternStrs []string) ([]*regexp.Regexp, error) {
	patterns := make([]*regexp.Regexp, 0, len(patternStrs))

	for _, p := range patternStrs {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}

		// Check if it's a glob pattern or explicit match
		if !strings.HasPrefix(p, "^") && !strings.HasSuffix(p, "$") {
			// Convert glob to regex
			p = globToRegex(p)
		}

		re, err := regexp.Compile(p)
		if err != nil {
			return nil, fmt.Errorf("invalid pattern '%s': %w", p, err)
		}
		patterns = append(patterns, re)
	}

	return patterns, nil
}

// globToRegex converts a glob pattern to a regex pattern
func globToRegex(glob string) string {
	var result strings.Builder
	result.WriteString("^")

	for i := 0; i < len(glob); i++ {
		c := glob[i]
		switch c {
		case '*':
			if i+1 < len(glob) && glob[i+1] == '*' {
				result.WriteString(".*")
				i++ // Skip next *
			} else {
				result.WriteString("[^/]*")
			}
		case '?':
			result.WriteString("[^/]")
		case '.', '(', ')', '+', '|', '^', '$', '@', '%':
			result.WriteString("\\")
			result.WriteByte(c)
		case '[':
			result.WriteByte(c)
		case ']':
			result.WriteByte(c)
		default:
			result.WriteByte(c)
		}
	}

	result.WriteString("$")
	return result.String()
}

// Start begins watching the directory
func (fw *FileWatcher) Start() error {
	// Add directories to watch
	if fw.config.Recursive {
		if err := fw.addRecursive(fw.config.WatchDir); err != nil {
			return fmt.Errorf("adding directories recursively: %w", err)
		}
	} else {
		if err := fw.watcher.Add(fw.config.WatchDir); err != nil {
			return fmt.Errorf("adding watch directory: %w", err)
		}
	}

	// Event processing loop
	for {
		select {
		case <-fw.ctx.Done():
			return nil

		case event, ok := <-fw.watcher.Events:
			if !ok {
				return nil
			}
			fw.handleEvent(event)

		case err, ok := <-fw.watcher.Errors:
			if !ok {
				return nil
			}
			log.Printf("Watcher error: %v", err)
		}
	}
}

func (fw *FileWatcher) addRecursive(dir string) error {
	return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			log.Printf("Watching directory: %s", path)
			return fw.watcher.Add(path)
		}
		return nil
	})
}

func (fw *FileWatcher) handleEvent(event fsnotify.Event) {
	// Only handle write and create events
	if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
		return
	}

	// Check if it's a directory
	info, err := os.Stat(event.Name)
	if err != nil {
		return
	}
	if info.IsDir() {
		// If recursive and new directory created, add it to watch
		if fw.config.Recursive && event.Has(fsnotify.Create) {
			log.Printf("New directory detected, adding to watch: %s", event.Name)
			if err := fw.watcher.Add(event.Name); err != nil {
				log.Printf("Failed to watch new directory: %v", err)
			}
		}
		return
	}

	// Check if file matches any pattern
	filename := filepath.Base(event.Name)
	if !fw.matchesPattern(filename) {
		return
	}

	// Debounce the event
	fw.debounce(event.Name, func() {
		fw.uploadFile(event.Name)
	})
}

func (fw *FileWatcher) matchesPattern(filename string) bool {
	for _, pattern := range fw.patterns {
		if pattern.MatchString(filename) {
			return true
		}
	}
	return false
}

func (fw *FileWatcher) debounce(path string, fn func()) {
	// Cancel existing timer if present
	if timer, exists := fw.debouncer[path]; exists {
		timer.Stop()
	}

	// Set new timer
	fw.debouncer[path] = time.AfterFunc(
		time.Duration(fw.config.DebounceMs)*time.Millisecond,
		func() {
			delete(fw.debouncer, path)
			fn()
		},
	)
}

func (fw *FileWatcher) uploadFile(filePath string) {
	log.Printf("Uploading file: %s", filePath)

	// Read file
	file, err := os.Open(filePath)
	if err != nil {
		log.Printf("Failed to open file %s: %v", filePath, err)
		return
	}
	defer file.Close()

	// Get file info for content type detection
	fileInfo, err := file.Stat()
	if err != nil {
		log.Printf("Failed to stat file %s: %v", filePath, err)
		return
	}

	// Detect content type from file extension
	contentType := mime.TypeByExtension(filepath.Ext(filePath))
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	// Build S3 key
	relPath, err := filepath.Rel(fw.config.WatchDir, filePath)
	if err != nil {
		relPath = filepath.Base(filePath)
	}
	s3Key := filepath.Join(fw.config.S3Prefix, relPath)
	// Use forward slashes for S3 keys
	s3Key = strings.ReplaceAll(s3Key, "\\", "/")

	log.Printf("Content-Type: %s", contentType)

	// Upload with retry logic
	var lastErr error
	for attempt := 1; attempt <= fw.config.RetryAttempts; attempt++ {
		// Reset file position for retry
		if _, err := file.Seek(0, 0); err != nil {
			log.Printf("Failed to seek file: %v", err)
			return
		}

		_, err = fw.s3Client.PutObject(fw.ctx, &s3.PutObjectInput{
			Bucket:        aws.String(fw.config.S3Bucket),
			Key:           aws.String(s3Key),
			Body:          file,
			ContentLength: aws.Int64(fileInfo.Size()),
			ContentType:   aws.String(contentType),
		})

		if err == nil {
			log.Printf("Successfully uploaded %s to s3://%s/%s", filePath, fw.config.S3Bucket, s3Key)
			return
		}

		lastErr = err
		log.Printf("Upload attempt %d/%d failed: %v", attempt, fw.config.RetryAttempts, err)

		if attempt < fw.config.RetryAttempts {
			time.Sleep(time.Duration(fw.config.RetryDelayMs) * time.Millisecond)
		}
	}

	log.Printf("Failed to upload %s after %d attempts: %v", filePath, fw.config.RetryAttempts, lastErr)
}

// Stop gracefully shuts down the file watcher
func (fw *FileWatcher) Stop() {
	fw.cancelFunc()
	fw.watcher.Close()

	// Cancel all pending debounce timers
	for _, timer := range fw.debouncer {
		timer.Stop()
	}
}
