package probing

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"sync"
	"time"
)

var (
	ErrNotFound = errors.New("probing: id not found")
	ErrExist    = errors.New("probing: id exists")
)

type Prober interface {
	AddHTTP(id string, probingInterval time.Duration, endpoints []string) error
	Remove(id string) error
	RemoveAll()
	Reset(id string) error
	Status(id string) (Status, error)
}

type prober struct {
	mu      sync.Mutex
	targets map[string]*status
	tr      http.RoundTripper
}

func NewProber(tr http.RoundTripper) Prober {
	p := &prober{targets: make(map[string]*status)}
	if tr == nil {
		p.tr = http.DefaultTransport
	} else {
		p.tr = tr
	}
	return p
}

func (p *prober) AddHTTP(id string, probingInterval time.Duration, endpoints []string) error {
	p.mu.Lock()
	defer p.mu.Unlock()
	if _, ok := p.targets[id]; ok {
		return ErrExist
	}

	s := &status{stopC: make(chan struct{})}
	p.targets[id] = s

	ticker := time.NewTicker(probingInterval)

	go func() {
		pinned := 0
		for {
			select {
			case <-ticker.C:
				start := time.Now()
				req, err := http.NewRequest("GET", endpoints[pinned], nil)
				if err != nil {
					panic(err)
				}
				resp, err := p.tr.RoundTrip(req)
				if err == nil && resp.StatusCode != http.StatusOK {
					err = fmt.Errorf("got unexpected HTTP status code %s from %s", resp.Status, endpoints[pinned])
					resp.Body.Close()
				}
				if err != nil {
					s.recordFailure(err)
					pinned = (pinned + 1) % len(endpoints)
					continue
				}

				var hh Health
				d := json.NewDecoder(resp.Body)
				err = d.Decode(&hh)
				resp.Body.Close()
				if err != nil || !hh.OK {
					s.recordFailure(err)
					pinned = (pinned + 1) % len(endpoints)
					continue
				}

				s.record(time.Since(start), hh.Now)
			case <-s.stopC:
				ticker.Stop()
				return
			}
		}
	}()

	return nil
}

func (p *prober) Remove(id string) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	s, ok := p.targets[id]
	if !ok {
		return ErrNotFound
	}
	close(s.stopC)
	delete(p.targets, id)
	return nil
}

func (p *prober) RemoveAll() {
	p.mu.Lock()
	defer p.mu.Unlock()

	for _, s := range p.targets {
		close(s.stopC)
	}
	p.targets = make(map[string]*status)
}

func (p *prober) Reset(id string) error {
	p.mu.Lock()
	defer p.mu.Unlock()

	s, ok := p.targets[id]
	if !ok {
		return ErrNotFound
	}
	s.reset()
	return nil
}

func (p *prober) Status(id string) (Status, error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	s, ok := p.targets[id]
	if !ok {
		return nil, ErrNotFound
	}
	return s, nil
}
