package crypto

import (
	"crypto/aes"
	"crypto/cipher"
	"encoding/binary"
	"errors"
	"fmt"
	"sync"
)

// Frame layout on the wire:
//
//	[8 bytes counter (big endian)][ciphertext + 16 byte auth tag]
//
// The counter travels in the clear because the receiver needs it to rebuild
// the nonce; it carries no secret, and the authentication tag covers it.
const (
	counterSize = 8
	nonceSize   = 12 // AES-GCM standard nonce
	tagSize     = 16
	// FrameOverhead is how much a frame grows over its plaintext.
	FrameOverhead = counterSize + tagSize
)

// Direction distinguishes the two halves of a tunnel.
//
// Both ends derive the same tunnel key, so without this they would encrypt
// different plaintexts under the same (key, nonce) pair — the one failure that
// breaks AES-GCM completely, revealing the XOR of both messages and allowing
// forgery.
type Direction uint8

const (
	// DirShareToUse marks traffic from the sharing side to the using side.
	DirShareToUse Direction = 1
	// DirUseToShare marks traffic in the opposite direction.
	DirUseToShare Direction = 2
)

// ErrCounterExhausted is returned once a sealer has used every counter value.
var ErrCounterExhausted = errors.New("tunnel counter exhausted, reconnect required")

// ErrReplay is returned for a frame whose counter was already seen.
var ErrReplay = errors.New("replayed or out-of-order tunnel frame")

// Sealer encrypts outgoing tunnel frames.
type Sealer struct {
	mu      sync.Mutex
	aead    cipher.AEAD
	dir     Direction
	counter uint64
}

// Opener decrypts incoming tunnel frames.
type Opener struct {
	mu       sync.Mutex
	aead     cipher.AEAD
	dir      Direction
	lastSeen uint64
	started  bool
}

// NewSealer creates a sealer for one direction of a tunnel.
func NewSealer(key []byte, dir Direction) (*Sealer, error) {
	aead, err := newAEAD(key)
	if err != nil {
		return nil, err
	}
	return &Sealer{aead: aead, dir: dir}, nil
}

// NewOpener creates an opener for one direction of a tunnel.
// The direction must be the one the *sender* used.
func NewOpener(key []byte, dir Direction) (*Opener, error) {
	aead, err := newAEAD(key)
	if err != nil {
		return nil, err
	}
	return &Opener{aead: aead, dir: dir}, nil
}

func newAEAD(key []byte) (cipher.AEAD, error) {
	if len(key) != keySize {
		return nil, fmt.Errorf("key is %d bytes, want %d", len(key), keySize)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("creating cipher: %w", err)
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("creating GCM: %w", err)
	}
	return aead, nil
}

// nonceFor builds the 12-byte nonce: direction, four zero bytes, counter.
// Distinct directions therefore never share a nonce under the same key.
func nonceFor(dir Direction, counter uint64) [nonceSize]byte {
	var nonce [nonceSize]byte
	nonce[0] = byte(dir)
	binary.BigEndian.PutUint64(nonce[4:], counter)
	return nonce
}

// Seal encrypts one frame and returns it ready for transmission.
func (s *Sealer) Seal(plaintext []byte) ([]byte, error) {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.counter == ^uint64(0) {
		return nil, ErrCounterExhausted
	}
	counter := s.counter
	s.counter++

	nonce := nonceFor(s.dir, counter)

	out := make([]byte, counterSize, counterSize+len(plaintext)+tagSize)
	binary.BigEndian.PutUint64(out, counter)

	// The counter prefix is authenticated as additional data, so it cannot be
	// altered to make a frame decrypt under a different nonce.
	return s.aead.Seal(out, nonce[:], plaintext, out[:counterSize]), nil
}

// Open decrypts one frame.
//
// Frames must arrive in order, which holds for both transports in use: a
// direct TCP connection and a relayed WebSocket both preserve ordering. A
// counter that does not advance means duplication or tampering.
func (o *Opener) Open(frame []byte) ([]byte, error) {
	if len(frame) < FrameOverhead {
		return nil, fmt.Errorf("frame is %d bytes, minimum is %d", len(frame), FrameOverhead)
	}

	counter := binary.BigEndian.Uint64(frame[:counterSize])

	o.mu.Lock()
	if o.started && counter <= o.lastSeen {
		o.mu.Unlock()
		return nil, ErrReplay
	}
	o.mu.Unlock()

	nonce := nonceFor(o.dir, counter)
	plaintext, err := o.aead.Open(nil, nonce[:], frame[counterSize:], frame[:counterSize])
	if err != nil {
		return nil, fmt.Errorf("authentication failed: %w", err)
	}

	// Only advance after the frame proves authentic, so a forged frame with a
	// high counter cannot make us reject the genuine ones that follow.
	o.mu.Lock()
	if counter > o.lastSeen || !o.started {
		o.lastSeen = counter
		o.started = true
	}
	o.mu.Unlock()

	return plaintext, nil
}
