"""
업데이트 클라이언트 — Gitea/GitHub Releases 호환.

자체 호스팅 Gitea 인스턴스 사용. Gitea API가 GitHub과 호환되어 endpoint URL만
바꾸면 동일 로직 동작.

흐름:
1. `check_for_update()`: Releases API → 최신 태그 조회 → 현재 버전과 비교
2. `download_update(asset_url)`: 새 .exe를 임시 폴더에 다운로드
3. `apply_update(new_exe)`: updater.exe 실행 + 메인 앱 종료

환경변수로 URL 오버라이드 가능 (테스트/마이그레이션 용도):
- CLOCKOUT_RELEASES_API: 전체 API endpoint URL
- CLOCKOUT_ASSET_NAME: 다운로드할 자산 파일명 (기본 main.exe)
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import urllib.request
import urllib.error
from dataclasses import dataclass
from pathlib import Path
from typing import Optional

# 자체 호스팅 Gitea 인스턴스 (GitHub 호환 API)
GIT_HOST = 'https://kindnick-git.duckdns.org'
GIT_OWNER = 'kindnick'
GIT_REPO = 'Clock_out_Time_Calculator'

# Gitea API endpoint: /api/v1/repos/{owner}/{repo}/releases/latest
# (GitHub의 경우: https://api.github.com/repos/{owner}/{repo}/releases/latest)
DEFAULT_RELEASES_API = f'{GIT_HOST}/api/v1/repos/{GIT_OWNER}/{GIT_REPO}/releases/latest'
RELEASES_API = os.environ.get('CLOCKOUT_RELEASES_API', DEFAULT_RELEASES_API)

# 다운로드할 .exe 자산 이름 (Releases 첨부 파일명과 일치해야 함)
ASSET_NAME = os.environ.get('CLOCKOUT_ASSET_NAME', 'main.exe')

USER_AGENT = 'ClockOutCalculator-Updater/1.0'


@dataclass
class ReleaseInfo:
    version: str  # 'v2.1.0' or '2.1.0'
    asset_url: str  # main.exe 다운로드 URL
    notes: str  # 릴리스 노트 (markdown)
    published_at: str

    @property
    def version_clean(self) -> str:
        """'v2.1.0' → '2.1.0'"""
        return self.version.lstrip('vV')


def _parse_version(s: str) -> tuple:
    """semver-ish 파싱. 'v2.1.0' / '2.1' → (2,1,0). 비교용."""
    s = s.lstrip('vV').strip()
    parts = s.split('.')
    out = []
    for p in parts:
        # 'rc1' 등 접미사 제거
        digits = ''
        for c in p:
            if c.isdigit():
                digits += c
            else:
                break
        out.append(int(digits) if digits else 0)
    while len(out) < 3:
        out.append(0)
    return tuple(out[:3])


def is_newer(remote: str, local: str) -> bool:
    """remote가 local보다 최신이면 True."""
    return _parse_version(remote) > _parse_version(local)


# 업데이트 체크 결과 상수 (info=None 일 때 reason 식별)
UP_TO_DATE = 'up_to_date'
NETWORK_ERROR = 'network_error'
NO_RELEASE = 'no_release'      # latest 응답 자체가 없음 (404 등)
NO_ASSET = 'no_asset'          # release는 있으나 main.exe 자산 없음


def check_for_update(current_version: str, timeout: int = 5):
    """Releases API 조회.

    Returns:
        (ReleaseInfo, None) — 새 버전 있음
        (None, UP_TO_DATE) — 이미 최신
        (None, NETWORK_ERROR) — 네트워크/타임아웃 실패
        (None, NO_RELEASE) — 저장소 비공개/리소스 없음 (404)
        (None, NO_ASSET) — 새 버전이지만 main.exe 자산 미첨부
    """
    try:
        req = urllib.request.Request(RELEASES_API, headers={'User-Agent': USER_AGENT})
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            data = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return None, NO_RELEASE
        return None, NETWORK_ERROR
    except (urllib.error.URLError, json.JSONDecodeError, TimeoutError, OSError):
        return None, NETWORK_ERROR

    tag = data.get('tag_name', '')
    if not tag:
        return None, NO_RELEASE
    if not is_newer(tag, current_version):
        return None, UP_TO_DATE

    asset_url = None
    for asset in data.get('assets', []):
        if asset.get('name') == ASSET_NAME:
            asset_url = asset.get('browser_download_url')
            break
    if not asset_url:
        return None, NO_ASSET

    info = ReleaseInfo(
        version=tag,
        asset_url=asset_url,
        notes=data.get('body', ''),
        published_at=data.get('published_at', ''),
    )
    return info, None


def download_update(asset_url: str, dest_dir: Optional[Path] = None,
                     progress_cb=None) -> Optional[Path]:
    """새 .exe를 임시 폴더에 다운로드.

    Args:
        asset_url: GitHub Releases asset URL
        dest_dir: 저장 위치 (기본: 메인 .exe 옆에 main_new.exe)
        progress_cb: callable(downloaded_bytes, total_bytes) — 진행률 콜백

    Returns:
        다운로드된 파일 경로, 실패 시 None.
    """
    if dest_dir is None:
        dest_dir = _exe_dir()
    dest_dir = Path(dest_dir)
    dest_dir.mkdir(parents=True, exist_ok=True)
    dest = dest_dir / 'main_new.exe'
    if dest.exists():
        try:
            dest.unlink()
        except OSError:
            return None

    try:
        req = urllib.request.Request(asset_url, headers={'User-Agent': USER_AGENT})
        with urllib.request.urlopen(req, timeout=30) as resp:
            total = int(resp.headers.get('Content-Length', 0))
            downloaded = 0
            chunk_size = 64 * 1024
            with open(dest, 'wb') as f:
                while True:
                    chunk = resp.read(chunk_size)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)
                    if progress_cb:
                        progress_cb(downloaded, total)
        return dest
    except (urllib.error.URLError, OSError, TimeoutError):
        if dest.exists():
            try:
                dest.unlink()
            except OSError:
                pass
        return None


def apply_update(new_exe: Path) -> bool:
    """updater.exe를 실행하여 파일 교체 + 재시작 트리거.

    호출 직후 메인 앱은 종료되어야 함 (Qt: QApplication.quit()).
    """
    target_exe = _current_exe()
    updater_exe = _find_updater()
    if not updater_exe or not target_exe:
        return False

    pid = os.getpid()
    try:
        # CREATE_NO_WINDOW + DETACHED_PROCESS — updater.exe도 windowed 빌드라
        # 정상적으로는 콘솔이 안 뜨지만, 안전하게 두 플래그 모두 적용해서
        # 어떤 환경에서도 cmd 창 깜빡임이 보이지 않도록.
        if sys.platform == 'win32':
            DETACHED_PROCESS = 0x00000008
            CREATE_NO_WINDOW = 0x08000000
            creationflags = DETACHED_PROCESS | CREATE_NO_WINDOW
        else:
            creationflags = 0
        subprocess.Popen(
            [str(updater_exe),
             '--pid', str(pid),
             '--new', str(new_exe),
             '--target', str(target_exe)],
            creationflags=creationflags,
            close_fds=True,
        )
        return True
    except OSError:
        return False


def _exe_dir() -> Path:
    """현재 .exe가 있는 폴더 (PyInstaller frozen) 또는 main.py 위치."""
    if getattr(sys, 'frozen', False):
        return Path(sys.executable).parent
    return Path(__file__).resolve().parent.parent


def _current_exe() -> Optional[Path]:
    """현재 실행 중인 메인 .exe. 개발 환경(.py)에선 None."""
    if getattr(sys, 'frozen', False):
        return Path(sys.executable)
    return None


def _find_updater() -> Optional[Path]:
    """updater.exe 찾기.

    1) 메인 .exe와 같은 폴더 (정상 배포 상태)
    2) PyInstaller _MEIPASS — main.exe에 내장된 fallback
       (main.py 시작 시 _ensure_updater_extracted()가 #1로 복사하므로 평소엔 미사용)
    """
    candidate = _exe_dir() / 'updater.exe'
    if candidate.exists():
        return candidate

    # _MEIPASS fallback — main.py 시작 추출이 권한 부족 등으로 실패한 경우
    if getattr(sys, 'frozen', False):
        meipass = getattr(sys, '_MEIPASS', None)
        if meipass:
            bundled = Path(meipass) / 'updater.exe'
            if bundled.exists():
                # 사용자 TEMP 폴더에 복사 (메인 종료 후에도 살아남도록)
                import tempfile, shutil
                tmp = Path(tempfile.gettempdir()) / 'clockout_updater.exe'
                try:
                    shutil.copy2(str(bundled), str(tmp))
                    return tmp
                except OSError:
                    pass

    return None
