package healthfile

import (
	"context"
	"errors"
	"os"
	"sync"
	"sync/atomic"
	"time"

	"gitea.dwysokinski.me/twhelp/corev3/internal/health"
)

type checkFunc func(ctx context.Context) health.Result

type Observer struct {
	runningMu     sync.Mutex
	wg            sync.WaitGroup
	running       atomic.Int32
	closeCh       chan struct{}
	path          string
	check         checkFunc
	checkInterval time.Duration
	checkTimeout  time.Duration
}

type Option func(o *Observer)

func WithCheckInterval(interval time.Duration) Option {
	return func(o *Observer) {
		o.checkInterval = interval
	}
}

func WithCheckTimeout(timeout time.Duration) Option {
	return func(o *Observer) {
		o.checkTimeout = timeout
	}
}

func LiveObserver(h *health.Health, path string, opts ...Option) *Observer {
	return newObserver(path, h.CheckLive, opts...)
}

func ReadyObserver(h *health.Health, path string, opts ...Option) *Observer {
	return newObserver(path, h.CheckReady, opts...)
}

const (
	checkTimeoutDefault  = 5 * time.Second
	checkIntervalDefault = 15 * time.Second
)

func newObserver(path string, check checkFunc, opts ...Option) *Observer {
	o := &Observer{
		path:          path,
		check:         check,
		checkTimeout:  checkTimeoutDefault,
		checkInterval: checkIntervalDefault,
	}

	for _, opt := range opts {
		opt(o)
	}

	return o
}

var ErrAlreadyRunning = errors.New("observer is already running")

func (o *Observer) Run(ctx context.Context) error {
	o.runningMu.Lock()
	if !o.running.CompareAndSwap(0, 1) {
		o.runningMu.Unlock()
		return ErrAlreadyRunning
	}
	o.wg.Add(1)
	o.closeCh = make(chan struct{})
	o.runningMu.Unlock()
	defer func() {
		o.running.Add(-1)
		o.wg.Done()
	}()

	ticker := time.NewTicker(o.checkInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-o.closeCh:
			return nil
		default:
		}

		o.tick(ctx)

		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-o.closeCh:
			return nil
		case <-ticker.C:
		}
	}
}

const filePerm = 0644

func (o *Observer) tick(parent context.Context) {
	ctx, cancel := context.WithTimeout(parent, o.checkTimeout)
	defer cancel()

	res := o.check(ctx)
	fileExists := checkFileExists(o.path)

	switch {
	case res.Status == health.StatusPass && !fileExists:
		_ = os.WriteFile(o.path, []byte("pass"), filePerm)
	case res.Status == health.StatusFail && fileExists:
		_ = os.Remove(o.path)
	}
}

func (o *Observer) Path() string {
	return o.path
}

func (o *Observer) Close() error {
	o.runningMu.Lock()
	defer o.runningMu.Unlock()

	if o.running.Load() == 0 {
		return nil
	}

	close(o.closeCh)
	o.wg.Wait()

	return nil
}

func checkFileExists(filePath string) bool {
	_, err := os.Stat(filePath)
	return !errors.Is(err, os.ErrNotExist)
}
