package service

import (
	"context"
	"fmt"
	"io/fs"
	"io/ioutil"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"

	"github.com/fatih/color"
	"github.com/fsnotify/fsnotify"
	"github.com/yaoapp/gou"
	"github.com/yaoapp/kun/log"
	"github.com/yaoapp/yao/config"
	"github.com/yaoapp/yao/engine"
	"github.com/yaoapp/yao/share"
)

var watchShutdown = make(chan bool, 1) // shutdown signal
var watchReady = make(chan bool, 1)    // ready signal
var excludes = map[string]bool{"ui": true, "db": true, "data": true}
var handlers = map[string]func(root string, file string, event string, cfg config.Config){
	"models": watchModel,
}

// Watch the application code change for hot update
func Watch(cfg config.Config) (err error) {
	go func() { err = watchStart(cfg) }()
	select {
	case <-watchReady:
		return nil
	}
}

// StopWatch stop watching the code change
func StopWatch() {
	watchShutdown <- true
	time.Sleep(200 * time.Millisecond)
}

func watchStart(cfg config.Config) error {

	root := cfg.Root

	// recive interrupt signal
	interrupt := make(chan os.Signal, 1)
	signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT)

	shutdown := make(chan bool, 1)

	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		return err
	}
	defer watcher.Close()

	root, err = filepath.Abs(root)
	if err != nil {
		return err
	}

	dirs, err := ioutil.ReadDir(root)
	if err != nil {
		return err
	}

	// Add path
	for _, dir := range dirs {
		if !dir.IsDir() {
			continue
		}

		name := dir.Name()
		if _, has := excludes[name]; has {
			continue
		}

		if strings.HasPrefix(name, ".") {
			continue
		}

		filename := filepath.Join(root, name)
		err := watcher.Add(filename)
		if err != nil {
			log.Error("[Watch] %s", err.Error())
			return err
		}

		fmt.Println(color.GreenString("[Watch] Watching %s", name))
		log.Info("[Watch] Watching: %s", filename)

		// sub dir
		depth := 0
		err = filepath.WalkDir(filename, func(path string, d fs.DirEntry, err error) error {
			depth = depth + 1
			if depth == 1 {
				return nil
			}

			if !d.IsDir() {
				return nil
			}

			log.Info("[Watch] Watching: %s", path)
			err = watcher.Add(path)
			if err != nil {
				log.Error("[Watch] %s", err.Error())
			}
			return nil
		})

		if err != nil {
			log.Error("[Watch] %s", err.Error())
		}
	}

	// event handler
	go func() {
		for {
			select {
			case <-shutdown:
				log.Info("[Watch] The event handler exit")
				return

			case event, ok := <-watcher.Events:
				if !ok {
					return
				}
				relpath := strings.TrimPrefix(event.Name, root)
				if strings.HasPrefix(relpath, string(os.PathSeparator)) {
					relpath = strings.TrimPrefix(relpath, string(os.PathSeparator))
				}

				pi := strings.Split(relpath, string(os.PathSeparator))
				widget := pi[0]

				watchHanler := watchReload
				if handler, has := handlers[widget]; has {
					watchHanler = handler
				}

				if _, has := excludes[widget]; !has {
					base := filepath.Base(event.Name)
					isdir := true
					if strings.HasSuffix(base, ".yao") || strings.HasSuffix(base, ".json") || strings.HasSuffix(base, ".js") {
						isdir = false
					}

					events := strings.Split(event.Op.String(), "|")
					for _, eventType := range events {

						// ADD / REMOVE Watching dir
						if isdir {
							switch eventType {
							case "CREATE":
								log.Info("[Watch] Watching: %s", event.Name)
								watcher.Add(event.Name)
								break
							case "REMOVE":
								log.Info("[Watch] Unwatching: %s", event.Name)
								watcher.Remove(event.Name)
								break
							}
							continue
						}

						log.Info("[Watch] %s %s", eventType, event.Name)
						watchHanler(root, event.Name, eventType, cfg)
					}
				}
				break

			case err, ok := <-watcher.Errors:
				if !ok {
					return
				}
				fmt.Println(color.RedString("[Watch] %s", err.Error()))
				log.Error("[Watch] %s", err.Error())
				break
			}
		}
	}()

	fmt.Println(color.GreenString("[Watch] Started"))
	watchReady <- true

	for {
		select {
		case <-watchShutdown:
			shutdown <- true
			log.Info("[Watch] Stopped")
			fmt.Println(color.YellowString("[Watch] Stopped"))
			return nil

		case <-interrupt:
			shutdown <- true
			log.Info("[Watch] Stopped")
			fmt.Println(color.YellowString("[Watch] Stopped"))
			return nil
		}
	}
}

func watchModel(root string, file string, event string, cfg config.Config) {
	name := share.SpecName(root, file)
	switch event {
	case "CREATE":
		content, err := ioutil.ReadFile(file)
		if err != nil {
			fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
			return
		}
		mod, err := gou.LoadModelReturn(string(content), name)
		if err != nil {
			fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
			return
		}

		mod.Migrate(true)
		fmt.Println(color.GreenString("[Watch] Model: %s Created", name))
		break

	case "WRITE":
		content, err := ioutil.ReadFile(file)
		if err != nil {
			fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
			return
		}
		mod, err := gou.LoadModelReturn(string(content), name)
		if err != nil {
			fmt.Println(color.RedString("[Watch] Model: %s %s", name, err.Error()))
			return
		}
		mod.Migrate(false)
		fmt.Println(color.GreenString("[Watch] Model: %s Reloaded", name))
		break

	case "REMOVE", "RENAME":
		delete(gou.Models, name)
		fmt.Println(color.GreenString("[Watch] Model: %s Removed", name))
		break
	}
}

func watchReload(root string, file string, event string, cfg config.Config) {

	switch event {
	case "CREATE", "WRITE", "REMOVE":
		err := engine.Load(config.Conf) // 加载脚本等
		if err != nil {
			fmt.Println(color.RedString("[Watch] Reload: %s", err.Error()))
		}

		// Restart Server
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		StopWithContext(ctx, func() {
			go Start()
			fmt.Println(color.GreenString("[Watch] Reload Completed"))
		})
	}
}
