// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package wire

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"slices"

	libnet "github.com/fatedier/golib/net"
)

const (
	ProtocolV1 = "v1"
	ProtocolV2 = "v2"

	WireVersionV2 = 2

	FrameTypeClientHello uint16 = 1
	FrameTypeServerHello uint16 = 2
	FrameTypeMessage     uint16 = 16

	MessageCodecJSON           = "json"
	UDPPacketCodecBinary       = "binary-v1"
	DefaultMaxFramePayloadSize = 64 * 1024

	MagicV2 = "FRP\x00\x02\r\n"
)

type Frame struct {
	Type    uint16
	Flags   uint16
	Payload []byte
}

type Conn struct {
	rw                  io.ReadWriter
	maxFramePayloadSize uint32
}

func NewConn(rw io.ReadWriter) *Conn {
	return &Conn{
		rw:                  rw,
		maxFramePayloadSize: DefaultMaxFramePayloadSize,
	}
}

func (c *Conn) ReadFrame() (*Frame, error) {
	header := make([]byte, 8)
	if _, err := io.ReadFull(c.rw, header); err != nil {
		return nil, err
	}

	frameType := binary.BigEndian.Uint16(header[0:2])
	flags := binary.BigEndian.Uint16(header[2:4])
	length := binary.BigEndian.Uint32(header[4:8])
	if flags != 0 {
		return nil, fmt.Errorf("unsupported frame flags: %d", flags)
	}
	if length > c.maxFramePayloadSize {
		return nil, fmt.Errorf("frame payload length %d exceeds limit %d", length, c.maxFramePayloadSize)
	}

	payload := make([]byte, length)
	if _, err := io.ReadFull(c.rw, payload); err != nil {
		return nil, err
	}
	return &Frame{
		Type:    frameType,
		Flags:   flags,
		Payload: payload,
	}, nil
}

func (c *Conn) WriteFrame(f *Frame) error {
	if f.Flags != 0 {
		return fmt.Errorf("unsupported frame flags: %d", f.Flags)
	}
	if len(f.Payload) > int(c.maxFramePayloadSize) {
		return fmt.Errorf("frame payload length %d exceeds limit %d", len(f.Payload), c.maxFramePayloadSize)
	}

	header := make([]byte, 8)
	binary.BigEndian.PutUint16(header[0:2], f.Type)
	binary.BigEndian.PutUint16(header[2:4], f.Flags)
	binary.BigEndian.PutUint32(header[4:8], uint32(len(f.Payload)))
	if _, err := c.rw.Write(header); err != nil {
		return err
	}
	_, err := c.rw.Write(f.Payload)
	return err
}

func (c *Conn) ReadJSONFrame(frameType uint16, out any) error {
	f, err := c.ReadFrame()
	if err != nil {
		return err
	}
	if f.Type != frameType {
		return fmt.Errorf("unexpected frame type %d, want %d", f.Type, frameType)
	}
	return c.UnmarshalFrame(f, out)
}

func (c *Conn) UnmarshalFrame(f *Frame, out any) error {
	return json.Unmarshal(f.Payload, out)
}

func NewJSONFrame(frameType uint16, in any) (*Frame, error) {
	payload, err := json.Marshal(in)
	if err != nil {
		return nil, err
	}
	return &Frame{
		Type:    frameType,
		Payload: payload,
	}, nil
}

func (c *Conn) WriteJSONFrame(frameType uint16, in any) error {
	f, err := NewJSONFrame(frameType, in)
	if err != nil {
		return err
	}
	return c.WriteFrame(f)
}

func WriteMagic(w io.Writer) error {
	_, err := io.WriteString(w, MagicV2)
	return err
}

func WriteMagicIfV2(w io.Writer, wireProtocol string) error {
	if wireProtocol != ProtocolV2 {
		return nil
	}
	return WriteMagic(w)
}

func CheckMagic(conn net.Conn) (out net.Conn, isV2 bool, err error) {
	sharedConn, r := libnet.NewSharedConnSize(conn, len(MagicV2))
	buf := make([]byte, len(MagicV2))
	if _, err = io.ReadFull(r, buf); err != nil {
		return nil, false, err
	}
	for i := range MagicV2 {
		if buf[i] != MagicV2[i] {
			return sharedConn, false, nil
		}
	}
	return conn, true, nil
}

type BootstrapInfo struct {
	Transport string `json:"transport,omitempty"`
	TLS       bool   `json:"tls,omitempty"`
	TCPMux    bool   `json:"tcpMux,omitempty"`
}

type ClientHello struct {
	Bootstrap    BootstrapInfo      `json:"bootstrap,omitempty"`
	Capabilities ClientCapabilities `json:"capabilities,omitempty"`
}

type ClientCapabilities struct {
	Message MessageCapabilities `json:"message,omitempty"`
	Crypto  CryptoCapabilities  `json:"crypto,omitempty"`
}

type MessageCapabilities struct {
	Codecs          []string `json:"codecs,omitempty"`
	UDPPacketCodecs []string `json:"udpPacketCodecs,omitempty"`
}

type CryptoCapabilities struct {
	Algorithms   []string `json:"algorithms,omitempty"`
	ClientRandom []byte   `json:"clientRandom,omitempty"`
}

type ServerHello struct {
	Selected ServerSelection `json:"selected,omitempty"`
	Error    string          `json:"error,omitempty"`
}

type ServerSelection struct {
	Message MessageSelection `json:"message,omitempty"`
	Crypto  CryptoSelection  `json:"crypto,omitempty"`
}

type MessageSelection struct {
	Codec          string `json:"codec,omitempty"`
	UDPPacketCodec string `json:"udpPacketCodec,omitempty"`
}

type CryptoSelection struct {
	Algorithm    string `json:"algorithm,omitempty"`
	ServerRandom []byte `json:"serverRandom,omitempty"`
}

func clientHelloWithCryptoRandom(bootstrap BootstrapInfo, clientRandom []byte) ClientHello {
	return ClientHello{
		Bootstrap: bootstrap,
		Capabilities: ClientCapabilities{
			Message: MessageCapabilities{
				Codecs:          []string{MessageCodecJSON},
				UDPPacketCodecs: []string{UDPPacketCodecBinary},
			},
			Crypto: CryptoCapabilities{
				Algorithms:   PreferredAEADAlgorithms(),
				ClientRandom: clientRandom,
			},
		},
	}
}

func DefaultServerHello() ServerHello {
	return ServerHello{
		Selected: ServerSelection{
			Message: MessageSelection{
				Codec: MessageCodecJSON,
			},
		},
	}
}

func Supports(list []string, value string) bool {
	return slices.Contains(list, value)
}

func ValidateClientHello(h ClientHello) error {
	if !Supports(h.Capabilities.Message.Codecs, MessageCodecJSON) {
		return fmt.Errorf("unsupported message codec")
	}
	return ValidateCryptoCapabilities(h.Capabilities.Crypto)
}
