// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package watch

import (
	"context"
	"fmt"

	consulapi "github.com/hashicorp/consul/api"
)

// watchFactory is a function that can create a new WatchFunc
// from a parameter configuration
type watchFactory func(params map[string]interface{}) (WatcherFunc, error)

// watchFuncFactory maps each type to a factory function
var watchFuncFactory map[string]watchFactory

func init() {
	watchFuncFactory = map[string]watchFactory{
		"key":           keyWatch,
		"keyprefix":     keyPrefixWatch,
		"services":      servicesWatch,
		"nodes":         nodesWatch,
		"service":       serviceWatch,
		"checks":        checksWatch,
		"event":         eventWatch,
		"connect_roots": connectRootsWatch,
		"connect_leaf":  connectLeafWatch,
		"agent_service": agentServiceWatch,
	}
}

// keyWatch is used to return a key watching function
func keyWatch(params map[string]interface{}) (WatcherFunc, error) {
	stale := false
	if err := assignValueBool(params, "stale", &stale); err != nil {
		return nil, err
	}

	var key string
	if err := assignValue(params, "key", &key); err != nil {
		return nil, err
	}
	if key == "" {
		return nil, fmt.Errorf("Must specify a single key to watch")
	}
	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		kv := p.client.KV()
		opts := makeQueryOptionsWithContext(p, stale)
		defer p.cancelFunc()
		pair, meta, err := kv.Get(key, &opts)
		if err != nil {
			return nil, nil, err
		}
		if pair == nil {
			return WaitIndexVal(meta.LastIndex), nil, err
		}
		return WaitIndexVal(meta.LastIndex), pair, err
	}
	return fn, nil
}

// keyPrefixWatch is used to return a key prefix watching function
func keyPrefixWatch(params map[string]interface{}) (WatcherFunc, error) {
	stale := false
	if err := assignValueBool(params, "stale", &stale); err != nil {
		return nil, err
	}

	var prefix string
	if err := assignValue(params, "prefix", &prefix); err != nil {
		return nil, err
	}
	if prefix == "" {
		return nil, fmt.Errorf("Must specify a single prefix to watch")
	}
	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		kv := p.client.KV()
		opts := makeQueryOptionsWithContext(p, stale)
		defer p.cancelFunc()
		pairs, meta, err := kv.List(prefix, &opts)
		if err != nil {
			return nil, nil, err
		}
		return WaitIndexVal(meta.LastIndex), pairs, err
	}
	return fn, nil
}

// servicesWatch is used to watch the list of available services
func servicesWatch(params map[string]interface{}) (WatcherFunc, error) {
	stale := false
	filter := ""
	if err := assignValueBool(params, "stale", &stale); err != nil {
		return nil, err
	}
	if err := assignValue(params, "filter", &filter); err != nil {
		return nil, err
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		catalog := p.client.Catalog()
		opts := makeQueryOptionsWithContext(p, stale)
		if filter != "" {
			opts.Filter = filter
		}
		defer p.cancelFunc()
		services, meta, err := catalog.Services(&opts)
		if err != nil {
			return nil, nil, err
		}
		return WaitIndexVal(meta.LastIndex), services, err
	}
	return fn, nil
}

// nodesWatch is used to watch the list of available nodes
func nodesWatch(params map[string]interface{}) (WatcherFunc, error) {
	stale := false
	filter := ""
	if err := assignValueBool(params, "stale", &stale); err != nil {
		return nil, err
	}
	if err := assignValue(params, "filter", &filter); err != nil {
		return nil, err
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		catalog := p.client.Catalog()
		opts := makeQueryOptionsWithContext(p, stale)
		if filter != "" {
			opts.Filter = filter
		}
		defer p.cancelFunc()
		nodes, meta, err := catalog.Nodes(&opts)
		if err != nil {
			return nil, nil, err
		}
		return WaitIndexVal(meta.LastIndex), nodes, err
	}
	return fn, nil
}

// serviceWatch is used to watch a specific service for changes
func serviceWatch(params map[string]interface{}) (WatcherFunc, error) {
	stale := false
	filter := ""
	if err := assignValueBool(params, "stale", &stale); err != nil {
		return nil, err
	}
	if err := assignValue(params, "filter", &filter); err != nil {
		return nil, err
	}

	var (
		service string
		tags    []string
	)
	if err := assignValue(params, "service", &service); err != nil {
		return nil, err
	}
	if service == "" {
		return nil, fmt.Errorf("Must specify a single service to watch")
	}
	if err := assignValueStringSlice(params, "tag", &tags); err != nil {
		return nil, err
	}

	passingOnly := false
	if err := assignValueBool(params, "passingonly", &passingOnly); err != nil {
		return nil, err
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		health := p.client.Health()
		opts := makeQueryOptionsWithContext(p, stale)
		if filter != "" {
			opts.Filter = filter
		}
		defer p.cancelFunc()
		nodes, meta, err := health.ServiceMultipleTags(service, tags, passingOnly, &opts)
		if err != nil {
			return nil, nil, err
		}
		return WaitIndexVal(meta.LastIndex), nodes, err
	}
	return fn, nil
}

// checksWatch is used to watch a specific checks in a given state
func checksWatch(params map[string]interface{}) (WatcherFunc, error) {
	stale := false
	if err := assignValueBool(params, "stale", &stale); err != nil {
		return nil, err
	}

	var service, state, filter string
	if err := assignValue(params, "service", &service); err != nil {
		return nil, err
	}
	if err := assignValue(params, "state", &state); err != nil {
		return nil, err
	}
	if err := assignValue(params, "filter", &filter); err != nil {
		return nil, err
	}
	if service != "" && state != "" {
		return nil, fmt.Errorf("Cannot specify service and state")
	}
	if service == "" && state == "" {
		state = "any"
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		health := p.client.Health()
		opts := makeQueryOptionsWithContext(p, stale)
		defer p.cancelFunc()
		var checks []*consulapi.HealthCheck
		var meta *consulapi.QueryMeta
		var err error
		if filter != "" {
			opts.Filter = filter
		}
		if state != "" {
			checks, meta, err = health.State(state, &opts)
		} else {
			checks, meta, err = health.Checks(service, &opts)
		}
		if err != nil {
			return nil, nil, err
		}
		return WaitIndexVal(meta.LastIndex), checks, err
	}
	return fn, nil
}

// eventWatch is used to watch for events, optionally filtering on name
func eventWatch(params map[string]interface{}) (WatcherFunc, error) {
	// The stale setting doesn't apply to events.

	var name string
	if err := assignValue(params, "name", &name); err != nil {
		return nil, err
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		event := p.client.Event()
		opts := makeQueryOptionsWithContext(p, false)
		defer p.cancelFunc()
		events, meta, err := event.List(name, &opts)
		if err != nil {
			return nil, nil, err
		}

		// Prune to only the new events
		for i := 0; i < len(events); i++ {
			if WaitIndexVal(event.IDToIndex(events[i].ID)).Equal(p.lastParamVal) {
				events = events[i+1:]
				break
			}
		}
		return WaitIndexVal(meta.LastIndex), events, err
	}
	return fn, nil
}

// connectRootsWatch is used to watch for changes to Connect Root certificates.
func connectRootsWatch(params map[string]interface{}) (WatcherFunc, error) {
	// We don't support stale since roots are cached locally in the agent.

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		agent := p.client.Agent()
		opts := makeQueryOptionsWithContext(p, false)
		defer p.cancelFunc()

		roots, meta, err := agent.ConnectCARoots(&opts)
		if err != nil {
			return nil, nil, err
		}

		return WaitIndexVal(meta.LastIndex), roots, err
	}
	return fn, nil
}

// connectLeafWatch is used to watch for changes to Connect Leaf certificates
// for given local service id.
func connectLeafWatch(params map[string]interface{}) (WatcherFunc, error) {
	// We don't support stale since certs are cached locally in the agent.

	var serviceName string
	if err := assignValue(params, "service", &serviceName); err != nil {
		return nil, err
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		agent := p.client.Agent()
		opts := makeQueryOptionsWithContext(p, false)
		defer p.cancelFunc()

		leaf, meta, err := agent.ConnectCALeaf(serviceName, &opts)
		if err != nil {
			return nil, nil, err
		}

		return WaitIndexVal(meta.LastIndex), leaf, err
	}
	return fn, nil
}

// agentServiceWatch is used to watch for changes to a single service instance
// on the local agent. Note that this state is agent-local so the watch
// mechanism uses `hash` rather than `index` for deciding whether to block.
func agentServiceWatch(params map[string]interface{}) (WatcherFunc, error) {
	// We don't support consistency modes since it's agent local data

	var serviceID string
	if err := assignValue(params, "service_id", &serviceID); err != nil {
		return nil, err
	}

	fn := func(p *Plan) (BlockingParamVal, interface{}, error) {
		agent := p.client.Agent()
		opts := makeQueryOptionsWithContext(p, false)
		defer p.cancelFunc()

		svc, _, err := agent.Service(serviceID, &opts)
		if err != nil {
			return nil, nil, err
		}

		// Return string ContentHash since we don't have Raft indexes to block on.
		return WaitHashVal(svc.ContentHash), svc, err
	}
	return fn, nil
}

func makeQueryOptionsWithContext(p *Plan, stale bool) consulapi.QueryOptions {
	ctx, cancel := context.WithCancel(context.Background())
	p.setCancelFunc(cancel)
	opts := consulapi.QueryOptions{AllowStale: stale}
	switch param := p.lastParamVal.(type) {
	case WaitIndexVal:
		opts.WaitIndex = uint64(param)
	case WaitHashVal:
		opts.WaitHash = string(param)
	}
	return *opts.WithContext(ctx)
}
