package store

import (
	"encoding/json"
	"io"
	"strings"

	"github.com/gogo/protobuf/proto"
	"github.com/hashicorp/raft"
	"github.com/sjy-dv/scdb/scdb/pkg/log"
	"github.com/sjy-dv/scdb/scdb/pkg/protobuf/scdbpb"
)

type fsm struct {
	db DB
}

func NewFSM(path string) (*fsm, error) {
	db, err := NewSCDB(path)
	if err != nil {
		return nil, err
	}
	return &fsm{
		db: db,
	}, nil
}

func (f *fsm) Get(key string) (string, error) {
	val, err := f.db.Get([]byte(key))
	if err != nil {
		return "", err
	}
	return string(val), nil
}

func (f *fsm) Apply(l *raft.Log) interface{} {
	var c command
	if err := json.Unmarshal(l.Data, &c); err != nil {
		panic("failed to unmarshal raft log")
	}
	switch strings.ToLower(c.Op) {
	case "set":
		return f.applySet(c.Key, c.Value)
	case "delete":
		return f.applyDelete(c.Key)
	default:
		panic("command type not support")
	}
}

// snapshot will be called when make snapshot, generate fsmsnapshot
func (f *fsm) Snapshot() (raft.FSMSnapshot, error) {
	log.Info("Generate FSMSnapshot")
	return &fsmSnapshot{
		db: f.db,
	}, nil
}

// Restore Cluster FSMSnapshot
func (f *fsm) Restore(rc io.ReadCloser) error {
	log.Info("Restore snapshot from FSMSnapshot")
	defer rc.Close()
	var (
		readBuf  []byte
		protoBuf *proto.Buffer
		err      error
		keyCount int = 0
	)
	log.Info("Starting Read all data...")
	if readBuf, err = io.ReadAll(rc); err != nil {
		log.Info("SnapShot restore failed")
		return err
	}
	protoBuf = proto.NewBuffer(readBuf)
	log.Infof("new protoBuf length %d bytes", len(protoBuf.Bytes()))
	for {
		item := &scdbpb.RaftKVItem{}
		if err = protoBuf.DecodeMessage(item); err == io.ErrUnexpectedEOF {
			break
		}
		if err != nil {
			log.Infof("Decode Message failed: %v", err)
			return err
		}
		log.Infof("Set key %v to %v count : %d", item.Key, item.Value, keyCount)
		err = f.db.Set(item.Key, item.Value)
		if err != nil {
			log.Warnf("Snapshot load failed: %v", err)
			return err
		}
		keyCount++
	}
	log.Infof("Restore total %d keys", keyCount)
	return nil
}

func (f *fsm) applySet(key, value string) interface{} {
	log.Infof("apply %s to %s\n", key, value)
	err := f.db.Set([]byte(key), []byte(value))
	return err
}

func (f *fsm) applyDelete(key string) interface{} {
	err := f.db.Delete([]byte(key))
	return err
}

func (f *fsm) Close() error {
	f.db.Close()
	return nil
}
