package gost

import (
	"bufio"
	"io"
	"strings"
	"sync"
	"time"
)

// Authenticator is an interface for user authentication.
type Authenticator interface {
	Authenticate(user, password string) bool
}

// LocalAuthenticator is an Authenticator that authenticates client by local key-value pairs.
type LocalAuthenticator struct {
	kvs     map[string]string
	period  time.Duration
	stopped chan struct{}
	mux     sync.RWMutex
}

// NewLocalAuthenticator creates an Authenticator that authenticates client by local infos.
func NewLocalAuthenticator(kvs map[string]string) *LocalAuthenticator {
	return &LocalAuthenticator{
		kvs:     kvs,
		stopped: make(chan struct{}),
	}
}

// Authenticate checks the validity of the provided user-password pair.
func (au *LocalAuthenticator) Authenticate(user, password string) bool {
	if au == nil {
		return true
	}

	au.mux.RLock()
	defer au.mux.RUnlock()

	if len(au.kvs) == 0 {
		return true
	}

	v, ok := au.kvs[user]
	return ok && (v == "" || password == v)
}

// Add adds a key-value pair to the Authenticator.
func (au *LocalAuthenticator) Add(k, v string) {
	au.mux.Lock()
	defer au.mux.Unlock()
	if au.kvs == nil {
		au.kvs = make(map[string]string)
	}
	au.kvs[k] = v
}

// Reload parses config from r, then live reloads the Authenticator.
func (au *LocalAuthenticator) Reload(r io.Reader) error {
	var period time.Duration
	kvs := make(map[string]string)

	if r == nil || au.Stopped() {
		return nil
	}

	// splitLine splits a line text by white space.
	// A line started with '#' will be ignored, otherwise it is valid.
	split := func(line string) []string {
		if line == "" {
			return nil
		}
		line = strings.Replace(line, "\t", " ", -1)
		line = strings.TrimSpace(line)

		if strings.IndexByte(line, '#') == 0 {
			return nil
		}

		var ss []string
		for _, s := range strings.Split(line, " ") {
			if s = strings.TrimSpace(s); s != "" {
				ss = append(ss, s)
			}
		}
		return ss
	}

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		ss := split(line)
		if len(ss) == 0 {
			continue
		}

		switch ss[0] {
		case "reload": // reload option
			if len(ss) > 1 {
				period, _ = time.ParseDuration(ss[1])
			}
		default:
			var k, v string
			k = ss[0]
			if len(ss) > 1 {
				v = ss[1]
			}
			kvs[k] = v
		}
	}

	if err := scanner.Err(); err != nil {
		return err
	}

	au.mux.Lock()
	defer au.mux.Unlock()

	au.period = period
	au.kvs = kvs

	return nil
}

// Period returns the reload period.
func (au *LocalAuthenticator) Period() time.Duration {
	if au.Stopped() {
		return -1
	}

	au.mux.RLock()
	defer au.mux.RUnlock()

	return au.period
}

// Stop stops reloading.
func (au *LocalAuthenticator) Stop() {
	select {
	case <-au.stopped:
	default:
		close(au.stopped)
	}
}

// Stopped checks whether the reloader is stopped.
func (au *LocalAuthenticator) Stopped() bool {
	select {
	case <-au.stopped:
		return true
	default:
		return false
	}
}
