package service

import (
	"bufio"
	"context"
	"fmt"
	"os"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto/request"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	agentPsutil "github.com/1Panel-dev/1Panel/agent/utils/psutil"
	"github.com/1Panel-dev/1Panel/agent/utils/websocket"
	"github.com/shirou/gopsutil/v4/net"
	"github.com/shirou/gopsutil/v4/process"
)

type ProcessService struct{}

type IProcessService interface {
	StopProcess(req request.ProcessReq) error
	GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error)
	GetListeningProcess(c context.Context) ([]ListeningProcess, error)
}

func NewIProcessService() IProcessService {
	return &ProcessService{}
}

func (ps *ProcessService) StopProcess(req request.ProcessReq) error {
	proc, err := process.NewProcess(req.PID)
	if err != nil {
		return err
	}
	if err := proc.Kill(); err != nil {
		return err
	}
	return nil
}

type ListeningProcess struct {
	PID      int32
	Port     map[uint32]struct{}
	Protocol uint32
	Name     string
}

func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningProcess, error) {
	conn, err := net.ConnectionsMaxWithContext(c, "inet", 32768)
	if err != nil {
		return nil, err
	}
	// One cache entry per (PID, socket type) so TCP and UDP sockets are not merged under one Protocol.
	type procKey struct {
		pid      int32
		protocol uint32
	}
	procCache := make(map[procKey]ListeningProcess, 64)

	for _, conn := range conn {
		if conn.Pid == 0 {
			continue
		}

		if (conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM) || (conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0) {
			key := procKey{pid: conn.Pid, protocol: conn.Type}
			if _, exists := procCache[key]; !exists {
				proc, err := process.NewProcess(conn.Pid)
				if err != nil {
					continue
				}
				procData := ListeningProcess{
					PID: conn.Pid,
				}
				procData.Name, _ = proc.Name()
				procData.Port = make(map[uint32]struct{})
				procData.Port[conn.Laddr.Port] = struct{}{}
				procData.Protocol = conn.Type
				procCache[key] = procData
			} else {
				p := procCache[key]
				p.Port[conn.Laddr.Port] = struct{}{}
				procCache[key] = p
			}
		}
	}

	procs := make([]ListeningProcess, 0, len(procCache))
	for _, proc := range procCache {
		procs = append(procs, proc)
	}

	return procs, nil
}

func (ps *ProcessService) GetProcessInfoByPID(pid int32) (*websocket.PsProcessData, error) {
	p, err := process.NewProcess(pid)
	if err != nil {
		return nil, fmt.Errorf("get process info by pid %v: %v", pid, err)
	}

	exists, err := p.IsRunning()
	if err != nil || !exists {
		return nil, fmt.Errorf("process %v is not running", pid)
	}

	data := &websocket.PsProcessData{
		PID: pid,
	}

	if name, err := p.Name(); err == nil {
		data.Name = name
	}

	if ppid, err := p.Ppid(); err == nil {
		data.PPID = ppid
	}

	if username, err := p.Username(); err == nil {
		data.Username = username
	}

	if status, err := p.Status(); err == nil {
		if len(status) > 0 {
			data.Status = status[0]
		}
	}

	if createTime, err := agentPsutil.NewProcessCreateTimeResolver().CreateTime(p); err == nil {
		data.StartTime = time.Unix(createTime/1000, 0).Format("2006-01-02 15:04:05")
	}

	if numThreads, err := p.NumThreads(); err == nil {
		data.NumThreads = numThreads
	}

	if connections, err := p.Connections(); err == nil {
		data.NumConnections = len(connections)

		var connects []websocket.ProcessConnect
		for _, conn := range connections {
			pc := websocket.ProcessConnect{
				Status: conn.Status,
				Laddr:  conn.Laddr,
				Raddr:  conn.Raddr,
				PID:    pid,
				Name:   data.Name,
			}
			connects = append(connects, pc)
		}
		data.Connects = connects
	}

	if cpuPercent, err := p.CPUPercent(); err == nil {
		data.CpuValue = cpuPercent
		data.CpuPercent = fmt.Sprintf("%.2f%%", cpuPercent)
	}

	if ioCounters, err := p.IOCounters(); err == nil {
		data.DiskRead = common.FormatBytes(ioCounters.ReadBytes)
		data.DiskWrite = common.FormatBytes(ioCounters.WriteBytes)
	}

	if cmdline, err := p.Cmdline(); err == nil {
		data.CmdLine = cmdline
	}

	if memDetail, err := getMemoryDetail(p.Pid); err == nil {
		data.Rss = common.FormatBytes(memDetail.RSS)
		data.VMS = common.FormatBytes(memDetail.VMS)
		data.HWM = common.FormatBytes(memDetail.HWM)
		data.Data = common.FormatBytes(memDetail.Data)
		data.Stack = common.FormatBytes(memDetail.Stack)
		data.Locked = common.FormatBytes(memDetail.Locked)
		data.Swap = common.FormatBytes(memDetail.Swap)
		data.Dirty = common.FormatBytes(memDetail.Dirty)
		data.RssValue = memDetail.RSS
		data.PSS = common.FormatBytes(memDetail.PSS)
		data.USS = common.FormatBytes(memDetail.USS)
		data.Shared = common.FormatBytes(memDetail.Shared)
		data.Text = common.FormatBytes(memDetail.Text)
	}

	if envs, err := p.Environ(); err == nil {
		data.Envs = envs
	}

	if openFiles, err := p.OpenFiles(); err == nil {
		data.OpenFiles = openFiles
	}

	return data, nil
}

type MemoryDetail struct {
	RSS    uint64
	VMS    uint64
	HWM    uint64
	Data   uint64
	Stack  uint64
	Locked uint64
	Swap   uint64

	PSS    uint64
	USS    uint64
	Shared uint64
	Text   uint64
	Dirty  uint64
}

func getMemoryDetail(pid int32) (*MemoryDetail, error) {
	mem := &MemoryDetail{}

	if err := readStatus(pid, mem); err != nil {
		return nil, err
	}

	if err := readSmapsRollup(pid, mem); err != nil {
		if err := readSmaps(pid, mem); err != nil {
			return nil, err
		}
	}
	return mem, nil
}

func readStatus(pid int32, mem *MemoryDetail) error {
	filePath := fmt.Sprintf("/proc/%d/status", pid)
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}

		key := strings.TrimSuffix(fields[0], ":")
		value, _ := strconv.ParseUint(fields[1], 10, 64)
		value *= 1024

		switch key {
		case "VmRSS":
			mem.RSS = value
		case "VmSize":
			mem.VMS = value
		case "VmData":
			mem.Data = value
		case "VmSwap":
			mem.Swap = value
		case "VmExe":
			mem.Text = value
		case "RssShmem":
			mem.Shared = value
		case "VmHWM":
			mem.HWM = value
		case "VmStk":
			mem.Stack = value
		case "VmLck":
			mem.Locked = value
		}
	}

	return scanner.Err()
}

func readSmapsRollup(pid int32, mem *MemoryDetail) error {
	filePath := fmt.Sprintf("/proc/%d/smaps_rollup", pid)
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}

		key := strings.TrimSuffix(fields[0], ":")
		value, _ := strconv.ParseUint(fields[1], 10, 64)
		value *= 1024

		switch key {
		case "Pss":
			mem.PSS = value
		case "Private_Clean", "Private_Dirty":
			mem.USS += value
		case "Shared_Clean", "Shared_Dirty":
			if mem.Shared == 0 {
				mem.Shared = value
			}
		}
	}

	return scanner.Err()
}

func readSmaps(pid int32, mem *MemoryDetail) error {
	filePath := fmt.Sprintf("/proc/%d/smaps", pid)
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)

	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Fields(line)
		if len(fields) < 2 {
			continue
		}

		key := strings.TrimSuffix(fields[0], ":")
		value, _ := strconv.ParseUint(fields[1], 10, 64)
		value *= 1024

		switch key {
		case "Pss":
			mem.PSS += value
		case "Private_Clean", "Private_Dirty":
			mem.USS += value
		case "Shared_Clean", "Shared_Dirty":
			if mem.Shared == 0 {
				mem.Shared += value
			}
		}
	}

	return scanner.Err()
}
