// File-server-Loop für MobileBackup2 Restore-Mode.
//
// Während Restore drives das iPhone das gespräch:
//   - Es schickt DLMessageDownloadFiles mit Filenamen
//   - Es erwartet Datei-Content via custom binary chunks (NICHT DLMessage!)
//
// Wire-Format für File-Transfer (auf demselben Socket wie DLMessage):
//
//	→ [4-byte filename-length big-endian][filename UTF-8]
//	→ [4-byte code=12 DATA][4-byte 0 length] (start)
//	→ [4-byte code=12 DATA][4-byte chunk-length][chunk-data] (repeat)
//	→ [4-byte code=0 DONE][4-byte 0]
//
//	Nach allen Files:
//	→ [4-byte 0 — no more files]
//	→ DLMessageStatusResponse [0, "___EmptyParameterString___", {}]
//
// Codes:
//
//	CODE_FILE_DONE  = 0  (end-of-file marker)
//	CODE_FILE_DATA  = 12 (data chunk)
//	CODE_FILE_ERROR = 6  (error)
package mobilebackup2

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

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

// 1-byte chunk-codes (matched libimobiledevice's CODE_FILE_DATA etc).
const (
	fileCodeDone  byte = 0x00 // CODE_SUCCESS — end of file
	fileCodeData  byte = 0x0c // CODE_FILE_DATA — chunk
	fileCodeError byte = 0x06 // CODE_ERROR_LOCAL — file not found / error

	// Apple sentinel: status-response "no error".
	emptyParameterString = "___EmptyParameterString___"
)

// FileProvider liefert Datei-Content für einen relativen Pfad.
// Returnt (content, true) wenn vorhanden, ([], false) sonst.
type FileProvider func(relpath string) ([]byte, bool)

// ServeFiles fängt das iPhone-driven file-serve-loop ab.
// Loopt bis DLMessageDisconnect oder Error.
//
// Files werden via `provider` geliefert. Wenn provider eine angefragte
// Datei nicht hat: Server returnt CODE_FILE_ERROR.
//
// onProgress wird bei jedem Mess-event aufgerufen (für UI/Log).
func (c *Client) ServeFiles(provider FileProvider, onProgress func(event string, info string)) error {
	if onProgress == nil {
		onProgress = func(string, string) {}
	}

	// Wenn iPhone uns ein ProcessMessage:Response mit ErrorCode=0 schickt,
	// signalisiert das: Restore-Operation completed successfully. Danach
	// trennt das iPhone die USB-Verbindung um in den Restore-Mode zu booten →
	// wir bekommen ein EOF. Das ist KEIN Fehler — es ist erwartetes Verhalten.
	successConfirmed := false

	for {
		t, args, err := c.dl.Receive()
		if err != nil {
			if successConfirmed {
				// iPhone hat success-Response geschickt + disconnected um zu rebooten.
				onProgress("ServeFiles", "device disconnected after successful Response — reboot expected")
				return nil
			}
			return fmt.Errorf("serve: receive: %w", err)
		}

		switch t {
		case dlmessage.TypeProcessMessage:
			if len(args) == 0 {
				continue
			}
			dict, ok := args[0].(map[string]interface{})
			if !ok {
				continue
			}
			msgName, _ := dict["MessageName"].(string)
			// 2026-05-28 DEBUG: log full dict to understand what iPhone sends
			onProgress("ProcessMessage", fmt.Sprintf("%s | full=%v", msgName, dict))
			if msgName == "DLMessageDisconnect" || msgName == "Disconnect" {
				return nil
			}
			// "Response" = iPhone signals operation completed — NICHT antworten,
			// nur weiter loopen + auf nächste Message warten.
			// Wenn ErrorCode=0 → success → nachfolgender EOF ist erwartet.
			if msgName == "Response" {
				ed, _ := dict["ErrorDescription"].(string)
				if ec, ok := dict["ErrorCode"]; ok {
					code, parsed := parseErrorCode(ec)
					if !parsed {
						return fmt.Errorf("serve: response error (unknown code type %T): %s", ec, ed)
					}
					if code == 0 {
						successConfirmed = true
					} else {
						return fmt.Errorf("serve: response error code=%d: %s", code, ed)
					}
				} else {
					if ed != "" {
						return fmt.Errorf("serve: response without error code: %s", ed)
					}
				}
				continue
			}
			// Andere Sub-Messages — Status OK respond.
			if err := c.sendStatusOK(); err != nil {
				return err
			}

		case dlmessage.TypeStatusResponse:
			// Device-Status — wir loggen + machen weiter
			onProgress("StatusResponse", fmt.Sprintf("%v", args))
			// Falls error: abbrechen
			if len(args) > 0 {
				if code, ok := args[0].(uint64); ok && code != 0 {
					return fmt.Errorf("serve: device error code=%d: %v", code, args)
				}
			}

		case dlmessage.TypeGetFreeDiskSpace:
			// Device fragt nach freiem Speicher — wir behaupten Schubladen voll Platz
			onProgress("GetFreeDiskSpace", "")
			// Antwort: [0, errStr, freeSpaceUint64]
			if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, uint64(1<<40)); err != nil {
				return err
			}

		case dlmessage.TypeContentsOfDirectory:
			// Device möchte Verzeichnis listen — wir antworten mit empty dict
			dirname := ""
			if len(args) > 0 {
				dirname, _ = args[0].(string)
			}
			onProgress("ContentsOfDirectory", dirname)
			// Antwort: status [0, "", {}]
			if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{}); err != nil {
				return err
			}

		case dlmessage.TypeCreateDirectory, dlmessage.TypeMoveItems, dlmessage.TypeRemoveItems, dlmessage.TypeCopyItem:
			onProgress(t, "")
			if err := c.sendStatusOK(); err != nil {
				return err
			}

		case dlmessage.TypeDownloadFiles:
			// Device fragt files an — args[0] ist []string
			var fileList []string
			if len(args) > 0 {
				if arr, ok := args[0].([]interface{}); ok {
					for _, item := range arr {
						if s, ok := item.(string); ok {
							fileList = append(fileList, s)
						}
					}
				}
			}
			onProgress("DownloadFiles", fmt.Sprintf("count=%d", len(fileList)))
			if err := c.sendFiles(fileList, provider, onProgress); err != nil {
				return fmt.Errorf("serve: send files: %w", err)
			}

		case dlmessage.TypeUploadFiles:
			// iPhone will UNS files uploaden (sein current state).
			// Wir ACK + receive raw upload-stream.
			onProgress("UploadFiles", "device uploading")
			if err := c.sendStatusOK(); err != nil {
				return err
			}
			if err := c.receiveFiles(onProgress); err != nil {
				return fmt.Errorf("serve: receive files: %w", err)
			}

		case dlmessage.TypeDisconnect:
			onProgress("Disconnect", "explicit")
			return nil

		default:
			// unbekannter Typ — log + status OK
			onProgress("Unknown", t)
			if err := c.sendStatusOK(); err != nil {
				return err
			}
		}
	}
}

func (c *Client) sendStatusOK() error {
	return c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{})
}

func parseErrorCode(v interface{}) (int64, bool) {
	switch n := v.(type) {
	case uint64:
		return int64(n), true
	case int64:
		return n, true
	case int:
		return int64(n), true
	case uint32:
		return int64(n), true
	case int32:
		return int64(n), true
	case float64:
		return int64(n), true
	case string:
		parsed, err := strconv.ParseInt(n, 10, 64)
		if err != nil {
			return 0, false
		}
		return parsed, true
	default:
		return 0, false
	}
}

// sendFiles antwortet auf DLMessageDownloadFiles. Wire-format aus libimobiledevice
// mb2_handle_send_file verifiziert:
//
//	per file:
//	  [4-byte BE filename-length][filename UTF-8]
//	  if file exists:
//	    per chunk: [4-byte BE (1+chunk-size)][1-byte 0x0c CODE_DATA][chunk-data]
//	    end-of-file: [4-byte BE 1][1-byte 0x00 CODE_SUCCESS]
//	  else:
//	    [4-byte BE 1][1-byte 0x06 CODE_ERROR_LOCAL]
//
//	after all files:
//	  [4-byte BE 0]  (terminator = "no more files")
//	  DLMessageStatusResponse [0, "___EmptyParameterString___", {<errors>}]
func (c *Client) sendFiles(fileList []string, provider FileProvider, onProgress func(string, string)) error {
	for _, fname := range fileList {
		content, ok := provider(fname)
		onProgress("send-file", fmt.Sprintf("%s (%d bytes, found=%v)", fname, len(content), ok))

		// filename header
		if err := c.writeRawU32(uint32(len(fname))); err != nil {
			return err
		}
		if err := c.deviceConn.Send([]byte(fname)); err != nil {
			return fmt.Errorf("write filename: %w", err)
		}

		if !ok {
			// ERROR: [length=1][CODE_ERROR_LOCAL]
			if err := c.writeRawU32(1); err != nil {
				return err
			}
			if err := c.deviceConn.Send([]byte{fileCodeError}); err != nil {
				return err
			}
			continue
		}

		// DATA chunks
		const maxChunk = 32 * 1024
		for offset := 0; offset < len(content); offset += maxChunk {
			end := offset + maxChunk
			if end > len(content) {
				end = len(content)
			}
			chunk := content[offset:end]
			// [length=1+chunk][CODE_DATA][chunk]
			if err := c.writeRawU32(uint32(1 + len(chunk))); err != nil {
				return err
			}
			if err := c.deviceConn.Send([]byte{fileCodeData}); err != nil {
				return err
			}
			if err := c.deviceConn.Send(chunk); err != nil {
				return err
			}
		}

		// END-OF-FILE: [length=1][CODE_SUCCESS]
		if err := c.writeRawU32(1); err != nil {
			return err
		}
		if err := c.deviceConn.Send([]byte{fileCodeDone}); err != nil {
			return err
		}
	}

	// Terminator: [length=0]
	if err := c.writeRawU32(0); err != nil {
		return err
	}

	// Final status
	return c.sendStatusOK()
}

func (c *Client) writeRawU32(v uint32) error {
	buf := make([]byte, 4)
	binary.BigEndian.PutUint32(buf, v)
	return c.deviceConn.Send(buf)
}

// receiveFiles empfängt iPhone's file-upload-stream nach DLMessageUploadFiles.
// Wire-format mirror von sendFiles (gleiche frames, andere Richtung):
//
//	per file:
//	  [4-byte BE path-len][path]
//	  per chunk: [4-byte BE (1+chunk-size)][1-byte code][data]
//	  end-of-file: [4-byte BE 1][1-byte 0x00 SUCCESS]
//	terminator: [4-byte BE 0]
//
// Wir discardieren den Inhalt — iPhone's "backup-of-backup" interessiert uns nicht,
// nur dass wir den Stream sauber durchlesen damit das Protokoll weitergeht.
func (c *Client) receiveFiles(onProgress func(string, string)) error {
	reader := c.deviceConn.Reader()
	totalFiles := 0
	totalBytes := uint64(0)

	for {
		// path-length-prefix
		var pathLenBuf [4]byte
		if _, err := io.ReadFull(reader, pathLenBuf[:]); err != nil {
			return fmt.Errorf("read path-len: %w", err)
		}
		pathLen := binary.BigEndian.Uint32(pathLenBuf[:])
		if pathLen == 0 {
			// terminator — no more files
			onProgress("upload-done", fmt.Sprintf("%d files, %d bytes total", totalFiles, totalBytes))
			return nil
		}
		if pathLen > 65536 {
			return fmt.Errorf("upload-path too large: %d", pathLen)
		}

		// path-bytes
		pathBuf := make([]byte, pathLen)
		if _, err := io.ReadFull(reader, pathBuf); err != nil {
			return fmt.Errorf("read path: %w", err)
		}
		path := string(pathBuf)
		totalFiles++

		// chunks
		fileBytes := uint64(0)
		for {
			var chunkLenBuf [4]byte
			if _, err := io.ReadFull(reader, chunkLenBuf[:]); err != nil {
				return fmt.Errorf("read chunk-len for %s: %w", path, err)
			}
			chunkLen := binary.BigEndian.Uint32(chunkLenBuf[:])
			if chunkLen == 0 {
				// some implementations send zero-length as terminator
				break
			}
			// 1-byte code
			var code [1]byte
			if _, err := io.ReadFull(reader, code[:]); err != nil {
				return fmt.Errorf("read chunk-code: %w", err)
			}
			dataLen := chunkLen - 1
			if dataLen > 0 {
				discard := make([]byte, dataLen)
				if _, err := io.ReadFull(reader, discard); err != nil {
					return fmt.Errorf("read chunk-data: %w", err)
				}
				fileBytes += uint64(dataLen)
			}
			// code 0x00 (CODE_SUCCESS) = end-of-file
			if code[0] == fileCodeDone {
				break
			}
		}
		totalBytes += fileBytes
		onProgress("upload-file", fmt.Sprintf("%s (%d bytes)", path, fileBytes))
	}
}

// ErrServeAborted ist ein Sentinel den FileProvider zurückgeben kann um Loop
// zu beenden (z.B. bei Disconnect-Trigger).
var ErrServeAborted = errors.New("serve aborted")
