/*
=================================================================================
* License: GPL-2.0 license
* Author: 众产®  https://ciy.cn/code
* Version: 0.1.3
=================================================================================
Web服务器	NewCiyWebServer
=================================================================================

=================================================================================
示例:

=================================================================================
Nginx配置示例:

=================================================================================
*/
package zciyon

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"net/http/fcgi"
	"net/http/httputil"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
)

type CiyWebServer struct {
	pathfn   map[string]map[string]func(http.ResponseWriter, *http.Request) bool
	mock     func(http.ResponseWriter, *http.Request) error //Mock回调
	phpproxy *httputil.ReverseProxy                         //PHP代理服务器
	exts     map[string]int                                 //静态服务器允许的扩展名，不设置全部允许
}

type contextKey string

var GhttpKey contextKey = contextKey("ciy")

type RequestContext struct {
	CiyAuth string
}

//在根上建立HandleFunc
//

func NewCiyWebServer() (*CiyWebServer, error) {
	web := &CiyWebServer{}
	web.exts = map[string]int{}
	web.pathfn = map[string]map[string]func(http.ResponseWriter, *http.Request) bool{}
	http.HandleFunc("/", web.WithRequestContext(web.runMainPath))
	http.HandleFunc("/ws/", web.runWebsocket)
	return web, nil
}
func (web *CiyWebServer) WithRequestContext(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ctx := context.WithValue(r.Context(), GhttpKey, &RequestContext{
			CiyAuth: "",
		})
		next(w, r.WithContext(ctx))
	}
}
func (thos *CiyWebServer) registerPath(path, fnname string, fn func(http.ResponseWriter, *http.Request) bool) {
	_, ok := thos.pathfn[path]
	if ok {
		thos.pathfn[path][fnname] = fn
	} else {
		thos.pathfn[path] = map[string]func(http.ResponseWriter, *http.Request) bool{fnname: fn}
	}
}
func (thos *CiyWebServer) SetExt(extstr string) { //允许的静态文件类型 .jpg;.png。不设置全部允许
	exts := strings.Split(extstr, ";")
	for _, ext := range exts {
		ext = strings.ToLower(strings.TrimSpace(ext))
		if !strings.HasPrefix(ext, ".") {
			continue
		}
		thos.exts[ext] = 1
	}
}
func (thos *CiyWebServer) runWebsocket(w http.ResponseWriter, r *http.Request) {
	funname := GetQuery("func", r)
	Clog("websocket", funname)
	clsmothod := strings.Split(funname, ".")
	if len(clsmothod) != 2 {
		Clog("websocket", fmt.Sprintf("func format error:%v", funname))
		return
	}
	for path := range thos.pathfn {
		if path == clsmothod[0] {
			for fnname, fn := range thos.pathfn[path] {
				if clsmothod[1] == fnname {
					fn(w, r)
					return
				}
			}
			Clog("websocket", fmt.Sprintf("RouterFunc not find mothod[%v] in [%v]", clsmothod[1], clsmothod[0]))
			return
		}
	}
	Clog("RouterFunc not find class:%v", clsmothod[0])
}
func (thos *CiyWebServer) runMainPath(w http.ResponseWriter, r *http.Request) {
	Clog("root", r.URL.Path)
	//将大于n毫秒的请求，保存到log
	//可以统计每个path.func的请求次数用时
	rets := make([]string, 0)
	if err := thos.execFunc(w, r); err == nil {
		return
	} else {
		rets = append(rets, err.Error())
	}
	if thos.mock != nil {
		if err := thos.execMock(w, r); err == nil {
			return
		} else {
			rets = append(rets, err.Error())
		}
	}
	if thos.phpproxy != nil {
		if err := thos.execPHP(w, r); err == nil {
			return
		} else {
			rets = append(rets, err.Error())
		}
	}
	if CiyRunMode == CIYRUN_DEV {
		ErrJSON(w, strings.Join(rets, "; "), nil)
	} else {
		http.NotFound(w, r)
	}
}
func (thos *CiyWebServer) execMock(w http.ResponseWriter, r *http.Request) error {
	Clog("execMock", r.URL.Path)
	//找文件或数据库mocks，若有返回。
	return thos.mock(w, r)
}
func (thos *CiyWebServer) execPHP(w http.ResponseWriter, r *http.Request) error {
	Clog("execPHP", r.URL.Path)
	//找本地goc文件，若有转发请求。
	//找到 func指向的.php文件是否存在，打开文件检查是否存在public static function json_xxx()   static function sse_xxx(
	thos.phpproxy.ServeHTTP(w, r)
	return nil
}
func (thos *CiyWebServer) execFunc(w http.ResponseWriter, r *http.Request) error {
	defer func() {
		if rc := recover(); rc != nil {
			funname := r.URL.Path
			if funname == "" {
				funname = GetQuery("sse", r)
			}
			errmsg := fmt.Sprintf("%v", rc)
			Log.Error(funname, errmsg)
			ErrJSON(w, errmsg)
		}
	}()
	Clog("execFunc", r.URL.Path)
	clsmothod := strings.Split(r.URL.Path, ".")
	if len(clsmothod) != 2 {
		return fmt.Errorf("RouterFunc format error:%v", r.URL.Path)
	}
	for path := range thos.pathfn {
		if path == clsmothod[0] {
			for fnname, fn := range thos.pathfn[path] {
				if clsmothod[1] == fnname {
					sttime := Timems()
					boo := fn(w, r)
					CiyVars.Func_runms += Timems() - sttime
					if boo {
						CiyVars.Func_succ++
					} else {
						CiyVars.Func_fail++
					}
					return nil
				}
			}
			return fmt.Errorf("RouterFunc not find mothod[%v] in [%v]", clsmothod[1], clsmothod[0])
		}
	}
	return fmt.Errorf("RouterFunc not find class:%v", clsmothod[0])
}
func (thos *CiyWebServer) runStatic(w http.ResponseWriter, r *http.Request) bool {

	Clog("runStatic", r.URL.Path)
	//模拟nginx功能
	//判断文件扩展名是否允许

	//判断文件@参数，如果无缓存则压缩图片，存到文件缓存，返回文件缓存地址

	//文件在10分钟内，被访问超过n次，则进入内存缓存。
	//缓存达到容量后，自动清理10分钟内访问次数最少的几个文件缓存
	//如果文件扩展名包含
	file := CiyWebDir + r.URL.Path
	if len(thos.exts) > 0 {
		ext := strings.ToLower(filepath.Ext(file))
		if ext != "" {
			if _, ok := thos.exts[ext]; !ok {
				http.NotFound(w, r)
				return false
			}
		}
	}
	err := FileExist(file)
	if err != nil {
		http.NotFound(w, r)
	}
	http.ServeFile(w, r, file)
	return true
}
func (thos *CiyWebServer) RouterStatic(path string) { //允许的静态目录
	thos.registerPath(path, "", thos.runStatic)
}
func (thos *CiyWebServer) RouterFunc(path string, fnmap map[string]map[string]func(http.ResponseWriter, *http.Request) bool) {
	for subpath, fnmap2 := range fnmap {
		for funstr, fn := range fnmap2 {
			thos.registerPath(path+"/"+subpath, funstr, fn)
		}
	}
}

func (thos *CiyWebServer) RouterHandle(path string, fn func(http.ResponseWriter, *http.Request)) {
	http.HandleFunc(path, fn)
}
func (thos *CiyWebServer) SetPHP(host string) {
	if host == "" {
		return
	}
	if ind := strings.Index(host, "://"); ind > -1 {
		host = host[ind+3:]
	}
	thos.phpproxy = httputil.NewSingleHostReverseProxy(
		&url.URL{
			Scheme: "http",
			Host:   host,
		},
	)
	_, err := http.Get("http://" + host)
	if err != nil {
		Clog("WebRun PHP err:", err)
		return
	}
	Clog("WebRun support PHP:", host)
}
func (thos *CiyWebServer) execMockFile(w http.ResponseWriter, r *http.Request) error {
	funname := r.URL.Path
	mockfile := CiyWebDir + "/ud/mock/" + funname + ".json"
	err := FileExist(mockfile)
	if err != nil {
		return err
	}
	http.ServeFile(w, r, mockfile)
	return nil
}
func (thos *CiyWebServer) SetMockFile() {
	if thos.mock != nil {
		return
	}
	thos.mock = thos.execMockFile
	Clog("WebRun support Mock File")
}
func (thos *CiyWebServer) SetMockFn(fn func(http.ResponseWriter, *http.Request) error) {
	if thos.mock != nil {
		return
	}
	thos.mock = fn
	Clog("WebRun support Mock Callback")
}

func (thos *CiyWebServer) Run(mode, ipsk string) {
	if mode == "fastcgi" {
		listener, err := net.Listen("tcp", ipsk)
		if err != nil {
			Clog("WebRun fastcgi listen err:", err)
			return
		}
		defer listener.Close()
		Clog("WebRun fastcgi running: " + ipsk)
		err = fcgi.Serve(listener, nil)
		if err != nil {
			Clog("WebRun fastcgi err:", err)
			return
		}
	} else if mode == "unixsock" {
		// sigchan := make(chan os.Signal, 1)
		// signal.Notify(sigchan, os.Interrupt)
		// signal.Notify(sigchan, syscall.SIGTERM)

		var sockf string
		switch runtime.GOOS {
		case "linux":
			sockf = "/run/" + ipsk
		default:
			Clog("WebRun unixsock err: unknown system " + runtime.GOOS)
			return
		}
		os.Remove(sockf)
		listener, err := net.Listen("unix", sockf)
		if err != nil {
			Clog("WebRun unixsock listen err:", err)
			return
		}
		defer listener.Close()
		Clog("WebRun unixsock running: " + sockf)
		err = fcgi.Serve(listener, nil)
		if err != nil {
			Clog("WebRun unixsock err:", err)
			return
		}
		// <-sigchan
		// os.Remove(sockf)
	} else {
		Clog("WebRun http running: " + ipsk)
		err := http.ListenAndServe(ipsk, nil)
		if err != nil {
			Clog("WebRun http err:", err)
			return
		}
	}
}
func (thos *CiyWebServer) RunHTTP2(parm string) {
	if parm == "" {
		return
	}
	parms := strings.Split(parm, ",")
	if len(parms) != 3 {
		Clog("WebRun http2 param err:", parm)
	}
	err := http.ListenAndServeTLS(parms[0], parms[1], parms[2], nil)
	if err != nil {
		Clog("WebRun http2 err:", err)
	}
	Clog("WebRun http2 running: " + parm)
}

func GetQuery(key string, r *http.Request) string {
	query := r.URL.Query()
	if query[key] == nil {
		return ""
	}
	return query[key][0]
}
func ErrJSON(w http.ResponseWriter, errmsg string, argext ...any) bool {
	jsn := map[string]any{
		"errmsg": errmsg,
	}
	var err error
	if len(argext) > 0 {
		for i, v := range argext {
			if i == 0 {
				if daterr, ok := v.(error); ok {
					err = daterr
					continue
				}
			}
			if datint, ok := v.(int); ok {
				if datint != 1 {
					jsn["code"] = datint
				}
			} else if datmap, ok := v.(map[string]any); ok {
				for key, val := range datmap {
					if key != "code" {
						jsn[key] = val
					}
				}
			}
		}
	}
	if err != nil {
		Log.Info("ErrJSON", errmsg+":"+err.Error())
	}
	w.Write(JSON_Byte(jsn))
	return false
}
func SuccJSON(w http.ResponseWriter, r *http.Request, obj ...map[string]any) bool {
	jsn := map[string]any{
		"code": 1,
	}
	for _, kv := range obj {
		for k, v := range kv {
			if k != "code" {
				jsn[k] = v
			}
		}
	}
	reqCtx := r.Context().Value(GhttpKey).(*RequestContext)
	if reqCtx != nil && reqCtx.CiyAuth != "" {
		jsn["_ciyauth"] = reqCtx.CiyAuth
	}
	w.Write(JSON_Byte(jsn))
	return true
}
