package raft

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"time"

	"github.com/hashicorp/raft"
	clientv3 "go.etcd.io/etcd/client/v3"
)

type FSM struct {
	store  *Store
	client *clientv3.Client
}

func NewFSM(endpoints []string, store *Store) (*FSM, error) {
	// Se connecter au cluster etcd
	client, err := clientv3.New(clientv3.Config{
		Endpoints:   endpoints,
		DialTimeout: 5 * time.Second,
	})
	if err != nil {
		return nil, err
	}
	return &FSM{
		store:  store,
		client: client,
	}, nil
}

// Apply applies a Raft log entry to the key-value store.
func (f *FSM) Apply(l *raft.Log) interface{} {
	switch l.Type {
	case raft.LogCommand:
		var c command
		if err := json.Unmarshal(l.Data, &c); err != nil {
			panic(fmt.Sprintf("failed to unmarshal command: %s", err.Error()))
		}

		switch c.Op {
		case "set":
			f.applySet(c.Key, c.Value)
		case "delete":
			f.applyDelete(c.Key)
		default:
			panic(fmt.Sprintf("unrecognized command op: %s", c.Op))
		}

		// On réplique sur etcd si ce n'est pas une reprise des logs et si le noeud est leader
		if l.Index > f.store.lastIndex && f.store.Raft.State() == raft.Leader {
			regex := regexp.MustCompile(`^/domain`)
			match := regex.MatchString(c.Key)

			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			if match {
				switch c.Op {
				case "set":
					f.client.Put(ctx, fmt.Sprintf("/deevirt/cluster/%s%s", f.store.conf.ClusterID, c.Key), string(c.Value))
				case "delete":
					f.client.Delete(ctx, fmt.Sprintf("/deevirt/cluster/%s%s", f.store.conf.ClusterID, c.Key))
				}
			}
			defer cancel()
		}
	default:
		println(l.Type.String())
	}

	return nil
}

// Snapshot returns a snapshot of the key-value store.
func (f *FSM) Snapshot() (raft.FSMSnapshot, error) {
	f.store.mu.Lock()
	defer f.store.mu.Unlock()

	// Clone the map.
	o := make(map[string][]byte)
	for k, v := range f.store.m {
		o[k] = v
	}

	return &fsmSnapshot{store: o}, nil
}

// Restore stores the key-value store to a previous state.
func (f *FSM) Restore(rc io.ReadCloser) error {
	o := make(map[string][]byte)

	if err := json.NewDecoder(rc).Decode(&o); err != nil {
		return err
	}

	// Set the state from the snapshot, no lock required according to
	// Hashicorp docs.
	f.store.m = o
	return nil
}

func (f *FSM) applySet(key string, value []byte) interface{} {
	f.store.mu.Lock()
	defer f.store.mu.Unlock()
	f.store.m[key] = value
	return nil
}

func (f *FSM) applyDelete(key string) interface{} {
	f.store.mu.Lock()
	defer f.store.mu.Unlock()
	delete(f.store.m, key)
	return nil
}

type fsmSnapshot struct {
	store map[string][]byte
}

func (f *fsmSnapshot) Persist(sink raft.SnapshotSink) error {
	err := func() error {
		// Encode data.
		b, err := json.Marshal(f.store)
		if err != nil {
			return err
		}

		// Write data to sink.
		if _, err := sink.Write(b); err != nil {
			return err
		}

		// Close the sink.
		return sink.Close()
	}()

	if err != nil {
		sink.Cancel()
	}

	return err
}

func (f *fsmSnapshot) Release() {
}