# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""Task 클래스 검증기 — task_decorator.py에서 분리

TaskValidator: @task 데코레이터로 등록된 Task 클래스의 시그니처 검증.
task_decorator.py의 rmi_task, _is_qobject_subclass 등을 참조.
"""

from __future__ import annotations


class TaskValidator:
    """Task 클래스 검증기"""

    REQUIRED_ATTRS = ['_tc_name', '_tc_mode', '_tc_restart', '_tc_delay']
    ATTR_DISPLAY = {'_tc_name': 'name', '_tc_mode': 'mode', '_tc_restart': 'restart', '_tc_delay': 'restart_delay'}

    def __init__(self):
        self._tasks = []
        self._invalid_tasks = []
        self._missing_attrs = []
        self._invalid_modes = []
        self._missing_run = []
        self._missing_init = []  # QObject 상속 시 __init__ 필수
        self._invalid_init = []
        self._invalid_run_sig = []
        self._invalid_restart = []  # run() 없이 restart 설정

    def add(self, task_id: str, task_class_name: str):
        self._tasks.append((task_id, task_class_name))

    def validate(self) -> bool:
        import inspect
        from .task_decorator import (
            rmi_task, _is_qobject_subclass, _VALID_MODES, _DEFAULT_RESTART_DELAY
        )

        self._invalid_tasks.clear()
        self._missing_attrs.clear()
        self._invalid_modes.clear()
        self._missing_run.clear()
        self._missing_init.clear()
        self._invalid_init.clear()
        self._invalid_run_sig.clear()
        self._invalid_restart.clear()

        for tid, tcn in self._tasks:
            cls = rmi_task.get(tcn)
            if cls is None:
                self._invalid_tasks.append((tid, tcn))
                continue

            missing = [self.ATTR_DISPLAY[a] for a in self.REQUIRED_ATTRS if not hasattr(cls, a)]
            if missing:
                self._missing_attrs.append((tid, tcn, missing))

            mode = getattr(cls, '_tc_mode', None)
            if mode and mode not in _VALID_MODES:
                self._invalid_modes.append((tid, tcn, mode))

            # __init__ 검사: QObject 상속 시 필수, 일반 클래스는 생략 허용
            is_qobject = _is_qobject_subclass(cls)
            has_run = self._find_run_method(cls)[0] is not None

            # run() 검사: QWidget은 UI 위젯이므로 run() 불필요
            if not has_run and not is_qobject:
                self._missing_run.append((tid, tcn))

            # restart 검사: run() 없이 restart/restart_delay 설정 시 오류
            if not has_run:
                has_restart = getattr(cls, '_tc_restart', False)
                restart_delay = getattr(cls, '_tc_delay', _DEFAULT_RESTART_DELAY)
                has_custom_delay = restart_delay != _DEFAULT_RESTART_DELAY
                if has_restart or has_custom_delay:
                    src, line = self._get_source_location(cls)
                    self._invalid_restart.append((tid, tcn, has_restart, restart_delay, src, line))
            has_init = '__init__' in cls.__dict__
            if is_qobject and not has_init:
                # QObject는 super().__init__() 호출 필요
                src, line = self._get_source_location(cls)
                self._missing_init.append((tid, tcn, src, line))
            elif has_init:
                sig = inspect.signature(cls.__init__)
                params = list(sig.parameters.values())
                # 첫 번째 파라미터는 self여야 함
                # 나머지 파라미터는 기본값이 있어야 함 (self만으로 호출 가능)
                valid = True
                if not params or params[0].name != 'self':
                    valid = False
                else:
                    for p in params[1:]:
                        if p.default is inspect.Parameter.empty and p.kind not in (
                            inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
                            valid = False
                            break
                if not valid:
                    param_names = [p.name for p in params]
                    src, line = self._get_source_location(cls, cls.__init__)
                    self._invalid_init.append((tid, tcn, param_names, src, line))

            run_method, run_name = self._find_run_method(cls)
            if run_method:
                params = list(inspect.signature(run_method).parameters.keys())
                if len(params) != 1 or params[0] != 'self':
                    src, line = self._get_source_location(cls, run_method)
                    self._invalid_run_sig.append((tid, tcn, run_name, params, src, line))

        return not (self._invalid_tasks or self._missing_attrs or self._invalid_modes or
                   self._missing_run or self._missing_init or self._invalid_init or
                   self._invalid_run_sig or self._invalid_restart)

    def _find_run_method(self, cls: type) -> tuple:
        for m in dir(cls):
            if m.startswith('__'): continue
            method = getattr(cls, m, None)
            if callable(method) and (getattr(method, '_rmi_run', False) or getattr(method, '_rmi_loop', False)):
                return (method, m)
        if hasattr(cls, 'run') and callable(getattr(cls, 'run', None)):
            return (getattr(cls, 'run'), 'run')
        if hasattr(cls, 'rmi_loop') and callable(getattr(cls, 'rmi_loop', None)):
            return (getattr(cls, 'rmi_loop'), 'rmi_loop')
        return (None, None)

    def _get_source_location(self, cls: type, method=None) -> tuple:
        import inspect, os
        src, line = None, 0
        try: src = inspect.getfile(cls)
        except Exception: pass
        if src is None:
            try: src = inspect.getsourcefile(cls)
            except Exception: pass
        if src: src = os.path.abspath(src)
        try:
            fn = getattr(method, '__func__', method) if method else cls
            _, line = inspect.getsourcelines(fn)
        except Exception: pass
        return (src, line)

    def print_errors(self):
        import sys
        from .task_decorator import _DEFAULT_RESTART_DELAY

        # ANSI Colors
        GREEN = "\033[92m"  # Bright green for borders
        RED = "\033[31m"
        YELLOW = "\033[33m"
        CYAN = "\033[36m"
        WHITE = "\033[37m"
        BOLD = "\033[1m"
        UNDERLINE = "\033[4m"
        RESET = "\033[0m"

        # Ensure UTF-8 output on Windows
        if sys.platform == 'win32':
            try:
                sys.stdout.reconfigure(encoding='utf-8')
            except Exception:
                pass

        W = 78  # Box width
        B = GREEN  # Border color
        print()
        print(f"{B}╔{'═' * W}╗{RESET}")
        print(f"{B}║{RESET}  {BOLD}{RED}[Validator] ERROR: @task validation failed!{RESET}{' ' * (W - 48)}{B}║{RESET}")
        print(f"{B}╠{'═' * W}╣{RESET}")

        def print_section(title: str, items: list):
            print(f"{B}║{RESET}  {YELLOW}{title}{RESET}{' ' * (W - len(title) - 2)}{B}║{RESET}")
            for item in items:
                # Truncate if too long
                text = item if len(item) <= W - 4 else item[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")

        def print_link(src: str, line: int):
            if src:
                link = f"{src}:{line}"
                # VSCode clickable link format
                display = f"    → {CYAN}{UNDERLINE}{link}{RESET}"
                # Calculate visible length (without ANSI codes)
                visible_len = 6 + len(link)  # "    → " + link
                padding = W - visible_len
                print(f"{B}║{RESET}{display}{' ' * padding}{B}║{RESET}")

        if self._invalid_tasks:
            items = [f"Task '{tid}': @task '{tcn}' not found" for tid, tcn in self._invalid_tasks]
            print_section("Unregistered @task:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._missing_attrs:
            items = [f"Task '{tid}': @task '{tcn}' missing: {', '.join(attrs)}"
                     for tid, tcn, attrs in self._missing_attrs]
            print_section("Missing @task attributes:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_modes:
            items = [f"Task '{tid}': @task '{tcn}' mode='{mode}' (must be thread/process)"
                     for tid, tcn, mode in self._invalid_modes]
            print_section("Invalid mode value:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._missing_run:
            items = [f"Task '{tid}': @task '{tcn}' must define run(self)"
                     for tid, tcn in self._missing_run]
            print_section("Missing run() method:", items)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._missing_init:
            print(f"{B}║{RESET}  {YELLOW}Missing __init__ (QObject requires super().__init__):{RESET}{' ' * (W - 54)}{B}║{RESET}")
            for tid, tcn, src, line in self._missing_init:
                text = f"Task '{tid}': @task '{tcn}' (QObject) must define __init__(self)"
                text = text if len(text) <= W - 4 else text[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")
                print_link(src, line)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_init:
            print(f"{B}║{RESET}  {YELLOW}Invalid __init__ signature:{RESET}{' ' * (W - 28)}{B}║{RESET}")
            for tid, tcn, params, src, line in self._invalid_init:
                text = f"Task '{tid}': @task '{tcn}' __init__ must be (self), got ({', '.join(params)})"
                text = text if len(text) <= W - 4 else text[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")
                print_link(src, line)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_run_sig:
            print(f"{B}║{RESET}  {YELLOW}Invalid @rmi_run method signature:{RESET}{' ' * (W - 36)}{B}║{RESET}")
            for tid, tcn, name, params, src, line in self._invalid_run_sig:
                text = f"Task '{tid}': @task '{tcn}' {name}() must be (self), got ({', '.join(params)})"
                text = text if len(text) <= W - 4 else text[:W - 7] + "..."
                padding = W - len(text) - 4
                print(f"{B}║{RESET}    {WHITE}{text}{RESET}{' ' * padding}{B}║{RESET}")
                print_link(src, line)
            print(f"{B}╟{'─' * W}╢{RESET}")

        if self._invalid_restart:
            title = "restart/restart_delay requires run() method"
            print(f"{B}║{RESET}  {title}{' ' * (W - len(title) - 2)}{B}║{RESET}")
            print(f"{B}║{RESET}{' ' * W}{B}║{RESET}")
            for tid, tcn, has_restart, delay, src, line in self._invalid_restart:
                opts = []
                if has_restart:
                    opts.append("restart=True")
                if delay != _DEFAULT_RESTART_DELAY:
                    opts.append(f"restart_delay={delay}")
                text1 = f"  Task '{tid}' ({tcn})"
                print(f"{B}║{RESET}{text1}{' ' * (W - len(text1))}{B}║{RESET}")
                text2 = f"    → {', '.join(opts)} set, but no run() defined"
                print(f"{B}║{RESET}{text2}{' ' * (W - len(text2))}{B}║{RESET}")
                text3 = f"    → restart only works with run() loop tasks"
                print(f"{B}║{RESET}{text3}{' ' * (W - len(text3))}{B}║{RESET}")
                if src:
                    link = f"{src}:{line}"
                    text4 = f"    → "
                    padding = W - len(text4) - len(link)
                    print(f"{B}║{RESET}{text4}{CYAN}{UNDERLINE}{link}{RESET}{' ' * padding}{B}║{RESET}")
            print(f"{B}║{RESET}{' ' * W}{B}║{RESET}")
            hint_text = "  Fix: remove restart/restart_delay, or add run(self) method"
            print(f"{B}║{RESET}{hint_text}{' ' * (W - len(hint_text))}{B}║{RESET}")
            print(f"{B}╟{'─' * W}╢{RESET}")

        # Hint section
        hint = "Hint: def __init__(self) and def run(self)  # runtime is auto-injected"
        print(f"{B}║{RESET}  {CYAN}{hint}{RESET}{' ' * (W - len(hint) - 2)}{B}║{RESET}")
        print(f"{B}╚{'═' * W}╝{RESET}")
        print()

    @property
    def has_errors(self) -> bool:
        return bool(self._invalid_tasks or self._missing_attrs or self._invalid_modes or
                   self._missing_run or self._missing_init or self._invalid_init or
                   self._invalid_run_sig or self._invalid_restart)
