package health

import (
	"context"
	"sync"
	"time"
)

type Health struct {
	liveChecks    []Checker
	readyChecks   []Checker
	maxConcurrent int
}

func New(opts ...Option) *Health {
	cfg := newConfig(opts...)
	return &Health{
		liveChecks:    cfg.liveChecks,
		readyChecks:   cfg.readyChecks,
		maxConcurrent: cfg.maxConcurrent,
	}
}

type Status string

const (
	StatusPass Status = "pass"
	StatusFail Status = "fail"
)

func (s Status) String() string {
	return string(s)
}

type SingleCheckResult struct {
	Status Status
	Err    error
	Time   time.Time
}

type Result struct {
	Status Status
	Checks map[string][]SingleCheckResult
}

func (h *Health) CheckLive(ctx context.Context) Result {
	return h.check(ctx, h.liveChecks)
}

func (h *Health) CheckReady(ctx context.Context) Result {
	return h.check(ctx, h.readyChecks)
}

type singleCheckResultWithName struct {
	name string
	SingleCheckResult
}

func (h *Health) check(ctx context.Context, checks []Checker) Result {
	limiterCh := make(chan struct{}, h.maxConcurrent)
	checkCh := make(chan singleCheckResultWithName)
	var wg sync.WaitGroup

	for _, ch := range checks {
		wg.Add(1)

		go func(ch Checker) {
			defer wg.Done()

			limiterCh <- struct{}{}
			defer func() {
				<-limiterCh
			}()

			check := SingleCheckResult{
				Status: StatusPass,
				Time:   time.Now(),
			}

			if err := ch.Check(ctx); err != nil {
				check.Status = StatusFail
				check.Err = err
			}

			checkCh <- singleCheckResultWithName{
				name:              ch.Name(),
				SingleCheckResult: check,
			}
		}(ch)
	}

	go func() {
		wg.Wait()
		close(limiterCh)
		close(checkCh)
	}()

	res := Result{
		Status: StatusPass,
		Checks: make(map[string][]SingleCheckResult, len(checks)),
	}

	for check := range checkCh {
		if check.Status == StatusFail {
			res.Status = StatusFail
		}

		res.Checks[check.name] = append(res.Checks[check.name], check.SingleCheckResult)
	}

	return res
}
