# FILE: _St/Auto8700/st_update_apply.py | ROLE: TEMP 실행용 _St ZIP 안전 적용·Auto8700 재시작
from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
import tempfile
import time
import zipfile
from pathlib import Path, PurePosixPath


def _log(path: Path, message: str) -> None:
    line = time.strftime("%Y-%m-%d %H:%M:%S") + " " + message
    try:
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("a", encoding="utf-8") as handle:
            handle.write(line + "\n")
    except Exception:
        pass


def _pid_alive(pid: int) -> bool:
    if pid <= 0:
        return False
    if sys.platform.startswith("win"):
        try:
            import ctypes
            SYNCHRONIZE = 0x00100000
            handle = ctypes.windll.kernel32.OpenProcess(SYNCHRONIZE, False, int(pid))
            if not handle:
                return False
            try:
                WAIT_TIMEOUT = 0x00000102
                return ctypes.windll.kernel32.WaitForSingleObject(handle, 0) == WAIT_TIMEOUT
            finally:
                ctypes.windll.kernel32.CloseHandle(handle)
        except Exception:
            return False
    try:
        os.kill(pid, 0)
        return True
    except OSError:
        return False


def wait_parent_exit(pid: int, timeout: float = 60.0) -> None:
    end = time.time() + max(1.0, timeout)
    while time.time() < end:
        if not _pid_alive(pid):
            return
        time.sleep(0.2)
    raise RuntimeError(f"Auto8700 종료 대기 시간 초과 pid={pid}")


def validate_members(zf: zipfile.ZipFile) -> list[zipfile.ZipInfo]:
    members: list[zipfile.ZipInfo] = []
    bad = zf.testzip()
    if bad:
        raise RuntimeError(f"ZIP CRC 오류: {bad}")
    for info in zf.infolist():
        name = info.filename.replace("\\", "/")
        p = PurePosixPath(name)
        if p.is_absolute() or ".." in p.parts:
            raise RuntimeError(f"위험 ZIP 경로: {info.filename}")
        if not p.parts or p.parts[0].casefold() != "_st":
            raise RuntimeError(f"base_root 위반: {info.filename}")
        members.append(info)
    return members


def apply_zip(zip_path: Path, st_root: Path, *, staging_parent: Path | None = None, expected_build: int | None = None) -> int:
    parent = staging_parent or Path(tempfile.gettempdir())
    staging = Path(tempfile.mkdtemp(prefix="st_update_apply_", dir=str(parent)))
    try:
        with zipfile.ZipFile(zip_path, "r") as zf:
            members = validate_members(zf)
            zf.extractall(staging, members=members)
        source_root = staging / "_St"
        if not source_root.is_dir():
            raise RuntimeError("압축해제 후 _St 루트가 없습니다.")
        if expected_build is not None:
            staged_build = read_build(source_root)
            if staged_build != int(expected_build):
                raise RuntimeError(f"압축해제 build 불일치 expected={expected_build} zip={staged_build}")
        copied = 0
        backup_root = staging / "__backup__"
        backed_up: list[tuple[Path, Path]] = []
        created_files: list[Path] = []
        try:
            for source in sorted(source_root.rglob("*")):
                rel = source.relative_to(source_root)
                target = st_root / rel
                if source.is_dir():
                    target.mkdir(parents=True, exist_ok=True)
                    continue
                target.parent.mkdir(parents=True, exist_ok=True)
                if target.exists():
                    backup = backup_root / rel
                    backup.parent.mkdir(parents=True, exist_ok=True)
                    shutil.copy2(target, backup)
                    backed_up.append((backup, target))
                else:
                    created_files.append(target)
                shutil.copy2(source, target)
                copied += 1
            if copied <= 0:
                raise RuntimeError("적용된 파일이 없습니다.")
            return copied
        except Exception:
            for target in reversed(created_files):
                try:
                    target.unlink(missing_ok=True)
                except OSError:
                    pass
            for backup, target in reversed(backed_up):
                try:
                    target.parent.mkdir(parents=True, exist_ok=True)
                    shutil.copy2(backup, target)
                except OSError:
                    pass
            raise
    finally:
        shutil.rmtree(staging, ignore_errors=True)


def read_build(st_root: Path) -> int:
    path = st_root / "_St_ver.html"
    if not path.is_file():
        return 0
    import re
    text = path.read_text(encoding="utf-8-sig", errors="replace")
    m = re.search(r'<meta\s+name=["\']st-build["\']\s+content=["\'](\d+)["\']', text, re.I)
    return int(m.group(1)) if m else 0


def restart_manager(manager: Path) -> None:
    if not manager.is_file():
        raise RuntimeError(f"Auto8700 manager 없음: {manager}")
    kwargs: dict = {"cwd": str(manager.parent)}
    if sys.platform.startswith("win"):
        flags = int(getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)) | int(getattr(subprocess, "CREATE_NEW_CONSOLE", 0))
        kwargs["creationflags"] = flags
    subprocess.Popen([sys.executable, str(manager), "--skip-update-check"], **kwargs)


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--parent-pid", type=int, required=True)
    parser.add_argument("--zip", dest="zip_path", required=True)
    parser.add_argument("--st-root", required=True)
    parser.add_argument("--manager", required=True)
    parser.add_argument("--expected-build", type=int, required=True)
    args = parser.parse_args(argv)

    zip_path = Path(args.zip_path).resolve()
    st_root = Path(args.st_root).resolve()
    manager = Path(args.manager).resolve()
    log_path = zip_path.parent / "st_update_apply.log"
    try:
        _log(log_path, f"[자동업데이트][UP-06][체크시작] Auto8700 종료 대기 / pid={args.parent_pid}")
        wait_parent_exit(args.parent_pid)
        _log(log_path, "[자동업데이트][UP-06][체크완료] Auto8700 종료 확인")
        _log(log_path, "[자동업데이트][UP-07][체크시작] _St 코드 적용")
        copied = apply_zip(zip_path, st_root, expected_build=int(args.expected_build))
        actual_build = read_build(st_root)
        if actual_build != int(args.expected_build):
            raise RuntimeError(f"적용 build 불일치 expected={args.expected_build} actual={actual_build}")
        _log(log_path, f"[자동업데이트][UP-07][체크완료] 적용 파일={copied} build={actual_build}")
        _log(log_path, "[자동업데이트][UP-08][체크시작] Auto8700 재실행")
        restart_manager(manager)
        _log(log_path, "[자동업데이트][UP-08][체크완료] Auto8700 재실행 요청 완료")
        return 0
    except Exception as exc:
        _log(log_path, f"[자동업데이트][UP-XX][체크실패] {exc}")
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
