// Manifest.db Runtime-Generator — baut die SQLite-DB die iOS während
// Restore liest um zu wissen welche Files im Backup liegen.
//
// Schema (Apple-public, verifiziert aus TechLockdown's extracted Manifest.db):
//
//	CREATE TABLE Files (
//	    fileID TEXT PRIMARY KEY,
//	    domain TEXT,
//	    relativePath TEXT,
//	    flags INTEGER,    -- 1=file, 2=directory
//	    file BLOB         -- NSKeyedArchive-encoded MBFile metadata
//	);
//	CREATE TABLE Properties (
//	    key TEXT PRIMARY KEY,
//	    value BLOB
//	);
//
// fileID-Berechnung: hex(SHA1(domain + "-" + relativePath))
package mobilebackup2

import (
	"bytes"
	"crypto/sha1"
	"database/sql"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"

	"howett.net/plist"

	_ "modernc.org/sqlite" // SQL-driver registration
)

// SystemGroupDomain — Apple's container-identifier für ConfigurationProfiles.
const SystemGroupDomain = "SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"

// File-entry für Manifest.db.
type DBEntry struct {
	Domain       string
	RelativePath string
	IsDirectory  bool
	Size         int64  // 0 für directories
	Mode         uint32 // POSIX mode (0o755 für dirs, 0o644 für files default)
}

// BuildManifestDB erstellt eine in-memory SQLite-DB, fügt die Einträge ein,
// und returnt die fertige DB als bytes (lesbar via plistlib-äquivalenten Code
// oder direkt von iOS's MobileBackup2-Service).
func BuildManifestDB(entries []DBEntry) ([]byte, error) {
	// SQLite-driver hat keinen In-Memory-to-bytes Pfad direkt — wir nutzen
	// eine tmp-Datei, lesen sie nach dem Schreiben.
	tmpFile, err := os.CreateTemp("", "manifest-*.db")
	if err != nil {
		return nil, fmt.Errorf("manifest_db: tmpfile: %w", err)
	}
	tmpPath := tmpFile.Name()
	tmpFile.Close()
	defer os.Remove(tmpPath)
	defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-shm"))
	defer os.Remove(filepath.Join(filepath.Dir(tmpPath), filepath.Base(tmpPath)+"-wal"))

	db, err := sql.Open("sqlite", tmpPath)
	if err != nil {
		return nil, fmt.Errorf("manifest_db: open: %w", err)
	}
	defer db.Close()

	// PRAGMA-Settings matched TL's extracted Manifest.db (2026-05-28 verifiziert):
	//   user_version=2 (app-defined backup-format magic — iOS may check this)
	//   auto_vacuum=2 (incremental — TL setzt das)
	//
	// journal_mode=wal NICHT gesetzt: WAL erzeugt -wal/-shm Files die wir nicht
	// mit auf das iPhone schicken können. iPhone bekommt die DB self-contained.
	pragmas := []string{
		`PRAGMA auto_vacuum = 2`, // muss VOR CREATE TABLE gesetzt sein
		`PRAGMA user_version = 2`,
	}
	for _, stmt := range pragmas {
		if _, err := db.Exec(stmt); err != nil {
			return nil, fmt.Errorf("manifest_db: pragma %q: %w", stmt, err)
		}
	}

	// Schema
	schema := []string{
		`CREATE TABLE Files (
			fileID TEXT PRIMARY KEY,
			domain TEXT,
			relativePath TEXT,
			flags INTEGER,
			file BLOB
		)`,
		`CREATE TABLE Properties (
			key TEXT PRIMARY KEY,
			value BLOB
		)`,
	}
	for _, stmt := range schema {
		if _, err := db.Exec(stmt); err != nil {
			return nil, fmt.Errorf("manifest_db: schema: %w", err)
		}
	}

	// Insert entries
	insert, err := db.Prepare(`INSERT INTO Files (fileID, domain, relativePath, flags, file) VALUES (?, ?, ?, ?, ?)`)
	if err != nil {
		return nil, fmt.Errorf("manifest_db: prepare: %w", err)
	}
	defer insert.Close()

	for _, e := range entries {
		fileID := ComputeFileID(e.Domain, e.RelativePath)
		flags := 1
		if e.IsDirectory {
			flags = 2
		}
		mbfileBlob, err := EncodeMBFile(e.RelativePath, e.IsDirectory, e.Size, e.Mode)
		if err != nil {
			return nil, fmt.Errorf("manifest_db: MBFile %s: %w", e.RelativePath, err)
		}
		if _, err := insert.Exec(fileID, e.Domain, e.RelativePath, flags, mbfileBlob); err != nil {
			return nil, fmt.Errorf("manifest_db: insert %s: %w", e.RelativePath, err)
		}
	}

	// Close + flush
	if err := db.Close(); err != nil {
		return nil, err
	}

	// Read file back as bytes
	data, err := os.ReadFile(tmpPath)
	if err != nil {
		return nil, fmt.Errorf("manifest_db: read tmp: %w", err)
	}
	return data, nil
}

// ComputeFileID — Apple's fileID-Berechnung: SHA1(domain + "-" + relativePath).
//
// Empirisch verifiziert mit TL's extracted Manifest.db:
//
//	domain="SysSharedContainerDomain-systemgroup.com.apple.configurationprofiles"
//	relativePath=""
//	→ fileID="9581eb754ee03b7f535293caf770235f0f37f8a8"
func ComputeFileID(domain, relativePath string) string {
	h := sha1.New()
	h.Write([]byte(domain + "-" + relativePath))
	return hex.EncodeToString(h.Sum(nil))
}

// EncodeMBFile — empirisch verifiziert gegen TL's bplist_02 (CloudConfigurationDetails-Entry):
//
//	LastModified: 1554229750 (2019-04-02)
//	LastStatusChange: 1554229750
//	Birth: 1554229750
//	InodeNumber: 51802
//	UserID: 501
//	GroupID: -2
//	Flags: 0
//	Mode: 33188 (0o100644 für regular file, 0o40755 für dir)
//	ProtectionClass: 4
//	Size: <variable>
//	RelativePath: UID(2)
//	$class: UID(3)
//
// NOTE 2026-05-27: TL-Embed-Werte für DIR-MBFiles (Size=0, ProtClass=0/4,
// Mode=040000 für Library) wurden ausgetestet — iPhone bail't ohne Reboot.
// Schlussfolgerung: TL mutiert die Embed-Werte zur Runtime, der Extract ist
// nicht 1:1 verwendbar. Rolled back zur originalen "skip Size/ProtClass für
// dirs"-Logik die wenigstens Reboot in Restore-Mode triggerte.
func EncodeMBFile(relativePath string, isDir bool, size int64, mode uint32) ([]byte, error) {
	// Mode: full POSIX mode = file-type-bits | perm-bits
	if mode == 0 {
		if isDir {
			mode = 0o40755 // directory + 0755
		} else {
			mode = 0o100644 // regular file + 0644
		}
	} else if mode < 0o100000 {
		// Just perm-bits given — add file-type bits
		if isDir {
			mode |= 0o40000
		} else {
			mode |= 0o100000
		}
	}

	mbFileObj := map[string]interface{}{
		"$class":           plist.UID(3),
		"LastModified":     int64(1554229750), // TL's timestamp (2019-04-02)
		"LastStatusChange": int64(1554229750),
		"Birth":            int64(1554229750),
		"GroupID":          int64(-2),
		"UserID":           int64(501),
		"InodeNumber":      int64(51802),
		"Flags":            int64(0),
		"Mode":             int64(mode),
		"RelativePath":     plist.UID(2),
	}
	if !isDir {
		mbFileObj["Size"] = size
		mbFileObj["ProtectionClass"] = int64(4)
	}

	// NSKeyedArchive envelope
	archive := map[string]interface{}{
		"$archiver": "NSKeyedArchiver",
		"$version":  int64(100000),
		"$top":      map[string]interface{}{"root": plist.UID(1)},
		"$objects": []interface{}{
			"$null",      // 0
			mbFileObj,    // 1
			relativePath, // 2
			map[string]interface{}{ // 3
				"$classname": "MBFile",
				"$classes":   []interface{}{"MBFile", "NSObject"},
			},
		},
	}

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

// DefaultRestoreEntries returnt die 4 Standard-Entries für Cloud-Config-
// Injection (matched TL's extracted Manifest.db):
//
//	1. root dir (relativePath="")
//	2. Library
//	3. Library/ConfigurationProfiles
//	4. Library/ConfigurationProfiles/CloudConfigurationDetails.plist (datei, size variable)
func DefaultRestoreEntries(cloudConfigSize int64) []DBEntry {
	return []DBEntry{
		{Domain: SystemGroupDomain, RelativePath: "", IsDirectory: true},
		{Domain: SystemGroupDomain, RelativePath: "Library", IsDirectory: true},
		{Domain: SystemGroupDomain, RelativePath: "Library/ConfigurationProfiles", IsDirectory: true},
		{
			Domain:       SystemGroupDomain,
			RelativePath: "Library/ConfigurationProfiles/CloudConfigurationDetails.plist",
			IsDirectory:  false,
			Size:         cloudConfigSize,
			Mode:         0o644,
		},
	}
}
