package loader

import (
	"context"
	"dbweb/core"
	cfg "dbweb/lib/config"
	"dbweb/lib/lsession"
	"dbweb/lib/render"
	"dbweb/lib/ws"
	"dbweb/lib/zip"
	_ "dbweb/modules/common" //引用后，执行init函数
	"encoding/gob"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"reflect"

	"regexp"
	"runtime"
	"time"

	"github.com/NYTimes/gziphandler"
	log "github.com/Sirupsen/logrus"

	"dbweb/lib/tempext"
	"path"
	"strings"

	"github.com/tdewolff/minify"
	"github.com/tdewolff/minify/css"
	"github.com/tdewolff/minify/html"
	"github.com/tdewolff/minify/js"
	"github.com/tdewolff/minify/json"
	"github.com/tdewolff/minify/svg"
	"github.com/tdewolff/minify/xml"

	"io"

	"github.com/linlexing/mapfun"
)

var (
	httpSrvs []*http.Server
	m        = minify.New()
	rootPath string
	gConfig  *cfg.Config
)

func init() {
	m.AddFunc(".js", js.Minify)
	m.AddFunc(".css", css.Minify)
	m.AddFunc("text/html", html.Minify)
	m.AddFuncRegexp(regexp.MustCompile("[/+]json$"), json.Minify)
	m.AddFunc("image/svg+xml", svg.Minify)
	m.AddFuncRegexp(regexp.MustCompile("[/+]xml$"), xml.Minify)
	gob.Register(map[string]interface{}{})
	_, filename, _, _ := runtime.Caller(0)
	rootPath = filepath.Dir(filepath.Dir(filepath.Dir(filename)))
}
func copyDirFun(srcPath, destPath string, cb func(dest, src string, srcinfo os.FileInfo) error, exts ...string) error {
	return filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
		if path != srcPath {
			if info.IsDir() {
				return nil
			}
			if len(exts) > 0 {
				bFound := false
				for _, ext := range exts {
					if ext == filepath.Ext(path) {
						bFound = true
						break
					}
				}
				//如果不在限定的扩展名内，则不复制
				if !bFound {
					return nil
				}
			}
			relPath, _ := filepath.Rel(srcPath, path)
			destName := filepath.Join(destPath, relPath)
			if err = cb(destName, path, info); err != nil {
				return err
			}
		}
		return nil
	})
}

//copyDir 复制目录,可指定限定的扩展名，未限定是所有
func copyDir(srcPath, destPath string, exts ...string) error {
	return copyDirFun(srcPath, destPath, func(destFileName, srcFileName string, info os.FileInfo) error {
		src, err := os.Open(srcFileName)
		if err != nil {
			return err
		}
		defer src.Close()
		var dest *os.File
		//log.Printf("copy views %s -> %s", path, destName)
		if err = os.MkdirAll(filepath.Dir(destFileName), os.ModeDir); err != nil {
			return err
		}
		dest, err = os.Create(destFileName)
		if err != nil {
			return err
		}
		defer func() {
			dest.Close()
			os.Chtimes(destFileName, time.Now(), info.ModTime())
		}()
		_, err = io.Copy(dest, src)
		return err
	}, exts...)

}

//收集所有
func collectionViews(viewPath, staticPath string, notMinify bool) {
	paths := []string{}
	for _, v := range core.ControllerNames() {
		c := core.FindController(v)
		paths = append(paths, filepath.Join(reflect.TypeOf(c.AppController).Elem().PkgPath(), core.ViewPath))
	}
	pathList := []string{"dbweb/" + core.ViewPath}
	for _, k := range mapfun.GroupStr(paths) {
		pathList = append(pathList, k)
	}
	//先删除所有的旧文件
	//log.Println("remove view path:", viewPath)
	if err := os.RemoveAll(viewPath); err != nil {
		log.Panic(err)
	}
	//再复制所有找到的目录

	for _, onePath := range pathList {
		onePath = filepath.Join(os.Getenv("GOPATH"), "src", onePath)
		if err := copyDirFun(onePath, viewPath, func(dest, src string, info os.FileInfo) error {
			if err := os.MkdirAll(filepath.Dir(dest), os.ModeDir); err != nil {
				return err
			}
			defer func() {
				os.Chtimes(dest, time.Now(), info.ModTime())
			}()
			return processViewFile(dest, src, staticPath, notMinify)
		}, ".html", ".md"); err != nil {
			log.Panic(err)
		}
	}
}

//收集所有
func collectionStatic(staticPath string) {
	paths := []string{}
	for _, v := range core.ControllerNames() {
		c := core.FindController(v)
		paths = append(paths, filepath.Join(reflect.TypeOf(c.AppController).Elem().PkgPath(), core.StaticPath))
	}
	pathList := []string{"dbweb/" + core.StaticPath}
	for _, k := range mapfun.GroupStr(paths) {
		pathList = append(pathList, k)
	}
	//先删除所有的旧文件
	if err := os.RemoveAll(staticPath); err != nil {
		log.Panic(err)
	}
	//再复制所有找到的目录
	for _, onePath := range pathList {
		onePath = filepath.Join(os.Getenv("GOPATH"), "src", onePath)
		if err := copyDir(onePath, staticPath); err != nil {
			log.Panic(err)
		}
	}

	//再复制前端build目录
	if _, err := os.Stat(core.FrontBuildPath); os.IsNotExist(err) {
		return
	} else if err != nil {
		log.Panic(err)
	}
	if err := copyDir(core.FrontBuildPath, filepath.Join(staticPath, "front")); err != nil {
		log.Panic(err)
	}
}
func updateDirectoryFromZip(srcZipFile string, modTime time.Time, destPath string) (bool, error) {
	//需要根据配置文件判断，是否需要更新views、static目录
	t, err := getFileModTime(srcZipFile)
	if os.IsNotExist(err) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	if t.After(modTime) {
		core.LOG.WithFields(
			log.Fields{
				"zipfile":  srcZipFile,
				"destpath": destPath,
				"filetime": t,
				"cfgtime":  modTime,
			}).Info("update")
		if err = os.RemoveAll(destPath); err != nil {
			return false, err
		}
		if err = os.MkdirAll(destPath, os.ModeDir); err != nil {
			return false, err
		}
		if err = zip.Unzip(srcZipFile, destPath); err != nil {
			return false, err
		}
		return true, nil
	}
	return false, nil

}
func getFileModTime(fileName string) (result time.Time, err error) {

	info, err := os.Stat(fileName)
	if err != nil {
		return
	}
	result = info.ModTime()

	return
}

//Close 强制关闭应用
func Close() {
	core.Close()
	defer lsession.Close()

}

//Shutdown 停止http服务
func Shutdown() {
	for _, one := range httpSrvs {
		if err := one.Shutdown(context.Background()); err != nil {
			core.LOG.Error(err)
		}
	}
}

//Service 启动服务
func Service(appPath, tmpPath string, config *cfg.Config) error {
	gConfig = config
	var err error
	core.Initinal(tmpPath, appPath, config.Driver, config.ConnectString)
	//注意顺序，ReadPatchTime用到appPath
	lastPatchTime := core.ReadPatchTime()
	if err := checkUpdate(config.UpdateURL); err != nil {
		//update 错误很正常，网络不可用
		core.LOG.Error(err, "update error")
	}
	var needSaveConfig bool
	//如果是开发环境，则需要收集view、static并生成相应的zip文件
	if config.Env == "Dev" {
		//由于view会自动生成static，所以要放前头
		startTime := time.Now()
		collectionStatic(core.DataFile(core.StaticPath))
		core.LOG.Println("collection static", time.Since(startTime))

		startTime = time.Now()
		collectionViews(core.DataFile(core.ViewPath), core.DataFile(core.StaticPath), config.NotMinify)
		core.LOG.Println("collection view", time.Since(startTime))

		startTime = time.Now()
		if err := zip.ZipDirectory(core.DataFile(core.ViewPath), core.DataFile("views.zip")); err != nil {
			return err
		}
		core.LOG.Println("zip view", time.Since(startTime))
		if lastPatchTime.ViewZipTime, err = getFileModTime(core.DataFile("views.zip")); err != nil {
			return err
		}

		startTime = time.Now()
		if err := zip.ZipDirectory(core.DataFile(core.StaticPath),
			core.DataFile("static.zip")); err != nil {
			return err
		}
		core.LOG.Println("zip static", time.Since(startTime))

		if lastPatchTime.StaticZipTime, err = getFileModTime(core.DataFile("static.zip")); err != nil {
			return err
		}
		needSaveConfig = true
	} else {
		if chged, err := updateDirectoryFromZip(core.DataFile("views.zip"),
			lastPatchTime.ViewZipTime, core.DataFile(core.ViewPath)); err != nil {
			core.LOG.WithFields(log.Fields{
				"file": "views.zip",
				"dir":  core.DataFile(core.ViewPath),
			}).Error(err)
			return err
		} else if chged {
			core.LOG.WithFields(log.Fields{
				"file": "views.zip",
			}).Info("updated")
			needSaveConfig = true
			if lastPatchTime.ViewZipTime, err = getFileModTime(core.DataFile("views.zip")); err != nil {
				return err
			}
		}
		if chged, err := updateDirectoryFromZip(core.DataFile("static.zip"),
			lastPatchTime.StaticZipTime, core.DataFile(core.StaticPath)); err != nil {
			core.LOG.WithFields(log.Fields{
				"file": "static.zip",
				"dir":  core.DataFile(core.ViewPath),
			}).Error(err)
			return err
		} else if chged {
			core.LOG.WithFields(log.Fields{
				"file": "static.zip",
			}).Info("updated")
			needSaveConfig = true
			if lastPatchTime.StaticZipTime, err = getFileModTime(
				core.DataFile("static.zip")); err != nil {
				return err
			}
		}
	}
	//判断是否要更新制度包目录
	insTime, err := core.ReadInstitutionConfigTime(
		core.DataFile(core.InstitutionPath))
	if !os.IsNotExist(err) && err != nil {
		core.LOG.Panic(err)
	}
	//如果记录的上次zip文件解开的时间，比制度目录中新，则用这个时间
	//避免zip文件时间比制度目录新，其中内容却一样，造成不断解包
	if lastPatchTime.InstitutionZipTime.After(insTime) {
		insTime = lastPatchTime.InstitutionZipTime
	}
	if chged, err := updateDirectoryFromZip(core.DataFile("institution.zip"),
		insTime, core.DataFile(core.InstitutionPath)); err != nil {
		core.LOG.WithFields(log.Fields{
			"file": "institution.zip",
			"dir":  core.DataFile(core.ViewPath),
		}).Error(err)
		return err
	} else if chged {
		core.LOG.WithFields(log.Fields{
			"file": "institution.zip",
		}).Info("unzip")
		needSaveConfig = true
		if lastPatchTime.InstitutionZipTime, err = getFileModTime(
			core.DataFile("institution.zip")); err != nil {
			return err
		}
	}
	//导入制度包
	if apply, tm, err := core.RestoreInstitution(
		core.DataFile(core.InstitutionPath), core.DB,
		lastPatchTime.InstitutionApplyTime); err != nil {
		return err
	} else if apply {
		core.LOG.Println("RestoreInstitution")
		lastPatchTime.InstitutionApplyTime = tm
		needSaveConfig = true
	}
	if needSaveConfig {
		if err := lastPatchTime.Save(); err != nil {
			return err
		}
	}
	//ProjectLabel
	core.GlobalCache.Set("ProjectLabel", core.GetOption(core.DB, "ProjectLabel"), 0)

	to := config.SessionTimeout
	if to <= 0 {
		to = 20
	}
	lsession.Timeout = to
	if err := lsession.Open(core.DataFile("session_data")); err != nil {
		return err
	}
	render.Directory = core.DataFile("views")
	render.Extensions = []string{".md", ".html"}
	render.Funcs = []template.FuncMap{
		template.FuncMap(tempext.GetFuncMap()),
		template.FuncMap{
			"version": core.Version,
		}}
	render.Layout = "_layout/normal"
	render.InitRender()
	http.Handle("/favicon.ico", http.NotFoundHandler())
	setupUpdateHandle()

	//静态文件
	if dirs, err := ioutil.ReadDir(core.DataFile(core.StaticPath)); err != nil {
		log.Panic(err)
	} else {
		for _, one := range dirs {
			//front特殊处理
			if one.Name() != "front" {
				if one.IsDir() {
					dirName := "/" + one.Name() + "/"
					core.LOG.WithFields(log.Fields{
						"path": one.Name(),
					}).Info("static")
					http.Handle(dirName, gziphandler.GzipHandler(http.StripPrefix(dirName, http.FileServer(http.Dir(
						core.DataFile(path.Join(core.StaticPath, one.Name())))))))
				}
			}
		}
	}
	//增加前端入口,必须要在静态之后
	http.Handle("/front/static/", gziphandler.GzipHandler(http.StripPrefix("/front/static", http.FileServer(http.Dir(
		core.DataFile(path.Join(core.StaticPath, "front/static")))))))
	http.Handle("/front/",  gziphandler.GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fileName := filepath.Join("static", r.URL.Path)
		if _, err := os.Stat(fileName); err == nil {
			http.ServeFile(w, r, fileName)
		} else {
			http.ServeFile(w, r, filepath.Join("static", "front", "index.html"))
		}
	})))
	if config.NotMinify {
		http.Handle("/", gziphandler.GzipHandler(http.HandlerFunc(httpProcess)))
	} else {
		http.Handle("/", gziphandler.GzipHandler(m.Middleware(http.HandlerFunc(httpProcess))))
	}
	//websocket
	hub := ws.NewHub()
	go hub.Run()

	http.HandleFunc("/sockets/room", func(res http.ResponseWriter, r *http.Request) {
		ws.ServeWs(hub, res, r)
	})
	var schema = "http"
	var port = 80
	if len(config.Bind) > 0 {
		schema, port = config.Bind[0].Schema, config.Bind[0].Port
	}
	for i := 1; i < len(config.Bind); i++ {
		newSvr := new(http.Server)
		newSvr.Handler = http.DefaultServeMux
		newSvr.Addr = fmt.Sprintf(":%d", config.Bind[i].Port)
		httpSrvs = append(httpSrvs, newSvr)
		go func(srv *http.Server, bind cfg.ConfigBind) {
			if strings.ToLower(bind.Schema) == "https" {
				if err := srv.ListenAndServeTLS(core.DataFile("server.crt"), core.DataFile("server.key")); err != nil {
					core.LOG.Fatal(err)
				}
			} else {

				if err := srv.ListenAndServe(); err != nil {
					core.LOG.Fatal(err)
				}
			}
		}(newSvr, config.Bind[i])
		core.LOG.WithFields(log.Fields{
			"schema": config.Bind[i].Schema,
			"port":   config.Bind[i].Port,
		}).Info("listen")
	}
	core.LOG.WithFields(log.Fields{
		"schema": config.Bind[0].Schema,
		"port":   config.Bind[0].Port,
	}).Info("listen")
	newSvr := new(http.Server)
	newSvr.Handler = http.DefaultServeMux
	newSvr.Addr = fmt.Sprintf(":%d", port)
	httpSrvs = append(httpSrvs, newSvr)
	go func() {

		if strings.ToLower(schema) == "https" {
			if err := newSvr.ListenAndServeTLS(core.DataFile("server.crt"), core.DataFile("server.key")); err != nil {
				core.LOG.Fatal(err)
			}
		} else {

			if err := newSvr.ListenAndServe(); err != nil {
				core.LOG.Fatal(err)
			}
		}

		core.LOG.Fatal(newSvr.ListenAndServe())
	}()
	return nil
}
func httpProcess(w http.ResponseWriter, r *http.Request) {
	if r.URL.String() == "/" {
		if len(gConfig.DefaultURL) > 0 {
			http.Redirect(w, r, gConfig.DefaultURL, http.StatusFound)
		} else {
			http.Redirect(w, r, "/login", http.StatusFound)
		}
		return
	}
	core.ElementHandle(w, r)
}
