"""
업데이트 관리자

메인 프로그램에서 사용하는 업데이트 관리 모듈입니다.
Supabase에서 버전 정보를 조회하고 업데이트를 준비합니다.

작성자: KH.Choi
최종 수정: 2026-02-18
"""

import json
import logging
import shutil
import subprocess
import sys
import tempfile
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Optional

import requests

from .__version__ import PROGRAM_ID, VERSION

logger = logging.getLogger(__name__)

UPDATER_CONFIG_FILENAME = "config.json"


# ============================================================================
# 데이터 클래스
# ============================================================================

@dataclass
class VersionInfo:
    """
    Supabase에서 조회한 버전 정보
    
    Attributes:
        version: 버전 번호
        is_stable: 안정판 여부
        release_note: 배포 노트
        download_url: 다운로드 URL
        min_required_version: 최소 요구 버전
    """
    version: str
    is_stable: bool
    release_note: Optional[str] = None
    download_url: Optional[str] = None
    min_required_version: Optional[str] = None


@dataclass
class UpdateConfig:
    """
    updater.exe로 전달되는 구성 정보
    
    Attributes:
        download_url: 다운로드 URL (zip 파일)
        target_path: 설치 대상 경로
        version: 업데이트할 버전
        restart_exe: 업데이트 후 실행할 실행파일명
    """
    download_url: str
    target_path: str
    version: str
    restart_exe: str = "voc_noti.exe"


# ============================================================================
# 예외 클래스
# ============================================================================

class UpdateError(Exception):
    """업데이트 관련 기본 예외"""
    pass


class NetworkError(UpdateError):
    """네트워크 연결 실패"""
    pass


class ConfigError(UpdateError):
    """설정 파일 오류"""
    pass


def _parse_ssl_verify(value: Any, default: bool = True) -> bool:
    """ssl_verify 값을 bool로 정규화"""
    if isinstance(value, bool):
        return value
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in {"true", "1", "yes", "on"}:
            return True
        if lowered in {"false", "0", "no", "off"}:
            return False
    if isinstance(value, (int, float)):
        return bool(value)
    return default


def _build_requests_verify_option(ssl_verify: bool, ca_bundle_path: str) -> bool | str:
    """requests.verify 옵션 생성 (bool 또는 CA 번들 경로)"""
    if not ssl_verify:
        return False

    bundle_path = str(ca_bundle_path or "").strip()
    if not bundle_path:
        return True

    bundle = Path(bundle_path).expanduser()
    if bundle.exists():
        return str(bundle)

    logger.warning(f"CA 번들 경로를 찾을 수 없습니다. 기본 인증서 검증 사용: {bundle}")
    return True


def _strip_json_comments(content: str) -> str:
    """JSON 문자열의 주석(//, /* */) 제거"""
    result = []
    i = 0
    in_string = False
    escape_next = False

    while i < len(content):
        char = content[i]

        if escape_next:
            result.append(char)
            escape_next = False
            i += 1
            continue

        if char == "\\" and in_string:
            result.append(char)
            escape_next = True
            i += 1
            continue

        if char == '"':
            in_string = not in_string
            result.append(char)
            i += 1
            continue

        if not in_string:
            if content[i : i + 2] == "//":
                while i < len(content) and content[i] != "\n":
                    i += 1
                continue
            if content[i : i + 2] == "/*":
                i += 2
                while i < len(content) - 1 and content[i : i + 2] != "*/":
                    i += 1
                i += 2
                continue

        result.append(char)
        i += 1

    return "".join(result)


def _select_environment_config(
    environments: dict[str, dict[str, Any]],
    requested_environment: Optional[str],
    default_environment: str,
) -> tuple[str, dict[str, Any]]:
    """환경 설정 선택 (요청 환경 > 기본 환경 > 우선순위)"""
    target = requested_environment or default_environment
    if target in environments and environments[target].get("enabled", True):
        return target, environments[target]

    enabled_items = [(k, v) for k, v in environments.items() if v.get("enabled", True)]
    if not enabled_items:
        return "", {}

    enabled_items.sort(key=lambda item: item[1].get("priority", 999))
    return enabled_items[0]


def load_updater_connection_config(
    config_path: Optional[Path] = None,
    environment: Optional[str] = None,
    timeout: int = 10,
) -> dict[str, Any]:
    """
    업데이터 연결 설정 로드

    우선순위:
    1) 원격 config_url (성공 시)
    2) 로컬 fallback
    """
    path = config_path or (Path(__file__).resolve().parent / UPDATER_CONFIG_FILENAME)
    if not path.exists():
        raise ConfigError(f"업데이터 설정 파일이 없습니다: {path}")

    try:
        local_raw = _strip_json_comments(path.read_text(encoding="utf-8"))
        local_config = json.loads(local_raw)
    except (OSError, json.JSONDecodeError) as e:
        raise ConfigError(f"업데이터 설정 파일 파싱 실패: {e}") from e

    fallback = local_config.get("fallback", {})
    default_env = local_config.get("default_environment", "main")
    config_url = local_config.get("config_url", "").strip()
    local_ssl_verify = _parse_ssl_verify(local_config.get("ssl_verify", True), default=True)
    local_ca_bundle = str(local_config.get("ca_bundle_path", "")).strip()

    # 1) 원격 설정 시도
    if config_url:
        try:
            headers = {
                "Cache-Control": "no-cache, no-store, must-revalidate",
                "Pragma": "no-cache",
                "Expires": "0",
            }
            response = requests.get(
                config_url,
                timeout=timeout,
                headers=headers,
                verify=_build_requests_verify_option(local_ssl_verify, local_ca_bundle),
            )
            response.raise_for_status()
            remote_raw = _strip_json_comments(response.text)
            remote = json.loads(remote_raw)

            envs = remote.get("environments", {})
            if isinstance(envs, dict) and envs:
                env_name, env_config = _select_environment_config(
                    environments=envs,
                    requested_environment=environment,
                    default_environment=remote.get("defaultEnvironment", default_env),
                )
                if env_config:
                    url = env_config.get("supabaseUrl", "").strip()
                    key = env_config.get("anonKey", "").strip()
                    if url and key:
                        env_ssl_verify = _parse_ssl_verify(
                            env_config.get("sslVerify", local_ssl_verify), default=local_ssl_verify
                        )
                        env_ca_bundle = str(env_config.get("caBundlePath", local_ca_bundle)).strip()
                        return {
                            "supabase_url": url,
                            "supabase_key": key,
                            "ssl_verify": env_ssl_verify,
                            "ca_bundle_path": env_ca_bundle,
                            "environment": env_name,
                            "source": "remote",
                        }
        except Exception as e:
            logger.warning(f"원격 연결 설정 로드 실패, fallback 사용: {e}")

    # 2) 로컬 fallback
    url = str(fallback.get("supabase_url", "")).strip()
    key = str(fallback.get("anon_key", "")).strip()
    fallback_ssl_verify = _parse_ssl_verify(
        fallback.get("ssl_verify", local_ssl_verify), default=local_ssl_verify
    )
    fallback_ca_bundle = str(fallback.get("ca_bundle_path", local_ca_bundle)).strip()
    env = environment or default_env

    if not url or not key:
        raise ConfigError("fallback 설정에 supabase_url 또는 anon_key가 없습니다.")

    return {
        "supabase_url": url,
        "supabase_key": key,
        "ssl_verify": fallback_ssl_verify,
        "ca_bundle_path": fallback_ca_bundle,
        "environment": env,
        "source": "fallback",
    }


def _resolve_connection_config_path(raw_path: str) -> Path:
    """연결 설정 파일 경로 해석 (상대경로/패킹 환경 지원)"""
    path = Path(raw_path)
    if path.is_absolute():
        return path

    # 1) 현재 작업 디렉토리 기준
    cwd_candidate = (Path.cwd() / path).resolve()
    if cwd_candidate.exists():
        return cwd_candidate

    # 2) 실행 파일 기준 (패킹 환경)
    if getattr(sys, "frozen", False):
        exe_candidate = (Path(sys.executable).parent / path).resolve()
        if exe_candidate.exists():
            return exe_candidate

    # 3) 모듈 기준 (개발 환경)
    module_candidate = (Path(__file__).resolve().parents[2] / path).resolve()
    return module_candidate


# ============================================================================
# 버전 비교 유틸리티
# ============================================================================

def compare_versions(v1: str, v2: str) -> int:
    """
    버전 비교
    
    Args:
        v1: 첫 번째 버전 (예: "3.2.0")
        v2: 두 번째 버전 (예: "3.1.0")
    
    Returns:
        -1: v1 < v2
         0: v1 == v2
         1: v1 > v2
    """
    try:
        parts1 = [int(x) for x in v1.split('.')]
        parts2 = [int(x) for x in v2.split('.')]
        
        # 길이 맞추기
        max_len = max(len(parts1), len(parts2))
        parts1.extend([0] * (max_len - len(parts1)))
        parts2.extend([0] * (max_len - len(parts2)))
        
        for p1, p2 in zip(parts1, parts2):
            if p1 < p2:
                return -1
            elif p1 > p2:
                return 1
        return 0
    except (ValueError, AttributeError):
        logger.warning(f"버전 비교 실패: {v1} vs {v2}")
        return 0


# ============================================================================
# UpdateManager 클래스
# ============================================================================

class UpdateManager:
    """
    자동 업데이트 관리자
    
    Supabase에서 버전 정보를 조회하고 업데이트를 처리합니다.
    
    Attributes:
        program_id: 프로그램 식별자
        current_version: 현재 버전
        supabase_url: Supabase 프로젝트 URL
        supabase_key: Supabase API 키
        check_interval: 업데이트 체크 간격 (시간)
    
    사용 예시:
        updater = UpdateManager(
            supabase_url="https://xxx.supabase.co",
            supabase_key="xxx"
        )
        
        # 업데이트 확인
        version_info = updater.check_for_updates()
        if version_info:
            # 업데이트 준비 및 실행
            updater.prepare_update(version_info)
            updater.launch_updater()
    """
    
    CONFIG_FILENAME = "voc_updater_config.json"
    UPDATER_EXE_NAME = "updater.exe"
    
    def __init__(
        self,
        supabase_url: str,
        supabase_key: str,
        program_id: str = PROGRAM_ID,
        current_version: str = VERSION,
        check_interval: int = 1,
        version_table: str = "program_version",
        ssl_verify: bool = True,
        ca_bundle_path: str = "",
    ):
        """
        UpdateManager 초기화
        
        Args:
            supabase_url: Supabase 프로젝트 URL
            supabase_key: Supabase API 키 (anon key)
            program_id: 프로그램 식별자
            current_version: 현재 버전
            check_interval: 업데이트 체크 간격 (시간)
        """
        self.program_id = program_id
        self.current_version = current_version
        self.supabase_url = supabase_url.rstrip('/')
        self.supabase_key = supabase_key
        self.check_interval = check_interval
        self.version_table = version_table
        self.ssl_verify = ssl_verify
        self.ca_bundle_path = ca_bundle_path
        
        self._latest_version: Optional[VersionInfo] = None
        self._stop_flag = threading.Event()
    
    @property
    def config_path(self) -> Path:
        """업데이트 설정 파일 경로"""
        return Path(tempfile.gettempdir()) / self.CONFIG_FILENAME
    
    @property
    def temp_updater_path(self) -> Path:
        """임시 updater.exe 경로"""
        return Path(tempfile.gettempdir()) / self.UPDATER_EXE_NAME

    @property
    def install_dir(self) -> Path:
        """메인 프로그램 설치 경로"""
        if getattr(sys, "frozen", False):
            return Path(sys.executable).parent
        return Path(__file__).resolve().parents[2]

    @staticmethod
    def _get_creation_flags() -> int:
        """운영체제별 프로세스 생성 플래그"""
        if sys.platform.startswith("win"):
            return subprocess.CREATE_NEW_PROCESS_GROUP
        return 0
    
    def check_for_updates(self) -> Optional[VersionInfo]:
        """
        Supabase에서 최신 버전 확인
        
        Returns:
            VersionInfo: 업데이트 가능한 경우 버전 정보
            None: 업데이트 없음
        """
        try:
            if not self.supabase_url:
                raise ConfigError("Supabase URL이 비어있습니다.")
            if not self.supabase_key:
                raise ConfigError("Supabase API 키가 비어있습니다.")

            # Supabase REST API 호출
            headers = {
                "apikey": self.supabase_key,
                "Authorization": f"Bearer {self.supabase_key}",
                "Content-Type": "application/json"
            }
            params = {
                "select": "*",
                "program_id": f"eq.{self.program_id}",
                "is_stable": "eq.true",
                "order": "created_at.desc",
                "limit": "1"
            }
            
            logger.info("업데이트 확인 중...")

            table_candidates = [self.version_table]
            if self.version_table == "program_version":
                table_candidates.append("program_versions")

            response = None
            verify_option = _build_requests_verify_option(self.ssl_verify, self.ca_bundle_path)
            for table_name in table_candidates:
                url = f"{self.supabase_url}/rest/v1/{table_name}"
                response = requests.get(
                    url,
                    headers=headers,
                    params=params,
                    timeout=10,
                    verify=verify_option,
                )
                if response.status_code == 404 and table_name != table_candidates[-1]:
                    logger.warning(f"버전 테이블 미존재: {table_name}, 다음 후보 시도")
                    continue
                response.raise_for_status()
                if table_name != self.version_table:
                    logger.info(f"버전 테이블 자동 전환: {self.version_table} -> {table_name}")
                    self.version_table = table_name
                break

            if response is None:
                raise NetworkError("버전 조회 응답이 없습니다.")
            
            data = response.json()
            if not data:
                logger.info("등록된 버전 정보가 없습니다.")
                return None
            
            latest = data[0]
            version_info = VersionInfo(
                version=latest.get("version", ""),
                is_stable=latest.get("is_stable", True),
                release_note=latest.get("release_note"),
                download_url=latest.get("download_url"),
                min_required_version=latest.get("min_required_version")
            )
            
            self._latest_version = version_info
            
            # 버전 비교
            if compare_versions(self.current_version, version_info.version) < 0:
                logger.info(f"새 버전 발견: {version_info.version}")
                return version_info
            else:
                logger.info(f"최신 버전 사용 중: {self.current_version}")
                return None
                
        except requests.exceptions.Timeout:
            logger.warning("업데이트 서버 연결 시간 초과")
            raise NetworkError("서버 연결 시간 초과")
        except requests.exceptions.SSLError as e:
            logger.warning(f"업데이트 SSL 검증 실패: {e}")
            raise NetworkError(
                "SSL 인증서 검증에 실패했습니다. 내부망 인증서 사용 시 update.ssl_verify 또는 update.ca_bundle_path 설정을 확인하세요."
            )
        except requests.exceptions.RequestException as e:
            logger.warning(f"업데이트 확인 실패: {e}")
            raise NetworkError(f"네트워크 오류: {e}")
        except ConfigError:
            raise
        except (KeyError, json.JSONDecodeError) as e:
            logger.error(f"버전 정보 파싱 오류: {e}")
            return None
    
    def is_update_available(self) -> bool:
        """
        업데이트 필요 여부 확인 (캐시된 버전 정보 사용)
        
        Returns:
            bool: 업데이트 가능 여부
        """
        if self._latest_version is None:
            return False
        return compare_versions(self.current_version, self._latest_version.version) < 0
    
    def prepare_update(self, version_info: VersionInfo) -> tuple[bool, str]:
        """
        업데이트 준비
        
        config.json을 생성하고 updater.exe를 임시 폴더로 복사합니다.
        
        Args:
            version_info: 업데이트할 버전 정보
        
        Returns:
            tuple[bool, str]: (성공 여부, 메시지)
        """
        try:
            # 다운로드 URL 확인
            if not version_info.download_url:
                return False, "다운로드 URL이 없습니다."

            install_dir = self.install_dir
            restart_exe = Path(sys.executable).name if getattr(sys, "frozen", False) else "voc_noti.exe"
            
            # config.json 생성
            config = UpdateConfig(
                download_url=version_info.download_url,
                target_path=str(install_dir),
                version=version_info.version,
                restart_exe=restart_exe
            )

            temp_config_path = self.config_path.with_suffix(".tmp")
            with open(temp_config_path, 'w', encoding='utf-8') as f:
                json.dump(
                    {
                        "download_url": config.download_url,
                        "target_path": config.target_path,
                        "version": config.version,
                        "restart_exe": config.restart_exe,
                    },
                    f,
                    ensure_ascii=False,
                    indent=2,
                )
            temp_config_path.replace(self.config_path)
            
            logger.info(f"업데이트 설정 저장: {self.config_path}")

            # updater.exe 복사
            updater_src = install_dir / self.UPDATER_EXE_NAME
            if not updater_src.exists():
                return False, f"updater.exe를 찾을 수 없습니다: {updater_src}"

            shutil.copy2(updater_src, self.temp_updater_path)
            logger.info(f"updater.exe 복사 완료: {self.temp_updater_path}")
            
            return True, "업데이트 준비 완료"
            
        except PermissionError as e:
            logger.error(f"파일 권한 오류: {e}")
            return False, f"파일 권한 오류: {e}"
        except Exception as e:
            logger.error(f"업데이트 준비 실패: {e}")
            return False, str(e)
    
    def launch_updater(self) -> bool:
        """
        updater.exe 실행
        
        Returns:
            bool: 실행 성공 여부
        """
        try:
            if not self.temp_updater_path.exists():
                logger.error(f"updater.exe가 없습니다: {self.temp_updater_path}")
                return False
            
            # updater.exe 실행
            subprocess.Popen(
                [str(self.temp_updater_path)],
                cwd=str(self.temp_updater_path.parent),
                creationflags=self._get_creation_flags(),
            )
            
            logger.info("updater.exe 실행 완료")
            return True
            
        except Exception as e:
            logger.error(f"updater.exe 실행 실패: {e}")
            return False
    
    def start_background_check(
        self,
        on_update_available: Optional[Callable[[VersionInfo], None]] = None,
        on_error: Optional[Callable[[Exception], None]] = None
    ) -> None:
        """
        백그라운드 업데이트 체크 시작
        
        Args:
            on_update_available: 업데이트 발견 시 콜백
            on_error: 에러 발생 시 콜백
        """
        def _check_loop():
            while not self._stop_flag.is_set():
                try:
                    version_info = self.check_for_updates()
                    if version_info and on_update_available:
                        on_update_available(version_info)
                except Exception as e:
                    logger.warning(f"백그라운드 업데이트 체크 오류: {e}")
                    if on_error:
                        on_error(e)
                
                # 다음 체크까지 대기
                self._stop_flag.wait(self.check_interval * 3600)
        
        thread = threading.Thread(target=_check_loop, daemon=True)
        thread.start()
        logger.info(f"백그라운드 업데이트 체크 시작 (주기: {self.check_interval}시간)")
    
    def stop_background_check(self) -> None:
        """백그라운드 업데이트 체크 중지"""
        self._stop_flag.set()
        logger.info("백그라운드 업데이트 체크 중지")
    
    def cleanup(self) -> None:
        """임시 파일 정리"""
        try:
            if self.config_path.exists():
                self.config_path.unlink()
            if self.temp_updater_path.exists():
                self.temp_updater_path.unlink()
            logger.info("업데이트 임시 파일 정리 완료")
        except Exception as e:
            logger.warning(f"임시 파일 정리 실패: {e}")


# ============================================================================
# 편의 함수
# ============================================================================

def create_update_manager_from_settings(settings: dict) -> UpdateManager:
    """
    settings.json에서 UpdateManager 생성
    
    Args:
        settings: settings.json에서 로드한 딕셔너리
    
    Returns:
        UpdateManager 인스턴스
    """
    update_settings = settings.get("update", {})

    # settings.json에서 직접 지정 시 우선 사용, 없으면 updater/config.json 사용
    supabase_url = str(update_settings.get("supabase_url", "")).strip()
    supabase_key = str(update_settings.get("supabase_key", "")).strip()
    ssl_verify_raw = update_settings.get("ssl_verify", None)
    ca_bundle_path = str(update_settings.get("ca_bundle_path", "")).strip()

    if not supabase_url or not supabase_key:
        config_path_raw = update_settings.get("connection_config_path", "")
        env_name = update_settings.get("environment")
        config_path = _resolve_connection_config_path(config_path_raw) if config_path_raw else None
        conn = load_updater_connection_config(config_path=config_path, environment=env_name)
        supabase_url = conn["supabase_url"]
        supabase_key = conn["supabase_key"]
        if ssl_verify_raw is None:
            ssl_verify_raw = conn.get("ssl_verify", True)
        if not ca_bundle_path:
            ca_bundle_path = str(conn.get("ca_bundle_path", "")).strip()
        logger.info(
            f"업데이터 연결 설정 로드: source={conn.get('source')} / environment={conn.get('environment')}"
        )

    ssl_verify = _parse_ssl_verify(ssl_verify_raw, default=True)

    return UpdateManager(
        supabase_url=supabase_url,
        supabase_key=supabase_key,
        program_id=update_settings.get("program_id", PROGRAM_ID),
        current_version=update_settings.get("current_version", VERSION),
        check_interval=update_settings.get("check_interval_hours", 1),
        version_table=update_settings.get("version_table", "program_version"),
        ssl_verify=ssl_verify,
        ca_bundle_path=ca_bundle_path,
    )
