package srvconn

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"
	"syscall"
	"time"

	"github.com/pkg/sftp"

	"github.com/jumpserver/koko/pkg/logger"
	"github.com/jumpserver/koko/pkg/model"
	"github.com/jumpserver/koko/pkg/service"
)

type UserSftpConn struct {
	User *model.User
	Addr string
	Dirs map[string]os.FileInfo

	modeTime time.Time
	logChan  chan *model.FTPLog

	closed chan struct{}

	searchDir *SearchResultDir
}

func (u *UserSftpConn) ReadDir(path string) (res []os.FileInfo, err error) {
	fi, restPath := u.ParsePath(path)
	if rootDir, ok := fi.(*UserSftpConn); ok {
		return rootDir.List()
	}

	if nodeDir, ok := fi.(*NodeDir); ok {
		return nodeDir.List()
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.ReadDir(restPath)
	}

	return nil, sftp.ErrSSHFxNoSuchFile
}

func (u *UserSftpConn) Stat(path string) (res os.FileInfo, err error) {
	fi, restPath := u.ParsePath(path)
	if rootDir, ok := fi.(*UserSftpConn); ok {
		return rootDir, nil
	}

	if nodeDir, ok := fi.(*NodeDir); ok {
		return nodeDir, nil
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.Stat(restPath)
	}

	return nil, sftp.ErrSSHFxNoSuchFile
}

func (u *UserSftpConn) ReadLink(path string) (name string, err error) {
	fi, restPath := u.ParsePath(path)
	if _, ok := fi.(*UserSftpConn); ok && restPath == "" {
		return "", sftp.ErrSshFxOpUnsupported
	}

	if _, ok := fi.(*NodeDir); ok {
		return "", sftp.ErrSshFxOpUnsupported
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.ReadLink(restPath)
	}

	return "", sftp.ErrSshFxOpUnsupported
}

func (u *UserSftpConn) Rename(oldNamePath, newNamePath string) (err error) {
	oldFi, oldRestPath := u.ParsePath(oldNamePath)
	newFi, newRestPath := u.ParsePath(newNamePath)
	if oldAssetDir, ok := oldFi.(*AssetDir); ok {
		if newAssetDir, newOk := newFi.(*AssetDir); newOk {
			if oldAssetDir == newAssetDir {
				return oldAssetDir.Rename(oldRestPath, newRestPath)
			}
		}

	}
	return sftp.ErrSshFxOpUnsupported
}

func (u *UserSftpConn) RemoveDirectory(path string) (err error) {
	fi, restPath := u.ParsePath(path)
	if _, ok := fi.(*UserSftpConn); ok && restPath == "" {
		return sftp.ErrSshFxPermissionDenied
	}

	if _, ok := fi.(*NodeDir); ok {
		return sftp.ErrSshFxPermissionDenied
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.RemoveDirectory(restPath)
	}
	return sftp.ErrSshFxPermissionDenied
}

func (u *UserSftpConn) Remove(path string) (err error) {
	fi, restPath := u.ParsePath(path)
	if _, ok := fi.(*UserSftpConn); ok && restPath == "" {
		return sftp.ErrSshFxPermissionDenied
	}

	if _, ok := fi.(*NodeDir); ok {
		return sftp.ErrSshFxPermissionDenied
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.Remove(restPath)
	}
	return sftp.ErrSshFxPermissionDenied
}

func (u *UserSftpConn) MkdirAll(path string) (err error) {
	fi, restPath := u.ParsePath(path)
	if _, ok := fi.(*UserSftpConn); ok && restPath == "" {
		return sftp.ErrSshFxPermissionDenied
	}

	if _, ok := fi.(*NodeDir); ok {
		return sftp.ErrSshFxPermissionDenied
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.MkdirAll(restPath)
	}
	return sftp.ErrSshFxPermissionDenied
}

func (u *UserSftpConn) Symlink(oldNamePath, newNamePath string) (err error) {
	oldFi, oldRestPath := u.ParsePath(oldNamePath)
	newFi, newRestPath := u.ParsePath(newNamePath)
	if oldAssetDir, ok := oldFi.(*AssetDir); ok {
		if newAssetDir, newOk := newFi.(*AssetDir); newOk {
			if oldAssetDir == newAssetDir {
				return oldAssetDir.Symlink(oldRestPath, newRestPath)
			}
		}
	}
	return sftp.ErrSshFxPermissionDenied
}

func (u *UserSftpConn) Create(path string) (*sftp.File, error) {
	fi, restPath := u.ParsePath(path)
	if _, ok := fi.(*UserSftpConn); ok {
		return nil, sftp.ErrSshFxPermissionDenied
	}

	if _, ok := fi.(*NodeDir); ok {
		return nil, sftp.ErrSshFxPermissionDenied
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.Create(restPath)
	}

	return nil, sftp.ErrSshFxPermissionDenied
}

func (u *UserSftpConn) Open(path string) (*sftp.File, error) {
	fi, restPath := u.ParsePath(path)
	if _, ok := fi.(*UserSftpConn); ok {
		return nil, sftp.ErrSshFxPermissionDenied
	}

	if _, ok := fi.(*NodeDir); ok {
		return nil, sftp.ErrSshFxPermissionDenied
	}
	if assetDir, ok := fi.(*AssetDir); ok {
		return assetDir.Open(restPath)
	}

	return nil, sftp.ErrSshFxPermissionDenied
}

func (u *UserSftpConn) Close() {
	for _, dir := range u.Dirs {
		if nodeDir, ok := dir.(*NodeDir); ok {
			nodeDir.close()
			continue
		}
		if assetDir, ok := dir.(*AssetDir); ok {
			assetDir.close()
			continue
		}
	}
	if u.searchDir != nil{
		u.searchDir.close()
	}
	close(u.closed)
}

func (u *UserSftpConn) Name() string {
	return "/"
}

func (u *UserSftpConn) Size() int64 { return 0 }

func (u *UserSftpConn) Mode() os.FileMode {
	return os.ModePerm | os.ModeDir
}

func (u *UserSftpConn) ModTime() time.Time { return u.modeTime }

func (u *UserSftpConn) IsDir() bool { return true }

func (u *UserSftpConn) Sys() interface{} {
	return &syscall.Stat_t{Uid: 0, Gid: 0}
}

func (u *UserSftpConn) List() (res []os.FileInfo, err error) {
	for _, item := range u.Dirs {
		res = append(res, item)
	}
	return
}

func (u *UserSftpConn) ParsePath(path string) (fi os.FileInfo, restPath string) {
	path = strings.TrimPrefix(path, "/")
	data := strings.Split(path, "/")
	if len(data) == 1 && data[0] == "" {
		fi = u
		return
	}
	var dirs map[string]os.FileInfo
	var ok bool

	if data[0] == SearchFolderName {
		dirs = u.searchDir.subDirs
		data = data[1:]
	} else {
		dirs = u.Dirs
	}
	for i := 0; i < len(data); i++ {
		fi, ok = dirs[data[i]]
		if !ok {
			restPath = strings.Join(data[i+1:], "/")
			break
		}
		if nodeDir, ok := fi.(*NodeDir); ok {
			nodeDir.loadNodeAsset(u)
			dirs = nodeDir.subDirs
			continue
		}
		if assetDir, ok := fi.(*AssetDir); ok {
			assetDir.loadSystemUsers()
			restPath = strings.Join(data[i+1:], "/")
			break
		}
	}
	return
}

func (u *UserSftpConn) initial() {
	nodeTrees := service.GetUserNodeTreeWithAsset(u.User.ID, "", "")
	if u.Dirs == nil {
		u.Dirs = map[string]os.FileInfo{}
	}
	u.searchDir = &SearchResultDir{
		folderName: SearchFolderName,
		modeTime:   time.Now().UTC(),
		subDirs:    map[string]os.FileInfo{}}

	for _, item := range nodeTrees {
		if item.Pid != "" {
			continue
		}
		typeName, ok := item.Meta["type"].(string)
		if !ok {
			continue
		}
		body, err := json.Marshal(item.Meta[typeName])
		if err != nil {
			logger.Errorf("Json Marshal err: %s", err)
			continue
		}
		switch typeName {
		case "node":
			node, err := model.ConvertMetaToNode(body)
			if err != nil {
				logger.Errorf("convert to node err: %s", err)
				continue
			}
			nodeDir := NewNodeDir(node)
			folderName := nodeDir.folderName
			for {
				_, ok := u.Dirs[folderName]
				if !ok {
					break
				}
				folderName = fmt.Sprintf("%s_", folderName)
			}
			if folderName != nodeDir.folderName {
				nodeDir.folderName = folderName
			}

			u.Dirs[folderName] = &nodeDir
		case "asset":
			asset, err := model.ConvertMetaToAsset(body)
			if err != nil {
				logger.Errorf("convert to asset err: %s", err)
				continue
			}
			if !asset.IsSupportProtocol("ssh") {
				continue
			}
			assetDir := NewAssetDir(u.User, asset, u.Addr, u.logChan)
			folderName := assetDir.folderName
			for {
				_, ok := u.Dirs[folderName]
				if !ok {
					break
				}
				folderName = fmt.Sprintf("%s_", folderName)
			}
			if folderName != assetDir.folderName {
				assetDir.folderName = folderName
			}
			u.Dirs[folderName] = &assetDir
		}
	}

}

func (u *UserSftpConn) loopPushFTPLog() {
	ftpLogList := make([]*model.FTPLog, 0, 1024)
	maxRetry := 0
	var err error
	tick := time.NewTicker(time.Second * 20)
	defer tick.Stop()
	for {
		select {
		case <-u.closed:
			if len(ftpLogList) == 0 {
				return
			}
		case <-tick.C:
			if len(ftpLogList) == 0 {
				continue
			}
		case logData, ok := <-u.logChan:
			if !ok {
				return
			}
			ftpLogList = append(ftpLogList, logData)
		}

		data := ftpLogList[len(ftpLogList)-1]
		err = service.PushFTPLog(data)
		if err == nil {
			ftpLogList = ftpLogList[:len(ftpLogList)-1]
			maxRetry = 0
			continue
		} else {
			logger.Errorf("Create FTP log err: %s", err.Error())
		}

		if maxRetry > 5 {
			ftpLogList = ftpLogList[1:]
		}
		maxRetry++
	}
}

func (u *UserSftpConn) Search(key string) (res []os.FileInfo, err error) {
	if u.searchDir == nil{
		logger.Errorf("not found search folder")
		return nil, fmt.Errorf("not found")
	}
	assetsTree, err := service.SearchPermAsset(u.User.ID, key)
	if err != nil {
		logger.Errorf("search asset err: %s", err)
		return nil, err
	}
	subDirs := map[string]os.FileInfo{}
	for _, item := range assetsTree {
		typeName, ok := item.Meta["type"].(string)
		if !ok {
			continue
		}
		body, err := json.Marshal(item.Meta[typeName])
		if err != nil {
			logger.Errorf("Search Json Marshal err: %s", err)
			continue
		}
		switch typeName {
		case "asset":
			asset, err := model.ConvertMetaToAsset(body)
			if err != nil {
				logger.Errorf("convert to asset err: %s", err)
				continue
			}
			if !asset.IsSupportProtocol("ssh") {
				continue
			}
			assetDir := NewAssetDir(u.User, asset, u.Addr, u.logChan)
			folderName := assetDir.folderName
			for {
				_, ok := subDirs[folderName]
				if !ok {
					break
				}
				folderName = fmt.Sprintf("%s_", folderName)
			}
			if folderName != assetDir.folderName {
				assetDir.folderName = folderName
			}
			subDirs[assetDir.folderName] = &assetDir
		}
	}
	u.searchDir.SetSubDirs(subDirs)
	return u.searchDir.List()
}

func NewUserSftpConn(user *model.User, addr string) *UserSftpConn {
	u := UserSftpConn{
		User:     user,
		Addr:     addr,
		Dirs:     map[string]os.FileInfo{},
		modeTime: time.Now().UTC(),
		logChan:  make(chan *model.FTPLog, 1024),
		closed:   make(chan struct{}),
	}
	u.initial()
	go u.loopPushFTPLog()
	return &u
}

func NewUserSftpConnWithAssets(user *model.User, addr string, assets ...model.Asset) *UserSftpConn {
	u := UserSftpConn{
		User:     user,
		Addr:     addr,
		Dirs:     map[string]os.FileInfo{},
		modeTime: time.Now().UTC(),
		logChan:  make(chan *model.FTPLog, 1024),
		closed:   make(chan struct{}),
	}
	for _, asset := range assets {
		if asset.IsSupportProtocol("ssh") {
			assetDir := NewAssetDir(u.User, asset, u.Addr, u.logChan)
			folderName := assetDir.folderName
			for {
				_, ok := u.Dirs[folderName]
				if !ok {
					break
				}
				folderName = fmt.Sprintf("%s_", folderName)
			}
			if folderName != assetDir.folderName {
				assetDir.folderName = folderName
			}
			u.Dirs[assetDir.folderName] = &assetDir
		}
	}
	go u.loopPushFTPLog()
	return &u
}
