import inspect
import sys
import traceback
import ast
from types import FunctionType

from ..data.config.tracer import TracerConfig
from ..data.config.logging import LoggingConfig
from ..utils.tracer import Tracer
from ..utils.logging import Logger


class Nakurity:
    _registry = []  # list of (obj, metadata)

    def __init__(self, logger=Logger()):
        self.tracer = Tracer(TracerConfig())
        self.logging = logger

    # ------------------------------------------------------------
    #  DECORATORS
    # ------------------------------------------------------------
    @classmethod
    def expect(cls, text: str):
        """Defines runtime expectations for a function or class."""
        def decorator(obj):
            cls._registry.append({
                "obj": obj,
                "expect": text.strip(),
                "comment": None,
                "require": [],
                "guard": None
            })
            return obj
        return decorator

    @classmethod
    def comment(cls, text: str):
        """
        Attach a human-readable hint for debugging.
        Adds descriptive comment to a previously defined function/class.
        """
        def decorator(obj):
            # If already registered via @expect, attach comment
            for entry in cls._registry:
                if entry["obj"] is obj:
                    entry["comment"] = text.strip()
                    break
            else:
                # Otherwise, register new with only comment
                cls._registry.append({
                    "obj": obj,
                    "expect": None,
                    "comment": text.strip(),
                    "require": [],
                    "guard": None
                })
            return obj
        return decorator

    @classmethod
    def require(cls, *names: str):
        """Require that specific identifiers or callables exist."""
        def decorator(obj):
            for entry in cls._registry:
                if entry["obj"] is obj:
                    entry["require"].extend(names)
                    break
            else:
                cls._registry.append({
                    "obj": obj,
                    "expect": None,
                    "comment": None,
                    "require": list(names),
                    "guard": None
                })
            return obj
        return decorator

    @classmethod
    def guard(cls, condition: str):
        """Ensure a condition evaluates true before runtime."""
        def decorator(obj):
            for entry in cls._registry:
                if entry["obj"] is obj:
                    entry["guard"] = condition
                    break
            else:
                cls._registry.append({
                    "obj": obj,
                    "expect": None,
                    "comment": None,
                    "require": [],
                    "guard": condition
                })
            return obj
        return decorator

    # ------------------------------------------------------------
    #  MAIN LINT ENTRYPOINT
    # ------------------------------------------------------------
    def lint(self):
        self.logging.debug("[Nakurity] 🧠 Pre-runtime lint initiated...\n")

        self._had_warnings = False

        for entry in Nakurity._registry:
            self._analyze_entry(entry)

        if self._had_warnings:
            self.logging.debug("\n⚠️  Nakurity lint completed with warnings.\n")
        else:
            self.logging.debug("\n✅ Nakurity lint completed successfully.\n")

    # ------------------------------------------------------------
    #  LINT HANDLERS
    # ------------------------------------------------------------
    def _analyze_entry(self, entry):
        obj = entry["obj"]
        name = getattr(obj, "__name__", "<unknown>")
        kind = "class" if inspect.isclass(obj) else "function"
        comment = entry.get("comment")
        expect = entry.get("expect")
        requires = entry.get("require")
        guard = entry.get("guard")

        self.logging.debug(f"🔍 Inspecting {kind}: {name}")
        if comment:
            self.logging.debug(f"   💬 {comment}")
        if expect:
            self.logging.debug(f"   📋 Expectations defined.")
        if requires:
            self.logging.debug(f"   📦 Requires: {', '.join(requires)}")
        if guard:
            self.logging.debug(f"   🧩 Guard: {guard}")
        self.logging.debug("")

        # --- Requirement check ---
        self._check_requirements(entry, name)

        # --- Guard check ---
        if guard:
            self._check_guard(guard, name)

        # --- Expectation check ---
        if expect:
            rules = self._parse_expectations(expect)
            self._static_check(entry, rules)
            self._simulate_runtime(entry, rules)

    # ------------------------------------------------------------
    #  REQUIRE / GUARD / PARSE / CHECK
    # ------------------------------------------------------------

    def _check_guard(self, entry):
        obj_name = entry["obj"].__name__
        cond = entry["guard"]
        try:
            ok = eval(cond, globals())
            if not ok:
                msg = f"⚠️ Guard failed for {obj_name}: {cond}"
                self.logging.debug(msg)
                if entry.get("comment"):
                    self.logging.debug(f"   💡 Hint: {entry['comment']}")
                return False
        except Exception as e:
            self.logging.debug(f"💥 Guard error in {obj_name}: {cond} -> {e}")
            if entry.get("comment"):
                self.logging.debug(f"   💡 Hint: {entry['comment']}")
            return False
        return True
    
    def _check_requirements(self, entry, obj_name):
        for name in entry["require"]:
            if name not in globals() and name not in sys.modules:
                self.logging.debug(f"⚠️ {obj_name}: required '{name}' missing.")
                self._had_warnings = True
                if entry.get("comment"):
                    self.logging.debug(f"   💡 Hint: {entry['comment']}")
            else:
                self.logging.debug(f"✅ {obj_name}: found required dependency '{name}'.")

    def _parse_expectations(self, text: str) -> dict:
        """
        Smart Parser that handles:
            - expects N args: ...
            - must call foo()
            - should not raise
            - returns <type>
            - performs <action>
        """
        rules = {"args": [], "return": None, "must_call": [], "no_exceptions": False, "perform": None}
        for line in text.splitlines():
            line = line.strip().lower()
            if not line:
                continue
            if "arg" in line and ":" in line:
                parts = line.split(":", 1)[1]
                args = [a.strip() for a in parts.replace(",", " ").split() if a.strip()]
                rules["args"].extend(args)
            elif line.startswith("expects"):
                num = [int(s) for s in line.split() if s.isdigit()]
                if num:
                    rules["expected_arg_count"] = num[0]
            elif "return" in line:
                rules["return"] = line.split("return")[-1].strip()
            elif "should not raise" in line or "no exception" in line:
                rules["no_exceptions"] = True
            elif "must call" in line:
                fn = line.split("must call")[-1].strip().replace("()", "")
                rules["must_call"].append(fn)
            elif "perform" in line:
                rules["perform"] = line.split("perform")[-1].strip()
        return rules

    def _static_check(self, entry, rules):
        obj = entry["obj"]
        name = obj.__name__

        if isinstance(obj, FunctionType):
            sig = inspect.signature(obj)
            if "expected_arg_count" in rules and len(sig.parameters) != rules["expected_arg_count"]:
                self._had_warnings = True
                self.logging.debug(f"⚠️ {name}: expected {rules['expected_arg_count']} args, found {len(sig.parameters)}")
            if rules["args"] and set(rules["args"]) != set(sig.parameters.keys()):
                self._had_warnings = True
                self.logging.debug(f"⚠️ {name}: arg names mismatch -> expected {rules['args']}, got {list(sig.parameters.keys())}")
            if rules["return"] and sig.return_annotation is inspect.Signature.empty:
                self._had_warnings = True
                self.logging.debug(f"⚠️ {name}: missing return annotation (expected {rules['return']})")
        elif inspect.isclass(obj):
            methods = inspect.getmembers(obj, predicate=inspect.isfunction)
            if not methods:
                self._had_warnings = True
                self.logging.debug(f"⚠️ {name}: class has no methods.")

    # ------------------------------------------------------------
    #  RUNTIME SIMULATION
    # ------------------------------------------------------------
    def _simulate_runtime(self, entry, rules):
        obj = entry["obj"]
        if not isinstance(obj, FunctionType):
            return

        sig = inspect.signature(obj)
        args = [self._dummy_value(p) for p in sig.parameters.values()]

        called_functions = set()

        try:
            src = inspect.getsource(obj)
            tree = ast.parse(src)
            for node in ast.walk(tree):
                if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
                    called_functions.add(node.func.id)
        except Exception as e:
            self._had_warnings = True
            self.logging.debug(f"⚠️ {obj.__name__}: AST inspection failed ({e})")

        # --- must_call early static detection ---
        for required_call in rules.get("must_call", []):
            if required_call not in called_functions:
                # We'll still trace runtime, but warn early if call is commented out
                src_preview = inspect.getsource(obj)
                if f"# {required_call}" in src_preview:
                    self._had_warnings = True
                    self.logging.debug(
                        f"⚠️ {obj.__name__}: found commented-out '{required_call}()' — "
                        f"call is ignored at runtime."
                    )

        # Local tracer to capture all function calls
        def trace_calls(frame, event, arg):
            if event == "call":
                called_functions.add(frame.f_code.co_name)
            return trace_calls

        try:
            sys.settrace(trace_calls)
            self.tracer.trace(inspect.currentframe(), "call", None)
            result = obj(*args)
            self.tracer.trace(inspect.currentframe(), "return", result)
        except Exception as e:
            self._had_warnings = True
            msg = f"💥 {obj.__name__}: raised {type(e).__name__} ({e})"
            if rules.get("no_exceptions"):
                msg += " [should not raise!]"
            self.logging.debug(msg)
            if entry.get("comment"):
                self.logging.debug(f"   💡 Hint: {entry['comment']}")
            self._trace_root_cause()
        finally:
            sys.settrace(None)  # disable tracing

        # --- must_call verification (after runtime) ---
        for required_call in rules.get("must_call", []):
            if required_call not in called_functions:
                self._had_warnings = True
                self.logging.debug(f"⚠️ {obj.__name__}: did not call required function '{required_call}()'")
            else:
                self.logging.debug(f"✅ {obj.__name__}: called required function '{required_call}()'")

        # --- return type check ---
        if "return" in rules and rules["return"]:
            expected = rules["return"]
            if expected in ("int", "float", "str", "bool"):
                if not isinstance(result, eval(expected)):
                    self._had_warnings = True
                    self.logging.debug(f"⚠️ {obj.__name__}: returned {type(result).__name__}, expected {expected}")

    def _dummy_value(self, param):
        name = param.name.lower()
        if "path" in name or "file" in name:
            return "dummy.txt"
        if "count" in name or "num" in name or "id" in name:
            return 1
        if "flag" in name or name.startswith("is_"):
            return True
        return "x"

    def _trace_root_cause(self):
        tb = traceback.format_exc().strip().splitlines()
        if tb:
            self.logging.debug("🔎 Root cause trace:")
            for line in tb[-5:]:
                self.logging.debug(f"   {line}")

    def _report_failure(self, entry, e):
        name = entry["obj"].__name__
        self._had_warnings = True
        self.logging.debug(f"💥 Error analyzing {name}: {e}")
        if entry.get("comment"):
            self.logging.debug(f"   💡 Hint: {entry['comment']}")
        self._trace_root_cause()

# -------------------------------------------------------------------------
#  EXAMPLE USAGE
# -------------------------------------------------------------------------
# from nakurity.core import Nakurity, NakurityRequirement
#
# @Nakurity.expect("""
# Expect:
#   - takes 1 argument: x
#   - returns int
#   - should not raise exception
# """)
# @Nakurity.comment("Simple doubling function")
# def double(x: int) -> int:
#     return x * 2
#
# @Nakurity.expect("""
# Expect:
#   - takes 1 argument: name
#   - returns str
#   - must call greet
# """)
# @Nakurity.require("greet")
# @Nakurity.guard("'greet' in globals()")
# def say_hi(name: str) -> str:
#     greet(name)
#     return f"Hello, {name}"
#
# def greet(name: str):
#     return f"Hi {name}"
#
# if __name__ == "__main__":
#     NakurityRequirement(sys.modules[__name__])