package main

import (
	"errors"
	"regexp"
	"strings"
	"time"

	log "github.com/sirupsen/logrus"
)

type App struct {
	name         string
	schedule     map[string]struct{}
	sources      []Addr
	destinations []Addr
	before       map[string]Addr
	after        map[string]Addr
}

func (c *Config) NewApp(name string, sources, destinations, schedule []string, before, after map[string]string) (*App, error) {
	log.WithFields(log.Fields{"name": name}).Debugf("starting")
	defer log.WithFields(log.Fields{"name": name}).Debugf("done")

	a := &App{
		name:         name,
		sources:      make([]Addr, 0),
		destinations: make([]Addr, 0),
		schedule:     make(map[string]struct{}, 0),
		before:       make(map[string]Addr),
		after:        make(map[string]Addr),
	}

	for _, v := range sources {
		src := Addr(v)
		if src.Box() == "" {
			err := errors.New("source box incorrect")
			log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		if _, ok := c.box[src.Box()]; !ok {
			err := errors.New("source box doesn't exist")
			log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		if src.Path() == "" {
			err := errors.New("source path incorrect")
			log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		a.sources = append(a.sources, src)
	}

	for _, v := range destinations {
		dest := Addr(v)
		if dest.Box() == "" {
			err := errors.New("destination box incorrect")
			log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		if _, ok := c.box[dest.Box()]; !ok {
			err := errors.New("destination box doesn't exist")
			log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		if dest.Path() == "" {
			err := errors.New("destination path incorrect")
			log.WithFields(log.Fields{"app": name, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		a.destinations = append(a.destinations, dest)
	}

	for _, v := range schedule {
		switch strings.ToLower(v) {
		case "hourly":
			a.schedule["hourly"] = struct{}{}
		case "daily":
			a.schedule["daily"] = struct{}{}
		case "weekly":
			a.schedule["weekly"] = struct{}{}
		case "monthly":
			a.schedule["monthly"] = struct{}{}
		case "yearly":
			a.schedule["yearly"] = struct{}{}
		default:
			err := errors.New("schedule incorrect")
			log.WithFields(log.Fields{"app": name, "schedule": v, "error": err}).Errorf("")
			return nil, err
		}

	}

	for k, v := range before {
		if _, err := regexp.Compile(k); err != nil {
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "call": "regexp.Compile", "attr": k, "error": err}).Errorf("")
			return nil, err
		}
		script := Addr(v)
		if script.Box() == "" {
			err := errors.New("before box incorrect")
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
			return nil, err
		}
		if script.Path() == "" {
			err := errors.New("before path incorrect")
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
			return nil, err
		}
		if _, ok := a.before[k]; ok {
			err := errors.New("before already exists")
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		a.before[k] = script
	}

	for k, v := range after {
		if _, err := regexp.Compile(k); err != nil {
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "call": "regexp.Compile", "attr": k, "error": err}).Errorf("")
			return nil, err
		}
		script := Addr(v)
		if script.Box() == "" {
			err := errors.New("after box incorrect")
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
			return nil, err
		}
		if script.Path() == "" {
			err := errors.New("after path incorrect")
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
			return nil, err
		}
		if _, ok := a.before[k]; ok {
			err := errors.New("after already exists")
			log.WithFields(log.Fields{"app": name, "schedule": k, "addr": v, "error": err}).Errorf("")
			return nil, err
		}

		a.after[k] = script
	}

	return a, nil
}

func (a *App) Cleanup(now time.Time) error {
	log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")

	for _, src := range a.sources {
		for _, s := range cfg.box[src.Box()].zfs.filesystems[src.Path()].snapshots {
			if !s.Valid() {
				if err := s.Delete(); err != nil {
					log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
					return err
				}
			} else if expired, err := s.Expired(now); err != nil {
				log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Expired", "error": err}).Errorf("")
				return err
			} else if expired {
				if err := s.Delete(); err != nil {
					log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
					return err
				}
			}
		}
		for _, dest := range a.destinations {
			if cfg.box[dest.Box()].online {
				dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
				for _, s := range cfg.box[dest2.Box()].zfs.filesystems[dest2.Path()].snapshots {
					if !s.Valid() {
						if err := s.Delete(); err != nil {
							log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
							return err
						}
					} else if expired, err := s.Expired(now); err != nil {
						log.WithFields(log.Fields{"app": a.name, "now": now, "box": dest.Box(), "snapshot": s.String(), "call": "Expired", "error": err}).Errorf("")
						return err
					} else if expired {
						if err := s.Delete(); err != nil {
							log.WithFields(log.Fields{"app": a.name, "now": now, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
							return err
						}
					}
				}
			}
		}
	}

	return nil
}

func (a *App) SanityCheck() error {
	log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")

	for _, src := range a.sources {
		b := cfg.box[src.Box()]
		if !b.online {
			err := errors.New("source box offline")
			log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "error": err}).Errorf("")
			return err
		}

		if _, ok := b.zfs.filesystems[src.Path()]; !ok {
			err := errors.New("source path doesn't exist")
			log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "path": src.Path(), "error": err}).Errorf("")
			return err
		}

		for _, s := range b.zfs.filesystems[src.Path()].snapshots {
			if !s.Valid() {
				if err := s.Delete(); err != nil {
					log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
					return err
				}
			}
		}
	}

	onlineDestinations := 0
	for _, dest := range a.destinations {
		b := cfg.box[dest.Box()]
		if b.online {
			onlineDestinations++
			for _, src := range a.sources {
				dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
				if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
					for _, s := range fs.snapshots {
						if !s.Valid() {
							if err := s.Delete(); err != nil {
								log.WithFields(log.Fields{"app": a.name, "box": src.Box(), "snapshot": s.String(), "call": "Delete", "error": err}).Errorf("")
								return err
							}
						}
					}
				}
			}
		}
	}
	if onlineDestinations == 0 {
		err := errors.New("no destination box online")
		log.WithFields(log.Fields{"app": a.name, "error": err}).Errorf("")
		return err
	}

	return nil
}

func (a *App) RunSchedule(schedule string, now time.Time) error {
	log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("done")

	snapshotName := SnapshotName(schedule, now)
	log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "snapshot": snapshotName}).Debugf("snapshot name")

	if err := a.RunBefore(schedule); err != nil {
		log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "RunBefore", "attr": schedule, "error": err}).Errorf("")
	}

	for _, src := range a.sources {
		srcFs := cfg.box[src.Box()].zfs.filesystems[src.Path()]
		if _, err := srcFs.TakeSnapshot(snapshotName); err != nil {
			log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "TakeSnapshot", "attr": snapshotName, "error": err}).Errorf("")
		}

	}

	if err := a.RunAfter(schedule); err != nil {
		log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "RunAfter", "attr": schedule, "error": err}).Errorf("")
	}

	return nil

}

func (a *App) Run(now time.Time) (string, error) {
	log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("done")

	if err := a.SanityCheck(); err != nil {
		log.WithFields(log.Fields{"app": a.name, "now": now, "call": "SanityCheck", "error": err}).Errorf("")
		return "", err
	}

	schedule, err := a.NextSchedule(now)
	if err != nil {
		log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("")
		return "", err
	}

	log.WithFields(log.Fields{"app": a.name, "now": now, "schedule": schedule}).Debugf("schedule")
	if schedule != "" {
		if err := a.RunSchedule(schedule, now); err != nil {
			log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("")
			return "", err
		}
	}

	for _, src := range a.sources {
		if err := src.SetManaged(true); err != nil {
			log.WithFields(log.Fields{"app": a.name, "call": "src.SetManaged", "error": err}).Errorf("")
			return "", err
		}
	}

	if err := a.Transfer(); err != nil {
		log.WithFields(log.Fields{"app": a.name, "call": "Transfer", "error": err}).Errorf("")
		return "", err
	}

	if err := a.Cleanup(now); err != nil {
		log.WithFields(log.Fields{"app": a.name, "call": "Cleanup", "error": err}).Errorf("")
		return "", err
	}

	return schedule, nil
}

func (a *App) NextSchedule(now time.Time) (string, error) {
	log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("done")

	// get a list of all the common timestamps in sources snapshots
	snapshots := make(map[string]map[time.Time]int)
	for _, v := range a.sources {
		log.WithFields(log.Fields{"app": a.name, "now": now, "box": v.Box(), "path": v.Path()}).Debugf("source")
		b := cfg.box[v.Box()] // we tested the boxes

		fs, ok := b.zfs.filesystems[v.Path()]
		if !ok {
			err := errors.New("path doesn't exist")
			log.WithFields(log.Fields{"app": a.name, "now": now, "box": v.Box(), "path": v.Path(), "error": err}).Errorf("")
			return "", err
		}

		log.WithFields(log.Fields{"app": a.name, "now": now, "box": v.Box(), "path": v.Path()}).Debugf("%d snapshots", len(fs.snapshots))

		for _, v2 := range fs.snapshots {
			if s, err := v2.Schedule(); err == nil {
				snapshots2, ok := snapshots[s]
				if !ok {
					snapshots2 = make(map[time.Time]int)
				}

				if t, err := v2.Timestamp(); err == nil {
					if t.After(now) {
						err := errors.New("snapshot in the future")
						log.WithFields(log.Fields{"app": a.name, "now": now, "source": v, "timestamp": t, "error": err}).Errorf("")
						return "", err
					}
					if count, ok := snapshots2[t]; ok {
						snapshots2[t] = count + 1
					} else {
						snapshots2[t] = 1
					}
				}

				snapshots[s] = snapshots2
			}
		}
	}

	t := time.Unix(0, 0)
	if _, ok := a.schedule["yearly"]; ok {
		if s, ok := snapshots["yearly"]; ok {
			for k, v := range s {
				if k.After(t) && v == len(a.sources) {
					t = k
				}
			}
		}
		if t.Year() < now.Year() {
			return "yearly", nil
		}
	}

	if _, ok := a.schedule["monthly"]; ok {
		if s, ok := snapshots["monthly"]; ok {
			for k, v := range s {
				if k.After(t) && v == len(a.sources) {
					t = k
				}
			}
		}
		if t.Year() < now.Year() || t.Month() != now.Month() {
			return "monthly", nil
		}
	}

	if _, ok := a.schedule["weekly"]; ok {
		if s, ok := snapshots["weekly"]; ok {
			for k, v := range s {
				if k.After(t) && v == len(a.sources) {
					t = k
				}
			}
		}
		ny, nw := now.ISOWeek()
		ty, tw := t.ISOWeek()
		if ty < ny || tw < nw {
			return "weekly", nil
		}
	}

	if _, ok := a.schedule["daily"]; ok {
		if s, ok := snapshots["daily"]; ok {
			for k, v := range s {
				if k.After(t) && v == len(a.sources) {
					t = k
				}
			}
		}
		if t.Year() < now.Year() || t.Month() != now.Month() || t.Day() < now.Day() {
			return "daily", nil
		}
	}

	if _, ok := a.schedule["hourly"]; ok {
		if s, ok := snapshots["hourly"]; ok {
			for k, v := range s {
				if k.After(t) && v == len(a.sources) {
					t = k
				}
			}
		}
		if t.Year() < now.Year() || t.Month() != now.Month() || t.Day() < now.Day() || t.Hour() < now.Hour() {
			return "hourly", nil
		}
	}

	return "", nil
}

func (a *App) RunBefore(schedule string) error {
	log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("done")

	for k, v := range a.before {
		re := regexp.MustCompile(k)
		if re.MatchString(schedule) {
			if _, err := v.Exec(); err != nil {
				log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "regex": k, "call": "Exec", "error": err}).Errorf("")
				return err
			}
		}
	}

	return nil
}

func (a *App) RunAfter(schedule string) error {
	log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule}).Debugf("done")

	for k, v := range a.after {
		re := regexp.MustCompile(k)
		if re.MatchString(schedule) {
			if _, err := v.Exec(); err != nil {
				log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "regex": k, "call": "Exec", "error": err}).Errorf("")
				return err
			}
		}
	}

	return nil
}

func (a *App) Transfer() error {
	log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
	defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")

	for _, src := range a.sources {
		dests := make([]Addr, 0)
		for _, dest := range a.destinations {
			dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
			if dest2.Online() {
				if err := dest2.Mkdir(); err != nil {
					log.WithFields(log.Fields{"app": a.name, "call": "Mkdir", "attr": dest, "error": err}).Errorf("")
					return err
				}
				if err := dest2.SetManaged(true); err != nil {
					log.WithFields(log.Fields{"app": a.name, "call": "SetManaged", "src": src, "dest": dest, "error": err}).Errorf("")
					return err
				}
				dests = append(dests, dest2)
			}
		}
		if n, err := TransferZfs(src, dests); err != nil {
			log.WithFields(log.Fields{"app": a.name, "call": "TransferZfs", "src": src, "dests": dests, "error": err}).Errorf("")
			return err
		} else if n > 0 {
			if err := src.SetBackedUp(true); err != nil {
				log.WithFields(log.Fields{"app": a.name, "call": "SetBackedUp", "src": src, "error": err}).Errorf("")
				return err
			}
		}

	}

	return nil
}
