/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package route

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"reflect"
	"strconv"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/shiningrush/droplet"
	"github.com/shiningrush/droplet/data"
	"github.com/shiningrush/droplet/wrapper"
	wgin "github.com/shiningrush/droplet/wrapper/gin"
	lua "github.com/yuin/gopher-lua"

	"github.com/apisix/manager-api/internal/conf"
	"github.com/apisix/manager-api/internal/core/entity"
	"github.com/apisix/manager-api/internal/core/store"
	"github.com/apisix/manager-api/internal/handler"
	"github.com/apisix/manager-api/internal/log"
	"github.com/apisix/manager-api/internal/utils"
	"github.com/apisix/manager-api/internal/utils/consts"
)

type Handler struct {
	routeStore    store.Interface
	svcStore      store.Interface
	upstreamStore store.Interface
	scriptStore   store.Interface
}

func NewHandler() (handler.RouteRegister, error) {
	return &Handler{
		routeStore:    store.GetStore(store.HubKeyRoute),
		svcStore:      store.GetStore(store.HubKeyService),
		upstreamStore: store.GetStore(store.HubKeyUpstream),
		scriptStore:   store.GetStore(store.HubKeyScript),
	}, nil
}

func (h *Handler) ApplyRoute(r *gin.Engine) {
	r.GET("/apisix/admin/routes/:id", wgin.Wraps(h.Get,
		wrapper.InputType(reflect.TypeOf(GetInput{}))))
	r.GET("/apisix/admin/routes", wgin.Wraps(h.List,
		wrapper.InputType(reflect.TypeOf(ListInput{}))))
	r.POST("/apisix/admin/routes", wgin.Wraps(h.Create,
		wrapper.InputType(reflect.TypeOf(entity.Route{}))))
	r.PUT("/apisix/admin/routes", wgin.Wraps(h.Update,
		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))
	r.PUT("/apisix/admin/routes/:id", wgin.Wraps(h.Update,
		wrapper.InputType(reflect.TypeOf(UpdateInput{}))))

	r.DELETE("/apisix/admin/routes/:ids", wgin.Wraps(h.BatchDelete,
		wrapper.InputType(reflect.TypeOf(BatchDelete{}))))

	r.PATCH("/apisix/admin/routes/:id", wgin.Wraps(h.Patch,
		wrapper.InputType(reflect.TypeOf(PatchInput{}))))
	r.PATCH("/apisix/admin/routes/:id/*path", wgin.Wraps(h.Patch,
		wrapper.InputType(reflect.TypeOf(PatchInput{}))))

	r.GET("/apisix/admin/notexist/routes", wgin.Wraps(h.Exist,
		wrapper.InputType(reflect.TypeOf(ExistCheckInput{}))))
}

type PatchInput struct {
	ID      string `auto_read:"id,path"`
	SubPath string `auto_read:"path,path"`
	Body    []byte `auto_read:"@body"`
}

func (h *Handler) Patch(c droplet.Context) (interface{}, error) {
	input := c.Input().(*PatchInput)
	reqBody := input.Body
	ID := input.ID
	subPath := input.SubPath

	stored, err := h.routeStore.Get(c.Context(), ID)
	if err != nil {
		return handler.SpecCodeResponse(err), err
	}

	res, err := utils.MergePatch(stored, subPath, reqBody)
	if err != nil {
		return handler.SpecCodeResponse(err), err
	}

	var route entity.Route
	err = json.Unmarshal(res, &route)
	if err != nil {
		return handler.SpecCodeResponse(err), err
	}

	ret, err := h.routeStore.Update(c.Context(), &route, false)
	if err != nil {
		return handler.SpecCodeResponse(err), err
	}

	return ret, nil
}

type GetInput struct {
	ID string `auto_read:"id,path" validate:"required"`
}

// swagger:operation GET /apisix/admin/routes getRouteList
//
// Return the route list according to the specified page number and page size, and can search routes by name and uri.
//
// ---
// produces:
// - application/json
// parameters:
// - name: page
//   in: query
//   description: page number
//   required: false
//   type: integer
// - name: page_size
//   in: query
//   description: page size
//   required: false
//   type: integer
// - name: name
//   in: query
//   description: name of route
//   required: false
//   type: string
// - name: uri
//   in: query
//   description: uri of route
//   required: false
//   type: string
// - name: label
//   in: query
//   description: label of route
//   required: false
//   type: string
// responses:
//   '0':
//     description: list response
//     schema:
//       type: array
//       items:
//         "$ref": "#/definitions/route"
//   default:
//     description: unexpected error
//     schema:
//       "$ref": "#/definitions/ApiError"
func (h *Handler) Get(c droplet.Context) (interface{}, error) {
	input := c.Input().(*GetInput)

	r, err := h.routeStore.Get(c.Context(), input.ID)
	if err != nil {
		return &data.SpecCodeResponse{StatusCode: http.StatusNotFound}, err
	}

	//format respond
	route := r.(*entity.Route)
	script, _ := h.scriptStore.Get(c.Context(), input.ID)
	if script != nil {
		route.Script = script.(*entity.Script).Script
	}

	//format
	if route.Upstream != nil && route.Upstream.Nodes != nil {
		route.Upstream.Nodes = entity.NodesFormat(route.Upstream.Nodes)
	}

	return route, nil
}

type ListInput struct {
	Name   string `auto_read:"name,query"`
	URI    string `auto_read:"uri,query"`
	Label  string `auto_read:"label,query"`
	Status string `auto_read:"status,query"`
	Host string `auto_read:"host,query"`
	ID string `auto_read:"id,query"`
	Desc string `auto_read:"desc,query"`
	store.Pagination
}

func uriContains(obj *entity.Route, uri string) bool {
	if strings.Contains(obj.URI, uri) {
		return true
	}
	for _, str := range obj.Uris {
		result := strings.Contains(str, uri)
		if result {
			return true
		}
	}
	return false
}

func (h *Handler) List(c droplet.Context) (interface{}, error) {
	input := c.Input().(*ListInput)
	labelMap, err := utils.GenLabelMap(input.Label)
	if err != nil {
		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			fmt.Errorf("%s: \"%s\"", err.Error(), input.Label)
	}

	ret, err := h.routeStore.List(c.Context(), store.ListInput{
		Predicate: func(obj interface{}) bool {
			if input.Name != "" && !strings.Contains(obj.(*entity.Route).Name, input.Name) {
				return false
			}

			if input.URI != "" && !uriContains(obj.(*entity.Route), input.URI) {
				return false
			}

			if input.Label != "" && !utils.LabelContains(obj.(*entity.Route).Labels, labelMap) {
				return false
			}

			if input.Status != "" && strconv.Itoa(int(obj.(*entity.Route).Status)) != input.Status {
				return false
			}

			if input.Host != "" && !strings.Contains(obj.(*entity.Route).Host, input.Host) {
				return false
			}

			if input.Desc != "" && !strings.Contains(obj.(*entity.Route).Desc, input.Desc) {
				return false
			}

			if obj != nil && obj.(*entity.Route) != nil && obj.(*entity.Route).ID != nil && input.ID != "" {
				if !strings.Contains(utils.InterfaceToString(obj.(*entity.Route).ID), input.ID) {
					return false // IDs do not match, so object should not be included in the filtered result
				}
			}

			return true
		},
		Format: func(obj interface{}) interface{} {
			route := obj.(*entity.Route)
			if route.Upstream != nil && route.Upstream.Nodes != nil {
				route.Upstream.Nodes = entity.NodesFormat(route.Upstream.Nodes)
			}
			return route
		},
		PageSize:   input.PageSize,
		PageNumber: input.PageNumber,
	})

	if err != nil {
		return nil, err
	}

	//format respond
	var route *entity.Route
	for i, item := range ret.Rows {
		route = item.(*entity.Route)
		id := utils.InterfaceToString(route.ID)
		script, _ := h.scriptStore.Get(c.Context(), id)
		if script != nil {
			route.Script = script.(*entity.Script).Script
		}
		ret.Rows[i] = route
	}

	return ret, nil
}

func generateLuaCode(script map[string]interface{}) (string, error) {
	scriptString, err := json.Marshal(script)
	if err != nil {
		return "", err
	}
	workDir, err := filepath.Abs(conf.WorkDir)
	if err != nil {
		return "", err
	}
	libDir := filepath.Join(workDir, "dag-to-lua/")
	if err := os.Chdir(libDir); err != nil {
		log.Errorf("Chdir to libDir failed: %s", err)
		return "", err
	}

	defer func() {
		if err := os.Chdir(workDir); err != nil {
			log.Errorf("Chdir to workDir failed: %s", err)
		}
	}()

	L := lua.NewState()
	defer L.Close()

	if err := L.DoString(`
	        local dag_to_lua = require 'dag-to-lua'
		local conf = [==[` + string(scriptString) + `]==]
	        code = dag_to_lua.generate(conf)
        `); err != nil {
		return "", err
	}

	code := L.GetGlobal("code")

	return code.String(), nil
}

func (h *Handler) Create(c droplet.Context) (interface{}, error) {
	input := c.Input().(*entity.Route)
	//check depend
	if input.ServiceID != nil {
		serviceID := utils.InterfaceToString(input.ServiceID)
		_, err := h.svcStore.Get(c.Context(), serviceID)
		if err != nil {
			if err == data.ErrNotFound {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
					fmt.Errorf(consts.IDNotFound, "service", input.ServiceID)
			}
			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
		}
	}
	if input.UpstreamID != nil {
		upstreamID := utils.InterfaceToString(input.UpstreamID)
		_, err := h.upstreamStore.Get(c.Context(), upstreamID)
		if err != nil {
			if err == data.ErrNotFound {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
					fmt.Errorf("upstream id: %s not found", input.UpstreamID)
			}
			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
		}
	}

	// If route's script_id is set, it must be equals to the route's id.
	if input.ScriptID != nil && (utils.InterfaceToString(input.ID) != utils.InterfaceToString(input.ScriptID)) {
		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			fmt.Errorf("script_id must be the same as id")
	}

	if input.Script != nil {
		if utils.InterfaceToString(input.ID) == "" {
			input.ID = utils.GetFlakeUidStr()
		}
		script := &entity.Script{}
		script.ID = utils.InterfaceToString(input.ID)
		script.Script = input.Script

		var err error
		// Explicitly to lua if input script is of the map type, otherwise
		// it will always represent a piece of lua code of the string type.
		if scriptConf, ok := input.Script.(map[string]interface{}); ok {
			// For lua code of map type, syntax validation is done by
			// the generateLuaCode function
			input.Script, err = generateLuaCode(scriptConf)
			if err != nil {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
			}
		} else {
			// For lua code of string type, use utility func to syntax validation
			err = utils.ValidateLuaCode(input.Script.(string))
			if err != nil {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
			}
		}

		//save original conf
		if _, err = h.scriptStore.Create(c.Context(), script); err != nil {
			return nil, err
		}

		// After saving the Script entity, always set route's script_id
		// the same as route's id.
		input.ScriptID = input.ID
	} else {
		// If script is unset, script_id must be unset neither.
		if input.ScriptID != nil {
			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
				fmt.Errorf("script_id cannot be set if script is unset")
		}
	}

	// check name existed
	ret, err := handler.NameExistCheck(c.Context(), h.routeStore, "route", input.Name, nil)
	if err != nil {
		return ret, err
	}

	// create
	res, err := h.routeStore.Create(c.Context(), input)
	if err != nil {
		return handler.SpecCodeResponse(err), err
	}

	return res, nil
}

type UpdateInput struct {
	ID string `auto_read:"id,path"`
	entity.Route
}

func (h *Handler) Update(c droplet.Context) (interface{}, error) {
	input := c.Input().(*UpdateInput)

	// check if ID in body is equal ID in path
	if err := handler.IDCompare(input.ID, input.Route.ID); err != nil {
		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
	}

	// if has id in path, use it
	if input.ID != "" {
		input.Route.ID = input.ID
	}

	//check depend
	if input.ServiceID != nil {
		serviceID := utils.InterfaceToString(input.ServiceID)
		_, err := h.svcStore.Get(c.Context(), serviceID)
		if err != nil {
			if err == data.ErrNotFound {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
					fmt.Errorf(consts.IDNotFound, "service", input.ServiceID)
			}
			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
		}
	}
	if input.UpstreamID != nil {
		upstreamID := utils.InterfaceToString(input.UpstreamID)
		_, err := h.upstreamStore.Get(c.Context(), upstreamID)
		if err != nil {
			if err == data.ErrNotFound {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
					fmt.Errorf("upstream id: %s not found", input.UpstreamID)
			}
			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
		}
	}

	// If route's script_id is set, it must be equals to the route's id.
	if input.Route.ScriptID != nil && (utils.InterfaceToString(input.ID) != utils.InterfaceToString(input.Route.ScriptID)) {
		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			fmt.Errorf("script_id must be the same as id")
	}

	if input.Script != nil {
		script := &entity.Script{}
		script.ID = input.ID
		script.Script = input.Script

		var err error
		// Explicitly to lua if input script is of the map type, otherwise
		// it will always represent a piece of lua code of the string type.
		if scriptConf, ok := input.Script.(map[string]interface{}); ok {
			// For lua code of map type, syntax validation is done by
			// the generateLuaCode function
			input.Route.Script, err = generateLuaCode(scriptConf)
			if err != nil {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
			}
		} else {
			// For lua code of string type, use utility func to syntax validation
			err = utils.ValidateLuaCode(input.Script.(string))
			if err != nil {
				return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err
			}
		}

		//save original conf
		if _, err = h.scriptStore.Update(c.Context(), script, true); err != nil {
			//if not exists, create
			if err.Error() == fmt.Sprintf("key: %s is not found", script.ID) {
				if _, err := h.scriptStore.Create(c.Context(), script); err != nil {
					return handler.SpecCodeResponse(err), err
				}
			} else {
				return handler.SpecCodeResponse(err), err
			}
		}

		// After updating the Script entity, always set route's script_id
		// the same as route's id.
		input.Route.ScriptID = input.ID
	} else {
		// If script is unset, script_id must be unset neither.
		if input.Route.ScriptID != nil {
			return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
				fmt.Errorf("script_id cannot be set if script is unset")
		}
		//remove exists script
		id := utils.InterfaceToString(input.Route.ID)
		script, _ := h.scriptStore.Get(c.Context(), id)
		if script != nil {
			if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(id, ",")); err != nil {
				log.Warnf("delete script %s failed", input.Route.ID)
			}
		}
	}

	// check name existed
	ret, err := handler.NameExistCheck(c.Context(), h.routeStore, "route", input.Name, input.ID)
	if err != nil {
		return ret, err
	}

	// create
	res, err := h.routeStore.Update(c.Context(), &input.Route, true)
	if err != nil {
		return handler.SpecCodeResponse(err), err
	}

	return res, nil
}

type BatchDelete struct {
	IDs string `auto_read:"ids,path"`
}

func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) {
	input := c.Input().(*BatchDelete)

	//delete route
	if err := h.routeStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil {
		return handler.SpecCodeResponse(err), err
	}

	//delete stored script
	if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil {
		//try again
		log.Warn("try to delete script %s again", input.IDs)
		if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil {
			return nil, nil
		}
	}

	return nil, nil
}

type ExistCheckInput struct {
	Name    string `auto_read:"name,query"`
	Exclude string `auto_read:"exclude,query"`
}

// swagger:operation GET /apisix/admin/notexist/routes checkRouteExist
//
// Return result of route exists checking by name and exclude id.
//
// ---
// produces:
// - application/json
// parameters:
// - name: name
//   in: query
//   description: name of route
//   required: false
//   type: string
// - name: exclude
//   in: query
//   description: id of route that exclude checking
//   required: false
//   type: string
// responses:
//   '0':
//     description: route not exists
//     schema:
//       "$ref": "#/definitions/ApiError"
//   default:
//     description: unexpected error
//     schema:
//       "$ref": "#/definitions/ApiError"
func (h *Handler) Exist(c droplet.Context) (interface{}, error) {
	input := c.Input().(*ExistCheckInput)
	name := input.Name
	exclude := input.Exclude

	ret, err := h.routeStore.List(c.Context(), store.ListInput{
		Predicate: func(obj interface{}) bool {
			r := obj.(*entity.Route)
			if r.Name == name && r.ID != exclude {
				return true
			}

			return false
		},
		PageSize:   0,
		PageNumber: 0,
	})
	if err != nil {
		return nil, err
	}

	if ret.TotalSize > 0 {
		return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest},
			consts.InvalidParam("Route name is reduplicate")
	}

	return nil, nil
}
