// Package encoder implements the send-only YAMN v2 client encoder.
package encoder

import (
	"bufio"
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"io"
	"net/mail"
	"os"
	"strconv"
	"strings"
	"time"

	"golang.org/x/crypto/blake2s"
	"golang.org/x/crypto/nacl/box"
)

const (
	maxChainLength = 10
	headerBytes    = 256
	encHeadBytes   = 160
	bodyBytes      = 17920
	maxPlainBytes  = 17910
	messageBytes   = maxChainLength*headerBytes + bodyBytes
	armorVersion   = "0.2.7"
)

var (
	ErrInvalidRequest = errors.New("invalid YAMN request")
	ErrInvalidKeyring = errors.New("invalid YAMN public keyring")
)

type Kind string

const (
	Email  Kind = "email"
	Usenet Kind = "usenet"
)

// Request contains one plaintext message and an entry-to-exit chain.
// PublicKeyring is a local pubring.mix path and is never fetched by Encode.
type Request struct {
	Kind          Kind
	PublicKeyring string
	Entry         string
	Chain         []string
	From          string
	ReplyTo       string
	To            string
	Subject       string
	Newsgroup     string
	Body          string
	References    string
}

type Result struct {
	EntryAddress string
	Envelope     []byte
}

type remailerKey struct {
	name, address    string
	keyID, publicKey []byte
}

// Encode creates an armored YAMN v2 packet. It performs no network I/O.
func Encode(r Request) (Result, error) {
	if err := Validate(r); err != nil {
		return Result{}, err
	}
	keys, err := loadKeyring(r.PublicKeyring)
	if err != nil {
		return Result{}, err
	}
	chain := make([]remailerKey, len(r.Chain))
	for i, name := range r.Chain {
		key, ok := keys[name]
		if !ok {
			return Result{}, fmt.Errorf("remailer %q not found in keyring", name)
		}
		chain[i] = key
	}
	plain, err := composeMessage(r)
	if err != nil {
		return Result{}, err
	}
	packet, err := encodePacket(plain, chain)
	if err != nil {
		return Result{}, err
	}
	return Result{EntryAddress: chain[0].address, Envelope: armor(packet)}, nil
}

func Validate(r Request) error {
	if r.Kind != Email && r.Kind != Usenet {
		return fmt.Errorf("%w: unsupported message kind", ErrInvalidRequest)
	}
	if strings.TrimSpace(r.PublicKeyring) == "" {
		return fmt.Errorf("%w: missing public keyring", ErrInvalidRequest)
	}
	if len(r.Chain) == 0 || len(r.Chain) > maxChainLength {
		return fmt.Errorf("%w: invalid chain length", ErrInvalidRequest)
	}
	for _, hop := range r.Chain {
		if !isRemailerName(hop) {
			return fmt.Errorf("%w: invalid remailer name", ErrInvalidRequest)
		}
	}
	if strings.TrimSpace(r.Entry) != "" && strings.TrimSpace(r.Entry) != r.Chain[0] {
		return fmt.Errorf("%w: entry does not match chain", ErrInvalidRequest)
	}
	if strings.TrimSpace(r.Body) == "" || len([]byte(r.Body)) > maxPlainBytes {
		return fmt.Errorf("%w: body is empty or too large", ErrInvalidRequest)
	}
	if r.Kind == Email {
		if _, err := mail.ParseAddress(r.To); err != nil {
			return fmt.Errorf("%w: invalid recipient address", ErrInvalidRequest)
		}
	} else {
		if strings.TrimSpace(r.Newsgroup) == "" || !validHeaderValue(r.Newsgroup) {
			return fmt.Errorf("%w: missing or invalid newsgroup", ErrInvalidRequest)
		}
		if _, err := mail.ParseAddress(r.To); err != nil {
			return fmt.Errorf("%w: invalid Usenet gateway recipient", ErrInvalidRequest)
		}
	}
	for _, value := range []string{r.From, r.ReplyTo, r.To, r.Subject, r.Newsgroup, r.References} {
		if !validHeaderValue(value) {
			return fmt.Errorf("%w: invalid header value", ErrInvalidRequest)
		}
	}
	return nil
}

func composeMessage(r Request) ([]byte, error) {
	var b strings.Builder
	b.WriteString("Content-Type: text/plain; charset=utf-8\nContent-Transfer-Encoding: 8bit\nMIME-Version: 1.0\n")
	if r.From != "" {
		b.WriteString("From: " + r.From + "\n")
	}
	if r.ReplyTo != "" {
		b.WriteString("Reply-To: " + r.ReplyTo + "\n")
	}
	if r.To != "" {
		b.WriteString("To: " + r.To + "\n")
	}
	if r.Subject != "" {
		b.WriteString("Subject: " + r.Subject + "\n")
	}
	if r.Newsgroup != "" {
		b.WriteString("Newsgroups: " + r.Newsgroup + "\n")
	}
	if r.References != "" {
		b.WriteString("References: " + r.References + "\n")
	}
	b.WriteString("\n")
	b.WriteString(r.Body)
	return []byte(b.String()), nil
}

func validHeaderValue(value string) bool { return !strings.ContainsAny(value, "\r\n\x00") }
func isRemailerName(s string) bool {
	if s == "" {
		return false
	}
	for _, c := range s {
		if !(c == '-' || c == '_' || c >= 'a' && c <= 'z' || c >= '0' && c <= '9') {
			return false
		}
	}
	return true
}

func loadKeyring(path string) (map[string]remailerKey, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("open public keyring: %w", err)
	}
	defer f.Close()
	keys := make(map[string]remailerKey)
	s := bufio.NewScanner(f)
	var current remailerKey
	var phase int
	for s.Scan() {
		line := strings.TrimSpace(s.Text())
		switch phase {
		case 0:
			parts := strings.Fields(line)
			if len(parts) != 7 {
				continue
			}
			id, e := hex.DecodeString(parts[2])
			if e != nil || len(id) != 16 {
				continue
			}
			from, e1 := time.Parse("2006-01-02", parts[5])
			until, e2 := time.Parse("2006-01-02", parts[6])
			if e1 != nil || e2 != nil {
				continue
			}
			now := time.Now().UTC()
			if now.Before(from) || now.After(until.Add(24*time.Hour)) {
				continue
			}
			current = remailerKey{name: parts[0], address: parts[1], keyID: id}
			phase = 1
		case 1:
			if line == "-----Begin Mix Key-----" {
				phase = 2
			}
		case 2:
			id, e := hex.DecodeString(line)
			if e != nil || !bytes.Equal(id, current.keyID) {
				phase = 0
			} else {
				phase = 3
			}
		case 3:
			key, e := hex.DecodeString(line)
			if e != nil || len(key) != 32 {
				phase = 0
			} else {
				current.publicKey = key
				phase = 4
			}
		case 4:
			if line == "-----End Mix Key-----" {
				keys[current.name] = current
			}
			phase = 0
		}
	}
	if err := s.Err(); err != nil {
		return nil, fmt.Errorf("read public keyring: %w", err)
	}
	if len(keys) == 0 {
		return nil, ErrInvalidKeyring
	}
	return keys, nil
}

func encodePacket(plain []byte, chain []remailerKey) ([]byte, error) {
	if len(chain) == 0 || len(chain) > maxChainLength || len(plain) > maxPlainBytes {
		return nil, ErrInvalidRequest
	}
	payload := make([]byte, messageBytes)
	if _, err := io.ReadFull(rand.Reader, payload); err != nil {
		return nil, err
	}
	copy(payload[maxChainLength*headerBytes:], plain)
	keys := make([][]byte, len(chain)-1)
	ivs := make([][]byte, len(chain)-1)
	for i := range keys {
		keys[i] = randomBytes(32)
		ivs[i] = randomBytes(12)
	}
	finalIV := randomBytes(16)
	finalID := randomBytes(16)
	exitAES := randomBytes(32)
	final := make([]byte, 64)
	copy(final, finalIV)
	final[16], final[17] = 1, 1
	copy(final[18:], finalID)
	binary.LittleEndian.PutUint32(final[34:38], uint32(len(plain)))
	copy(payload[maxChainLength*headerBytes:], aesCTR(payload[maxChainLength*headerBytes:], exitAES, finalIV))
	shiftHeaders(payload)
	if len(chain) > 1 {
		deterministic(payload, keys, ivs, len(chain), 0)
	}
	exitData := slotData(1, finalID, exitAES, final, antiTag(payload))
	copy(payload[:headerBytes], encryptedHeader(chain[len(chain)-1], exitData))
	for hop := 0; hop < len(chain)-1; hop++ {
		partial := ivs[hop]
		next := chain[len(chain)-hop-1].address
		info := make([]byte, 64)
		copy(info, partial)
		copy(info[12:], []byte(next))
		encryptAll(payload, keys[hop], ivs[hop])
		shiftHeaders(payload)
		deterministic(payload, keys, ivs, len(chain), hop+1)
		data := slotData(0, randomBytes(16), keys[hop], info, antiTag(payload))
		copy(payload[:headerBytes], encryptedHeader(chain[len(chain)-hop-2], data))
	}
	return payload, nil
}

func slotData(kind byte, packetID, aesKey, info, tag []byte) []byte {
	b := make([]byte, encHeadBytes)
	b[0], b[1], b[2] = 2, kind, 0
	copy(b[3:], packetID)
	copy(b[19:], aesKey)
	binary.LittleEndian.PutUint16(b[51:53], uint16(time.Now().UTC().Unix()/86400))
	copy(b[53:], info)
	copy(b[117:], tag)
	return b
}

func encryptedHeader(key remailerKey, data []byte) []byte {
	var recipient, sender [32]byte
	copy(recipient[:], key.publicKey)
	if _, err := io.ReadFull(rand.Reader, sender[:]); err != nil {
		panic(err)
	}
	var nonce [24]byte
	if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
		panic(err)
	}
	sealed := box.Seal(nil, data, &nonce, &recipient, &sender)
	b := make([]byte, headerBytes)
	copy(b, key.keyID)
	copy(b[16:], sender[:])
	copy(b[48:], nonce[:])
	copy(b[72:], sealed)
	return b
}

func aesCTR(input, key, iv []byte) []byte {
	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err)
	}
	output := make([]byte, len(input))
	cipher.NewCTR(block, iv).XORKeyStream(output, input)
	return output
}
func randomBytes(n int) []byte {
	b := make([]byte, n)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		panic(err)
	}
	return b
}
func shiftHeaders(p []byte) {
	copy(p[headerBytes:maxChainLength*headerBytes], p[:(maxChainLength-1)*headerBytes])
}
func antiTag(p []byte) []byte {
	h, _ := blake2s.New256(nil)
	h.Write(p[headerBytes:])
	return h.Sum(nil)
}
func seqIV(partial []byte, slot int) []byte {
	iv := make([]byte, 16)
	copy(iv[:4], partial[:4])
	binary.LittleEndian.PutUint32(iv[4:8], uint32(slot))
	copy(iv[8:], partial[4:])
	return iv
}
func encryptAll(p, key, partial []byte) {
	for slot := 0; slot <= maxChainLength; slot++ {
		start := slot * headerBytes
		end := start + headerBytes
		if slot == maxChainLength {
			start = maxChainLength * headerBytes
			end = len(p)
		}
		copy(p[start:end], aesCTR(p[start:end], key, seqIV(partial, slot)))
	}
}
func deterministic(p []byte, keys, ivs [][]byte, chainLen, hop int) {
	bottom := maxChainLength - 1
	top := bottom - (chainLen - hop - 2)
	for slot := top; slot <= bottom; slot++ {
		fake := make([]byte, headerBytes)
		use := bottom
		for i := bottom - slot + hop; i >= 0; i-- {
			copy(fake, aesCTR(fake, keys[i], seqIV(ivs[i], use)))
			use--
		}
		copy(p[slot*headerBytes:(slot+1)*headerBytes], fake)
	}
}

func armor(payload []byte) []byte {
	h, _ := blake2s.New256(nil)
	h.Write(payload)
	var b bytes.Buffer
	b.WriteString("::\nRemailer-Type: yamn-" + armorVersion + "\n\n-----BEGIN REMAILER MESSAGE-----\n")
	b.WriteString(strconv.Itoa(len(payload)) + "\n")
	b.WriteString(hex.EncodeToString(h.Sum(nil)) + "\n")
	encoded := base64.StdEncoding.EncodeToString(payload)
	for len(encoded) > 0 {
		n := 64
		if len(encoded) < n {
			n = len(encoded)
		}
		b.WriteString(encoded[:n])
		b.WriteByte('\n')
		encoded = encoded[n:]
	}
	b.WriteString("\n-----END REMAILER MESSAGE-----\n")
	return b.Bytes()
}
