// Package dlmessage implementiert Apple's DLMessage RPC-Protocol das von
// MobileBackup2, NotificationProxy und ähnlichen Apple-Services genutzt wird.
//
// Wire format:
//
//	[4 bytes: total length big-endian] [XML plist payload]
//
// Payload ist ein plist-Array:
//
//	[<DLMessage-type-string>, <arg1>, <arg2>, ...]
//
// Beispiel für DLMessageProcessMessage:
//
//	["DLMessageProcessMessage", {<command-dict>}]
//
// Reference: libimobiledevice's MobileBackup2-Protocol-Reverse-Engineering
// + verifiziert aus TechLockdown's libimobiledevice.MobileBackup2Client
// Symbol-Liste (Receive, SendFiles, Start, BaseVersionExchange).
package dlmessage

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"

	"howett.net/plist"
)

// DLMessage-Type-Konstanten — Apple's enum für ProcessMessage-Arten.
const (
	TypeVersionExchange = "DLMessageVersionExchange"
	TypeDeviceReady     = "DLMessageDeviceReady"
	TypeProcessMessage  = "DLMessageProcessMessage"
	TypeStatusResponse  = "DLMessageStatusResponse"
	TypeDisconnect      = "DLMessageDisconnect"
	TypePing            = "DLMessagePing"

	// File-Operations
	TypeContentsOfDirectory = "DLContentsOfDirectory"
	TypeDownloadFiles       = "DLMessageDownloadFiles"
	TypeUploadFiles         = "DLMessageUploadFiles"
	TypeCopyItem            = "DLMessageCopyItem"
	TypeRemoveItems         = "DLMessageRemoveItems"
	TypeCreateDirectory     = "DLMessageCreateDirectory"
	TypeMoveItems           = "DLMessageMoveItems"
	TypeGetFreeDiskSpace    = "DLMessageGetFreeDiskSpace"
)

// Conn ist ein Wrapper über eine generische bidirektionale Connection
// die DLMessage-Frames lesen/schreiben kann. Die underlying Connection
// kommt von go-ios's DeviceConnectionInterface.
type Conn struct {
	rw io.ReadWriter
}

// New erzeugt einen Conn der DLMessages über `rw` sendet/empfängt.
func New(rw io.ReadWriter) *Conn {
	return &Conn{rw: rw}
}

// DebugMode dumpt alle Send/Receive-bytes als hex. Aktiviere via env REBREAK_DLMSG_DEBUG=1.
var DebugMode = false

// Send schreibt eine DLMessage als [length-prefix][xml-plist].
//
// args wird zu einem plist-Array gemacht. Erstes Element ist der
// Message-Type-String, danach kommen die ProcessMessage-Daten.
//
// Beispiel:
//
//	conn.Send(TypeProcessMessage, map[string]interface{}{
//	    "MessageName": "Hello",
//	    "ProtocolVersion": "2.1",
//	})
func (c *Conn) Send(messageType string, args ...interface{}) error {
	arr := make([]interface{}, 0, 1+len(args))
	arr = append(arr, messageType)
	arr = append(arr, args...)

	var buf bytes.Buffer
	enc := plist.NewEncoderForFormat(&buf, plist.XMLFormat)
	if err := enc.Encode(arr); err != nil {
		return fmt.Errorf("dlmessage: encode plist: %w", err)
	}

	// length-prefix als big-endian uint32
	payload := buf.Bytes()
	hdr := make([]byte, 4)
	binary.BigEndian.PutUint32(hdr, uint32(len(payload)))

	if DebugMode {
		fmt.Printf("[dlmsg→] %d bytes payload (header: %x)\n", len(payload), hdr)
		preview := payload
		if len(preview) > 500 {
			preview = preview[:500]
		}
		fmt.Printf("[dlmsg→] %s\n", string(preview))
	}
	if _, err := c.rw.Write(hdr); err != nil {
		return fmt.Errorf("dlmessage: write header: %w", err)
	}
	if _, err := c.rw.Write(payload); err != nil {
		return fmt.Errorf("dlmessage: write payload: %w", err)
	}
	return nil
}

// Receive liest eine DLMessage und returnt den Type-String + die restlichen
// Array-Elemente als []interface{}.
func (c *Conn) Receive() (messageType string, args []interface{}, err error) {
	// 4-byte length-prefix
	hdr := make([]byte, 4)
	if _, err = io.ReadFull(c.rw, hdr); err != nil {
		return "", nil, fmt.Errorf("dlmessage: read header: %w", err)
	}
	length := binary.BigEndian.Uint32(hdr)
	if length == 0 {
		return "", nil, errors.New("dlmessage: zero-length frame")
	}
	if length > 64*1024*1024 {
		return "", nil, fmt.Errorf("dlmessage: frame too large (%d bytes)", length)
	}

	// payload
	payload := make([]byte, length)
	if _, err = io.ReadFull(c.rw, payload); err != nil {
		return "", nil, fmt.Errorf("dlmessage: read payload: %w", err)
	}

	if DebugMode {
		fmt.Printf("[dlmsg←] %d bytes payload (header: %x)\n", len(payload), hdr)
		preview := payload
		if len(preview) > 500 {
			preview = preview[:500]
		}
		fmt.Printf("[dlmsg←] %s\n", string(preview))
		fmt.Printf("[dlmsg←] hex: %x\n", preview[:min(80, len(preview))])
	}

	// decode plist-array
	var arr []interface{}
	if _, derr := plist.Unmarshal(payload, &arr); derr != nil {
		return "", nil, fmt.Errorf("dlmessage: parse plist: %w", derr)
	}
	if len(arr) == 0 {
		return "", nil, errors.New("dlmessage: empty array")
	}

	t, ok := arr[0].(string)
	if !ok {
		return "", nil, fmt.Errorf("dlmessage: first element not string: %T", arr[0])
	}
	return t, arr[1:], nil
}

// SendProcessMessage sendet `DLMessageProcessMessage` mit dem command-dict.
// Convenience für den häufigsten Send-Pattern.
func (c *Conn) SendProcessMessage(cmd map[string]interface{}) error {
	return c.Send(TypeProcessMessage, cmd)
}

// SendStatusResponse sendet `DLMessageStatusResponse` mit error-code + msg.
func (c *Conn) SendStatusResponse(errorCode int, errorStr string, errorDict map[string]interface{}) error {
	return c.Send(TypeStatusResponse, errorCode, errorStr, errorDict)
}

// ReceiveProcessMessage erwartet als Antwort einen DLMessageProcessMessage und
// returnt das command-dict. Strict — wirft Error bei anderem Type.
func (c *Conn) ReceiveProcessMessage() (map[string]interface{}, error) {
	t, args, err := c.Receive()
	if err != nil {
		return nil, err
	}
	if t != TypeProcessMessage {
		return nil, fmt.Errorf("dlmessage: expected ProcessMessage, got %s (args: %v)", t, args)
	}
	if len(args) == 0 {
		return nil, errors.New("dlmessage: ProcessMessage has no payload")
	}
	dict, ok := args[0].(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("dlmessage: ProcessMessage payload not a dict: %T", args[0])
	}
	return dict, nil
}
