"""
GeoMedical App Updater
별도 프로세스로 실행되어 app.exe 업데이트를 처리합니다.
"""

import os
import sys
import time
import shutil
import psutil
import argparse
import subprocess
from pathlib import Path

def kill_all_app_processes(exe_path):
    """app.exe와 관련된 모든 프로세스 종료"""
    exe_name = os.path.basename(exe_path)
    killed_processes = []
    
    print(f"[UPDATER] Searching for all {exe_name} processes...")
    
    for proc in psutil.process_iter(['pid', 'name', 'exe']):
        try:
            if proc.info['name'] and proc.info['name'].lower() == exe_name.lower():
                print(f"[UPDATER] Found process: PID {proc.info['pid']}, Name: {proc.info['name']}")
                
                # 프로세스 종료 시도
                try:
                    process = psutil.Process(proc.info['pid'])
                    process.terminate()
                    killed_processes.append(proc.info['pid'])
                    print(f"[UPDATER] Terminated process {proc.info['pid']}")
                except Exception as e:
                    print(f"[UPDATER] Could not terminate process {proc.info['pid']}: {e}")
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            continue
    
    # 종료 확인 및 강제 종료
    if killed_processes:
        print(f"[UPDATER] Waiting for {len(killed_processes)} processes to exit...")
        time.sleep(3)
        
        # 여전히 실행 중인 프로세스 강제 종료
        for pid in killed_processes:
            try:
                process = psutil.Process(pid)
                if process.is_running():
                    print(f"[UPDATER] Force killing process {pid}...")
                    process.kill()
            except psutil.NoSuchProcess:
                continue
            except Exception as e:
                print(f"[UPDATER] Error force killing process {pid}: {e}")
        
        # 추가 대기
        time.sleep(2)
    
    return len(killed_processes)

def wait_for_process_exit(pid, timeout=30, force_kill=True):
    """특정 PID의 프로세스가 종료될 때까지 대기"""
    print(f"[UPDATER] Waiting for process {pid} to exit...")
    start_time = time.time()
    
    while time.time() - start_time < timeout:
        try:
            proc = psutil.Process(pid)
            if not proc.is_running():
                break
            time.sleep(0.5)
        except psutil.NoSuchProcess:
            break
    else:
        # 타임아웃 - 강제 종료 시도
        if force_kill:
            print(f"[UPDATER] Warning: Process {pid} did not exit within {timeout} seconds")
            print(f"[UPDATER] Attempting to force kill process {pid}...")
            try:
                proc = psutil.Process(pid)
                proc.terminate()  # 먼저 정상 종료 시도
                time.sleep(2)
                
                if proc.is_running():
                    proc.kill()  # 강제 종료
                    time.sleep(1)
                    
                if not proc.is_running():
                    print(f"[UPDATER] Process {pid} force killed successfully")
                    return True
                else:
                    print(f"[UPDATER] Error: Could not kill process {pid}")
                    return False
            except psutil.NoSuchProcess:
                print(f"[UPDATER] Process {pid} has exited")
                return True
            except Exception as e:
                print(f"[UPDATER] Error killing process: {e}")
                return False
        else:
            print(f"[UPDATER] Error: Process {pid} did not exit within {timeout} seconds")
            return False
    
    print(f"[UPDATER] Process {pid} has exited")
    return True

def backup_current_exe(exe_path):
    """현재 실행 파일 백업"""
    backup_path = f"{exe_path}.backup"
    
    # 기존 백업 파일 삭제
    if os.path.exists(backup_path):
        try:
            os.remove(backup_path)
            print(f"[UPDATER] Removed old backup: {backup_path}")
        except Exception as e:
            print(f"[UPDATER] Warning: Could not remove old backup: {e}")
    
    # 현재 파일 백업
    try:
        shutil.copy2(exe_path, backup_path)
        print(f"[UPDATER] Created backup: {backup_path}")
        return backup_path
    except Exception as e:
        print(f"[UPDATER] Error creating backup: {e}")
        return None

def update_exe(old_exe_path, new_exe_path):
    """exe 파일 교체"""
    print(f"[UPDATER] Replacing {old_exe_path} with {new_exe_path}")
    
    # 새 파일 존재 확인
    if not os.path.exists(new_exe_path):
        print(f"[UPDATER] Error: New exe not found: {new_exe_path}")
        return False
    
    # 새 파일 크기 확인
    new_size = os.path.getsize(new_exe_path)
    if new_size < 1024 * 1024:  # 1MB 미만
        print(f"[UPDATER] Error: New exe too small ({new_size:,} bytes, minimum: 1,048,576 bytes)")
        return False
    
    print(f"[UPDATER] New exe size verified: {new_size:,} bytes ({new_size // (1024*1024)} MB)")
    
    # 새 파일 무결성 검증 (PE 헤더 확인)
    try:
        with open(new_exe_path, 'rb') as f:
            header = f.read(64)
            if len(header) < 64 or not header.startswith(b'MZ'):
                print(f"[UPDATER] Error: New exe file does not have valid PE header")
                return False
        print(f"[UPDATER] New exe PE header verified")
    except Exception as e:
        print(f"[UPDATER] Error reading new exe header: {e}")
        return False
    
    # Windows Handle Killer 사용 (만약 있다면)
    try:
        subprocess.run(['handle', '-p', os.path.basename(old_exe_path), '-c'], 
                      capture_output=True, timeout=5)
    except:
        pass
    
    # 기존 파일 삭제 시도 (더 적극적인 방법)
    max_attempts = 15
    for attempt in range(max_attempts):
        try:
            if os.path.exists(old_exe_path):
                # 파일 속성 변경 시도
                try:
                    os.chmod(old_exe_path, 0o777)
                except:
                    pass
                
                # 삭제 시도
                os.remove(old_exe_path)
                print(f"[UPDATER] Removed old exe: {old_exe_path}")
                break
        except PermissionError as e:
            print(f"[UPDATER] Attempt {attempt + 1}/{max_attempts}: File still in use - {e}")
            
            # 5번째 시도마다 프로세스 다시 확인
            if attempt % 5 == 4:
                print("[UPDATER] Re-checking for remaining processes...")
                kill_all_app_processes(old_exe_path)
            
            time.sleep(2)  # 대기 시간 증가
        except Exception as e:
            print(f"[UPDATER] Unexpected error removing file: {e}")
            time.sleep(1)
    else:
        print(f"[UPDATER] Error: Could not remove old exe after {max_attempts} attempts")
        print("[UPDATER] This may be due to file handles not being released")
        
        # 마지막 시도: 파일명 변경 후 새 파일 복사
        try:
            temp_name = f"{old_exe_path}.old"
            print(f"[UPDATER] Attempting to rename old file to {temp_name}")
            os.rename(old_exe_path, temp_name)
        except Exception as e:
            print(f"[UPDATER] Could not rename old file: {e}")
            return False
    
    # 새 파일로 교체 (안전한 방법 사용)
    try:
        print(f"[UPDATER] Starting safe file replacement...")
        
        # 1. 먼저 복사 시도 (더 안전함)
        temp_target = f"{old_exe_path}.tmp"
        shutil.copy2(new_exe_path, temp_target)
        print(f"[UPDATER] Copied new file to temporary location")
        
        # 2. 복사된 파일 검증
        copied_size = os.path.getsize(temp_target)
        if copied_size != new_size:
            print(f"[UPDATER] Error: Size mismatch after copy ({copied_size} != {new_size})")
            os.remove(temp_target)
            return False
        
        # 3. PE 헤더 재검증
        with open(temp_target, 'rb') as f:
            header = f.read(64)
            if not header.startswith(b'MZ'):
                print(f"[UPDATER] Error: Copied file has invalid PE header")
                os.remove(temp_target)
                return False
        
        print(f"[UPDATER] Temporary file verified: {copied_size:,} bytes")
        
        # 4. 원자적 교체 (rename)
        if os.path.exists(old_exe_path):
            os.replace(temp_target, old_exe_path)  # 원자적 교체
        else:
            os.rename(temp_target, old_exe_path)
            
        print(f"[UPDATER] Successfully replaced {os.path.basename(old_exe_path)} with new version")
        
        # 5. 최종 검증
        replaced_size = os.path.getsize(old_exe_path)
        print(f"[UPDATER] Final verification: {replaced_size:,} bytes")
        
        # 6. 원본 새 파일 삭제
        if os.path.exists(new_exe_path):
            os.remove(new_exe_path)
            print(f"[UPDATER] Cleaned up original new file")
        
        # 7. 권한 설정
        try:
            os.chmod(old_exe_path, 0o755)
        except:
            pass
        
        return True
    except Exception as e:
        print(f"[UPDATER] Error during file replacement: {e}")
        import traceback
        traceback.print_exc()
        
        # 임시 파일 정리
        temp_target = f"{old_exe_path}.tmp"
        if os.path.exists(temp_target):
            try:
                os.remove(temp_target)
            except:
                pass
        
        return False

def restore_backup(exe_path, backup_path):
    """백업에서 복구"""
    if not backup_path or not os.path.exists(backup_path):
        print(f"[UPDATER] No backup available for restore")
        return False
    
    try:
        if os.path.exists(exe_path):
            os.remove(exe_path)
        shutil.move(backup_path, exe_path)
        print(f"[UPDATER] Restored from backup: {backup_path}")
        return True
    except Exception as e:
        print(f"[UPDATER] Error restoring backup: {e}")
        return False

def launch_exe(exe_path, original_args=None):
    """새 exe 실행"""
    if not os.path.exists(exe_path):
        print(f"[UPDATER] Error: Exe not found for launch: {exe_path}")
        return False
    
    try:
        # 파일 크기 및 무결성 재확인
        file_size = os.path.getsize(exe_path)
        print(f"[UPDATER] Exe file size: {file_size:,} bytes")
        
        # PE 헤더 재확인
        with open(exe_path, 'rb') as f:
            header = f.read(64)
            if not header.startswith(b'MZ'):
                print(f"[UPDATER] Error: Exe file has invalid PE header")
                return False
        print(f"[UPDATER] Exe PE header verified")
        
        # 원래 작업 디렉토리로 이동
        original_dir = os.path.dirname(exe_path)
        
        # 실행 명령 구성
        cmd = [exe_path]
        if original_args:
            cmd.extend(original_args)
        
        print(f"[UPDATER] Launching: {' '.join(cmd)}")
        print(f"[UPDATER] Working directory: {original_dir}")
        
        # Windows에서 새 프로세스로 실행 (완전히 분리)
        import platform
        if platform.system() == "Windows":
            try:
                # 방법 1: start 명령을 통한 실행 시도 (DETACHED_PROCESS 사용)
                # 부모 프로세스와 완전히 분리된 프로세스 생성
                start_cmd = ['cmd', '/c', 'start', '/B', 'GeoMedical Helper', '/D', original_dir, exe_path]
                print(f"[UPDATER] Trying start command: {' '.join(start_cmd)}")
                
                result = subprocess.run(start_cmd, 
                                      cwd=original_dir, 
                                      capture_output=True, 
                                      text=True, 
                                      timeout=10)
                
                if result.returncode == 0:
                    print(f"[UPDATER] Start command executed successfully")
                else:
                    print(f"[UPDATER] Start command failed (code {result.returncode})")
                    if result.stderr:
                        print(f"[UPDATER] Start stderr: {result.stderr}")
                    if result.stdout:
                        print(f"[UPDATER] Start stdout: {result.stdout}")
                    raise Exception("Start command failed")
                    
            except Exception as e1:
                print(f"[UPDATER] Start command failed: {e1}")
                print(f"[UPDATER] Trying DETACHED_PROCESS method...")
                
                # 방법 2: DETACHED_PROCESS로 완전히 분리된 프로세스 생성
                try:
                    # CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
                    creation_flags = 0x00000200 | 0x00000008  # CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS
                    process = subprocess.Popen([exe_path], 
                                             cwd=original_dir,
                                             creationflags=creation_flags,
                                             close_fds=True,
                                             stdin=subprocess.DEVNULL,
                                             stdout=subprocess.DEVNULL,
                                             stderr=subprocess.DEVNULL)
                    print(f"[UPDATER] Detached process launched with PID: {process.pid}")
                    
                except Exception as e2:
                    print(f"[UPDATER] Detached process failed: {e2}")
                    print(f"[UPDATER] Trying simple detached execution...")
                    
                    # 방법 3: 가장 간단한 분리 방법
                    try:
                        process = subprocess.Popen([exe_path], 
                                                 cwd=original_dir,
                                                 creationflags=0x00000008,  # DETACHED_PROCESS
                                                 close_fds=True)
                        print(f"[UPDATER] Simple detached process launched with PID: {process.pid}")
                    except Exception as e3:
                        print(f"[UPDATER] All launch methods failed: {e3}")
                        raise e3
                
        else:
            # Linux/Unix - 완전히 분리된 프로세스
            subprocess.Popen(cmd, cwd=original_dir, 
                           start_new_session=True, 
                           close_fds=True,
                           stdin=subprocess.DEVNULL,
                           stdout=subprocess.DEVNULL,
                           stderr=subprocess.DEVNULL)
        
        print(f"[UPDATER] Process launch initiated")
        
        # 짧은 대기 (분리된 프로세스이므로 긴 대기 불필요)
        print(f"[UPDATER] Waiting 2 seconds for process stabilization...")
        time.sleep(2)
        
        # 프로세스가 실제로 실행되었는지 확인
        try:
            import psutil
            exe_name = os.path.basename(exe_path).lower()
            running_processes = []
            
            for proc in psutil.process_iter(['pid', 'name']):
                try:
                    if proc.info['name'] and proc.info['name'].lower() == exe_name:
                        running_processes.append(proc.info['pid'])
                except (psutil.NoSuchProcess, psutil.AccessDenied):
                    continue
            
            if running_processes:
                print(f"[UPDATER] ✓ New process(es) detected: PIDs {running_processes}")
                print(f"[UPDATER] ✓ Application launched successfully - updater will now exit")
                return True
            else:
                print(f"[UPDATER] ⚠ No new {exe_name} process found (may still be starting)")
                print(f"[UPDATER] ⚠ Assuming launch successful - updater will exit")
                return True  # 분리된 프로세스이므로 감지되지 않을 수 있음
                
        except ImportError:
            print(f"[UPDATER] psutil not available, assuming launch successful")
            print(f"[UPDATER] ✓ Process launched - updater will now exit")
            return True
            
    except Exception as e:
        print(f"[UPDATER] Error launching exe: {e}")
        import traceback
        traceback.print_exc()
        return False

def main():
    print("=" * 60)
    print("GeoMedical App Updater v1.0")
    print("=" * 60)
    
    parser = argparse.ArgumentParser(description='Update GeoMedical App')
    parser.add_argument('--pid', type=int, required=True, help='PID of app.exe to replace')
    parser.add_argument('--old-exe', required=True, help='Path to current app.exe')
    parser.add_argument('--new-exe', required=True, help='Path to new app.exe')
    parser.add_argument('--auto-launch', action='store_true', help='Auto launch after update')
    parser.add_argument('--args', nargs='*', help='Arguments to pass to new exe')
    
    args = parser.parse_args()
    
    # 경로 정규화
    old_exe_path = os.path.abspath(args.old_exe)
    new_exe_path = os.path.abspath(args.new_exe)
    
    print(f"[UPDATER] Update request:")
    print(f"  - Process PID: {args.pid}")
    print(f"  - Current exe: {old_exe_path}")
    print(f"  - New exe: {new_exe_path}")
    print(f"  - Auto launch: {args.auto_launch}")
    print(f"[UPDATER] Process: {os.path.basename(new_exe_path)} → {os.path.basename(old_exe_path)}")
    
    # 1. 특정 PID 프로세스 종료 대기
    if not wait_for_process_exit(args.pid, timeout=10):  # 짧은 타임아웃
        print(f"[UPDATER] Target process {args.pid} did not exit normally")
    
    # 2. 모든 app.exe 프로세스 강제 종료
    killed_count = kill_all_app_processes(old_exe_path)
    print(f"[UPDATER] Killed {killed_count} app.exe processes")
    
    # 3. 파일 핸들 해제를 위한 추가 대기
    print("[UPDATER] Waiting for file handles to be released...")
    time.sleep(5)
    
    # 2. 백업 생성
    backup_path = backup_current_exe(old_exe_path)
    
    # 3. 업데이트 수행
    if update_exe(old_exe_path, new_exe_path):
        print("[UPDATER] Update successful!")
        
        # 3.1. 업데이트 검증
        if not os.path.exists(old_exe_path):
            print(f"[UPDATER] ERROR: Updated exe file missing: {old_exe_path}")
            if backup_path and os.path.exists(backup_path):
                print("[UPDATER] Attempting to restore from backup...")
                restore_backup(old_exe_path, backup_path)
            sys.exit(1)
        
        final_size = os.path.getsize(old_exe_path)
        print(f"[UPDATER] Final verification: {final_size:,} bytes")
        
        # 3.2. 실행 가능성 테스트 (간단한 파일 확인만)
        print(f"[UPDATER] Verifying executable...")
        try:
            # 파일 크기 재확인
            if os.path.getsize(old_exe_path) > 1024 * 1024:
                print(f"[UPDATER] ✓ Executable file verified")
            else:
                print(f"[UPDATER] ⚠ Executable file may be too small")
        except Exception as e:
            print(f"[UPDATER] ⚠ Executable verification failed: {e}")
            # 검증 실패해도 진행
        
        # 4. 백업 파일 정리
        if backup_path and os.path.exists(backup_path):
            try:
                os.remove(backup_path)
                print(f"[UPDATER] Removed backup file")
            except:
                pass
        
        # 5. 새 exe 실행 (배치 파일 방식으로 완전 분리)
        if args.auto_launch:
            print("[UPDATER] Preparing to launch updated application...")
            print("[UPDATER] Creating detached launch script...")
            
            # 실행 전 마지막 파일 확인
            if os.path.exists(old_exe_path):
                final_size = os.path.getsize(old_exe_path)
                print(f"[UPDATER] Pre-launch file check: {final_size:,} bytes")
                
                # 배치 파일을 통한 완전 분리 실행
                try:
                    exe_dir = os.path.dirname(old_exe_path)
                    exe_name = os.path.basename(old_exe_path)
                    
                    print(f"[UPDATER] Creating batch file for: {exe_name} in {exe_dir}")
                    
                    # 완전히 분리된 실행을 위한 배치 스크립트 (라인별로 생성)
                    batch_lines = [
                        "@echo off",
                        "title GeoMedical Helper Launcher",
                        "echo [LAUNCHER] Starting GeoMedical Helper...",
                        f'echo [LAUNCHER] Working directory: {exe_dir}',
                        f'echo [LAUNCHER] Executable: {exe_name}',
                        "",
                        "REM Change to application directory",
                        f'cd /d "{exe_dir}"',
                        "",
                        "REM Wait for updater to exit",
                        "timeout /t 2 /nobreak >nul",
                        "",
                        "REM Launch application in separate process group",
                        "echo [LAUNCHER] Launching application...",
                        f'start "GeoMedical Helper" /D "{exe_dir}" "{exe_name}"',
                        "",
                        "REM Verify launch",
                        "timeout /t 2 /nobreak >nul",
                        f'tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I /N "{exe_name}">NUL',
                        "if %ERRORLEVEL%==0 (",
                        "    echo [LAUNCHER] Application started successfully",
                        ") else (",
                        "    echo [LAUNCHER] Could not verify application start",
                        ")",
                        "",
                        "echo [LAUNCHER] Launcher exiting...",
                        "REM Auto-delete this batch file",
                        'del "%~f0" >nul 2>&1'
                    ]
                    
                    batch_content = "\n".join(batch_lines)
                    
                    # Windows 임시 디렉토리 사용 (권한 문제 회피)
                    import tempfile
                    temp_dir = tempfile.gettempdir()
                    batch_path = os.path.join(temp_dir, "geomedical_launch.bat")
                    
                    print(f"[UPDATER] Batch content length: {len(batch_content)} characters")
                    print(f"[UPDATER] Writing to temp dir: {batch_path}")
                    
                    # 파일 생성 및 내용 확인
                    try:
                        with open(batch_path, 'w', encoding='cp949', newline='\r\n') as f:
                            f.write(batch_content)
                            f.flush()  # 강제로 디스크에 쓰기
                            os.fsync(f.fileno())  # OS 레벨에서 디스크 동기화
                    except Exception as write_e:
                        print(f"[UPDATER] Write error: {write_e}")
                        raise write_e
                    
                    # 파일 생성 확인
                    if os.path.exists(batch_path):
                        file_size = os.path.getsize(batch_path)
                        print(f"[UPDATER] ✓ Batch file created: {file_size} bytes")
                        
                        # 내용 확인 (처음 몇 줄)
                        try:
                            with open(batch_path, 'r', encoding='cp949') as f:
                                first_line = f.readline().strip()
                                print(f"[UPDATER] First line: '{first_line}'")
                        except Exception as read_e:
                            print(f"[UPDATER] Could not read back batch file: {read_e}")
                    else:
                        print(f"[UPDATER] ✗ Batch file not created!")
                        raise Exception("Batch file creation failed")
                    
                    # 배치 파일을 새 프로세스 그룹에서 실행
                    print(f"[UPDATER] Launching batch file...")
                    print(f"[UPDATER] Command: cmd /c {batch_path}")
                    
                    # 잠시 대기 후 실행 (파일 시스템 안정화)
                    time.sleep(1)
                    
                    subprocess.Popen(['cmd', '/c', batch_path], 
                                   cwd=exe_dir,
                                   creationflags=0x00000200 | 0x00000010,  # CREATE_NEW_PROCESS_GROUP | CREATE_NEW_CONSOLE
                                   close_fds=True)
                    
                    print("[UPDATER] ✓ Detached launch script initiated")
                    print("[UPDATER] ✓ Update process completed successfully")
                    
                except Exception as e:
                    print(f"[UPDATER] Launch script creation failed: {e}")
                    print(f"[UPDATER] Trying simple fallback method...")
                    
                    # 간단한 fallback: 직접 start 명령 사용
                    try:
                        simple_batch = f"""@echo off
echo Starting GeoMedical Helper...
cd /d "{exe_dir}"
start "" "{exe_name}"
"""
                        fallback_path = os.path.join(exe_dir, "simple_start.bat")
                        with open(fallback_path, 'w', encoding='cp949') as f:
                            f.write(simple_batch)
                        
                        print(f"[UPDATER] Created simple batch: {fallback_path}")
                        subprocess.Popen(['cmd', '/c', fallback_path], cwd=exe_dir)
                        print("[UPDATER] Simple fallback initiated")
                        
                    except Exception as e2:
                        print(f"[UPDATER] Even simple fallback failed: {e2}")
                        print(f"[UPDATER] Please manually start: {old_exe_path}")
            else:
                print(f"[UPDATER] ✗ Cannot launch - exe file missing: {old_exe_path}")
        
        print("[UPDATER] Updater process exiting in 2 seconds...")
        time.sleep(2)  # 배치 파일이 실행될 시간 확보
        print("[UPDATER] Goodbye!")
        sys.exit(0)
    else:
        print("[UPDATER] Update failed!")
        
        # 백업에서 복구 시도
        if backup_path:
            if restore_backup(old_exe_path, backup_path):
                print("[UPDATER] Restored from backup")
                
                # 복구된 버전 실행
                if args.auto_launch:
                    launch_exe(old_exe_path, args.args)
            else:
                print("[UPDATER] Error: Could not restore from backup")
        
        sys.exit(1)

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"[UPDATER] Fatal error: {e}")
        import traceback
        traceback.print_exc()
        input("Press Enter to exit...")
        sys.exit(1)