from __future__ import annotations

from typing import Any, Mapping

from .errors import error_response

_RESERVED_KEYS = {"policy", "limits", "tracer", "request_id"}

_CANONICAL_KEYS: dict[str, tuple[str, ...]] = {
    "kantan_web_search": ("query", "max_results", "recency", "domains_allow", "domains_block"),
    "kantan_web_fetch": ("url", "headers", "method", "timeout_ms"),
    "kantan_web_extract": ("html", "mode", "max_chars"),
    "kantan_browser_open": ("url", "viewport", "timeout_ms"),
    "kantan_browser_act": ("page_id", "actions", "timeout_ms"),
    "kantan_browser_extract": ("page_id", "selectors", "extract"),
    "kantan_fs_list": ("path", "glob", "recursive", "max_entries"),
    "kantan_fs_search": ("root", "pattern", "file_glob", "max_matches"),
    "kantan_fs_read": ("path", "range", "head", "tail", "max_bytes"),
    "kantan_fs_write": ("path", "text", "mode"),
    "kantan_fs_apply_patch": ("patch", "strip"),
    "kantan_shell_run": ("cmd", "cwd", "env", "timeout_ms"),
}

_ALIAS_MAP: dict[str, dict[str, list[str]]] = {
    "kantan_web_search": {
        "query": ["q", "search", "search_term", "term", "keyword", "text", "searchTerm"],
        "max_results": ["limit", "top_k", "k", "n", "count", "max", "num_results", "maxResults"],
        "recency": ["time_range", "freshness", "recent"],
        "domains_allow": [
            "allowed_domains",
            "allow_domains",
            "domain_allow",
            "site",
            "sites",
            "include_domains",
            "domainsAllow",
        ],
        "domains_block": [
            "blocked_domains",
            "block_domains",
            "domain_block",
            "exclude_domains",
            "domainsBlock",
        ],
    },
    "kantan_web_fetch": {
        "url": ["uri", "link", "href"],
        "headers": ["header", "request_headers", "requestHeaders"],
        "method": ["http_method", "httpMethod"],
        "timeout_ms": [
            "timeout",
            "timeout_sec",
            "timeout_s",
            "timeout_seconds",
            "timeoutMs",
            "timeoutMS",
            "timeoutSec",
            "timeoutSeconds",
        ],
    },
    "kantan_web_extract": {
        "html": ["content", "body", "raw_html", "rawHtml"],
        "mode": ["format", "output"],
        "max_chars": ["max_length", "limit", "max", "maxChars"],
    },
    "kantan_browser_open": {
        "url": ["uri", "link", "href"],
        "viewport": ["window", "size", "viewport_size", "viewportSize"],
        "timeout_ms": [
            "timeout",
            "timeout_sec",
            "timeout_s",
            "timeout_seconds",
            "timeoutMs",
            "timeoutMS",
            "timeoutSec",
            "timeoutSeconds",
        ],
    },
    "kantan_browser_act": {
        "page_id": ["pageId", "page", "id"],
        "actions": ["action", "steps", "ops", "operations"],
        "timeout_ms": [
            "timeout",
            "timeout_sec",
            "timeout_s",
            "timeout_seconds",
            "timeoutMs",
            "timeoutMS",
            "timeoutSec",
            "timeoutSeconds",
        ],
    },
    "kantan_browser_extract": {
        "page_id": ["pageId", "page", "id"],
        "selectors": ["selector", "css", "css_selector", "cssSelector"],
        "extract": ["fields", "types", "what", "include"],
    },
    "kantan_fs_list": {
        "path": ["dir", "directory", "root", "cwd"],
        "glob": ["pattern", "file_glob", "filter", "fileGlob"],
        "recursive": ["recurse", "deep", "recursive_search", "recursiveSearch"],
        "max_entries": ["limit", "max_results", "max", "n", "count", "maxEntries"],
    },
    "kantan_fs_search": {
        "root": ["path", "dir", "directory"],
        "pattern": ["query", "regex", "text"],
        "file_glob": ["glob", "file_pattern", "filePattern"],
        "max_matches": ["limit", "max_results", "max", "n", "count", "maxMatches"],
    },
    "kantan_fs_read": {
        "path": ["file", "file_path", "filename", "filePath"],
        "range": ["line_range", "lineRange", "range_lines"],
        "head": ["first", "start", "head_lines", "lines", "line_count", "headLines"],
        "tail": ["last", "end", "tail_lines", "tailLines"],
        "max_bytes": ["max_size", "limit", "max", "maxBytes"],
    },
    "kantan_fs_write": {
        "path": ["file", "file_path", "filename", "filePath"],
        "text": ["content", "body", "data"],
        "mode": ["write_mode", "file_mode", "open_mode", "writeMode"],
    },
    "kantan_fs_apply_patch": {
        "patch": ["diff", "patch_text", "unified_diff", "patchText"],
        "strip": ["strip_components", "strip_level", "p", "stripLevel"],
    },
    "kantan_shell_run": {
        "cmd": ["command", "args", "argv", "shell_command", "command_line", "commandLine", "cmdline"],
        "cwd": ["workdir", "working_directory", "dir", "workingDirectory"],
        "env": ["environment", "env_vars", "envVars"],
        "timeout_ms": [
            "timeout",
            "timeout_sec",
            "timeout_s",
            "timeout_seconds",
            "timeoutMs",
            "timeoutMS",
            "timeoutSec",
            "timeoutSeconds",
        ],
    },
}

_SECONDS_ALIASES = {
    "timeout",
    "timeout_sec",
    "timeout_s",
    "timeout_seconds",
    "timeoutSec",
    "timeoutSeconds",
}

_FS_WRITE_MODE_MAP = {
    "w": "overwrite",
    "write": "overwrite",
    "overwrite": "overwrite",
    "a": "append",
    "append": "append",
    "x": "create_only",
    "create": "create_only",
    "create_new": "create_only",
    "create_only": "create_only",
}

_WEB_FETCH_METHOD_MAP = {
    "get": "GET",
    "post": "POST",
}

_WEB_EXTRACT_MODE_MAP = {
    "md": "markdown",
    "markdown": "markdown",
    "text": "text",
    "plain": "text",
    "plaintext": "text",
}

_BROWSER_EXTRACT_MAP = {
    "text": "text",
    "link": "links",
    "links": "links",
    "table": "tables",
    "tables": "tables",
}

_ACTION_TYPE_MAP = {
    "tap": "click",
    "click": "click",
    "type": "fill",
    "input": "fill",
    "fill": "fill",
    "press": "press",
    "keydown": "press",
    "wait": "wait_for",
    "wait_for": "wait_for",
    "wait_for_selector": "wait_for",
    "sleep": "wait_for",
    "scroll": "scroll",
    "select": "select",
    "choose": "select",
}


def normalize_tool_payload(tool_name: str, payload: Mapping[str, Any]) -> tuple[dict[str, Any], dict | None]:
    if not isinstance(payload, Mapping):
        return {}, error_response("INVALID_ARGUMENT", "tool input must be JSON object")
    canonical_keys = _CANONICAL_KEYS.get(tool_name)
    if canonical_keys is None:
        return dict(payload), None
    normalized: dict[str, Any] = {}
    source_map: dict[str, str] = {}
    for key in canonical_keys:
        if key in payload:
            normalized[key] = payload[key]
            source_map[key] = key
    for canonical, aliases in _ALIAS_MAP.get(tool_name, {}).items():
        if canonical in normalized:
            continue
        for alias in aliases:
            if alias in payload:
                normalized[canonical] = payload[alias]
                source_map[canonical] = alias
                break
    for reserved in _RESERVED_KEYS:
        normalized.pop(reserved, None)
    error = _normalize_values(tool_name, normalized, source_map)
    if error is not None:
        return {}, error
    return normalized, None


def _normalize_values(
    tool_name: str, payload: dict[str, Any], source_map: Mapping[str, str]
) -> dict | None:
    if tool_name == "kantan_web_search":
        error = _coerce_int_field(payload, "max_results")
        if error is not None:
            return error
        payload["domains_allow"] = _ensure_list(payload.get("domains_allow"))
        payload["domains_block"] = _ensure_list(payload.get("domains_block"))
        return None
    if tool_name == "kantan_web_fetch":
        error = _coerce_timeout_field(payload, "timeout_ms", source_map)
        if error is not None:
            return error
        error = _normalize_method(payload)
        if error is not None:
            return error
        return None
    if tool_name == "kantan_web_extract":
        error = _coerce_int_field(payload, "max_chars")
        if error is not None:
            return error
        return _normalize_extract_mode(payload)
    if tool_name == "kantan_browser_open":
        return _coerce_timeout_field(payload, "timeout_ms", source_map)
    if tool_name == "kantan_browser_act":
        error = _coerce_timeout_field(payload, "timeout_ms", source_map)
        if error is not None:
            return error
        actions, error = _normalize_actions(payload.get("actions"))
        if error is not None:
            return error
        if actions is not None:
            payload["actions"] = actions
        return None
    if tool_name == "kantan_browser_extract":
        selectors = _ensure_list(payload.get("selectors"))
        if selectors is not None:
            payload["selectors"] = selectors
        extract, error = _normalize_extract(payload.get("extract"))
        if error is not None:
            return error
        if extract is not None:
            payload["extract"] = extract
        return None
    if tool_name == "kantan_fs_list":
        error = _coerce_bool_field(payload, "recursive")
        if error is not None:
            return error
        return _coerce_int_field(payload, "max_entries")
    if tool_name == "kantan_fs_search":
        return _coerce_int_field(payload, "max_matches")
    if tool_name == "kantan_fs_read":
        range_value, error = _normalize_range(payload.get("range"))
        if error is not None:
            return error
        if range_value is not None:
            payload["range"] = range_value
        error = _coerce_int_field(payload, "head")
        if error is not None:
            return error
        error = _coerce_int_field(payload, "tail")
        if error is not None:
            return error
        return _coerce_int_field(payload, "max_bytes")
    if tool_name == "kantan_fs_write":
        return _normalize_fs_write_mode(payload)
    if tool_name == "kantan_fs_apply_patch":
        return _coerce_int_field(payload, "strip")
    if tool_name == "kantan_shell_run":
        error = _coerce_timeout_field(payload, "timeout_ms", source_map)
        if error is not None:
            return error
        cmd, error = _normalize_cmd(payload.get("cmd"))
        if error is not None:
            return error
        if cmd is not None:
            payload["cmd"] = cmd
        env, error = _normalize_env(payload.get("env"))
        if error is not None:
            return error
        if env is not None:
            payload["env"] = env
        return None
    return None


def _normalize_actions(actions: Any) -> tuple[list[dict[str, Any]] | None, dict | None]:
    if actions is None:
        return None, None
    if isinstance(actions, Mapping):
        actions_list = [actions]
    elif isinstance(actions, list):
        actions_list = actions
    else:
        return None, error_response("INVALID_ARGUMENT", "actions must be list")
    normalized: list[dict[str, Any]] = []
    for item in actions_list:
        if not isinstance(item, Mapping):
            return None, error_response("INVALID_ARGUMENT", "action must be object")
        normalized_action, error = _normalize_action(item)
        if error is not None:
            return None, error
        normalized.append(normalized_action)
    return normalized, None


def _normalize_action(action: Mapping[str, Any]) -> tuple[dict[str, Any], dict | None]:
    action_type = _first_value(action, ["type", "action", "action_type"])
    if action_type is None:
        return {}, error_response("INVALID_ARGUMENT", "action.type is required")
    if not isinstance(action_type, str):
        return {}, error_response("INVALID_ARGUMENT", "action.type must be string")
    action_type_norm = _ACTION_TYPE_MAP.get(action_type.strip().lower(), action_type)
    normalized: dict[str, Any] = {"type": action_type_norm}
    selector = _first_value(action, ["selector", "css", "css_selector", "xpath"])
    if selector is not None:
        normalized["selector"] = selector
    text = _first_value(action, ["text", "input", "content"])
    if text is None:
        text = _first_value(action, ["value"])
    if text is not None:
        normalized["text"] = text
    key = _first_value(action, ["key", "keys"])
    if key is not None:
        normalized["key"] = key
    value = _first_value(action, ["value", "option", "label"])
    if value is not None:
        normalized["value"] = value
    y = _first_value(action, ["y", "deltaY", "scrollY"])
    if y is not None:
        y_value, error = _coerce_int(y, "actions.y")
        if error is not None:
            return {}, error
        normalized["y"] = y_value
    timeout_value, from_seconds = _extract_timeout(action)
    if timeout_value is not None:
        timeout_ms, error = _coerce_timeout(timeout_value, "actions.timeout_ms", from_seconds)
        if error is not None:
            return {}, error
        normalized["timeout_ms"] = timeout_ms
    return normalized, None


def _normalize_extract(value: Any) -> tuple[list[str] | None, dict | None]:
    if value is None:
        return None, None
    values = _ensure_list(value)
    if values is None:
        return None, None
    normalized: list[str] = []
    for item in values:
        if not isinstance(item, str):
            return None, error_response("INVALID_ARGUMENT", "extract must be string")
        key = item.strip().lower()
        normalized.append(_BROWSER_EXTRACT_MAP.get(key, key))
    return normalized, None


def _normalize_extract_mode(payload: dict[str, Any]) -> dict | None:
    mode = payload.get("mode")
    if mode is None:
        return None
    if not isinstance(mode, str):
        return error_response("INVALID_ARGUMENT", "mode must be string")
    payload["mode"] = _WEB_EXTRACT_MODE_MAP.get(mode.strip().lower(), mode)
    return None


def _normalize_fs_write_mode(payload: dict[str, Any]) -> dict | None:
    mode = payload.get("mode")
    if mode is None:
        return None
    if not isinstance(mode, str):
        return error_response("INVALID_ARGUMENT", "mode must be string")
    payload["mode"] = _FS_WRITE_MODE_MAP.get(mode.strip().lower(), mode)
    return None


def _normalize_method(payload: dict[str, Any]) -> dict | None:
    method = payload.get("method")
    if method is None:
        return None
    if not isinstance(method, str):
        return error_response("INVALID_ARGUMENT", "method must be string")
    lowered = method.strip().lower()
    payload["method"] = _WEB_FETCH_METHOD_MAP.get(lowered, method.upper())
    return None


def _normalize_range(value: Any) -> tuple[dict[str, int] | None, dict | None]:
    if value is None:
        return None, None
    if isinstance(value, Mapping):
        start_value = _first_value(value, ["start_line", "start", "line_start", "from"])
        end_value = _first_value(value, ["end_line", "end", "line_end", "to"])
        normalized: dict[str, int] = {}
        if start_value is not None:
            start_line, error = _coerce_int(start_value, "range.start_line")
            if error is not None:
                return None, error
            normalized["start_line"] = start_line
        if end_value is not None:
            end_line, error = _coerce_int(end_value, "range.end_line")
            if error is not None:
                return None, error
            normalized["end_line"] = end_line
        return normalized, None
    if isinstance(value, (list, tuple)) and len(value) == 2:
        start_line, error = _coerce_int(value[0], "range.start_line")
        if error is not None:
            return None, error
        end_line, error = _coerce_int(value[1], "range.end_line")
        if error is not None:
            return None, error
        return {"start_line": start_line, "end_line": end_line}, None
    return None, error_response("INVALID_ARGUMENT", "range must be object or [start, end]")


def _normalize_cmd(value: Any) -> tuple[str | list[str] | None, dict | None]:
    if value is None:
        return None, None
    if isinstance(value, list):
        return [str(item) for item in value], None
    if isinstance(value, str):
        return value, None
    return None, error_response("INVALID_ARGUMENT", "cmd must be string or list")


def _normalize_env(value: Any) -> tuple[dict[str, str] | None, dict | None]:
    if value is None:
        return None, None
    if isinstance(value, Mapping):
        return {str(k): str(v) for k, v in value.items()}, None
    if isinstance(value, list):
        env: dict[str, str] = {}
        for item in value:
            if not isinstance(item, str) or "=" not in item:
                return None, error_response(
                    "INVALID_ARGUMENT", "env must be object or list of KEY=VALUE"
                )
            key, val = item.split("=", 1)
            env[key] = val
        return env, None
    return None, error_response("INVALID_ARGUMENT", "env must be object or list of KEY=VALUE")


def _coerce_timeout_field(
    payload: dict[str, Any], field: str, source_map: Mapping[str, str]
) -> dict | None:
    if field not in payload:
        return None
    if payload[field] is None:
        return None
    from_seconds = source_map.get(field) in _SECONDS_ALIASES
    timeout_ms, error = _coerce_timeout(payload[field], field, from_seconds)
    if error is not None:
        return error
    payload[field] = timeout_ms
    return None


def _coerce_int_field(payload: dict[str, Any], field: str) -> dict | None:
    if field not in payload:
        return None
    if payload[field] is None:
        return None
    value, error = _coerce_int(payload[field], field)
    if error is not None:
        return error
    payload[field] = value
    return None


def _coerce_bool_field(payload: dict[str, Any], field: str) -> dict | None:
    if field not in payload:
        return None
    if payload[field] is None:
        return None
    value, error = _coerce_bool(payload[field], field)
    if error is not None:
        return error
    payload[field] = value
    return None


def _coerce_timeout(value: Any, field: str, from_seconds: bool) -> tuple[int, dict | None]:
    number, error = _coerce_number(value, field)
    if error is not None:
        return 0, error
    if from_seconds:
        number *= 1000
    return int(number), None


def _coerce_int(value: Any, field: str) -> tuple[int, dict | None]:
    number, error = _coerce_number(value, field)
    if error is not None:
        return 0, error
    return int(number), None


def _coerce_number(value: Any, field: str) -> tuple[float, dict | None]:
    if isinstance(value, bool):
        return 0.0, error_response("INVALID_ARGUMENT", f"{field} must be number")
    if isinstance(value, (int, float)):
        return float(value), None
    if isinstance(value, str):
        stripped = value.strip()
        if not stripped:
            return 0.0, error_response("INVALID_ARGUMENT", f"{field} must be number")
        try:
            return float(stripped), None
        except ValueError:
            return 0.0, error_response("INVALID_ARGUMENT", f"{field} must be number")
    return 0.0, error_response("INVALID_ARGUMENT", f"{field} must be number")


def _coerce_bool(value: Any, field: str) -> tuple[bool, dict | None]:
    if isinstance(value, bool):
        return value, None
    if isinstance(value, (int, float)) and value in (0, 1):
        return bool(value), None
    if isinstance(value, str):
        lowered = value.strip().lower()
        if lowered in {"true", "1", "yes", "y"}:
            return True, None
        if lowered in {"false", "0", "no", "n"}:
            return False, None
    return False, error_response("INVALID_ARGUMENT", f"{field} must be boolean")


def _ensure_list(value: Any) -> list[Any] | None:
    if value is None:
        return None
    if isinstance(value, list):
        return value
    if isinstance(value, tuple):
        return list(value)
    return [value]


def _first_value(data: Mapping[str, Any], keys: list[str]) -> Any:
    for key in keys:
        if key in data:
            return data[key]
    return None


def _extract_timeout(action: Mapping[str, Any]) -> tuple[Any | None, bool]:
    for key in [
        "timeout_ms",
        "timeout",
        "timeout_sec",
        "timeout_s",
        "timeout_seconds",
        "timeoutMs",
        "timeoutMS",
        "timeoutSec",
        "timeoutSeconds",
    ]:
        if key in action:
            return action[key], key in _SECONDS_ALIASES
    return None, False
