//go:build windows

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"syscall"
	"time"
	"unsafe"
)

var (
	user32   = syscall.NewLazyDLL("user32.dll")
	kernel32 = syscall.NewLazyDLL("kernel32.dll")
	ole32    = syscall.NewLazyDLL("ole32.dll")
	shell32  = syscall.NewLazyDLL("shell32.dll")
	gdi32    = syscall.NewLazyDLL("gdi32.dll")

	procRegisterClassExW     = user32.NewProc("RegisterClassExW")
	procCreateWindowExW      = user32.NewProc("CreateWindowExW")
	procDefWindowProcW       = user32.NewProc("DefWindowProcW")
	procShowWindow           = user32.NewProc("ShowWindow")
	procUpdateWindow         = user32.NewProc("UpdateWindow")
	procGetMessageW          = user32.NewProc("GetMessageW")
	procTranslateMessage     = user32.NewProc("TranslateMessage")
	procDispatchMessageW     = user32.NewProc("DispatchMessageW")
	procPostQuitMessage      = user32.NewProc("PostQuitMessage")
	procPostMessageW         = user32.NewProc("PostMessageW")
	procMessageBoxW          = user32.NewProc("MessageBoxW")
	procSetWindowTextW       = user32.NewProc("SetWindowTextW")
	procGetWindowTextW       = user32.NewProc("GetWindowTextW")
	procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
	procEnableWindow         = user32.NewProc("EnableWindow")
	procSendMessageW         = user32.NewProc("SendMessageW")
	procLoadCursorW          = user32.NewProc("LoadCursorW")
	procGetStockObject       = gdi32.NewProc("GetStockObject")

	procGetModuleHandleW    = kernel32.NewProc("GetModuleHandleW")
	procCreateMutexW        = kernel32.NewProc("CreateMutexW")
	procWaitForSingleObject = kernel32.NewProc("WaitForSingleObject")
	procReleaseMutex        = kernel32.NewProc("ReleaseMutex")
	procCloseHandle         = kernel32.NewProc("CloseHandle")
	procGetDiskFreeSpaceExW = kernel32.NewProc("GetDiskFreeSpaceExW")

	procCoInitializeEx   = ole32.NewProc("CoInitializeEx")
	procCoUninitialize   = ole32.NewProc("CoUninitialize")
	procCoCreateInstance = ole32.NewProc("CoCreateInstance")
	procCoTaskMemFree    = ole32.NewProc("CoTaskMemFree")
)

const (
	WS_OVERLAPPEDWINDOW = 0x00CF0000
	WS_VISIBLE          = 0x10000000
	WS_CHILD            = 0x40000000
	WS_TABSTOP          = 0x00010000
	WS_BORDER           = 0x00800000
	WS_VSCROLL          = 0x00200000
	WS_EX_CLIENTEDGE    = 0x00000200

	ES_AUTOHSCROLL   = 0x0080
	ES_READONLY      = 0x0800
	BS_AUTOCHECKBOX  = 0x00000003
	CBS_DROPDOWNLIST = 0x0003
	SS_LEFT          = 0x00000000
	SS_NOPREFIX      = 0x00000080

	SW_SHOW       = 5
	CW_USEDEFAULT = 0x80000000

	WM_DESTROY      = 0x0002
	WM_CLOSE        = 0x0010
	WM_COMMAND      = 0x0111
	WM_SETFONT      = 0x0030
	BM_GETCHECK     = 0x00F0
	BST_CHECKED     = 1
	CB_RESETCONTENT = 0x014B
	CB_ADDSTRING    = 0x0143
	CB_GETCURSEL    = 0x0147
	CB_SETCURSEL    = 0x014E

	WM_APP              = 0x8000
	WM_APP_SOURCE_DONE  = WM_APP + 1
	WM_APP_INSTALL_DONE = WM_APP + 2
	WM_APP_BACKUPS_DONE = WM_APP + 3
	WM_APP_RESTORE_DONE = WM_APP + 4
	WM_APP_CONFIRM      = WM_APP + 5

	COLOR_BTNFACE    = 15
	IDC_ARROW        = 32512
	DEFAULT_GUI_FONT = 17

	MB_OK              = 0x00000000
	MB_ICONERROR       = 0x00000010
	MB_ICONINFORMATION = 0x00000040
	MB_ICONQUESTION    = 0x00000020
	MB_YESNO           = 0x00000004
	IDYES              = 6

	COINIT_APARTMENTTHREADED = 0x2
	CLSCTX_INPROC_SERVER     = 0x1
	FOS_NOCHANGEDIR          = 0x00000008
	FOS_PICKFOLDERS          = 0x00000020
	FOS_FORCEFILESYSTEM      = 0x00000040
	FOS_PATHMUSTEXIST        = 0x00000800
	FOS_DONTADDTORECENT      = 0x02000000
	SIGDN_FILESYSPATH        = 0x80058000

	WAIT_OBJECT_0        = 0x00000000
	WAIT_ABANDONED       = 0x00000080
	ERROR_ALREADY_EXISTS = 183

	ID_TARGET_EDIT    = 1001
	ID_BROWSE         = 1002
	ID_FIRST          = 1003
	ID_PILLAR_EDIT    = 1004
	ID_CHECK          = 1005
	ID_INSTALL        = 1006
	ID_BACKUP_COMBO   = 1007
	ID_BACKUP_REFRESH = 1008
	ID_RESTORE        = 1009
)

type WNDCLASSEX struct {
	CbSize        uint32
	Style         uint32
	LpfnWndProc   uintptr
	CbClsExtra    int32
	CbWndExtra    int32
	HInstance     syscall.Handle
	HIcon         syscall.Handle
	HCursor       syscall.Handle
	HbrBackground syscall.Handle
	LpszMenuName  *uint16
	LpszClassName *uint16
	HIconSm       syscall.Handle
}

type POINT struct{ X, Y int32 }
type MSG struct {
	Hwnd           uintptr
	Message        uint32
	WParam, LParam uintptr
	Time           uint32
	Pt             POINT
	LPrivate       uint32
}

type GUID struct {
	Data1        uint32
	Data2, Data3 uint16
	Data4        [8]byte
}

var (
	mainHwnd                                                                                             uintptr
	hTarget, hBrowse, hFirst, hPillar, hCheck, hInstall, hStatus, hBackupCombo, hBackupRefresh, hRestore uintptr
	exeRoot                                                                                              string
	sourceValid                                                                                          bool
	busy                                                                                                 bool
	validatedSource                                                                                      *SourceState
	installerMutex                                                                                       syscall.Handle
	backupPaths                                                                                          []string

	resultMu      sync.Mutex
	sourceResult  sourceCheckResult
	installResult opResult
	backupsResult backupsScanResult
	restoreResult opResult
	confirmReq    *confirmRequest
)

type sourceCheckResult struct {
	State *SourceState
	Err   error
}
type opResult struct {
	Text, BackupPath, Rollback string
	Err                        error
	SourceChanged              bool
	Canceled                   bool
}
type backupsScanResult struct {
	Paths []string
	Err   error
}
type confirmRequest struct {
	Text string
	Ch   chan bool
}

func u16(s string) *uint16    { p, _ := syscall.UTF16PtrFromString(s); return p }
func loword(v uintptr) uint16 { return uint16(v & 0xffff) }
func hiword(v uintptr) uint16 { return uint16((v >> 16) & 0xffff) }

func main() {
	runtime.LockOSThread()
	exe, err := os.Executable()
	if err != nil {
		return
	}
	exe, _ = filepath.Abs(exe)
	exeRoot = filepath.Dir(exe)

	procCoInitializeEx.Call(0, COINIT_APARTMENTTHREADED)
	defer procCoUninitialize.Call()
	if !acquireInstallerMutex() {
		return
	}
	defer procCloseHandle.Call(uintptr(installerMutex))

	hInst, _, _ := procGetModuleHandleW.Call(0)
	className := u16("JimSSngCorpsePillarInstallerWindow")
	cursor, _, _ := procLoadCursorW.Call(0, IDC_ARROW)
	wc := WNDCLASSEX{CbSize: uint32(unsafe.Sizeof(WNDCLASSEX{})), LpfnWndProc: syscall.NewCallback(wndProc), HInstance: syscall.Handle(hInst), HCursor: syscall.Handle(cursor), HbrBackground: syscall.Handle(COLOR_BTNFACE + 1), LpszClassName: className}
	if r, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
		return
	}
	hwnd, _, _ := procCreateWindowExW.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(u16(AppName))), WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT, 780, 620, 0, 0, hInst, 0)
	if hwnd == 0 {
		return
	}
	mainHwnd = hwnd
	createControls(hwnd, hInst)
	procShowWindow.Call(hwnd, SW_SHOW)
	procUpdateWindow.Call(hwnd)
	startBackupScan()
	var msg MSG
	for {
		r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
		if int32(r) <= 0 {
			break
		}
		procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
		procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
	}
}

func acquireInstallerMutex() bool {
	name := u16("Local\\JimSSng_CorpseColorPillar_Installer_v1")
	h, _, _ := procCreateMutexW.Call(0, 1, uintptr(unsafe.Pointer(name)))
	if h == 0 {
		return false
	}
	installerMutex = syscall.Handle(h)
	if errno, ok := syscall.GetLastError().(syscall.Errno); ok && errno == syscall.Errno(ERROR_ALREADY_EXISTS) {
		procCloseHandle.Call(h)
		msgBox(0, "이미 같은 자동병합기가 실행 중입니다.", AppName, MB_OK|MB_ICONERROR)
		return false
	}
	return true
}

func createControls(hwnd, hInst uintptr) {
	font, _, _ := procGetStockObject.Call(DEFAULT_GUI_FONT)
	mk := func(ex uint32, cls, text string, style uint32, x, y, w, h int32, id int) uintptr {
		c, _, _ := procCreateWindowExW.Call(uintptr(ex), uintptr(unsafe.Pointer(u16(cls))), uintptr(unsafe.Pointer(u16(text))), uintptr(style), uintptr(x), uintptr(y), uintptr(w), uintptr(h), hwnd, uintptr(id), hInst, 0)
		if c != 0 {
			procSendMessageW.Call(c, WM_SETFONT, font, 1)
		}
		return c
	}
	mk(0, "STATIC", "설치 원본: EXE 옆 data / installer_sources 자동 인식", WS_CHILD|WS_VISIBLE|SS_NOPREFIX, 20, 18, 720, 22, 0)
	mk(0, "STATIC", "대상 폴더", WS_CHILD|WS_VISIBLE, 20, 53, 80, 22, 0)
	hTarget = mk(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD|WS_VISIBLE|WS_TABSTOP|ES_AUTOHSCROLL, 105, 48, 530, 25, ID_TARGET_EDIT)
	hBrowse = mk(0, "BUTTON", "찾기", WS_CHILD|WS_VISIBLE|WS_TABSTOP, 645, 47, 95, 28, ID_BROWSE)
	hFirst = mk(0, "BUTTON", "모드폴더 없음", WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_AUTOCHECKBOX, 105, 82, 170, 25, ID_FIRST)

	mk(0, "STATIC", "시체 빛기둥 지정", WS_CHILD|WS_VISIBLE, 20, 124, 130, 22, 0)
	hPillar = mk(WS_EX_CLIENTEDGE, "EDIT", DefaultPillarCode, WS_CHILD|WS_VISIBLE|WS_TABSTOP|ES_AUTOHSCROLL, 155, 119, 140, 25, ID_PILLAR_EDIT)
	mk(0, "STATIC", "사용 가능한 색상: red, orange, gold, peach, white, yellow, green, cyan, blue, navy, pink, magenta", WS_CHILD|WS_VISIBLE|SS_NOPREFIX, 20, 156, 720, 20, 0)
	mk(0, "STATIC", "뒤 숫자는 1=기본, 2=노이즈, 3=실크입니다. 예: gold2, red1, magenta3", WS_CHILD|WS_VISIBLE|SS_NOPREFIX, 20, 178, 720, 20, 0)

	hCheck = mk(0, "BUTTON", "검사", WS_CHILD|WS_VISIBLE|WS_TABSTOP, 105, 218, 100, 30, ID_CHECK)
	hInstall = mk(0, "BUTTON", "설치", WS_CHILD|WS_VISIBLE|WS_TABSTOP, 215, 218, 100, 30, ID_INSTALL)
	procEnableWindow.Call(hInstall, 0)
	mk(0, "STATIC", "※ [검사]는 설치 원본만 확인하고 대상은 [설치] 시 검사합니다.", WS_CHILD|WS_VISIBLE|SS_NOPREFIX, 330, 224, 410, 20, 0)

	mk(0, "STATIC", "검사 결과", WS_CHILD|WS_VISIBLE, 20, 272, 100, 22, 0)
	hStatus = mk(WS_EX_CLIENTEDGE, "STATIC", "아직 검사하지 않았습니다.", WS_CHILD|WS_VISIBLE|SS_LEFT|SS_NOPREFIX, 20, 298, 720, 96, 0)

	mk(0, "STATIC", "백업 목록", WS_CHILD|WS_VISIBLE, 20, 420, 100, 22, 0)
	hBackupCombo = mk(0, "COMBOBOX", "", WS_CHILD|WS_VISIBLE|WS_TABSTOP|CBS_DROPDOWNLIST|WS_VSCROLL, 20, 446, 510, 180, ID_BACKUP_COMBO)
	hBackupRefresh = mk(0, "BUTTON", "백업 새로고침", WS_CHILD|WS_VISIBLE|WS_TABSTOP, 540, 444, 120, 30, ID_BACKUP_REFRESH)
	hRestore = mk(0, "BUTTON", "선택 백업 복구", WS_CHILD|WS_VISIBLE|WS_TABSTOP, 665, 444, 110, 30, ID_RESTORE)
	mk(0, "STATIC", "복구 시 해당 백업 이후 같은 파일에 적용한 다른 모드 수정도 함께 되돌아갑니다.", WS_CHILD|WS_VISIBLE|SS_NOPREFIX, 20, 490, 720, 22, 0)
	mk(0, "STATIC", "백업은 EXE 옆 backups 폴더에 보존되며 자동 삭제하지 않습니다.", WS_CHILD|WS_VISIBLE|SS_NOPREFIX, 20, 514, 720, 22, 0)
}

func wndProc(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
	switch msg {
	case WM_COMMAND:
		if hiword(wParam) == 0 {
			switch int(loword(wParam)) {
			case ID_BROWSE:
				if !busy {
					if p, ok := pickFolder(hwnd); ok {
						setText(hTarget, p)
					}
				}
			case ID_CHECK:
				if !busy {
					startSourceCheck()
				}
			case ID_INSTALL:
				if !busy {
					startInstallFromUI()
				}
			case ID_BACKUP_REFRESH:
				if !busy {
					startBackupScan()
				}
			case ID_RESTORE:
				if !busy {
					startRestoreFromUI()
				}
			}
		}
		return 0
	case WM_APP_SOURCE_DONE:
		finishSourceCheck()
		return 0
	case WM_APP_INSTALL_DONE:
		finishInstall()
		return 0
	case WM_APP_BACKUPS_DONE:
		finishBackupScan()
		return 0
	case WM_APP_RESTORE_DONE:
		finishRestore()
		return 0
	case WM_APP_CONFIRM:
		handleConfirm()
		return 0
	case WM_CLOSE:
		if busy {
			msgBox(hwnd, "현재 작업이 진행 중입니다. 작업 완료 후 종료하세요.", AppName, MB_OK|MB_ICONINFORMATION)
			return 0
		}
	case WM_DESTROY:
		procPostQuitMessage.Call(0)
		return 0
	}
	r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
	return r
}

func setBusy(v bool) {
	busy = v
	controls := []uintptr{hTarget, hBrowse, hFirst, hPillar, hCheck, hBackupCombo, hBackupRefresh, hRestore}
	for _, h := range controls {
		if h != 0 {
			if v {
				procEnableWindow.Call(h, 0)
			} else {
				procEnableWindow.Call(h, 1)
			}
		}
	}
	if hInstall != 0 {
		if !v && sourceValid {
			procEnableWindow.Call(hInstall, 1)
		} else {
			procEnableWindow.Call(hInstall, 0)
		}
	}
}
func setText(hwnd uintptr, s string) { procSetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(u16(s)))) }
func getText(hwnd uintptr) string {
	n, _, _ := procGetWindowTextLengthW.Call(hwnd)
	buf := make([]uint16, n+1)
	procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), n+1)
	return syscall.UTF16ToString(buf)
}
func isChecked(hwnd uintptr) bool {
	r, _, _ := procSendMessageW.Call(hwnd, BM_GETCHECK, 0, 0)
	return r == BST_CHECKED
}
func post(msg uint32) { procPostMessageW.Call(mainHwnd, uintptr(msg), 0, 0) }
func msgBox(hwnd uintptr, text, title string, flags uintptr) int {
	r, _, _ := procMessageBoxW.Call(hwnd, uintptr(unsafe.Pointer(u16(text))), uintptr(unsafe.Pointer(u16(title))), flags)
	return int(r)
}

func startSourceCheck() {
	setBusy(true)
	setText(hStatus, "설치 원본을 검사 중입니다...")
	go func() {
		st, err := ValidateSource(exeRoot)
		resultMu.Lock()
		sourceResult = sourceCheckResult{st, err}
		resultMu.Unlock()
		post(WM_APP_SOURCE_DONE)
	}()
}
func finishSourceCheck() {
	resultMu.Lock()
	r := sourceResult
	resultMu.Unlock()
	if r.Err != nil {
		sourceValid = false
		validatedSource = nil
		setText(hStatus, "검사 실패\r\n"+r.Err.Error())
	} else {
		sourceValid = true
		validatedSource = r.State
		setText(hStatus, fmt.Sprintf("검사 완료\r\n필수 source %d개와 모든 색상 variant를 확인했습니다.", len(r.State.Files)))
	}
	setBusy(false)
}

func startInstallFromUI() {
	if !sourceValid || validatedSource == nil {
		msgBox(mainHwnd, "먼저 [검사]를 완료하세요.", AppName, MB_OK|MB_ICONERROR)
		return
	}
	selected := getText(hTarget)
	first := isChecked(hFirst)
	code, err := ParsePillarCode(getText(hPillar))
	if err != nil {
		msgBox(mainHwnd, err.Error(), AppName, MB_OK|MB_ICONERROR)
		return
	}
	session := cloneSourceState(validatedSource)
	setBusy(true)
	setText(hStatus, "대상 구조와 병합 계획을 확인 중입니다...")
	go installWorker(selected, first, code, session)
}

func cloneSourceState(s *SourceState) *SourceState {
	n := &SourceState{Files: map[string]string{}}
	for k, v := range s.Files {
		n.Files[k] = v
	}
	return n
}

func installWorker(selected string, first bool, code string, session *SourceState) {
	res := opResult{}
	st, err := ValidateSource(exeRoot)
	if err != nil || !SameSourceState(st, session) {
		res.Err = fmt.Errorf("검사 후 설치 원본이 변경되었습니다. 다시 [검사]하세요")
		res.SourceChanged = true
		storeInstall(res)
		return
	}
	target, err := ResolveTarget(selected, first)
	if err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	if pathInside(target.DataRoot, exeRoot) || pathInside(exeRoot, target.DataRoot) {
		res.Err = fmt.Errorf("source와 target이 같은 경로 영역에 있습니다")
		storeInstall(res)
		return
	}
	release, err := acquireTargetMutex(target.DataRoot)
	if err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	defer release()
	plan, err := BuildInstallPlan(exeRoot, target.DataRoot, code)
	if err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	snapA, err := SnapshotPlan(plan)
	if err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	ch := make(chan bool, 1)
	resultMu.Lock()
	confirmReq = &confirmRequest{Text: fmt.Sprintf("다음 설정으로 설치합니다.\n\n시체 빛기둥: %s\n대상 data: %s\n수정/추가 파일: %d개\n\n계속하시겠습니까?", code, target.DataRoot, len(plan.Writes)), Ch: ch}
	resultMu.Unlock()
	post(WM_APP_CONFIRM)
	if !<-ch {
		res.Canceled = true
		res.Text = "설치를 취소했습니다."
		storeInstall(res)
		return
	}
	snapB, err := SnapshotPlan(plan)
	if err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	if !SameSnapshot(snapA, snapB) {
		res.Err = fmt.Errorf("설치 준비 중 target이 외부에서 변경되었습니다. 다시 설치를 시도하세요")
		storeInstall(res)
		return
	}
	if err := checkDiskSpace(exeRoot, target, plan); err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	if err := os.MkdirAll(target.DataRoot, 0755); err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	bdir, _, err := CreateBackup(exeRoot, plan, "BeforeInstall")
	if err != nil {
		res.Err = err
		storeInstall(res)
		return
	}
	res.BackupPath = bdir
	if err := ApplyPlan(plan); err != nil {
		rb := RollbackBackup(bdir, target)
		res.Err = err
		if rb != nil {
			res.Rollback = "롤백 실패: " + rb.Error()
		} else {
			res.Rollback = "자동 롤백 완료"
		}
		writeFailureLog(selected, target, code, "write", err, res.Rollback, bdir)
		if first {
			_ = os.RemoveAll(target.ModRoot)
		}
		storeInstall(res)
		return
	}
	if first {
		mb, err := os.ReadFile(filepath.Join(exeRoot, "modinfo.json"))
		if err == nil {
			err = atomicWrite(filepath.Join(target.MPQRoot, "modinfo.json"), mb)
		}
		if err != nil {
			rb := RollbackBackup(bdir, target)
			_ = os.RemoveAll(target.ModRoot)
			res.Err = fmt.Errorf("modinfo.json 설치 실패: %w", err)
			if rb != nil {
				res.Rollback = "롤백 실패: " + rb.Error()
			} else {
				res.Rollback = "자동 롤백 완료"
			}
			writeFailureLog(selected, target, code, "modinfo", res.Err, res.Rollback, bdir)
			storeInstall(res)
			return
		}
	}
	res.Text = fmt.Sprintf("설치 완료\r\n빛기둥: %s\r\n대상: %s\r\n백업: %s", code, target.DataRoot, bdir)
	storeInstall(res)
}

func storeInstall(r opResult) {
	resultMu.Lock()
	installResult = r
	resultMu.Unlock()
	post(WM_APP_INSTALL_DONE)
}
func finishInstall() {
	resultMu.Lock()
	r := installResult
	resultMu.Unlock()
	if r.SourceChanged {
		sourceValid = false
		validatedSource = nil
	}
	if r.Err != nil {
		setText(hStatus, "설치 실패\r\n"+r.Err.Error()+func() string {
			if r.Rollback != "" {
				return "\r\n" + r.Rollback
			}
			return ""
		}())
	} else {
		setText(hStatus, r.Text)
	}
	setBusy(false)
	if r.Err == nil && !r.Canceled {
		startBackupScan()
	}
}

func handleConfirm() {
	resultMu.Lock()
	req := confirmReq
	resultMu.Unlock()
	if req == nil {
		return
	}
	yes := msgBox(mainHwnd, req.Text, "설치 확인", MB_YESNO|MB_ICONQUESTION) == IDYES
	req.Ch <- yes
}

func startBackupScan() {
	if busy {
		return
	}
	setBusy(true)
	setText(hStatus, "백업 목록을 검사 중입니다...")
	go func() {
		paths, err := ListValidBackups(exeRoot)
		resultMu.Lock()
		backupsResult = backupsScanResult{paths, err}
		resultMu.Unlock()
		post(WM_APP_BACKUPS_DONE)
	}()
}
func finishBackupScan() {
	resultMu.Lock()
	r := backupsResult
	resultMu.Unlock()
	backupPaths = r.Paths
	procSendMessageW.Call(hBackupCombo, CB_RESETCONTENT, 0, 0)
	for _, p := range r.Paths {
		name := filepath.Base(p)
		procSendMessageW.Call(hBackupCombo, CB_ADDSTRING, 0, uintptr(unsafe.Pointer(u16(name))))
	}
	if len(r.Paths) > 0 {
		procSendMessageW.Call(hBackupCombo, CB_SETCURSEL, 0, 0)
	}
	if r.Err != nil {
		setText(hStatus, "백업 검사 실패\r\n"+r.Err.Error())
	} else {
		setText(hStatus, fmt.Sprintf("백업 목록 새로고침 완료: %d개", len(r.Paths)))
	}
	setBusy(false)
}

func startRestoreFromUI() {
	idx, _, _ := procSendMessageW.Call(hBackupCombo, CB_GETCURSEL, 0, 0)
	if int32(idx) < 0 || int(idx) >= len(backupPaths) {
		msgBox(mainHwnd, "복구할 백업을 선택하세요.", AppName, MB_OK|MB_ICONERROR)
		return
	}
	selected := getText(hTarget)
	if strings.TrimSpace(selected) == "" {
		msgBox(mainHwnd, "복구할 현재 모드 폴더를 대상 폴더에 지정하세요.", AppName, MB_OK|MB_ICONERROR)
		return
	}
	if msgBox(mainHwnd, "선택한 백업 상태로 복구합니다.\n\n이 백업 이후 같은 파일에 적용한 다른 모드의 수정도 함께 되돌아갑니다.\n복구 전에 현재 상태는 BeforeRestore로 다시 백업합니다.\n\n계속하시겠습니까?", "백업 복구", MB_YESNO|MB_ICONQUESTION) != IDYES {
		return
	}
	bdir := backupPaths[int(idx)]
	setBusy(true)
	setText(hStatus, "백업을 복구 중입니다...")
	go restoreWorker(selected, bdir)
}
func restoreWorker(selected, bdir string) {
	res := opResult{}
	target, err := ResolveTarget(selected, false)
	if err != nil {
		res.Err = err
		storeRestore(res)
		return
	}
	release, err := acquireTargetMutex(target.DataRoot)
	if err != nil {
		res.Err = err
		storeRestore(res)
		return
	}
	defer release()
	before, err := RestoreBackup(exeRoot, bdir, target)
	if err != nil {
		res.Err = err
		res.BackupPath = before
		storeRestore(res)
		return
	}
	res.Text = "복구 완료\r\n복구 전 현재 상태 백업: " + before
	storeRestore(res)
}
func storeRestore(r opResult) {
	resultMu.Lock()
	restoreResult = r
	resultMu.Unlock()
	post(WM_APP_RESTORE_DONE)
}
func finishRestore() {
	resultMu.Lock()
	r := restoreResult
	resultMu.Unlock()
	if r.Err != nil {
		setText(hStatus, "복구 실패\r\n"+r.Err.Error())
	} else {
		setText(hStatus, r.Text)
	}
	setBusy(false)
	startBackupScan()
}

func pathInside(child, parent string) bool {
	c, _ := filepath.Abs(child)
	p, _ := filepath.Abs(parent)
	c = strings.ToLower(filepath.Clean(c))
	p = strings.ToLower(filepath.Clean(p))
	if c == p {
		return true
	}
	return strings.HasPrefix(c, p+string(filepath.Separator))
}

func acquireTargetMutex(dataRoot string) (func(), error) {
	sum := sha256.Sum256([]byte(strings.ToLower(filepath.Clean(dataRoot))))
	name := "Local\\JimSSng_D2R_DataTarget_" + hex.EncodeToString(sum[:8])
	h, _, _ := procCreateMutexW.Call(0, 0, uintptr(unsafe.Pointer(u16(name))))
	if h == 0 {
		return nil, fmt.Errorf("target mutex 생성 실패")
	}
	r, _, _ := procWaitForSingleObject.Call(h, 0)
	if r != WAIT_OBJECT_0 && r != WAIT_ABANDONED {
		procCloseHandle.Call(h)
		return nil, fmt.Errorf("같은 target을 다른 자동병합기가 사용 중입니다")
	}
	return func() { procReleaseMutex.Call(h); procCloseHandle.Call(h) }, nil
}

func freeBytes(path string) (uint64, error) {
	var avail, total, free uint64
	r, _, e := procGetDiskFreeSpaceExW.Call(uintptr(unsafe.Pointer(u16(path))), uintptr(unsafe.Pointer(&avail)), uintptr(unsafe.Pointer(&total)), uintptr(unsafe.Pointer(&free)))
	if r == 0 {
		return 0, e
	}
	return avail, nil
}
func checkDiskSpace(exeRoot string, target TargetInfo, plan *InstallPlan) error {
	var writeBytes uint64
	for _, w := range plan.Writes {
		writeBytes += uint64(len(w.Data))
		if st, err := os.Stat(w.Dest); err == nil {
			writeBytes += uint64(st.Size())
		}
	}
	needTarget := writeBytes*3 + 16*1024*1024
	needBackup := writeBytes + 16*1024*1024
	ft, err := freeBytes(filepath.VolumeName(target.DataRoot) + "\\")
	if err == nil && ft < needTarget {
		return fmt.Errorf("target 볼륨 여유 공간이 부족합니다")
	}
	fb, err := freeBytes(filepath.VolumeName(exeRoot) + "\\")
	if err == nil && fb < needBackup {
		return fmt.Errorf("backup 볼륨 여유 공간이 부족합니다")
	}
	return nil
}

func writeFailureLog(selected string, target TargetInfo, code, phase string, err error, rollback, bdir string) {
	name := time.Now().Format("20060102_150405.000") + "_INSTALL_FAILED.log"
	text := fmt.Sprintf("Installer: %s %s\r\nMod: %s %s\r\nTime: %s\r\nSource root: %s\r\nSelected target: %s\r\nResolved dataRoot: %s\r\nFirst install: %v\r\nPillar: %s\r\nPhase: %s\r\nError: %v\r\nRollback: %s\r\nBackup folder: %s\r\n", AppName, AppVersion, ModName, ModVersion, time.Now().Format(time.RFC3339), exeRoot, selected, target.DataRoot, target.FirstInstall, code, phase, err, rollback, bdir)
	_ = os.WriteFile(filepath.Join(exeRoot, name), []byte(text), 0644)
}

// IFileOpenDialog / FOS_PICKFOLDERS. No legacy SHBrowseForFolder is used.
func pickFolder(owner uintptr) (string, bool) {
	clsid := GUID{Data1: 0xDC1C5A9C, Data2: 0xE88A, Data3: 0x4DDE, Data4: [8]byte{0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7}}
	iid := GUID{Data1: 0xD57C7288, Data2: 0xD4AD, Data3: 0x4768, Data4: [8]byte{0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60}}
	var obj uintptr
	hr, _, _ := procCoCreateInstance.Call(uintptr(unsafe.Pointer(&clsid)), 0, CLSCTX_INPROC_SERVER, uintptr(unsafe.Pointer(&iid)), uintptr(unsafe.Pointer(&obj)))
	if int32(hr) < 0 || obj == 0 {
		return "", false
	}
	defer comRelease(obj)
	opts := uintptr(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR | FOS_DONTADDTORECENT)
	if h := comCall(obj, 9, opts); int32(h) < 0 {
		return "", false
	}
	comCall(obj, 17, uintptr(unsafe.Pointer(u16("대상 모드 폴더 선택"))))
	if h := comCall(obj, 3, owner); int32(h) < 0 {
		return "", false
	}
	var item uintptr
	if h := comCall(obj, 20, uintptr(unsafe.Pointer(&item))); int32(h) < 0 || item == 0 {
		return "", false
	}
	defer comRelease(item)
	var psz uintptr
	if h := comCall(item, 5, SIGDN_FILESYSPATH, uintptr(unsafe.Pointer(&psz))); int32(h) < 0 || psz == 0 {
		return "", false
	}
	defer procCoTaskMemFree.Call(psz)
	return utf16PtrToString((*uint16)(unsafe.Pointer(psz))), true
}
func comVTable(obj uintptr) *[64]uintptr {
	return (*[64]uintptr)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(obj))))
}
func comCall(obj uintptr, index int, args ...uintptr) uintptr {
	vt := comVTable(obj)
	a := append([]uintptr{obj}, args...)
	r, _, _ := syscall.SyscallN(vt[index], a...)
	return r
}
func comRelease(obj uintptr) {
	if obj != 0 {
		comCall(obj, 2)
	}
}
func utf16PtrToString(p *uint16) string {
	if p == nil {
		return ""
	}
	var a []uint16
	for i := 0; ; i++ {
		v := *(*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + uintptr(i*2)))
		if v == 0 {
			break
		}
		a = append(a, v)
	}
	return syscall.UTF16ToString(a)
}
