
import json
import threading
import time
import http.client
from dataclasses import dataclass
from typing import Optional, Dict, Any, Callable, List, Union, TypedDict, Literal
from enum import IntEnum
import websocket


class ActivityType(IntEnum):
    PLAYING = 0
    STREAMING = 1
    LISTENING = 2
    WATCHING = 3
    CUSTOM = 4
    COMPETING = 5



ActivityTypeName = Literal["PLAYING", "STREAMING", "LISTENING", "WATCHING", "CUSTOM", "COMPETING"]
ActivityTypeLike = Union[ActivityType, int, str, ActivityTypeName]


def normalize_activity_type(value: ActivityTypeLike) -> ActivityType:
    if isinstance(value, ActivityType):
        return value
    if isinstance(value, int):
        try:
            return ActivityType(value)
        except ValueError as exc:
            raise ValueError(f"알 수 없는 활동 타입 값: {value}") from exc
    if isinstance(value, str):
        candidate = value.strip()
        if not candidate:
            raise ValueError("활동 타입 문자열이 비어 있음")
        if candidate.isdigit():
            return normalize_activity_type(int(candidate))
        try:
            return ActivityType[candidate.upper()]
        except KeyError as exc:
            raise ValueError(f"알 수 없는 활동 타입 이름: {value}") from exc
    raise TypeError(f"지원하지 않는 활동 타입 입력: {value!r}")


class DiscordRPCError(Exception):

    pass


class WebSocketNotConnectedError(DiscordRPCError):
   
    pass


class RPCConfig(TypedDict, total=False):
   
    RESTART_INTERVAL_MS: int  
    STATUS_CHECK_INTERVAL_MS: int  
    LOGIN_RETRIES: int  
    LOGIN_RETRY_DELAY_MS: int  

class RPCUser(TypedDict, total=False):
    userID: str
    token: str
    name: str
    type: Union[ActivityTypeName, ActivityType, int]
    state: str
    details: str
    large_image: str
    largete: str
    small_image: str
    smallte: str
    button1: str
    button1link: str
    button2: str
    button2link: str


@dataclass
class RPCSettings:
    restart_interval_ms: int = 10 * 60 * 1000
    status_check_interval_ms: int = 60 * 1000
    login_retries: int = 3
    login_retry_delay_ms: int = 5000
    application_id: str = "817229550684471297"

    @classmethod
    def from_overrides(cls, overrides: Optional[RPCConfig]) -> 'RPCSettings':
        if not overrides:
            return cls()
        instance = cls()
        mapping = {
            "RESTART_INTERVAL_MS": "restart_interval_ms",
            "STATUS_CHECK_INTERVAL_MS": "status_check_interval_ms",
            "LOGIN_RETRIES": "login_retries",
            "LOGIN_RETRY_DELAY_MS": "login_retry_delay_ms",
            "APPLICATION_ID": "application_id",
        }
        for key, value in overrides.items():
            attr = mapping.get(key)
            if attr and value is not None:
                setattr(instance, attr, value)
        return instance


class RichPresenceAssets:
   
    
    CDN_PREFIXES = (
        'https://cdn.discordapp.com/',
        'http://cdn.discordapp.com/',
        'https://media.discordapp.net/',
        'http://media.discordapp.net/',
    )
    
    def __init__(self, activity: 'RichPresence', assets: Optional[Dict[str, Any]] = None):
        self.activity = activity
        self.large_image: Optional[str] = None
        self.large_text: Optional[str] = None
        self.small_image: Optional[str] = None
        self.small_text: Optional[str] = None
        if assets:
            self._patch(assets)
    
    def _patch(self, assets: Dict[str, Any]) -> None:
       
        self.large_image = assets.get('large_image') or assets.get('largeImage')
        self.large_text = assets.get('large_text') or assets.get('largeText')
        self.small_image = assets.get('small_image') or assets.get('smallImage')
        self.small_text = assets.get('small_text') or assets.get('smallText')
    
    @staticmethod
    def parse_image(image: Optional[str]) -> Optional[str]:
       
        if not isinstance(image, str) or not image:
            return None
        
        if image.startswith(('http://', 'https://')):
            for prefix in RichPresenceAssets.CDN_PREFIXES:
                if image.startswith(prefix):
                    return f"mp:{image[len(prefix):]}"
            raise ValueError('INVALID_URL: Discord CDN 주소만 허용')
        
        return image
    
    def to_json(self) -> Optional[Dict[str, str]]:
       
        result = {}
        if self.large_image:
            result['large_image'] = self.parse_image(self.large_image)
        if self.large_text:
            result['large_text'] = self.large_text
        if self.small_image:
            result['small_image'] = self.parse_image(self.small_image)
        if self.small_text:
            result['small_text'] = self.small_text
        return result if result else None
    
    def set_large_image(self, image: str) -> 'RichPresenceAssets':
       
        self.large_image = self.parse_image(image)
        return self
    
    def set_small_image(self, image: str) -> 'RichPresenceAssets':
       
        self.small_image = self.parse_image(image)
        return self
    
    def set_large_text(self, text: str) -> 'RichPresenceAssets':
       
        self.large_text = text
        return self
    
    def set_small_text(self, text: str) -> 'RichPresenceAssets':
       
        self.small_text = text
        return self


class RichPresence:
    
    
    def __init__(self, client: 'Client', data: Optional[Dict[str, Any]] = None):
        if not client:
            raise ValueError("클라이언트가 필요함")
        self.client = client
        self.name: Optional[str] = None
        self.type: ActivityType = ActivityType.PLAYING
        self.application_id: Optional[str] = None
        self.state: Optional[str] = None
        self.details: Optional[str] = None
        self.url: Optional[str] = None
        self.timestamps: Dict[str, int] = {}
        self.buttons: List[str] = []
        self.metadata: Dict[str, List[str]] = {"button_urls": []}
        self.assets = RichPresenceAssets(self)
        
        if data:
            self._patch(data)
    
    def _patch(self, data: Dict[str, Any]) -> None:
        
        if 'name' in data:
            self.name = data['name']
        if 'type' in data:
            self.type = normalize_activity_type(data['type'])
        if 'application_id' in data or 'applicationId' in data:
            self.application_id = data.get('application_id') or data.get('applicationId')
        if 'state' in data:
            self.state = data['state']
        if 'details' in data:
            self.details = data['details']
        if 'url' in data:
            self.url = data['url']
        if 'assets' in data:
            self.assets._patch(data['assets'])
        if 'timestamps' in data:
            self.timestamps = data['timestamps']
        if 'buttons' in data:
            self.buttons = data['buttons']
        if 'metadata' in data:
            self.metadata = data['metadata']
    
    def set_application_id(self, app_id: str) -> 'RichPresence':
        
        self.application_id = app_id
        return self
    
    def set_type(self, activity_type: ActivityTypeLike) -> 'RichPresence':
        
        self.type = normalize_activity_type(activity_type)
        return self
    
    def set_state(self, state: str) -> 'RichPresence':
        
        self.state = state
        return self
    
    def set_name(self, name: str) -> 'RichPresence':
        
        self.name = name
        return self
    
    def set_details(self, details: str) -> 'RichPresence':
        
        self.details = details
        return self
    
    def set_start_timestamp(self, timestamp: Union[int, float]) -> 'RichPresence':
        
        if not self.timestamps:
            self.timestamps = {}
        self.timestamps['start'] = int(timestamp)
        return self
    
    def set_assets_large_image(self, image: str) -> 'RichPresence':
        
        self.assets.set_large_image(image)
        return self
    
    def set_assets_large_text(self, text: str) -> 'RichPresence':
        
        self.assets.set_large_text(text)
        return self
    
    def set_assets_small_image(self, image: str) -> 'RichPresence':
        
        self.assets.set_small_image(image)
        return self
    
    def set_assets_small_text(self, text: str) -> 'RichPresence':
        
        self.assets.set_small_text(text)
        return self
    
    def add_button(self, name: str, url: str) -> 'RichPresence':
            
        if not name or not url:
            raise ValueError('버튼 이름과 링크가 필요함')
        self.buttons.append(name)
        if 'button_urls' not in self.metadata:
            self.metadata['button_urls'] = []
        self.metadata['button_urls'].append(url)
        return self
    
    def to_json(self) -> Dict[str, Any]:
        
        result: Dict[str, Any] = {
            'type': int(self.type)
        }
        
        if self.name:
            result['name'] = self.name
        if self.application_id:
            result['application_id'] = self.application_id
        if self.state:
            result['state'] = self.state
        if self.details:
            result['details'] = self.details
        if self.url:
            result['url'] = self.url
        
        assets_json = self.assets.to_json()
        if assets_json:
            result['assets'] = assets_json
        if self.timestamps:
            result['timestamps'] = self.timestamps
        if self.buttons:
            result['buttons'] = self.buttons
        if self.metadata.get('button_urls'):
            result['metadata'] = {'button_urls': self.metadata['button_urls']}
        
        return result


class Client:
    
    
    API_BASE = "discord.com"
    API_VERSION = "v10"
    USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    GATEWAY_VERSION = "10"
    
    def __init__(self, options: Optional[Dict[str, Any]] = None):
        self.token: Optional[str] = None
        self.user: Optional[Dict[str, Any]] = None
        self.ws: Optional[websocket.WebSocketApp] = None
        self.session_id: Optional[str] = None
        self.ready_at: Optional[float] = None
        self.options = options or {}
        self._event_handlers: Dict[str, List[Callable]] = {}
        self._heartbeat_thread: Optional[threading.Thread] = None
        self._running = False
    
    def on(self, event: str, handler: Callable) -> 'Client':
        
        if event not in self._event_handlers:
            self._event_handlers[event] = []
        self._event_handlers[event].append(handler)
        return self
    
    def _emit(self, event: str, *args: Any) -> None:
        
        handlers = self._event_handlers.get(event, [])
        for handler in handlers:
            try:
                handler(*args)
            except Exception as e:
                print(f"이벤트 처리 중 오류 ({event}): {e}")
    
    def _make_request(self, method: str, path: str, headers: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
        
        conn = http.client.HTTPSConnection(self.API_BASE)
        default_headers = {"User-Agent": self.USER_AGENT}
        if headers:
            default_headers.update(headers)
        
        try:
            conn.request(method, path, headers=default_headers)
            response = conn.getresponse()
            data = response.read().decode()
            
            if response.status == 200:
                return json.loads(data)
            else:
                raise DiscordRPCError(f"HTTP {response.status} 응답 오류: {data}")
        finally:
            conn.close()
    
    def _fetch_user_info(self) -> Dict[str, Any]:
        
        if not self.token:
            raise ValueError("토큰이 필요함")
        
        return self._make_request(
            "GET",
            f"/api/{self.API_VERSION}/users/@me",
            headers={
                "Authorization": self.token,
                "Content-Type": "application/json"
            }
        )
    
    def _get_gateway_url(self) -> str:
        
        data = self._make_request("GET", f"/api/{self.API_VERSION}/gateway")
        return f"{data['url']}?v={self.GATEWAY_VERSION}&encoding=json"
    
    def _connect_websocket(self) -> None:
        
        gateway_url = self._get_gateway_url()
        self._running = True
        
        def on_message(ws: websocket.WebSocketApp, message: str) -> None:
            try:
                packet = json.loads(message)
                self._handle_packet(packet)
            except Exception as e:
                if self._running:
                    self._emit('error', DiscordRPCError(str(e)))
        
        def on_error(ws: websocket.WebSocketApp, error: Exception) -> None:
            if self._running:
                self._emit('error', error)
        
        def on_close(ws: websocket.WebSocketApp, *args: Any) -> None:
            self._running = False
        
        def on_open(ws: websocket.WebSocketApp) -> None:
            self._send_identify()
        
        self.ws = websocket.WebSocketApp(
            gateway_url,
            on_message=on_message,
            on_error=on_error,
            on_close=on_close,
            on_open=on_open
        )
        
        ws_thread = threading.Thread(target=self.ws.run_forever, daemon=True)
        ws_thread.start()
        time.sleep(1)  
    
    def _send_identify(self) -> None:
        
        identify = {
            "op": 2,
            "d": {
                "token": self.token,
                "properties": {
                    "os": "Windows",
                    "browser": "Chrome",
                    "device": ""
                },
                "intents": 0
            }
        }
        self._send_json(identify)
    
    def _send_json(self, data: Dict[str, Any]) -> None:
        
        if not self.ws or not self._running:
            raise WebSocketNotConnectedError("웹소켓이 연결되지 않음")
        self.ws.send(json.dumps(data))
    
    def _handle_packet(self, packet: Dict[str, Any]) -> None:
        
        op = packet.get('op')
        if op == 0:  
            self._handle_dispatch(packet)
        elif op == 10:
            interval = packet.get('d', {}).get('heartbeat_interval', 41250)
            self._start_heartbeat(interval)
        elif op == 11: 
            pass
    
    def _handle_dispatch(self, packet: Dict[str, Any]) -> None:

        event_type = packet.get('t')
        if event_type == 'READY':
            self.session_id = packet.get('d', {}).get('session_id')
            self.ready_at = time.time()
            self._emit('ready')
    
    def _start_heartbeat(self, interval_ms: int) -> None:
        
        if self._heartbeat_thread:
            return
        
        def heartbeat_loop() -> None:
            while self._running:
                time.sleep(interval_ms / 1000.0)
                if self._running and self.ws:
                    try:
                        self._send_json({"op": 1, "d": None})
                    except Exception:
                        break
        
        self._heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True)
        self._heartbeat_thread.start()
    
    def _set_activity(self, activity: RichPresence) -> None:
        
        if not self.ws or not self._running:
            raise WebSocketNotConnectedError("웹소켓이 연결되지 않음")
        
        activity_data = activity.to_json()
        activity_type = normalize_activity_type(activity_data.get('type', ActivityType.PLAYING))
        
        since = activity_data.get('timestamps', {}).get('start') or int(time.time() * 1000)
        
        activity_obj: Dict[str, Any] = {
            "name": activity_data.get('name', '알수없음'),
            "type": activity_type,
        }
        
       
        for key in ['application_id', 'state', 'details', 'url', 'assets', 'timestamps', 'buttons', 'metadata']:
            if key in activity_data and activity_data[key]:
                activity_obj[key] = activity_data[key]
        
        presence = {
            "op": 3,
            "d": {
                "since": since,
                "activities": [activity_obj],
                "status": "online",
                "afk": False
            }
        }
        
        self._send_json(presence)
    
    def login(self, token: str) -> str:
        
        if not token:
            raise ValueError("토큰이 필요함")
        
        self.token = token
        user_info = self._fetch_user_info()
        
        self.user = {
            "id": user_info.get("id"),
            "username": user_info.get("username"),
            "discriminator": user_info.get("discriminator"),
            "avatar": user_info.get("avatar"),
            "bot": user_info.get("bot", False),
            "setActivity": lambda activity: self._set_activity(activity)
        }
        
        self._connect_websocket()
        return self.token
    
    def destroy(self) -> None:
        
        self._running = False
        if self.ws:
            try:
                self.ws.close()
            except Exception:
                pass
            self.ws = None
        self._event_handlers.clear()
        self._heartbeat_thread = None


class RPCManager:
    DEFAULT_APPLICATION_ID = "817229550684471297"
    
    def __init__(self, config: Optional[RPCConfig] = None, users: Optional[List[RPCUser]] = None):
        import signal
        import sys
        
        self.clients: List[Client] = []
        self._restarting = False
        overrides = dict(config) if config else {}
        overrides.setdefault("APPLICATION_ID", self.DEFAULT_APPLICATION_ID)
        self.settings = RPCSettings.from_overrides(overrides)
        self.users = users or []
        self._setup_signal_handlers()
    
    def _setup_signal_handlers(self) -> None:
        
        import signal
        import sys
        
        def handler(sig, frame):
            print('\n종료 신호(SIGINT) 감지, 클라이언트 종료 중...')
            self.shutdown()
            sys.exit(0)
        signal.signal(signal.SIGINT, handler)
    
    @staticmethod
    def _start_background_task(task: Callable[[], None]) -> None:
        threading.Thread(target=task, daemon=True).start()
    
    def get_active_users(self) -> List[Dict[str, Any]]:
        
        return [u for u in self.users if u.get('token')]
    
    def _format_user_tag(self, user_data: Dict[str, Any]) -> str:
        
        name = user_data.get('name', '알수없음')
        user_id = user_data.get('userID', '알수없음')
        return f"{name} ({user_id})"

    def _resolve_user_activity_type(self, user_data: Dict[str, Any]) -> ActivityType:
        raw_type = user_data.get('type', ActivityType.PLAYING)
        try:
            return normalize_activity_type(raw_type)
        except (ValueError, TypeError) as exc:
            print(f"{self._format_user_tag(user_data)} 잘못된 활동 타입 '{raw_type}' - 기본값 PLAYING 사용 ({exc})")
            return ActivityType.PLAYING
    
    def safe_login(
        self,
        client: Client,
        token: str,
        retries: Optional[int] = None,
        user_data: Optional[Dict[str, Any]] = None,
    ) -> bool:
        
        retries = retries or self.settings.login_retries
        user_tag = self._format_user_tag(user_data) if user_data else None
        for attempt in range(1, retries + 1):
            try:
                client.login(token)
                return True
            except Exception as err:
                prefix = f"{user_tag} " if user_tag else ""
                print(f"{prefix}로그인 실패 ({attempt}/{retries}) | {err}")
                if attempt < retries:
                    time.sleep(self.settings.login_retry_delay_ms / 1000.0)
        return False
    
    def _create_presence(self, client: Client, user_data: Dict[str, Any]) -> RichPresence:
        
        r = RichPresence(client)
        r.set_application_id(self.settings.application_id)
        r.set_type(self._resolve_user_activity_type(user_data))
        r.set_state(user_data.get('state', ''))
        r.set_name(user_data.get('name', ''))
        r.set_details(user_data.get('details', ''))
        r.set_start_timestamp(time.time() * 1000)
        
        if user_data.get('large_image'):
            r.set_assets_large_image(user_data['large_image'])
        if user_data.get('largete'):
            r.set_assets_large_text(user_data['largete'])
        if user_data.get('small_image'):
            r.set_assets_small_image(user_data['small_image'])
        if user_data.get('smallte'):
            r.set_assets_small_text(user_data['smallte'])
        
        if user_data.get('button1') and user_data.get('button1link'):
            r.add_button(user_data['button1'], user_data['button1link'])
        if user_data.get('button2') and user_data.get('button2link'):
            r.add_button(user_data['button2'], user_data['button2link'])
        
        return r
    
    def _setup_client_handlers(self, client: Client, user_data: Dict[str, Any]) -> None:
        
        user_id = user_data.get('userID')
        name = user_data.get('name', '알수없음')
        user_tag = self._format_user_tag(user_data)
        
        def on_ready():
            print(f"{user_tag} 준비 완료 - RPC 설정 시도")
            try:
                r = self._create_presence(client, user_data)
                client.user['setActivity'](r)
            except Exception as err:
                print(f"{user_tag} RPC 설정 오류: {err}")
        
        def on_error(error):
            print(f"{user_tag} 클라이언트 오류 발생: {error}")
        
        client.on('ready', on_ready)
        client.on('error', on_error)
    
    def initialize_clients(self) -> None:
        
        users = self.get_active_users()
        if not users:
            print('등록된 사용자가 없습니다.')
            return
        
        for user_data in users:
            try:
                token = user_data.get('token', '').strip()
                if not token:
                    print(f"{self._format_user_tag(user_data)} 토큰이 없습니다. 건너뜁니다.")
                    continue
                
                client = Client()
                self._setup_client_handlers(client, user_data)
                
                if not self.safe_login(client, token, user_data=user_data):
                    print(f"{self._format_user_tag(user_data)} 로그인 완전 실패 (모든 재시도 소진)")
                    continue
                
                self.clients.append(client)
            except Exception as err:
                print(f"{self._format_user_tag(user_data)} 초기화 오류: {err}")
    
    def status_check_loop(self) -> None:
        
        while True:
            time.sleep(self.settings.status_check_interval_ms / 1000.0)
            for client in self.clients:
                if not client.user:
                    continue
                try:
                    username = client.user.get('username', '알수없음')
                    print(f"클라이언트 상태 확인: {username}")
                except Exception as err:
                    print(f"상태 확인 오류: {err}")
    
    def restart_loop(self) -> None:
        
        while True:
            time.sleep(self.settings.restart_interval_ms / 1000.0)
            if self._restarting:
                continue
            
            self._restarting = True
            try:
                print("[재시작] 모든 클라이언트 종료 중...")
                self.shutdown()
                print("[재시작] 재초기화 시작...")
                self.initialize_clients()
                print("[재시작] 재초기화 완료.")
            except Exception as e:
                print(f"[재시작] 오류: {e}")
            finally:
                self._restarting = False
    
    def shutdown(self) -> None:
        
        for client in self.clients:
            try:
                client.destroy()
            except Exception:
                pass
        self.clients.clear()
    
    def run(self) -> None:
                
        self.initialize_clients()
        
        self._start_background_task(self.status_check_loop)
        self._start_background_task(self.restart_loop)
        
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            self.shutdown()