package nym

import (
	"encoding/binary"
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"time"

	"golang.org/x/net/websocket"
)

// Client sends payloads through the Nym mixnet via an embedded nym-client subprocess.
type Client struct {
	journalistAddr string
	recipient      []byte // parsed 96-byte wire form of journalistAddr
	wsPort         int
	dryRun         bool
	process        *exec.Cmd
	ws             *websocket.Conn
	binFS          fs.FS
	configDir      string
}

func New(journalistAddr string, binFS fs.FS) (*Client, error) {
	recipient, err := ParseRecipient(journalistAddr)
	if err != nil {
		return nil, fmt.Errorf("parse journalist nym address: %w", err)
	}
	return &Client{journalistAddr: journalistAddr, recipient: recipient, wsPort: 1978, binFS: binFS}, nil
}

func NewDryRun(journalistAddr string) *Client {
	return &Client{journalistAddr: journalistAddr, dryRun: true}
}

// Start extracts the nym-client binary, inits config, starts the subprocess,
// and connects to its WebSocket. No-op in dry-run mode.
func (c *Client) Start() error {
	if c.dryRun {
		fmt.Printf("[dry-run] Nym client skipped — journalist: %s\n", c.journalistAddr)
		return nil
	}

	binPath, tmpDir, err := extractBin(c.binFS)
	if err != nil {
		return fmt.Errorf("extract nym-client: %w", err)
	}

	homeDir, _ := os.UserHomeDir()
	c.configDir = filepath.Join(homeDir, ".nymdrop-server")

	// Init (idempotent — ignore error if already initialised).
	exec.Command(binPath, "init", "--id", "nymdrop-server").Run()

	cmd := exec.Command(binPath, "run", "--id", "nymdrop-server",
		"--port", fmt.Sprintf("%d", c.wsPort))
	cmd.Stdout = os.Stderr // route nym-client noise to our stderr
	cmd.Stderr = os.Stderr
	if err := cmd.Start(); err != nil {
		os.RemoveAll(tmpDir)
		return fmt.Errorf("nym-client start: %w", err)
	}
	c.process = cmd

	wsURL := fmt.Sprintf("ws://127.0.0.1:%d", c.wsPort)
	for i := 0; i < 120; i++ {
		ws, err := websocket.Dial(wsURL, "", "http://localhost/")
		if err == nil {
			c.ws = ws
			return nil
		}
		time.Sleep(500 * time.Millisecond)
	}
	return fmt.Errorf("nym-client websocket did not start in time")
}

// Send delivers payload to the journalist's Nym address through the mixnet
// using nym-client's binary websocket protocol (tag || recipient || conn_id ||
// data_len || data). This avoids the JSON+base64 wrapping of the old text
// protocol, which inflated every payload by ~4/3 before it ever reached the
// nym-client's own 16MB hardcoded websocket message limit.
func (c *Client) Send(payload []byte) error {
	if c.dryRun {
		fmt.Printf("[dry-run] payload received: %d bytes — would send to %s\n",
			len(payload), c.journalistAddr)
		return nil
	}

	req := make([]byte, 0, 1+len(c.recipient)+8+8+len(payload))
	req = append(req, ReqSend)
	req = append(req, c.recipient...)
	req = binary.BigEndian.AppendUint64(req, 0) // connection_id: none
	req = binary.BigEndian.AppendUint64(req, uint64(len(payload)))
	req = append(req, payload...)

	if err := websocket.Message.Send(c.ws, req); err != nil {
		return fmt.Errorf("ws send: %w", err)
	}
	return nil
}

// Stop terminates the subprocess.
func (c *Client) Stop() {
	if c.ws != nil {
		c.ws.Close()
	}
	if c.process != nil && c.process.Process != nil {
		c.process.Process.Kill()
	}
}

func extractBin(binFS fs.FS) (string, string, error) {
	tmpDir, err := os.MkdirTemp("", "nymdrop-server-*")
	if err != nil {
		return "", "", err
	}
	name := "nym-client"
	if runtime.GOOS == "windows" {
		name += ".exe"
	}
	binPath := filepath.Join(tmpDir, name)
	data, err := fs.ReadFile(binFS, "bin/nym-client-linux-amd64")
	if err != nil {
		os.RemoveAll(tmpDir)
		return "", "", err
	}
	if err := os.WriteFile(binPath, data, 0700); err != nil {
		os.RemoveAll(tmpDir)
		return "", "", err
	}
	return binPath, tmpDir, nil
}

// Ensure unused import io is not flagged.
var _ = io.EOF
