package system

import (
	"bytes"
	"context"
	"go/parser"
	"go/printer"
	"go/token"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"strings"

	"github.com/go-kratos/kratos/v2/log"
	"github.com/mholt/archives"
	cp "github.com/otiai10/copy"
	"github.com/pkg/errors"

	"kra/internal/conf"
	"kra/pkg/utils"
)

// AutoCodePluginRepo 插件仓储接口
type AutoCodePluginRepo interface {
	GetMenusByIDs(ctx context.Context, ids []uint) ([]SysBaseMenu, error)
	GetApisByIDs(ctx context.Context, ids []uint) ([]SysApi, error)
}

// AutoCodePluginUsecase 插件用例
type AutoCodePluginUsecase struct {
	repo   AutoCodePluginRepo
	config *conf.AutoCodeConfig
	log    *log.Helper
}

// NewAutoCodePluginUsecase 创建插件用例
func NewAutoCodePluginUsecase(
	repo AutoCodePluginRepo,
	config *conf.AutoCodeConfig,
	logger log.Logger,
) *AutoCodePluginUsecase {
	return &AutoCodePluginUsecase{
		repo:   repo,
		config: config,
		log:    log.NewHelper(logger),
	}
}

// Install 插件安装
func (uc *AutoCodePluginUsecase) Install(file *multipart.FileHeader) (web, server int, err error) {
	const PLUGINPATH = "./kra-plug-temp/"
	defer os.RemoveAll(PLUGINPATH)

	if _, err = os.Stat(PLUGINPATH); os.IsNotExist(err) {
		os.Mkdir(PLUGINPATH, os.ModePerm)
	}

	src, err := file.Open()
	if err != nil {
		return -1, -1, err
	}
	defer src.Close()

	out, err := os.Create(PLUGINPATH + file.Filename)
	if err != nil {
		return -1, -1, err
	}
	defer out.Close()

	_, err = io.Copy(out, src)
	if err != nil {
		return -1, -1, err
	}

	paths, err := utils.Unzip(PLUGINPATH+file.Filename, PLUGINPATH)
	if err != nil {
		return -1, -1, err
	}

	paths = uc.filterFile(paths)
	var webIndex = -1
	var serverIndex = -1
	webPlugin := ""
	serverPlugin := ""

	for i := range paths {
		paths[i] = filepath.ToSlash(paths[i])
		pathArr := strings.Split(paths[i], "/")
		ln := len(pathArr)

		if ln < 4 {
			continue
		}
		if pathArr[2]+"/"+pathArr[3] == `server/plugin` && len(serverPlugin) == 0 {
			serverPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
		}
		if pathArr[2]+"/"+pathArr[3] == `web/plugin` && len(webPlugin) == 0 {
			webPlugin = filepath.Join(pathArr[0], pathArr[1], pathArr[2], pathArr[3])
		}
	}

	if len(serverPlugin) == 0 && len(webPlugin) == 0 {
		uc.log.Error("非标准插件，请按照文档自动迁移使用")
		return webIndex, serverIndex, errors.New("非标准插件，请按照文档自动迁移使用")
	}

	if len(serverPlugin) != 0 {
		err = uc.installation(serverPlugin, uc.config.Server, uc.config.Server)
		if err != nil {
			return webIndex, serverIndex, err
		}
	}

	if len(webPlugin) != 0 {
		err = uc.installation(webPlugin, uc.config.Server, uc.config.Web)
		if err != nil {
			return webIndex, serverIndex, err
		}
	}

	return 1, 1, err
}

// installation 安装插件
func (uc *AutoCodePluginUsecase) installation(path string, formPath string, toPath string) error {
	arr := strings.Split(filepath.ToSlash(path), "/")
	ln := len(arr)
	if ln < 3 {
		return errors.New("路径格式错误")
	}
	name := arr[ln-3]

	form := filepath.Join(uc.config.Root, formPath, path)
	to := filepath.Join(uc.config.Root, toPath, "plugin")

	if _, err := os.Stat(to + name); err == nil {
		uc.log.Error("已存在同名插件，请自行手动安装", "to", to)
		return errors.New(toPath + "已存在同名插件，请自行手动安装")
	}

	return cp.Copy(form, to, cp.Options{Skip: uc.skipMacSpecialDocument})
}

// filterFile 过滤文件
func (uc *AutoCodePluginUsecase) filterFile(paths []string) []string {
	np := make([]string, 0, len(paths))
	for _, path := range paths {
		if ok, _ := uc.skipMacSpecialDocument(nil, path, ""); ok {
			continue
		}
		np = append(np, path)
	}
	return np
}

// skipMacSpecialDocument 跳过Mac特殊文件
func (uc *AutoCodePluginUsecase) skipMacSpecialDocument(_ os.FileInfo, src, _ string) (bool, error) {
	if strings.Contains(src, ".DS_Store") || strings.Contains(src, "__MACOSX") {
		return true, nil
	}
	return false, nil
}

// PubPlug 发布插件
func (uc *AutoCodePluginUsecase) PubPlug(plugName string) (zipPath string, err error) {
	if plugName == "" {
		return "", errors.New("插件名称不能为空")
	}

	// 防止路径穿越
	plugName = filepath.Clean(plugName)

	webPath := filepath.Join(uc.config.Root, uc.config.Web, "plugin", plugName)
	serverPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", plugName)

	// 判断目录是否存在
	if _, err = os.Stat(webPath); err != nil {
		return "", errors.New("web路径不存在")
	}
	if _, err = os.Stat(serverPath); err != nil {
		return "", errors.New("server路径不存在")
	}

	fileName := plugName + ".zip"

	// 创建zip文件
	files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{
		webPath:    plugName + "/web/plugin/" + plugName,
		serverPath: plugName + "/server/plugin/" + plugName,
	})
	if err != nil {
		return "", err
	}

	out, err := os.Create(fileName)
	if err != nil {
		return "", err
	}
	defer out.Close()

	format := archives.CompressedArchive{
		Archival: archives.Zip{},
	}

	if err = format.Archive(context.Background(), out, files); err != nil {
		return "", err
	}

	return filepath.Join(uc.config.Root, uc.config.Server, fileName), nil
}

// InitMenuRequest 初始化菜单请求
type InitMenuRequest struct {
	PlugName   string `json:"plugName"`
	ParentMenu string `json:"parentMenu"`
	Menus      []uint `json:"menus"`
}

// InitMenu 初始化菜单
func (uc *AutoCodePluginUsecase) InitMenu(ctx context.Context, menuInfo InitMenuRequest) error {
	menuPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", menuInfo.PlugName, "initialize", "menu.go")

	src, err := os.ReadFile(menuPath)
	if err != nil {
		return err
	}

	fileSet := token.NewFileSet()
	astFile, err := parser.ParseFile(fileSet, "", src, 0)
	if err != nil {
		return err
	}

	arrayAst := utils.FindArray(astFile, "model", "SysBaseMenu")
	if arrayAst == nil {
		return errors.New("未找到菜单数组")
	}

	// 查询菜单
	menus, err := uc.repo.GetMenusByIDs(ctx, menuInfo.Menus)
	if err != nil {
		return err
	}

	// 添加父菜单
	parentMenu := SysBaseMenu{
		ParentId:  0,
		Path:      menuInfo.PlugName + "Menu",
		Name:      menuInfo.PlugName + "Menu",
		Hidden:    false,
		Component: "view/routerHolder.vue",
		Sort:      0,
		Meta: Meta{
			Title: menuInfo.ParentMenu,
			Icon:  "school",
		},
	}

	allMenus := append([]SysBaseMenu{parentMenu}, menus...)

	// 转换为utils.SysBaseMenu类型
	utilsMenus := make([]utils.SysBaseMenu, len(allMenus))
	for i, m := range allMenus {
		utilsMenus[i] = utils.SysBaseMenu{
			ParentId:  m.ParentId,
			Path:      m.Path,
			Name:      m.Name,
			Hidden:    m.Hidden,
			Component: m.Component,
			Sort:      m.Sort,
			Title:     m.Meta.Title,
			Icon:      m.Meta.Icon,
		}
		// 转换参数
		for _, p := range m.Parameters {
			utilsMenus[i].Parameters = append(utilsMenus[i].Parameters, utils.SysBaseMenuParameter{
				Type:  p.Type,
				Key:   p.Key,
				Value: p.Value,
			})
		}
		// 转换按钮
		for _, b := range m.MenuBtn {
			utilsMenus[i].MenuBtn = append(utilsMenus[i].MenuBtn, utils.SysBaseMenuBtn{
				Name: b.Name,
				Desc: b.Desc,
			})
		}
	}

	menuExpr := utils.CreateMenuStructAst(utilsMenus)
	arrayAst.Elts = *menuExpr

	var out []byte
	bf := bytes.NewBuffer(out)
	printer.Fprint(bf, fileSet, astFile)

	return os.WriteFile(menuPath, bf.Bytes(), 0666)
}

// InitApiRequest 初始化API请求
type InitApiRequest struct {
	PlugName string `json:"plugName"`
	APIs     []uint `json:"apis"`
}

// InitAPI 初始化API
func (uc *AutoCodePluginUsecase) InitAPI(ctx context.Context, apiInfo InitApiRequest) error {
	apiPath := filepath.Join(uc.config.Root, uc.config.Server, "plugin", apiInfo.PlugName, "initialize", "api.go")

	src, err := os.ReadFile(apiPath)
	if err != nil {
		return err
	}

	fileSet := token.NewFileSet()
	astFile, err := parser.ParseFile(fileSet, "", src, 0)
	if err != nil {
		return err
	}

	arrayAst := utils.FindArray(astFile, "model", "SysApi")
	if arrayAst == nil {
		return errors.New("未找到API数组")
	}

	// 查询API
	apis, err := uc.repo.GetApisByIDs(ctx, apiInfo.APIs)
	if err != nil {
		return err
	}

	// 转换为utils.SysApi类型
	utilsApis := make([]utils.SysApi, len(apis))
	for i, a := range apis {
		utilsApis[i] = utils.SysApi{
			Path:        a.Path,
			Description: a.Description,
			ApiGroup:    a.ApiGroup,
			Method:      a.Method,
		}
	}

	apisExpr := utils.CreateApiStructAst(utilsApis)
	arrayAst.Elts = *apisExpr

	var out []byte
	bf := bytes.NewBuffer(out)
	printer.Fprint(bf, fileSet, astFile)

	return os.WriteFile(apiPath, bf.Bytes(), 0666)
}
