package installer

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"
	log "github.com/sirupsen/logrus"
)

const (
	resultFile = "result.json"
)

type Result struct {
	Success    bool
	Error      string
	ExecutedAt time.Time
}

// ResultHandler handles reading and writing update results
type ResultHandler struct {
	resultFile string
}

// NewResultHandler creates a new communicator with the given directory path
// The result file will be created as "result.json" in the specified directory
func NewResultHandler(installerDir string) *ResultHandler {
	// Create it if it doesn't exist
	// do not care if already exists
	_ = os.MkdirAll(installerDir, 0o700)

	rh := &ResultHandler{
		resultFile: filepath.Join(installerDir, resultFile),
	}
	return rh
}

func (rh *ResultHandler) GetErrorResultReason() string {
	result, err := rh.tryReadResult()
	if err == nil && !result.Success {
		return result.Error
	}

	if err := rh.cleanup(); err != nil {
		log.Warnf("failed to cleanup result file: %v", err)
	}

	return ""
}

func (rh *ResultHandler) WriteSuccess() error {
	result := Result{
		Success:    true,
		ExecutedAt: time.Now(),
	}
	return rh.write(result)
}

func (rh *ResultHandler) WriteErr(errReason error) error {
	result := Result{
		Success:    false,
		Error:      errReason.Error(),
		ExecutedAt: time.Now(),
	}
	return rh.write(result)
}

func (rh *ResultHandler) Watch(ctx context.Context) (Result, error) {
	log.Infof("start watching result: %s", rh.resultFile)

	// Check if file already exists (updater finished before we started watching)
	if result, err := rh.tryReadResult(); err == nil {
		log.Infof("installer result: %v", result)
		return result, nil
	}

	dir := filepath.Dir(rh.resultFile)

	if err := rh.waitForDirectory(ctx, dir); err != nil {
		return Result{}, err
	}

	return rh.watchForResultFile(ctx, dir)
}

func (rh *ResultHandler) waitForDirectory(ctx context.Context, dir string) error {
	ticker := time.NewTicker(300 * time.Millisecond)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-ticker.C:
			if info, err := os.Stat(dir); err == nil && info.IsDir() {
				return nil
			}
		}
	}
}

func (rh *ResultHandler) watchForResultFile(ctx context.Context, dir string) (Result, error) {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Error(err)
		return Result{}, err
	}

	defer func() {
		if err := watcher.Close(); err != nil {
			log.Warnf("failed to close watcher: %v", err)
		}
	}()

	if err := watcher.Add(dir); err != nil {
		return Result{}, fmt.Errorf("failed to watch directory: %v", err)
	}

	// Check again after setting up watcher to avoid race condition
	// (file could have been created between initial check and watcher setup)
	if result, err := rh.tryReadResult(); err == nil {
		log.Infof("installer result: %v", result)
		return result, nil
	}

	for {
		select {
		case <-ctx.Done():
			return Result{}, ctx.Err()
		case event, ok := <-watcher.Events:
			if !ok {
				return Result{}, errors.New("watcher closed unexpectedly")
			}

			if result, done := rh.handleWatchEvent(event); done {
				return result, nil
			}
		case err, ok := <-watcher.Errors:
			if !ok {
				return Result{}, errors.New("watcher closed unexpectedly")
			}
			return Result{}, fmt.Errorf("watcher error: %w", err)
		}
	}
}

func (rh *ResultHandler) handleWatchEvent(event fsnotify.Event) (Result, bool) {
	if event.Name != rh.resultFile {
		return Result{}, false
	}

	if event.Has(fsnotify.Create) {
		result, err := rh.tryReadResult()
		if err != nil {
			log.Debugf("error while reading result: %v", err)
			return result, true
		}
		log.Infof("installer result: %v", result)
		return result, true
	}

	return Result{}, false
}

// Write writes the update result to a file for the UI to read
func (rh *ResultHandler) write(result Result) error {
	log.Infof("write out installer result to: %s", rh.resultFile)
	// Ensure directory exists
	dir := filepath.Dir(rh.resultFile)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		log.Errorf("failed to create directory %s: %v", dir, err)
		return err
	}

	data, err := json.Marshal(result)
	if err != nil {
		return err
	}

	// Write to a temporary file first, then rename for atomic operation
	tmpPath := rh.resultFile + ".tmp"
	if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
		log.Errorf("failed to create temp file: %s", err)
		return err
	}

	// Atomic rename
	if err := os.Rename(tmpPath, rh.resultFile); err != nil {
		if cleanupErr := os.Remove(tmpPath); cleanupErr != nil {
			log.Warnf("Failed to remove temp result file: %v", err)
		}
		return err
	}

	return nil
}

func (rh *ResultHandler) cleanup() error {
	err := os.Remove(rh.resultFile)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}
		return err
	}
	log.Debugf("delete installer result file: %s", rh.resultFile)
	return nil
}

// tryReadResult attempts to read and validate the result file
func (rh *ResultHandler) tryReadResult() (Result, error) {
	data, err := os.ReadFile(rh.resultFile)
	if err != nil {
		return Result{}, err
	}

	var result Result
	if err := json.Unmarshal(data, &result); err != nil {
		return Result{}, fmt.Errorf("invalid result format: %w", err)
	}

	if err := rh.cleanup(); err != nil {
		log.Warnf("failed to cleanup result file: %v", err)
	}

	return result, nil
}
