package logic

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/shirou/gopsutil/v4/process"
)

/*
	思路：由于tabby比较特殊，所以分两类：普通sftp客户端日志和tabby客户端日志

	普通：
		<86>Jan 27 16:48:22 admin02 sftp-server[3965704]: open "/tmp/id_ed25519" flags WRITE,CREATE,TRUNCATE mode 0666
		<86>Jan 27 16:48:22 admin02 sftp-server[3965704]: close "/tmp/id_ed25519" bytes read 0 written 419
		解析到open动作以及CREATE标志，记录文件位置；等到解析到同文件的close动作表示写文件完成，立即扫描该文件

	tabby：
		<86>Jan 27 16:00:53 admin02 sftp-server[3761842]: open "/tmp/X680G55_9800195108886297_20251230-012816_bmcblackinfo.tar.tabby-upload" flags WRITE,CREATE mode 0666
		<86>Jan 27 16:00:53 admin02 sftp-server[3761842]: close "/tmp/X680G55_9800195108886297_20251230-012816_bmcblackinfo.tar.tabby-upload" bytes read 0 written 1412082
		<86>Jan 27 16:00:53 admin02 sftp-server[3761842]: remove name "/tmp/X680G55_9800195108886297_20251230-012816_bmcblackinfo.tar"
		<86>Jan 27 16:00:53 admin02 sftp-server[3761842]: sent status No such file
		<86>Jan 27 16:00:53 admin02 sftp-server[3761842]: rename old "/tmp/X680G55_9800195108886297_20251230-012816_bmcblackinfo.tar.tabby-upload" new "/tmp/X680G55_9800195108886297_20251230-012816_bmcblackinfo.tar"
		解析到open动作以及CREATE标志，记录文件位置；等到解析到同文件的rename动作表示写文件完成，立即扫描该文件

*/

var (
	ReMatch        = regexp.MustCompile(`<\d+>([A-Z][a-z]+\s+\d+\s+\d+:\d+:\d+)\s+[a-zA-Z0-9]+\s+sftp-server\[(\d+)\]:\s+([a-z]+)\s+(.*)$`)
	LogItemMap     = make(map[string][]*SftpLogItem) // key为文件名，value为两个对象指针，一个记录文件上传开始，一个记录文件上传结束
	LogItemMapLock = sync.RWMutex{}
)

type Action int

const (
	ActionOpenDir Action = iota
	ActionCloseDir
	ActionOpenFile
	ActionCloseFile
	ActionRemove
	ActionRename
)

const (
	TabbySuffix = ".tabby-upload"
	ScanSuffix  = ".scanning"
)

// SftpLogItem sftp日志类型，Flag的key可以为：name(remove) old(rename) new(rename) flags(open) mode(open) read(close) written(close)
type SftpLogItem struct {
	Pid      int
	UserInfo string // uid或用户名
	Time     time.Time
	Act      Action
	Target   string         // 动作的目标，针对open、close，是文件路径，针对rename，是文件原名
	Flag     map[string]any // 动作的参数，针对open、close、rename才有
	IsTabby  bool           // 针对tabby需要特殊处理
}

// FinalFile 获取最终文件路径
func (sli *SftpLogItem) FinalFile() (string, error) {
	if !sli.IsTabby {
		return sli.Target, nil
	}
	switch sli.Act {
	case ActionOpenFile, ActionCloseFile:
		return strings.TrimSuffix(sli.Target, TabbySuffix), nil
	case ActionRename:
		if sli.Flag == nil {
			return strings.TrimSuffix(sli.Target, TabbySuffix), nil
		}
		nname, have := sli.Flag["new"]
		if !have {
			return strings.TrimSuffix(sli.Target, TabbySuffix), nil
		}
		return nname.(string), nil
	default:
		return "", fmt.Errorf("unexcept action: %d", sli.Act)
	}
}

func ParseItem(str string) (*SftpLogItem, error) {
	str = strings.Trim(strings.Trim(str, "\n"), " ")
	fields := ReMatch.FindStringSubmatch(str)
	if len(fields) == 0 {
		return nil, nil
	}
	var act Action
	switch fields[3] {
	case "open":
		act = ActionOpenFile
	case "close":
		act = ActionCloseFile
	case "rename":
		act = ActionRename
	default:
		return nil, nil
	}
	time_raw, err := time.Parse("Jan _2 15:04:05", fields[1])
	if err != nil {
		return nil, err
	}
	time_raw = time_raw.AddDate(time.Now().Year()-time_raw.Year(), 0, 0)
	pid, err := strconv.Atoi(fields[2])
	if err != nil {
		return nil, err
	}
	proc, err := process.NewProcess(int32(pid))
	if err != nil {
		return nil, err
	}
	uinfo, err := proc.Username()
	if err != nil {
		uinfo = fmt.Sprintf("%d",proc.Uids())
	} else {

	}

	result := SftpLogItem{Act: act, Time: time_raw, Pid: pid}
	switch result.Act {
	case ActionOpenFile:
		err = parseOpen(fields[4], &result)
	case ActionCloseFile:
		err = parseClose(fields[4], &result)
	case ActionRename:
		err = parseRename(fields[4], &result)
	}
	return &result, err
}

// parseClose
func parseClose(str string, target *SftpLogItem) error {
	str = strings.Trim(strings.Trim(str, "\n"), " ")
	fields := strings.Fields(str)
	last := ""
	for _, v := range fields {
		if strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`) {
			target.Target = strings.Trim(v, `"`)
			target.IsTabby = strings.HasSuffix(target.Target, TabbySuffix)
			last = v
			continue
		}
		switch v {
		case "bytes", "read", "written":
			last = v
		default:
			i, err := strconv.Atoi(v)
			if err != nil {
				last = v
				break
			}
			if last != "" {
				if target.Flag == nil {
					target.Flag = make(map[string]any)
				}
				target.Flag[last] = i
				last = ""
			} else {
				return fmt.Errorf("unknown parse close log: %s", str)
			}
		}
	}
	return nil
}

func parseOpen(str string, target *SftpLogItem) error {
	str = strings.Trim(strings.Trim(str, "\n"), " ")
	fields := strings.Fields(str)
	last := ""
	for _, v := range fields {
		if strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`) {
			target.Target = strings.Trim(v, `"`)
			target.IsTabby = strings.HasSuffix(target.Target, TabbySuffix)
			last = v
			continue
		}
		switch v {
		case "flags", "mode":
			last = v
		default:
			if last != "" {
				if target.Flag == nil {
					target.Flag = make(map[string]any)
				}
				target.Flag[last] = v
				last = ""
			} else {
				return fmt.Errorf("unknown parse open log: %s", str)
			}
		}
	}
	return nil
}

func parseRename(str string, target *SftpLogItem) error {
	str = strings.Trim(strings.Trim(str, "\n"), " ")
	fields := strings.Fields(str)
	last := ""
	for _, v := range fields {
		switch v {
		case "old", "new":
			last = v
		default:
			if strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`) {
				if last != "" {
					if target.Flag == nil {
						target.Flag = make(map[string]any)
					}
					target.Flag[last] = strings.Trim(v, `"`)
					if last == "old" {
						target.Target = strings.Trim(v, `"`)
						target.IsTabby = strings.HasSuffix(target.Target, TabbySuffix)
					}
					last = ""
				} else {
					return fmt.Errorf("unknown parse rename log: %s", str)
				}
				last = v
			} else {
				return fmt.Errorf("unknown parse rename log: %s", str)
			}
		}
	}
	return nil
}

// HandleSftpLog 处理sftp日志
func HandleSftpLog(str string) {
	item, err := ParseItem(str)
	if err != nil {
		log.Printf("parse sftp log error,log: %s, err: %v", str, err)
	}
	if item == nil {
		return
	}
	switch item.Act {
	case ActionOpenFile:
		if item.Flag == nil {
			return
		}
		flag, have := item.Flag["flags"]
		if !have {
			return
		}
		f := flag.(string)
		if strings.Contains(f, "CREATE") {
			LogItemMapLock.Lock()
			ii := make([]*SftpLogItem, 0, 2)
			ii = append(ii, item)
			LogItemMap[item.Target] = ii
			LogItemMapLock.Unlock()
		}
	case ActionCloseFile:
		if item.IsTabby {
			return
		}
		rl := LogItemMapLock.RLocker()
		rl.Lock()
		i, have := LogItemMap[item.Target]
		rl.Unlock()
		if !have {
			log.Printf("close file but not found open: %s", item.Target)

			return
		}
		i[1] = item
		scanFile(i)
	case ActionRename:
		if !item.IsTabby {
			return
		}
		rl := LogItemMapLock.RLocker()
		rl.Lock()
		i, have := LogItemMap[item.Target]
		rl.Unlock()
		if !have {
			log.Printf("rename file but not found open: %s", item.Target)
			return
		}
		i[1] = item
		scanFile(i)
	}
}

func scanFile(logItems []*SftpLogItem) {
	if len(logItems) < 2 {
		return
	}
	// // 记录权限
	filePath, err := logItems[0].FinalFile()
	if err != nil {
		log.Printf("get final file path error: %v", err)
		return
	}
	fileStat, err := os.Stat(filePath)
	fileMode := fileStat.Mode()
	os.Chmod(filePath, 0000)
	// 修改名称
	reFilePath := fmt.Sprintf("%s%s", filePath, ScanSuffix)
	os.Rename(filePath, reFilePath)
	// 扫描文件
	cmd := exec.Command("clamdscan", "-i", "--no-summary", reFilePath)
	output, err := cmd.CombinedOutput()
	if cmd.ProcessState.ExitCode() != 0 {
		// 有问题
		content := string(output)
		log.Printf("find virus file: %s", content)
	} else {
		// 没问题
		os.Rename(reFilePath, filePath)
		os.Chmod(filePath, fileMode)
	}
}
