package snapshot

import (
	"encoding/csv"
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/ethereum/go-ethereum/core/state"
	"github.com/ethereum/go-ethereum/trie"
	log "github.com/sirupsen/logrus"

	iter "github.com/vulcanize/go-eth-state-node-iterator"
)

type trackedIter struct {
	trie.NodeIterator
	tracker *iteratorTracker
}

func (it *trackedIter) Next(descend bool) bool {
	ret := it.NodeIterator.Next(descend)
	if !ret {
		it.tracker.stopChan <- it
	}
	return ret
}

type iteratorTracker struct {
	recoveryFile string

	startChan chan *trackedIter
	stopChan  chan *trackedIter
	started   map[*trackedIter]struct{}
	stopped   []*trackedIter

	haltChan chan struct{}
	done     chan struct{}
}

func newTracker(file string, buf int) iteratorTracker {
	return iteratorTracker{
		recoveryFile: file,
		startChan:    make(chan *trackedIter, buf),
		stopChan:     make(chan *trackedIter, buf),
		started:      map[*trackedIter]struct{}{},
		haltChan:     make(chan struct{}),
		done:         make(chan struct{}),
	}
}

func (tr *iteratorTracker) captureSignal() {
	sigChan := make(chan os.Signal, 1)

	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		sig := <-sigChan
		log.Errorf("Signal received (%v), stopping", sig)
		tr.haltAndDump()
		os.Exit(1)
	}()
}

// listens for starts/stops and manages current state
func (tr *iteratorTracker) run() {
loop:
	for {
		select {
		case start := <-tr.startChan:
			tr.started[start] = struct{}{}
		case stop := <-tr.stopChan:
			tr.stopped = append(tr.stopped, stop)
		case <-tr.haltChan:
			break loop
		default:
		}
	}
	tr.done <- struct{}{}
}

func (tr *iteratorTracker) tracked(it trie.NodeIterator) (ret *trackedIter) {
	ret = &trackedIter{it, tr}
	tr.startChan <- ret
	return
}

// dumps iterator path and bounds to a text file so it can be restored later
func (tr *iteratorTracker) dump() error {
	log.Info("Dumping recovery state to: ", tr.recoveryFile)
	var rows [][]string
	for it, _ := range tr.started {
		var endPath []byte
		if impl, ok := it.NodeIterator.(*iter.PrefixBoundIterator); ok {
			endPath = impl.EndPath
		}
		rows = append(rows, []string{
			fmt.Sprintf("%x", it.Path()),
			fmt.Sprintf("%x", endPath),
		})
	}
	file, err := os.Create(tr.recoveryFile)
	if err != nil {
		return err
	}
	defer file.Close()
	out := csv.NewWriter(file)
	return out.WriteAll(rows)
}

// attempts to read iterator state from file
// if file doesn't exist, returns an empty slice with no error
func (tr *iteratorTracker) restore(tree state.Trie) ([]trie.NodeIterator, error) {
	file, err := os.Open(tr.recoveryFile)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	defer file.Close()
	in := csv.NewReader(file)
	in.FieldsPerRecord = 2
	rows, err := in.ReadAll()
	if err != nil {
		return nil, err
	}
	var ret []trie.NodeIterator
	for _, row := range rows {
		// pick up where each interval left off
		var paths [2][]byte
		for i, val := range row {
			if len(val) != 0 {
				if _, err = fmt.Sscanf(val, "%x", &paths[i]); err != nil {
					return nil, err
				}
			}
		}

		it := iter.NewPrefixBoundIterator(tree, paths[0], paths[1])
		ret = append(ret, tr.tracked(it))
	}
	return ret, nil
}

func (tr *iteratorTracker) haltAndDump() error {
	tr.haltChan <- struct{}{}
	<-tr.done

	// drain any pending events
	close(tr.startChan)
	for start := range tr.startChan {
		tr.started[start] = struct{}{}
	}
	close(tr.stopChan)
	for stop := range tr.stopChan {
		tr.stopped = append(tr.stopped, stop)
	}

	for _, stop := range tr.stopped {
		delete(tr.started, stop)
	}

	if len(tr.started) == 0 {
		// if the tracker state is empty, erase any existing recovery file
		err := os.Remove(tr.recoveryFile)
		if os.IsNotExist(err) {
			err = nil
		}
		return err
	}
	return tr.dump()
}
