package main

import (
	_ "embed"

	"encoding/json"
	"errors"
	"fmt"
	"os"
	"regexp"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/tailscale/hujson"
	"github.com/tidwall/pretty"
)

type Config struct {
	ScheduleDuration map[string]string     `json:"schedule,omitempty"`
	Box              map[string]*BoxConfig `json:"box,omitempty"`
	Email            *EmailConfig          `json:"email,omitempty"`
	Apps             []*AppConfig          `json:"apps,omitempty"`
	Timezone         string                `json:"timezone,omitempty"`
	Admin            *AdminConfig          `json:"admin,omitempty"`
	box              map[string]*Box       `json:"-"`
	apps             map[string]*App       `json:"-"`
	timezone         *time.Location        `json:"-"`
}

var (
	cfgMx  sync.Mutex
	cfgRun bool
)

//go:embed assets/backup.sample.json
var sampleCfg []byte

type BoxConfig struct {
	Addr string `json:"addr"`
	User string `json:"user"`
	Key  string `json:"key"`
}

type AppConfig struct {
	Name         string            `json:"name"`
	Schedule     []string          `json:"schedule"`
	Sources      []string          `json:"src"`
	Destinations []string          `json:"dest"`
	Before       map[string]string `json:"before,omitempty"`
	After        map[string]string `json:"after,omitempty"`
	Active       bool              `json:"active,omitempty"`
}

// Load config from file
func LoadConfigFile(path string) (*Config, error) {
	log.WithFields(log.Fields{"path": path}).Debugf("starting")
	defer log.WithFields(log.Fields{"path": path}).Debugf("done")

	b, err := os.ReadFile(path)
	if err != nil {
		log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
		return nil, err
	}

	return LoadConfigByte(b)

}

// Load config from string
func LoadConfigByte(conf []byte) (*Config, error) {
	log.WithFields(log.Fields{}).Debugf("starting")
	defer log.WithFields(log.Fields{}).Debugf("done")

	c := &Config{}
	if err := json.Unmarshal(sampleCfg, c); err != nil {
		log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("")
		return nil, err
	}

	b, err := hujson.Standardize(conf)
	if err != nil {
		log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
		return nil, err
	}

	if err := json.Unmarshal(b, c); err != nil {
		log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
		return nil, err
	}

	c.timezone, err = time.LoadLocation(c.Timezone)
	if err != nil {
		log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("")
		return nil, err
	}

	if c.Email != nil {
		if len(c.Email.SmtpHost) == 0 {
			err := fmt.Errorf("no smtp")
			log.WithFields(log.Fields{"error": err}).Errorf("")
			return nil, err
		}

		if len(c.Email.FromEmail) == 0 {
			err := fmt.Errorf("no email from")
			log.WithFields(log.Fields{"error": err}).Errorf("")
			return nil, err
		}

		if len(c.Email.ToEmail) == 0 {
			err := fmt.Errorf("no email to")
			log.WithFields(log.Fields{"error": err}).Errorf("")
			return nil, err
		}
	}

	for k, v := range c.ScheduleDuration {
		switch k {
		case "hourly":
		case "daily":
		case "weekly":
		case "monthly":
		case "yearly":
			if _, err := Expiration(time.Now(), v); err != nil {
				log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
				return nil, err
			}
		default:
			err := errors.New("invalid schedule")
			log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
			return nil, err
		}
	}

	c.box = make(map[string]*Box)
	for k, v := range c.Box {
		if b, err := c.NewBox(k, v.Addr, v.User, v.Key); err != nil {
			log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
			return nil, err
		} else {
			if _, ok := c.box[k]; ok {
				err := errors.New("already exists")
				log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("")
				return nil, err
			}
			c.box[k] = b
		}
	}

	c.apps = make(map[string]*App)
	for _, v := range c.Apps {
		if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
			log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
			return nil, err
		} else {
			if _, ok := c.apps[v.Name]; ok {
				err := errors.New("app already exists")
				log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
				return nil, err
			}
			c.apps[v.Name] = a
			for k := range a.schedule {
				if dur, ok := c.ScheduleDuration[k]; ok {
					re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
					if !re.MatchString(dur) {
						err := errors.New("incorrect schedule duration")
						log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
						return nil, err
					}
				} else {
					err := errors.New("undefined schedule duration")
					log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
					return nil, err
				}
			}
		}
	}

	return c, nil
}

// Save config
func (c *Config) Save() error {
	log.WithFields(log.Fields{}).Debugf("starting")
	defer log.WithFields(log.Fields{}).Debugf("done")

	cfgMx.Lock()
	defer cfgMx.Unlock()

	b, err := json.Marshal(cfg)
	if err != nil {
		log.WithFields(log.Fields{"error": err, "call": "json.Marshal"}).Errorf("")
		return err
	}

	r := pretty.PrettyOptions(b, &pretty.Options{Indent: "        "})

	f, err := os.Create(*cfgFile)
	if err != nil {
		log.WithFields(log.Fields{"error": err, "call": "os.Open"}).Errorf("")
		return err
	}

	if _, err := f.Write(r); err != nil {
		log.WithFields(log.Fields{"error": err, "call": "File.Write"}).Errorf("")
		return err
	}

	return nil

}

// Run config
func (c *Config) Run() {
	log.WithFields(log.Fields{}).Debugf("starting")
	defer log.WithFields(log.Fields{}).Debugf("done")

	if cfgRun {
		return
	}

	cfgMx.Lock()
	defer cfgMx.Unlock()

	cfgRun = true
	defer func() { cfgRun = false }()

	e := NewEmail(time.Now())

	var wg sync.WaitGroup

	// Setup boxes
	for _, b := range c.box {
		wg.Add(1)
		go func(box *Box) {
			defer wg.Done()
			if err := box.Open(); err != nil {
				log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("")
				e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name))
				return
			}
		}(b)
	}

	wg.Wait()

	// Run each app
	for _, a := range cfg.apps {
		wg.Add(1)
		go func(app *App) {
			if sched, err := app.Run(e.startTime); err != nil {
				e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err))
			} else if *debug {
				if sched != "" {
					e.AddItem(fmt.Sprintf(" - App : Success backing up %s (%s)", app.name, sched))
				} else {
					e.AddItem(fmt.Sprintf(" - App : No backup for %s", app.name))
				}
			}
			wg.Done()
		}(a)
	}

	wg.Wait()

	// Cleanup
	for _, a := range cfg.apps {
		for _, src := range a.sources {
			if b, ok := c.box[src.Box()]; ok {
				if fs, ok := b.zfs.filesystems[src.Path()]; ok {
					fs.srcApps = append(fs.srcApps, a)
				}
			}
			for _, dest := range a.destinations {
				if b, ok := c.box[dest.Box()]; ok {
					dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
					if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
						fs.destApps = append(fs.destApps, a)
					} else {
						e.AddItem(fmt.Sprintf(" - Dest : No folder (%s)", dest2.String()))
					}
				}
			}
		}
	}

	for _, b := range cfg.box {
		if b.online {
			for _, fs := range b.zfs.filesystems {
				if len(fs.srcApps) > 0 && !fs.backedUp {
					log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up")
					e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path))
				}
				if len(fs.destApps) == 0 && !fs.backedUp && fs.managed {
					log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed")
					e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path))
				}
			}
		}
	}

	// Stop
	for _, b := range c.box {
		if err := b.Close(); err != nil {
			log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("")
		}
	}

	if len(e.items) > 0 {
		if err := e.Send(cfg.Email.SmtpHost, cfg.Email.FromEmail, cfg.Email.ToEmail); err != nil {
			log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
		}
	}
}
