# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.0 - Task Error Handler                                            ║
║  친절한 에러 메시지 출력 모듈                                                ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from __future__ import annotations
from typing import List, Dict, Any, Optional, Tuple
import inspect
import difflib


# ANSI Colors
class _C:
    G = "\033[92m"   # Green (border)
    R = "\033[31m"   # Red
    Y = "\033[33m"   # Yellow
    C = "\033[36m"   # Cyan
    W = "\033[37m"   # White
    U = "\033[4m"    # Underline
    X = "\033[0m"    # Reset


def _get_caller_location(skip: int = 2) -> Tuple[str, int]:
    """호출 위치 찾기"""
    frame = inspect.currentframe()
    try:
        for _ in range(skip + 1):
            if frame.f_back:
                frame = frame.f_back
        return frame.f_code.co_filename, frame.f_lineno
    except (AttributeError, ValueError):
        return "unknown", 0
    finally:
        del frame


def _find_similar(name: str, candidates: List[str]) -> Optional[str]:
    """Did you mean? 제안"""
    matches = difflib.get_close_matches(name, candidates, n=1, cutoff=0.6)
    return matches[0] if matches else None


def _print_banner(title: str, lines: List, width: int = 72, show_loc: bool = True, skip: int = 3):
    """에러 배너 출력"""
    B, X = _C.G, _C.X
    print()
    print(f"{B}┌{'─' * width}┐{X}")
    print(f"{B}│{X}{_C.Y}  {title}{X}{' ' * (width - len(title) - 2)}{B}│{X}")
    print(f"{B}│{X}{' ' * width}{B}│{X}")

    for line in lines:
        text, color = (line, _C.W) if isinstance(line, str) else line
        if not text:
            print(f"{B}│{X}{' ' * width}{B}│{X}")
            continue
        text = text if len(text) <= width - 2 else text[:width - 5] + "..."
        print(f"{B}│{X}  {color}{text}{X}{' ' * (width - len(text) - 2)}{B}│{X}")

    if show_loc:
        fn, ln = _get_caller_location(skip)
        print(f"{B}│{X}{' ' * width}{B}│{X}")
        link = f"{fn}:{ln}"
        print(f"{B}│{X}  → {_C.C}{_C.U}{link}{X}{' ' * (width - len(link) - 6)}{B}│{X}")

    print(f"{B}└{'─' * width}┘{X}")
    print()


# ─────────────────────────────────────────────────────────────────────────────
# Error Handlers
# ─────────────────────────────────────────────────────────────────────────────

def task_not_found_error(task_id: str, tasks: Dict[str, Any], method: str = "get_client"):
    """Task 미발견 에러"""
    lines = [(f"Task '{task_id}' not found", _C.R), ""]
    if tasks:
        lines.append(("Available tasks:", _C.W))
        for tid, ti in tasks.items():
            lines.append((f"  - {tid} ({getattr(ti, 'task_class_name', '?')}, {getattr(ti, 'mode', '?')})", _C.W))
        sim = _find_similar(task_id, list(tasks.keys()))
        if sim:
            lines += ["", (f"Did you mean '{sim}'?", _C.C)]
    else:
        lines.append(("No tasks registered.", _C.W))
    _print_banner(f"Alaska Error - {method}()", lines, skip=4)


def smblock_not_found_error(name: str, available: List[str]):
    """SmBlock 미발견 에러"""
    lines = [(f"SmBlock '{name}' not found", _C.R), ""]
    if available:
        lines.append(("Available SmBlocks:", _C.W))
        for n in available:
            lines.append((f"  - {n}", _C.W))
        sim = _find_similar(name, available)
        if sim:
            lines += ["", (f"Did you mean '{sim}'?", _C.C)]
    else:
        lines += [
            ("No SmBlocks configured. Add to config.json:", _C.W),
            ('  "platform_config": { "_smblock": { "name": {...} } }', _C.W)
        ]
    _print_banner("Alaska Error - get_smblock()", lines, skip=4)


def rmi_timeout_error(caller: str, target: str, method: str, timeout: float):
    """RMI 타임아웃 에러"""
    lines = [
        (f"RMI timeout after {timeout:.1f}s", _C.R), "",
        (f"  {caller} → {target}.{method}()", _C.W), "",
        ("Causes: blocked task, crash, high load, short timeout", _C.C),
        ("Fix: check run(), increase timeout, use .nowait()", _C.W)
    ]
    _print_banner("Alaska RMI Timeout", lines, skip=4)


def pickle_error(obj_type: str, task_id: str):
    """Pickle 에러"""
    lines = [
        (f"Cannot pickle '{obj_type}'", _C.R), "",
        ("Qt objects cannot be serialized for process mode", _C.W), "",
        ("Fix: use @task (auto thread), mode='thread', or send data only", _C.C)
    ]
    _print_banner(f"Alaska Pickle Error - {task_id}", lines, skip=4)


def config_mandatory_field_error(field: str, filepath: str = None):
    """Config 필수 필드 누락 에러"""
    FIELD_HINTS = {
        "app_info.name": ("Application name", '"app_info": { "name": "MyApp" }'),
        "app_info.version": ("Version string", '"app_info": { "version": "1.0.0" }'),
        "app_info.id": ("Application ID", '"app_info": { "id": "my-app" }'),
        "task_config": ("Task definitions", '"task_config": { "task_name": {...} }'),
    }

    hint = FIELD_HINTS.get(field, ("Required field", f'"{field}": <value>'))
    lines = [
        (f"Missing mandatory field: '{field}'", _C.R), "",
        (f"  Description: {hint[0]}", _C.W),
        (f"  Example: {hint[1]}", _C.C), "",
    ]

    if filepath:
        lines.append((f"  File: {filepath}", _C.W))

    lines += [
        "",
        ("Required fields:", _C.Y),
        ("  - app_info.name     (application name)", _C.W),
        ("  - app_info.version  (version string)", _C.W),
        ("  - app_info.id       (application id)", _C.W),
        ("  - task_config       (task definitions)", _C.W),
    ]
    _print_banner("Alaska Config Error", lines, skip=4)


# ─────────────────────────────────────────────────────────────────────────────
# Exception Classes
# ─────────────────────────────────────────────────────────────────────────────

class AlaskaError(Exception):
    """Alaska 기본 예외"""
    pass


class TaskNotFoundError(AlaskaError):
    """Task 미발견"""
    def __init__(self, task_id: str, available_tasks: Dict[str, Any] = None):
        self.task_id = task_id
        self.available_tasks = available_tasks or {}
        super().__init__(f"Task '{task_id}' not found")


class SmBlockNotFoundError(AlaskaError):
    """SmBlock 미발견"""
    def __init__(self, name: str, available: List[str] = None):
        self.name = name
        self.available_smblocks = available or []
        super().__init__(f"SmBlock '{name}' not found")


class RmiTimeoutError(AlaskaError):
    """RMI 타임아웃"""
    def __init__(self, caller: str, target: str, method: str, timeout: float, reason: str = "timeout"):
        self.caller, self.target, self.method, self.timeout = caller, target, method, timeout
        self.reason = reason
        super().__init__(f"RMI {reason}: {caller} → {target}.{method}() after {timeout}s")


class TaskRemovedError(AlaskaError):
    """삭제된 Task에 대한 RMI 호출"""
    def __init__(self, task_id: str):
        self.task_id = task_id
        super().__init__(f"Task '{task_id}' has been removed")


class InjectionError(AlaskaError):
    """의존성 주입 실패"""
    def __init__(self, task_id: str, field: str, message: str):
        self.task_id, self.field = task_id, field
        super().__init__(f"Injection failed: {task_id}.{field}: {message}")


class InvokeDeadlockError(AlaskaError):
    """Self-RMI 데드락 감지 (InvokeDispenser 단일 스레드 보호)"""
    def __init__(self, caller: str, target: str):
        self.caller, self.target = caller, target
        super().__init__(f"Self-RMI deadlock: {caller} → {target}")


class CircularRmiError(AlaskaError):
    """순환 RMI 감지 (chain set 기반)"""
    def __init__(self, caller: str, target: str, chain: set):
        self.caller, self.target, self.chain = caller, target, chain
        super().__init__(f"Circular RMI: {caller} → {target}, chain={chain}")
