// Package mobilebackup2 implementiert Apple's MobileBackup2-Protocol für
// das `com.apple.mobile.mobilebackup2`-Service.
//
// Reverse-engineered aus TechLockdown's MobileBackup2Client + libimobiledevice
// reference. Protokoll: DLMessage-Frames mit Restore-Command + file-serve-loop.
//
// Architektur:
//
//	┌─────────────┐  DLMessage   ┌────────────────┐
//	│ Our Code    │ ────────────▶│ mobilebackup2   │
//	│ (Restore +  │              │ (iPhone service)│
//	│  file-loop) │ ◀────────────│                 │
//	└─────────────┘              └────────────────┘
//
// Restore-Flow:
//  1. ServiceOpen ("com.apple.mobile.mobilebackup2")
//  2. BaseVersionExchange (verhandle Protocol-Version)
//  3. Start (sende "Restore" ProcessMessage mit Options)
//  4. ServeFiles-Loop:
//     - Receive DLMessage from device
//     - Switch on type (ContentsOfDirectory, DownloadFiles, ...)
//     - Respond with our embedded backup-files
//     - Loop until DLMessageDisconnect
//  5. Reboot (triggered by separate diagnostics-Service)
package mobilebackup2

import (
	"errors"
	"fmt"
	"os"

	ios "github.com/danielpaulus/go-ios/ios"
	"howett.net/plist"

	"github.com/raynis/rebreak-supervise-magic/internal/dlmessage"
)

// Service-Name aus TL-Binary-Strings: "com.apple.mobilebackup2" (OHNE .mobile.)
// Old iOS hatte "com.apple.mobile.mobilebackup2", aber iOS 7+ ohne.
const serviceName = "com.apple.mobilebackup2"

// Supported protocol versions. libimobiledevice's standard ist {2.0, 2.1}.
// Wir nutzen genau das was libimobiledevice nutzt (iOS-bewährt).
var supportedVersions = []float64{2.0, 2.1}

// Client kapselt eine offene MobileBackup2-Session.
type Client struct {
	deviceConn ios.DeviceConnectionInterface
	dl         *dlmessage.Conn

	// negotiated state
	protocolVersion float64
}

// Open startet die MobileBackup2-Session via Lockdown.
//
// CAVEAT: mobilebackup2 service braucht EscrowBag im StartService-Request
// (go-ios's ConnectToService schickt das nicht). Wir bauen custom-StartService
// + port-connect + SSL-enable selbst.
func Open(device ios.DeviceEntry) (*Client, error) {
	// Lockdown-Session öffnen
	lockdown, err := ios.ConnectLockdownWithSession(device)
	if err != nil {
		return nil, fmt.Errorf("mobilebackup2: lockdown session: %w", err)
	}
	defer lockdown.Close()

	// PairRecord lesen — enthält EscrowBag
	pairRecord, err := ios.ReadPairRecord(device.Properties.SerialNumber)
	if err != nil {
		return nil, fmt.Errorf("mobilebackup2: read pair record: %w", err)
	}
	if len(pairRecord.EscrowBag) == 0 {
		return nil, fmt.Errorf("mobilebackup2: no EscrowBag in pair record — re-pair iPhone with passcode unlock")
	}

	// Custom StartService request mit EscrowBag
	req := map[string]interface{}{
		"Label":      "rebreak-supervise-magic",
		"Request":    "StartService",
		"Service":    serviceName,
		"EscrowBag":  pairRecord.EscrowBag,
	}
	if err := lockdown.Send(req); err != nil {
		return nil, fmt.Errorf("mobilebackup2: send StartService: %w", err)
	}
	respBytes, err := lockdown.ReadMessage()
	if err != nil {
		return nil, fmt.Errorf("mobilebackup2: read StartService response: %w", err)
	}

	// Parse response
	var resp struct {
		Port             uint16
		Service          string
		EnableServiceSSL bool
		Error            string
	}
	if _, err := plistUnmarshal(respBytes, &resp); err != nil {
		return nil, fmt.Errorf("mobilebackup2: parse StartService response: %w", err)
	}
	if resp.Error != "" {
		return nil, fmt.Errorf("mobilebackup2: StartService error: %s", resp.Error)
	}
	if resp.Port == 0 {
		return nil, fmt.Errorf("mobilebackup2: StartService returned no port")
	}

	// Connect zur returned port via usbmux
	muxConn, err := ios.NewUsbMuxConnectionSimple()
	if err != nil {
		return nil, fmt.Errorf("mobilebackup2: new usbmux: %w", err)
	}
	if err := muxConn.Connect(device.DeviceID, resp.Port); err != nil {
		return nil, fmt.Errorf("mobilebackup2: muxConn.Connect(port=%d): %w", resp.Port, err)
	}
	deviceConn := muxConn.ReleaseDeviceConnection()

	// SSL-Enable falls service das verlangt (mobilebackup2 immer SSL)
	if resp.EnableServiceSSL {
		if err := deviceConn.EnableSessionSsl(pairRecord); err != nil {
			deviceConn.Close()
			return nil, fmt.Errorf("mobilebackup2: enable SSL: %w", err)
		}
	}

	// WICHTIG: nicht deviceConn.Conn() nutzen — das returnt die raw TCP socket
	// ohne TLS-Layer. Wir wrappen deviceConn als ReadWriter via Send/Reader.
	return &Client{
		deviceConn: deviceConn,
		dl:         dlmessage.New(&deviceConnReadWriter{c: deviceConn}),
	}, nil
}

// deviceConnReadWriter adaptiert go-ios's DeviceConnectionInterface zum io.ReadWriter.
// Send() schickt via TLS-Layer (wenn SSL enabled), Reader() liest dito.
type deviceConnReadWriter struct {
	c ios.DeviceConnectionInterface
}

func (d *deviceConnReadWriter) Write(p []byte) (int, error) {
	if err := d.c.Send(p); err != nil {
		return 0, err
	}
	return len(p), nil
}

func (d *deviceConnReadWriter) Read(p []byte) (int, error) {
	return d.c.Reader().Read(p)
}

// plistUnmarshal — helper für StartService-response-parsing.
func plistUnmarshal(data []byte, v interface{}) (int, error) {
	return plist.Unmarshal(data, v)
}

// Close beendet die Session.
func (c *Client) Close() error {
	if c.deviceConn != nil {
		c.deviceConn.Close()
		c.deviceConn = nil
	}
	return nil
}

// BaseVersionExchange — iOS 26-style: **DEVICE initiates**, host responds.
//
// Reverse-engineered aus TL's MobileBackup2Client.BaseVersionExchange
// disassembly (safesurfer.go calls): ReceivePacket → memequal → SendPacket →
// ReceivePacket → memequal.
//
// Flow:
//
//	← DLMessageVersionExchange [<device-version-major>, <device-version-minor>]
//	→ DLMessageVersionExchange ["DLVersionsOk", <chosen-version>]
//	← DLMessageVersionExchange (confirmation)
func (c *Client) BaseVersionExchange() error {
	// Step 1: iPhone sendet zuerst seine version-info.
	t, args, err := c.dl.Receive()
	if err != nil {
		return fmt.Errorf("mobilebackup2: receive initial VersionExchange: %w", err)
	}
	if t != dlmessage.TypeVersionExchange {
		return fmt.Errorf("mobilebackup2: expected VersionExchange initial, got %s args=%v", t, args)
	}
	fmt.Printf("[mb2] iPhone initial VersionExchange: %v\n", args)

	// Parse device-version (typically [major, minor] or [chosen_version, ...])
	deviceMajor := uint64(2)
	if len(args) > 0 {
		if v, ok := args[0].(uint64); ok {
			deviceMajor = v
		}
	}

	// Step 2: send our acceptance with DLVersionsOk + chosen version
	// matching device's major (or 2.1 as default).
	chosenVersion := 2.1
	if deviceMajor > 0 && deviceMajor < 100 {
		chosenVersion = float64(deviceMajor) + 0.1
	}
	if err := c.dl.Send(dlmessage.TypeVersionExchange, "DLVersionsOk", chosenVersion); err != nil {
		return fmt.Errorf("mobilebackup2: send DLVersionsOk: %w", err)
	}
	c.protocolVersion = chosenVersion

	// Step 3: receive confirmation
	t2, args2, err := c.dl.Receive()
	if err != nil {
		return fmt.Errorf("mobilebackup2: receive VersionExchange confirmation: %w", err)
	}
	fmt.Printf("[mb2] VersionExchange confirmation: type=%s args=%v\n", t2, args2)
	return nil
}

// ProtocolVersion returnt die verhandelte Version (gültig nach BaseVersionExchange).
func (c *Client) ProtocolVersion() float64 {
	return c.protocolVersion
}

// SendRequest — generischer ProcessMessage-Sender mit MessageName + extras.
// Beispiel:
//
//	c.SendRequest("Hello", map[string]interface{}{"SupportedProtocolVersions": [2.1]})
func (c *Client) SendRequest(messageName string, extra map[string]interface{}) error {
	cmd := map[string]interface{}{
		"MessageName": messageName,
	}
	for k, v := range extra {
		cmd[k] = v
	}
	return c.dl.SendProcessMessage(cmd)
}

// SendStatusResponse — generischer StatusResponse-Sender.
func (c *Client) SendStatusResponse(code int, errStr string, extra map[string]interface{}) error {
	return c.dl.SendStatusResponse(code, errStr, extra)
}

// Receive — generischer DLMessage-Receiver (gibt Type-String + Args zurück).
// Caller muss auf Message-Type-string switchen.
func (c *Client) Receive() (string, []interface{}, error) {
	return c.dl.Receive()
}

// Hello sendet das übliche Hello-Handshake nach VersionExchange.
//
//	→ ProcessMessage {MessageName: "Hello", SupportedProtocolVersions: [2.0, 2.1]}
//	← StatusResponse [0, "___EmptyParameterString___", {}]
//
// Returns ErrHandshakeRejected wenn Device nicht-zero-error returnt.
func (c *Client) Hello() error {
	if err := c.SendRequest("Hello", map[string]interface{}{
		"SupportedProtocolVersions": supportedVersions,
	}); err != nil {
		return err
	}
	t, args, err := c.dl.Receive()
	if err != nil {
		return fmt.Errorf("hello: %w", err)
	}
	if t != dlmessage.TypeProcessMessage && t != dlmessage.TypeStatusResponse {
		return fmt.Errorf("hello: unexpected response type: %s", t)
	}
	// Bei StatusResponse: erstes arg ist error-code (uint64). 0 = OK.
	if t == dlmessage.TypeStatusResponse && len(args) > 0 {
		if code, ok := args[0].(uint64); ok && code != 0 {
			return fmt.Errorf("hello rejected: code=%d args=%v", code, args)
		}
	}
	return nil
}

// ErrHandshakeRejected — Device rejected unser Hello.
var ErrHandshakeRejected = errors.New("mobilebackup2: hello rejected by device")

// SendHello — sendet das Hello-Handshake (DLMessageProcessMessage{MessageName:Hello}).
// Required nach VersionExchange + VOR Restore-Command (libimobiledevice-protocol).
//
// Apple responds with Status [0, "___EmptyParameterString___", {}] bei Erfolg.
func (c *Client) SendHello() error {
	if err := c.SendRequest("Hello", map[string]interface{}{
		"SupportedProtocolVersions": supportedVersions,
	}); err != nil {
		return fmt.Errorf("hello send: %w", err)
	}
	t, args, err := c.dl.Receive()
	if err != nil {
		return fmt.Errorf("hello recv: %w", err)
	}
	fmt.Printf("[mb2] Hello response: type=%s args=%v\n", t, args)
	return nil
}

// Start initiiert eine Restore-Operation. Übergibt das target-UDID + Restore-Options.
// Device wird dann files anfragen via DLMessageDownloadFiles / DLContentsOfDirectory.
// Diese muss caller via Receive-Loop bedienen (siehe ServeFiles).
func (c *Client) Start(targetUDID string, options map[string]interface{}) error {
	// Defaults verifiziert 2026-05-28 für TL-parity (install-screen + Settings preserved).
	// Siehe memory: project_supervise_tl_parity_achieved
	//   RemoveItemsNotRestored=true   → triggert iOS install-screen + Setup-Skip zu Kamera
	//   RestoreSystemFiles=false      → verhindert System-File-Reset (Sprache/Region/Apple-Account bleiben)
	//   RestorePreserveSettings=true  → User-Settings bleiben
	//   RestoreDontCopyBackup=false   → iPhone fetcht Backup-Files
	defaultOptions := map[string]interface{}{
		"RemoveItemsNotRestored":  true,
		"RestoreDontCopyBackup":   false,
		"RestorePreserveSettings": true,
		"RestoreSystemFiles":      false,
	}
	if v, ok := envBool("REBREAK_MB2_REMOVE_ITEMS_NOT_RESTORED"); ok {
		defaultOptions["RemoveItemsNotRestored"] = v
	}
	if v, ok := envBool("REBREAK_MB2_RESTORE_PRESERVE_SETTINGS"); ok {
		defaultOptions["RestorePreserveSettings"] = v
	}
	if v, ok := envBool("REBREAK_MB2_RESTORE_SYSTEM_FILES"); ok {
		defaultOptions["RestoreSystemFiles"] = v
	}
	if v, ok := envBool("REBREAK_MB2_DONT_COPY_BACKUP"); ok {
		defaultOptions["RestoreDontCopyBackup"] = v
	}
	if v, ok := envBool("REBREAK_MB2_APPLY"); ok {
		defaultOptions["Apply"] = v
	}
	for k, v := range options {
		defaultOptions[k] = v
	}
	fmt.Printf("[mb2] Restore options: %+v\n", defaultOptions)
	return c.SendRequest("Restore", map[string]interface{}{
		"TargetIdentifier": targetUDID,
		"SourceIdentifier": targetUDID,
		"Options":          defaultOptions,
	})
}

func envBool(name string) (bool, bool) {
	v, ok := os.LookupEnv(name)
	if !ok {
		return false, false
	}
	switch v {
	case "1", "true", "TRUE", "True":
		return true, true
	case "0", "false", "FALSE", "False":
		return false, true
	default:
		return false, false
	}
}
