from __future__ import annotations

import ast
import inspect
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from lsprotocol import types

from .runtime import _default_builtins


@dataclass
class FunctionSig:
    name: str
    params: List[str]
    doc: str = ""

    @property
    def label(self) -> str:
        params = ", ".join(self.params)
        return f"{self.name}({params})"


def load_signatures() -> Dict[str, FunctionSig]:
    sigs: dict[str, FunctionSig] = {}
    sigs.update(_builtin_sigs())
    sigs.update(_runtime_helper_sigs())
    sigs.update(_avrae_function_sigs())
    return sigs


def _builtin_sigs() -> Dict[str, FunctionSig]:
    sigs: dict[str, FunctionSig] = {}
    for name, fn in _default_builtins().items():
        try:
            sig = inspect.signature(fn)
        except (TypeError, ValueError):
            continue
        params = [p.name for p in sig.parameters.values()]
        sigs[name] = FunctionSig(name=name, params=params, doc=fn.__doc__ or "")
    return sigs


def _runtime_helper_sigs() -> Dict[str, FunctionSig]:
    helpers = {
        "get_gvar": (
            ["address"],
            "Retrieves and returns the value of a global variable (gvar) by address.",
        ),
        "get_svar": (
            ["name", "default=None"],
            "Gets a server variable by name, returning default if it is not present.",
        ),
        "get_uvar": (
            ["name", "default=None"],
            "Gets a user variable by name as a string, returning default if it is not present.",
        ),
        "get_uvars": (
            [],
            "Returns the mapping of user variables available to the caller (values are strings).",
        ),
        "set_uvar": (
            ["name", "value"],
            "Sets a user variable (stored as a string) and returns the stored value.",
        ),
        "set_uvar_nx": (
            ["name", "value"],
            "Sets a user variable only if it does not already exist; returns the stored string value.",
        ),
        "delete_uvar": (
            ["name"],
            "Deletes a user variable and returns its previous value or None if missing.",
        ),
        "uvar_exists": (
            ["name"],
            "Returns whether a user variable is set.",
        ),
        "exists": (
            ["name"],
            "Returns whether a name is set in the current evaluation context.",
        ),
        "get": (
            ["name", "default=None"],
            "Gets the value of a name using local > cvar > uvar resolution order; returns default if not set.",
        ),
        "using": (
            ["**imports"],
            "Imports one or more gvars as modules into the current namespace with the provided aliases.",
        ),
        "signature": (
            ["data=0"],
            "Generates a signed invocation signature encoding invocation context and optional 5-bit user data.",
        ),
        "verify_signature": (
            ["data"],
            "Verifies a signature generated by signature(); returns context data or raises ValueError when invalid.",
        ),
        "print": (
            ["*values"],
            "Writes values to alias output using the configured separator/end (mirrors Python print).",
        ),
        "character": (
            [],
            "Returns the active character object for this alias, or raises if none is available.",
        ),
        "combat": (
            [],
            "Returns the current combat context if one exists, otherwise None.",
        ),
        "argparse": (
            ["args", "character=None", "splitter=argsplit", "parse_ephem=True"],
            "Parses alias arguments using Avrae's argparse helper.",
        ),
    }
    return {name: FunctionSig(name=name, params=params, doc=doc) for name, (params, doc) in helpers.items()}


def _avrae_function_sigs() -> Dict[str, FunctionSig]:
    sigs: dict[str, FunctionSig] = {}
    module_path = Path(__file__).resolve().parent.parent / "avrae" / "aliasing" / "api" / "functions.py"
    if not module_path.exists():
        return sigs
    try:
        tree = ast.parse(module_path.read_text())
    except Exception:
        return sigs

    for node in tree.body:
        if isinstance(node, ast.FunctionDef) and not node.name.startswith("_"):
            params: list[str] = []
            defaults = list(node.args.defaults)
            default_offset = len(node.args.args) - len(defaults)
            for idx, arg in enumerate(node.args.args):
                default_val = None
                if idx >= default_offset:
                    default_node = defaults[idx - default_offset]
                    try:
                        default_val = ast.literal_eval(default_node)
                    except Exception:
                        default_val = None
                params.append(f"{arg.arg}={default_val}" if default_val is not None else arg.arg)
            doc = ast.get_docstring(node) or ""
            sigs[node.name] = FunctionSig(name=node.name, params=params, doc=doc)
    return sigs


def signature_help_for_code(code: str, line: int, character: int, sigs: Dict[str, FunctionSig]) -> Optional[types.SignatureHelp]:
    try:
        tree = ast.parse(code)
    except SyntaxError:
        return None

    target_call: ast.Call | None = None
    target_depth = -1

    class Finder(ast.NodeVisitor):
        def __init__(self):
            self.stack: list[ast.AST] = []

        def visit_Call(self, node: ast.Call):
            nonlocal target_call, target_depth
            if hasattr(node, "lineno") and hasattr(node, "col_offset"):
                start = (node.lineno - 1, node.col_offset)
                end_line = getattr(node, "end_lineno", node.lineno) - 1
                end_col = getattr(node, "end_col_offset", node.col_offset)
                if _pos_within((line, character), start, (end_line, end_col)):
                    depth = len(self.stack)
                    # Prefer the most nested call covering the cursor
                    if depth >= target_depth:
                        target_call = node
                        target_depth = depth
            self.stack.append(node)
            self.generic_visit(node)
            self.stack.pop()

    Finder().visit(tree)

    if not target_call:
        return None

    if isinstance(target_call.func, ast.Name):
        name = target_call.func.id
    else:
        return None

    if name not in sigs:
        return None

    fsig = sigs[name]
    sig_info = types.SignatureInformation(
        label=fsig.label,
        documentation=fsig.doc,
        parameters=[types.ParameterInformation(label=p) for p in fsig.params],
    )
    active_param = _active_param_index(target_call, (line, character), fsig.params)
    return types.SignatureHelp(signatures=[sig_info], active_signature=0, active_parameter=active_param)


def _pos_within(pos: Tuple[int, int], start: Tuple[int, int], end: Tuple[int, int]) -> bool:
    (line, col) = pos
    (sl, sc) = start
    (el, ec) = end
    if line < sl or line > el:
        return False
    if line == sl and col < sc:
        return False
    if line == el and col > ec:
        return False
    return True


def _active_param_index(call: ast.Call, cursor: Tuple[int, int], params: List[str]) -> int:
    if not params:
        return 0

    # Build spans for positional args and keywords in source order.
    spans: list[tuple[Tuple[int, int], Tuple[int, int], ast.AST]] = []
    for arg in call.args:
        spans.append((_node_start(arg), _node_end(arg), arg))
    for kw in call.keywords:
        spans.append((_node_start(kw), _node_end(kw), kw))
    spans.sort(key=lambda s: (s[0][0], s[0][1]))

    def _clamp(idx: int) -> int:
        return max(0, min(idx, max(len(params) - 1, 0)))

    for idx, (start, end, node) in enumerate(spans):
        if _pos_within(cursor, start, end):
            if isinstance(node, ast.keyword) and node.arg and node.arg in params:
                return _clamp(params.index(node.arg))
            return _clamp(idx)

    # If cursor is after some args but not inside one, infer next argument slot.
    before_count = sum(1 for start, _, _ in spans if start <= cursor)
    return _clamp(before_count)


def _node_start(node: ast.AST) -> Tuple[int, int]:
    return (getattr(node, "lineno", 1) - 1, getattr(node, "col_offset", 0))


def _node_end(node: ast.AST) -> Tuple[int, int]:
    return (
        getattr(node, "end_lineno", getattr(node, "lineno", 1)) - 1,
        getattr(node, "end_col_offset", getattr(node, "col_offset", 0)),
    )
