// Package testca provides an in-process fake CA server for use in tests.
// It supports a small, fixed set of PVs and is compatible with the goca Client.
package testca

import (
	"encoding/binary"
	"fmt"
	"io"
	"math"
	"net"
	"sync"
	"time"

	"github.com/uopi/goca/proto"
)

// PVSpec describes one test PV hosted by the fake server.
type PVSpec struct {
	Name    string
	DBFType int    // proto.DBF* constant
	Count   uint32 // element count (1 for scalars)
	Value   any    // initial value
	Access  uint32 // proto.AccessRead | proto.AccessWrite
}

// Server is an in-process fake CA server.  It listens on a random TCP port and
// an ephemeral UDP port.  Use Addr() to get the addresses for client config.
type Server struct {
	tcpLn net.Listener
	udpCn *net.UDPConn

	mu       sync.RWMutex
	pvs      map[string]*serverPV
	subs     map[uint32]*serverSub // subID → sub
	getFault int                   // fault to inject on the next READ_NOTIFY

	done chan struct{}
}

// Fault modes for SetGetFault, used to exercise the client's error paths.
const (
	GetFaultNone       = iota // serve replies normally
	GetFaultDisconnect        // drop the connection mid-request
	GetFaultCorrupt           // reply with an undecodable (too-short) payload
)

type serverPV struct {
	spec PVSpec
	mu   sync.RWMutex
	val  any
}

type serverSub struct {
	pvName  string
	subID   uint32
	cid     uint32
	sid     uint32
	conn    net.Conn
	dbrType uint16
	count   uint32
}

// New creates and starts a fake CA server hosting the given PVs.
func New(pvs []PVSpec) (*Server, error) {
	tcpLn, err := net.Listen("tcp4", "127.0.0.1:0")
	if err != nil {
		return nil, fmt.Errorf("testca: tcp listen: %w", err)
	}
	udpCn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
	if err != nil {
		tcpLn.Close()
		return nil, fmt.Errorf("testca: udp listen: %w", err)
	}

	s := &Server{
		tcpLn: tcpLn,
		udpCn: udpCn,
		pvs:   make(map[string]*serverPV, len(pvs)),
		subs:  make(map[uint32]*serverSub),
		done:  make(chan struct{}),
	}
	for _, spec := range pvs {
		s.pvs[spec.Name] = &serverPV{spec: spec, val: spec.Value}
	}

	go s.acceptLoop()
	go s.udpLoop()
	return s, nil
}

// TCPAddr returns the TCP "host:port" address clients should connect to.
func (s *Server) TCPAddr() string { return s.tcpLn.Addr().String() }

// UDPAddr returns the UDP "host:port" address clients should search against.
func (s *Server) UDPAddr() string { return s.udpCn.LocalAddr().String() }

// Close shuts down the server.
func (s *Server) Close() {
	close(s.done)
	s.tcpLn.Close()
	s.udpCn.Close()
}

// SetGetFault arms a fault to be injected on the next READ_NOTIFY request.
// It is reset to GetFaultNone after a single request is faulted.
func (s *Server) SetGetFault(mode int) {
	s.mu.Lock()
	s.getFault = mode
	s.mu.Unlock()
}

// Disconnect pushes a SERVER_DISCONN message to every connected subscriber,
// simulating an orderly server shutdown that forces clients to reconnect.
func (s *Server) Disconnect() {
	msg := proto.BuildMessage(proto.Header{Command: proto.CmdServerDisc}, nil)
	seen := make(map[net.Conn]bool)
	s.mu.RLock()
	for _, sub := range s.subs {
		if sub.conn != nil && !seen[sub.conn] {
			seen[sub.conn] = true
			_, _ = sub.conn.Write(msg)
		}
	}
	s.mu.RUnlock()
}

// SetValue updates a PV's value and pushes a monitor event to all subscribers.
func (s *Server) SetValue(pvName string, val any) error {
	s.mu.RLock()
	pv, ok := s.pvs[pvName]
	s.mu.RUnlock()
	if !ok {
		return fmt.Errorf("testca: unknown PV %q", pvName)
	}
	pv.mu.Lock()
	pv.val = val
	pv.mu.Unlock()

	// Push to all subscribers of this PV.
	s.mu.RLock()
	for _, sub := range s.subs {
		if sub.pvName == pvName {
			if msg := s.buildEventMsg(sub, pv); msg != nil {
				_, _ = sub.conn.Write(msg)
			}
		}
	}
	s.mu.RUnlock()
	return nil
}

// -------------------------------------------------------------------------- //
// UDP search loop                                                               //
// -------------------------------------------------------------------------- //

func (s *Server) udpLoop() {
	buf := make([]byte, 65536)
	for {
		_ = s.udpCn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
		n, src, err := s.udpCn.ReadFromUDP(buf)
		if err != nil {
			select {
			case <-s.done:
				return
			default:
				continue
			}
		}
		s.handleUDP(buf[:n], src)
	}
}

func (s *Server) handleUDP(data []byte, src *net.UDPAddr) {
	for len(data) >= proto.HeaderSize {
		hdr, n, err := proto.DecodeHeader(newBytesReader(data))
		if err != nil {
			return
		}
		payEnd := n + int(hdr.PayloadSize)
		if payEnd > len(data) {
			return
		}
		payload := data[n:payEnd]
		data = data[payEnd:]

		if hdr.Command != proto.CmdSearch {
			continue
		}
		// Extract PV name from payload (null-terminated).
		pvName := nullStr(payload)

		s.mu.RLock()
		_, ok := s.pvs[pvName]
		s.mu.RUnlock()
		if !ok {
			continue
		}

		// Reply: data_type = TCP port, parameter2 = searchID.
		tcpPort := s.tcpLn.Addr().(*net.TCPAddr).Port
		// Use 0xFFFFFFFF IP to tell client to use sender's IP.
		reply := buildSearchReply(hdr.Parameter2, tcpPort)
		_, _ = s.udpCn.WriteToUDP(reply, src)
	}
}

func buildSearchReply(searchID uint32, port int) []byte {
	payload := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}
	h := proto.Header{
		Command:    proto.CmdSearch,
		DataType:   uint16(port),
		Parameter1: proto.MinorVersion,
		Parameter2: searchID,
	}
	return proto.BuildMessage(h, payload)
}

// -------------------------------------------------------------------------- //
// TCP accept + per-connection handler                                          //
// -------------------------------------------------------------------------- //

func (s *Server) acceptLoop() {
	for {
		conn, err := s.tcpLn.Accept()
		if err != nil {
			select {
			case <-s.done:
				return
			default:
				continue
			}
		}
		go s.handleConn(conn)
	}
}

func (s *Server) handleConn(conn net.Conn) {
	defer conn.Close()

	// Track CID → (pvName, SID).
	type chanInfo struct {
		pvName string
		sid    uint32
	}
	sidSeq := uint32(1000)
	channels := make(map[uint32]*chanInfo) // cid → info

	for {
		hdr, _, err := proto.DecodeHeader(conn)
		if err != nil {
			return
		}
		var payload []byte
		if hdr.PayloadSize > 0 {
			payload = make([]byte, hdr.PayloadSize)
			if _, err = io.ReadFull(conn, payload); err != nil {
				return
			}
		}

		switch hdr.Command {
		case proto.CmdVersion:
			// Respond with VERSION.
			reply := proto.BuildMessage(proto.Header{
				Command:   proto.CmdVersion,
				DataCount: proto.MinorVersion,
			}, nil)
			conn.Write(reply)

		case proto.CmdHostName, proto.CmdClientName:
			// Ignore client identity.

		case proto.CmdCreateChan:
			cid := hdr.Parameter1
			pvName := nullStr(payload)

			s.mu.RLock()
			pv, ok := s.pvs[pvName]
			s.mu.RUnlock()

			if !ok {
				// CREATE_FAIL
				fail := proto.BuildMessage(proto.Header{
					Command:    proto.CmdCreateFail,
					Parameter1: cid,
				}, nil)
				conn.Write(fail)
				continue
			}

			sid := sidSeq
			sidSeq++
			channels[cid] = &chanInfo{pvName: pvName, sid: sid}

			// CREATE_CHAN reply: DataType=dbfType, DataCount=count, p1=cid, p2=sid.
			pv.mu.RLock()
			dbfType := pv.spec.DBFType
			count := pv.spec.Count
			pv.mu.RUnlock()

			reply := proto.BuildMessage(proto.Header{
				Command:    proto.CmdCreateChan,
				DataType:   uint16(dbfType),
				DataCount:  count,
				Parameter1: cid,
				Parameter2: sid,
			}, nil)
			conn.Write(reply)

			// ACCESS_RIGHTS: p1=cid, p2=access.
			access := pv.spec.Access
			if access == 0 {
				access = proto.AccessRead | proto.AccessWrite
			}
			ar := proto.BuildMessage(proto.Header{
				Command:    proto.CmdAccessRights,
				Parameter1: cid,
				Parameter2: access,
			}, nil)
			conn.Write(ar)

		case proto.CmdEventAdd:
			// p1=SID (channel), p2=subscriptionID (client-assigned).
			sid := hdr.Parameter1
			subID := hdr.Parameter2

			// Find channel by SID.
			var pvName string
			for _, info := range channels {
				if info.sid == sid {
					pvName = info.pvName
					break
				}
			}
			if pvName == "" {
				continue
			}

			s.mu.Lock()
			sub := &serverSub{
				pvName:  pvName,
				subID:   subID,
				sid:     sid,
				conn:    conn,
				dbrType: hdr.DataType,
				count:   hdr.DataCount,
			}
			s.subs[subID] = sub
			s.mu.Unlock()

			// Send initial value.
			s.mu.RLock()
			pv := s.pvs[pvName]
			s.mu.RUnlock()
			if msg := s.buildEventMsg(sub, pv); msg != nil {
				conn.Write(msg)
			}

		case proto.CmdEventCancel:
			// p1=SID, p2=subscriptionID.
			subID := hdr.Parameter2
			s.mu.Lock()
			delete(s.subs, subID)
			s.mu.Unlock()

		case proto.CmdReadNotify:
			// p1=SID, p2=ioid (per CA spec).
			sid := hdr.Parameter1
			ioid := hdr.Parameter2

			var pvName string
			var cid uint32
			for c, info := range channels {
				if info.sid == sid {
					pvName = info.pvName
					cid = c
					break
				}
			}
			if pvName == "" {
				continue
			}

			// Fault injection: consume a one-shot armed fault, if any.
			s.mu.Lock()
			fault := s.getFault
			s.getFault = GetFaultNone
			s.mu.Unlock()
			switch fault {
			case GetFaultDisconnect:
				// Drop the connection mid-request: the client's in-flight GET
				// unblocks with ok=false ("circuit disconnected during GET").
				conn.Close()
				return
			case GetFaultCorrupt:
				// Reply with a too-short payload so DecodeTimeValue fails
				// ("failed to decode GET reply").
				reply := proto.BuildMessage(proto.Header{
					Command:    proto.CmdReadNotify,
					DataType:   hdr.DataType,
					DataCount:  hdr.DataCount,
					Parameter1: cid,
					Parameter2: ioid,
				}, []byte{0, 0, 0, 0})
				conn.Write(reply)
				continue
			}

			s.mu.RLock()
			pv := s.pvs[pvName]
			s.mu.RUnlock()

			pv.mu.RLock()
			val := pv.val
			pv.mu.RUnlock()

			var replyPayload []byte
			switch hdr.DataType {
			case proto.DBRCtrlDouble, proto.DBRCtrlLong, proto.DBRCtrlEnum, proto.DBRCtrlString,
				proto.DBRCtrlFloat, proto.DBRCtrlShort, proto.DBRCtrlChar:
				replyPayload = s.encodeCtrlValue(hdr.DataType, pv)
			default:
				replyPayload = s.encodeTimeValue(hdr.DataType, hdr.DataCount, val)
			}
			reply := proto.BuildMessage(proto.Header{
				Command:    proto.CmdReadNotify,
				DataType:   hdr.DataType,
				DataCount:  hdr.DataCount,
				Parameter1: cid,  // echo client channel ID (matches real EPICS IOC)
				Parameter2: ioid, // echo ioid so client can match the reply
			}, replyPayload)
			conn.Write(reply)

		case proto.CmdWrite:
			// p1=SID, p2=0.
			sid := hdr.Parameter1
			var pvName string
			for _, info := range channels {
				if info.sid == sid {
					pvName = info.pvName
					break
				}
			}
			if pvName == "" {
				continue
			}
			s.mu.RLock()
			pv := s.pvs[pvName]
			s.mu.RUnlock()

			val := decodeWritePayload(hdr.DataType, payload)
			if val == nil {
				continue
			}
			pv.mu.Lock()
			pv.val = val
			pv.mu.Unlock()

			// Push update to all subscribers.
			s.mu.RLock()
			for _, sub := range s.subs {
				if sub.pvName == pvName {
					if msg := s.buildEventMsg(sub, pv); msg != nil {
						sub.conn.Write(msg)
					}
				}
			}
			s.mu.RUnlock()

		case proto.CmdEcho:
			// No response needed (server echoes are optional).
		}
	}
}

// -------------------------------------------------------------------------- //
// Value encoding helpers                                                       //
// -------------------------------------------------------------------------- //

func (s *Server) buildEventMsg(sub *serverSub, pv *serverPV) []byte {
	pv.mu.RLock()
	val := pv.val
	pv.mu.RUnlock()
	payload := s.encodeTimeValue(sub.dbrType, sub.count, val)
	if payload == nil {
		return nil
	}
	return proto.BuildMessage(proto.Header{
		Command:    proto.CmdEventAdd,
		DataType:   sub.dbrType,
		DataCount:  sub.count,
		Parameter1: 1, // ECA_NORMAL
		Parameter2: sub.subID,
	}, payload)
}

// encodeTimeValue builds a DBR_TIME_* payload for val.
func (s *Server) encodeTimeValue(dbrType uint16, _ uint32, val any) []byte {
	now := time.Now().UTC()
	sec := uint32(now.Unix() - 631152000) // EPICS epoch offset
	nsec := uint32(now.Nanosecond())

	hdr := make([]byte, 12)
	// status=0, severity=0
	binary.BigEndian.PutUint32(hdr[4:], sec)
	binary.BigEndian.PutUint32(hdr[8:], nsec)

	var body []byte
	switch dbrType {
	case proto.DBRTimeDouble:
		body = make([]byte, 12) // 4 pad + 8 value
		f := toF64(val)
		binary.BigEndian.PutUint64(body[4:], math.Float64bits(f))

	case proto.DBRTimeFloat:
		body = make([]byte, 4)
		binary.BigEndian.PutUint32(body, math.Float32bits(float32(toF64(val))))

	case proto.DBRTimeLong:
		body = make([]byte, 4)
		binary.BigEndian.PutUint32(body, uint32(int32(toF64(val))))

	case proto.DBRTimeShort, proto.DBRTimeEnum:
		body = make([]byte, 4) // 2-byte RISC pad + 2-byte value
		binary.BigEndian.PutUint16(body[2:], uint16(int16(toF64(val))))

	case proto.DBRTimeChar:
		body = make([]byte, 4) // RISC_pad0[0:2] + RISC_pad1[2] + value[3]
		body[3] = byte(uint8(toF64(val)))

	case proto.DBRTimeString:
		body = make([]byte, 40)
		if str, ok := val.(string); ok {
			copy(body, str)
		}

	default:
		return nil
	}

	return append(hdr, body...)
}

// encodeCtrlValue builds a DBR_CTRL_* payload for a GET reply.
// The fake server returns minimal but structurally correct payloads so that
// the client-side decode functions succeed.
func (s *Server) encodeCtrlValue(dbrType uint16, pv *serverPV) []byte {
	pv.mu.RLock()
	val := pv.val
	access := pv.spec.Access
	pv.mu.RUnlock()

	switch dbrType {
	case proto.DBRCtrlDouble: // 88 bytes
		p := make([]byte, 88)
		if access == proto.AccessRead {
			// status=0, severity=0 (read-only signalled via ACCESS_RIGHTS, not here)
		}
		// units at [8:16] (empty string = "")
		// value at [80:88]
		binary.BigEndian.PutUint64(p[80:], math.Float64bits(toF64(val)))
		return p

	case proto.DBRCtrlFloat, proto.DBRCtrlShort, proto.DBRCtrlChar: // map to Long-style
		// Treat as CtrlLong (48 bytes); caller asked for these types but our
		// NativeCtrlType maps them to Double/Long anyway.  Return a valid stub.
		p := make([]byte, 48)
		binary.BigEndian.PutUint32(p[44:], uint32(int32(toF64(val))))
		return p

	case proto.DBRCtrlLong: // 48 bytes
		p := make([]byte, 48)
		binary.BigEndian.PutUint32(p[44:], uint32(int32(toF64(val))))
		return p

	case proto.DBRCtrlEnum: // 424 bytes
		p := make([]byte, 424)
		// no_str at [4:6] = 0 (no enum strings in fake server)
		binary.BigEndian.PutUint16(p[422:], uint16(int16(toF64(val))))
		return p

	case proto.DBRCtrlString: // 44 bytes: status(2)+severity(2)+value[40]
		p := make([]byte, 44)
		if str, ok := val.(string); ok {
			copy(p[4:], str)
		}
		return p

	default:
		return nil
	}
}

func decodeWritePayload(dbrType uint16, payload []byte) any {
	switch dbrType {
	case proto.DBRDouble:
		if len(payload) < 8 {
			return nil
		}
		return math.Float64frombits(binary.BigEndian.Uint64(payload))
	case proto.DBRLong:
		if len(payload) < 4 {
			return nil
		}
		return int32(binary.BigEndian.Uint32(payload))
	case proto.DBRShort, proto.DBREnum:
		if len(payload) < 2 {
			return nil
		}
		return int16(binary.BigEndian.Uint16(payload))
	case proto.DBRString:
		return nullStr(payload)
	default:
		return nil
	}
}

func toF64(v any) float64 {
	switch x := v.(type) {
	case float64:
		return x
	case float32:
		return float64(x)
	case int:
		return float64(x)
	case int32:
		return float64(x)
	case int16:
		return float64(x)
	case int64:
		return float64(x)
	default:
		return 0
	}
}

func nullStr(b []byte) string {
	for i, c := range b {
		if c == 0 {
			return string(b[:i])
		}
	}
	return string(b)
}

// -------------------------------------------------------------------------- //
// Minimal io.Reader for proto.DecodeHeader                                   //
// -------------------------------------------------------------------------- //

type bytesReader struct {
	b []byte
	i int
}

func newBytesReader(b []byte) *bytesReader { return &bytesReader{b: b} }

func (r *bytesReader) Read(p []byte) (int, error) {
	if r.i >= len(r.b) {
		return 0, io.EOF
	}
	n := copy(p, r.b[r.i:])
	r.i += n
	return n, nil
}
