package main

import (
	"bytes"
	"encoding/csv"
	"errors"
	"fmt"
	"regexp"
	"sort"
	"strings"
	"sync"

	log "github.com/sirupsen/logrus"
)

type BoxZfs struct {
	filesystems map[string]*ZfsFs
	box         *Box
	online      bool
	mx          sync.Mutex
}

type ZfsFs struct {
	path      string
	managed   bool
	backedUp  bool
	zfs       *BoxZfs
	snapshots map[string]*ZfsSnapshot
	srcApps   []*App
	destApps  []*App
	mx        sync.Mutex
}

type ZfsSnapshot struct {
	name string
	fs   *ZfsFs
}

func (z *BoxZfs) Open() error {
	log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")

	z.mx.Lock()
	defer z.mx.Unlock()

	if z.online {
		return nil
	}

	z.filesystems = make(map[string]*ZfsFs)

	zfsList, err := z.box.Exec("zfs list -H -t filesystem -o name,mountpoint")
	if err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t filesystem -o name,mountpoint", "error": err}).Errorf("")
		return err
	}

	csvReader := csv.NewReader(bytes.NewBufferString(zfsList))
	csvReader.Comma = '\t'
	csvReader.FieldsPerRecord = 2

	csvData, err := csvReader.ReadAll()
	if err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
		return err
	}

	for _, rec := range csvData {
		log.WithFields(log.Fields{"name": z.box.name, "zfs-name": rec[0], "zfs-mount": rec[1]}).Debugf("zfs list -t filesystem")
		if rec[1] != "legacy" {
			fs := &ZfsFs{
				path:      rec[0],
				zfs:       z,
				snapshots: make(map[string]*ZfsSnapshot),
			}
			z.filesystems[rec[0]] = fs
			log.WithFields(log.Fields{"name": z.box.name, "fs": rec[0]}).Infof("new filesystem")
		}
	}

	log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll"}).Infof("")

	zfsList, err = z.box.Exec("zfs list -H -t snapshot -o name")
	if err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t snapshot -o name", "error": err}).Errorf("")
		return err
	}

	csvReader = csv.NewReader(bytes.NewBufferString(zfsList))
	csvReader.Comma = '\t'
	csvReader.FieldsPerRecord = 1

	csvData, err = csvReader.ReadAll()
	if err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
		return err
	}

	for _, rec := range csvData {
		log.WithFields(log.Fields{"name": z.box.name, "zfs-snapshot": rec[0]}).Debugf("zfs list -t snapshot")

		s := strings.Split(rec[0], `@`)
		if fs, ok := z.filesystems[s[0]]; ok {
			snap := &ZfsSnapshot{
				name: s[1],
				fs:   fs,
			}
			fs.snapshots[s[1]] = snap
			log.WithFields(log.Fields{"name": z.box.name, "fs": s[0], "snapshot": s[1]}).Infof("new snapshot")
		}
	}

	zfsList, err = z.box.Exec("zfs get -H -o name,value " + zfsManagedPropertyName)
	if err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs get -H -o name,value,source " + zfsManagedPropertyName, "error": err}).Errorf("")
		return err
	}

	csvReader = csv.NewReader(bytes.NewBufferString(zfsList))
	csvReader.Comma = '\t'
	csvReader.FieldsPerRecord = 2

	csvData, err = csvReader.ReadAll()
	if err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
		return err
	}

	for _, rec := range csvData {
		log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Debugf("zfs get " + zfsManagedPropertyName)

		if fs, ok := z.filesystems[rec[0]]; ok {
			if rec[1] == "+" {
				fs.managed = true
				log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Infof("managed fs")
			}
		}
	}

	z.online = true

	return nil
}

func (z *BoxZfs) Close() error {
	log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")

	z.mx.Lock()
	defer z.mx.Unlock()

	for _, fs := range z.filesystems {
		fs.mx.Lock()
		defer fs.mx.Unlock()
	}

	z.online = false

	return nil
}

func (z *BoxZfs) Mkdir(path string) error {
	log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")

	if !z.online {
		err := errors.New("zfs offline")
		log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("")
		return err
	}

	z.mx.Lock()
	defer z.mx.Unlock()

	b := z.box
	if !b.online {
		err := errors.New("box offline")
		log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("")
		return err
	}

	if _, ok := z.filesystems[path]; ok {
		return nil
	}

	if _, err := b.Exec(fmt.Sprintf("zfs create -p %s", path)); err != nil {
		log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "error": err}).Errorf("")
		return err
	}

	newPath := ""
	for _, p := range strings.Split(path, "/") {
		if newPath == "" {
			newPath = p
		} else {
			newPath = newPath + "/" + p
		}

		if _, ok := z.filesystems[newPath]; !ok {
			fs := &ZfsFs{
				path:      newPath,
				managed:   false,
				zfs:       z,
				snapshots: make(map[string]*ZfsSnapshot),
				srcApps:   make([]*App, 0),
				destApps:  make([]*App, 0),
			}
			z.filesystems[newPath] = fs
		}

	}

	return nil
}

func (fs *ZfsFs) TakeSnapshot(name string) (*ZfsSnapshot, error) {
	log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting")
	defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done")

	if !fs.zfs.online {
		err := errors.New("zfs offline")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return nil, err
	}

	re := regexp.MustCompile(`^[a-zA-Z0-9\-\._]{1,255}$`)
	if !re.MatchString(name) {
		err := errors.New("unsupported name")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return nil, err
	}

	fs.mx.Lock()
	defer fs.mx.Unlock()

	if _, ok := fs.snapshots[name]; ok {
		err := errors.New("already exists")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return nil, err
	}

	if _, err := fs.zfs.box.Exec("zfs snapshot " + fs.path + "@" + name); err != nil {
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return nil, err
	}

	s := &ZfsSnapshot{
		name: name,
		fs:   fs,
	}

	fs.snapshots[name] = s

	return s, nil
}

func (fs *ZfsFs) DelSnapshot(name string) error {
	log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting")
	defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done")

	if !fs.zfs.online {
		err := errors.New("zfs offline")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return err
	}

	fs.mx.Lock()
	defer fs.mx.Unlock()

	if _, ok := fs.snapshots[name]; !ok {
		err := errors.New("doesn't exist")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return err
	}

	log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("zfs destroy " + fs.path + "@" + name)

	if _, err := fs.zfs.box.Exec("zfs destroy " + fs.path + "@" + name); err != nil {
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
		return err
	}

	delete(fs.snapshots, name)

	return nil

}

func (fs *ZfsFs) AddSnapshot(s *ZfsSnapshot) error {
	log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("done")

	if !fs.zfs.online {
		err := errors.New("zfs offline")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("")
		return err
	}

	fs.mx.Lock()
	defer fs.mx.Unlock()

	if _, ok := fs.snapshots[s.name]; ok {
		err := errors.New("already exist")
		log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("")
		return err
	}

	fs.snapshots[s.name] = s

	return nil
}

func (fs *ZfsFs) ValidSnapshots() []*ZfsSnapshot {
	tab := make([]*ZfsSnapshot, 0)

	for _, s := range fs.snapshots {
		if s.Valid() {
			tab = append(tab, s)
		}
	}

	sort.Slice(tab, func(i, j int) bool {
		ti, _ := tab[i].Timestamp()
		tj, _ := tab[j].Timestamp()
		return ti.Before(tj)
	})

	return tab
}
