import json
import sys
import threading
import typing
from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Optional, Union

import websocket

from . import _internal

# Import internal state and utilities
from ._internal import (
    _COLOR_CYAN,
    _COLOR_RED,
    _COLOR_RESET,
    _COLOR_YELLOW,
    Call,
    _call_no_return,
    _call_return,
    _generate_id,
    _ping_stop,
    _ping_thread,
    _ws_send,
    app_host,
    app_scheme,
    bot_context,
    bot_id,
    bot_params,
    bot_port,
    conversation_id,
    convert_to_dict,
    current_args,
    is_dev_mode,
    json_data,
    pending_calls,
    pending_responses,
    response_data,
    token,
    verbose,
    ws_client,
    ws_lock,
)
from .bot_types import *

# Import generated API functions
from .generated_api import (
    bot_get,
    conversation_bots,
    conversation_get,
    conversation_users,
    email_send,
    file_create,
    file_get,
    file_update,
    google_search,
    image_gen,
    message_delete,
    message_edit,
    message_history,
)
from .generated_api import message_send as _generated_message_send
from .generated_api import message_typing as _generated_message_typing
from .generated_api import query_files, text_gen, user_get
from .generated_enums import *


# Wrappers for generated functions with original behavior preserved
def message_send(
    id: Optional[str] = None,
    text: Optional[str] = None,
    image: Optional[Image] = None,
    images: Optional[List[Optional[Image]]] = None,
    markdown: Optional[str] = None,
    mention_user_ids: Optional[List[str]] = None,
    only_user_ids: Optional[List[str]] = None,
    lang: Optional[UserLang] = None,
    visibility: Optional[MessageVisibility] = None,
    color: Optional[MessageColor] = None,
    buttons: Optional[List[Button]] = None,
    mood: Optional[Mood] = None,
    impersonate_user_id: Optional[str] = None,
    files: Optional[List[File]] = None,
    file_ids: Optional[List[str]] = None,
    parent_message_id: Optional[str] = None,
) -> Message:
    """
    Send a message to the active conversation
    """

    print("[DEBUG] message_send")

    # Convert files to file_ids if provided (original behavior)
    actual_file_ids = file_ids
    if files is not None and file_ids is None:
        actual_file_ids = [file.id for file in files]

    # Auto-fill parent_message_id from current_args if not provided (original behavior)
    actual_parent_message_id = parent_message_id
    if actual_parent_message_id is None:
        actual_parent_message_id = getattr(
            _internal.current_args.get("message", None), "parent_message_id", None
        )

    return _generated_message_send(
        id=id,
        text=text,
        image=image,
        images=images,
        markdown=markdown,
        mention_user_ids=mention_user_ids,
        only_user_ids=only_user_ids,
        lang=lang,
        visibility=visibility,
        color=color,
        buttons=buttons,
        mood=mood,
        impersonate_user_id=impersonate_user_id,
        file_ids=actual_file_ids,
        parent_message_id=actual_parent_message_id,
    )


def message_typing(
    parent_message_id: Optional[str] = None,
    future_message_id: Optional[str] = None,
) -> None:
    """
    Show a typing indicator in the active conversation
    """
    # Auto-fill parent_message_id from current_args if not provided (original behavior)
    actual_parent_message_id = parent_message_id
    if actual_parent_message_id is None:
        actual_parent_message_id = getattr(
            _internal.current_args.get("message", None), "parent_message_id", None
        )

    _generated_message_typing(
        parent_message_id=actual_parent_message_id,
        future_message_id=future_message_id,
    )


def message_arg_map(dict: Dict[Any, Any]):
    return {
        "message": Message(**dict["message"]),
        "conversation": Conversation(**dict["conversation"]),
    }


def conversation_arg_map(dict: Dict[Any, Any]):
    return {
        "conversation": Conversation(**dict["conversation"]),
    }


def conversation_user_arg_map(dict: Dict[Any, Any]):
    return {
        "user": User(**dict["user"]),
        "conversation": Conversation(**dict["conversation"]),
    }


def live_user_visible_arg_map(dict: Dict[Any, Any]):
    return {
        "live_user": LiveUser(**dict["live_user"]),
        "conversation": Conversation(**dict["conversation"]),
    }


arg_map: Dict[str, Callable[[Dict[Any, Any]], Dict[Any, Any]]] = {
    "message_moderate": message_arg_map,
    "message_direct": message_arg_map,
    "message_add": message_arg_map,
    "conversation_start": conversation_arg_map,
    "conversation_user_add": conversation_user_arg_map,
    "conversation_user_show": conversation_user_arg_map,
    "conversation_update": conversation_arg_map,
    "user_visible": live_user_visible_arg_map,
}


def _ws_receive_loop() -> None:
    """Background thread to receive WebSocket messages"""
    while True:
        try:
            if _internal.ws_client is None:
                break
            raw = _internal.ws_client.recv()
            if not raw:
                break
            msg = json.loads(raw)
            msg_type = msg.get("type")
            msg_id = msg.get("id")

            if msg_type == "response" and msg_id:
                # This is a response to a call we made
                _internal.response_data[msg_id] = msg.get("data")
                event = _internal.pending_responses.get(msg_id)
                if event:
                    event.set()
            elif msg_type == "callback":
                # Server is calling us back (rarely used)
                func_name = msg.get("data", {}).get("func")
                func_params = msg.get("data", {}).get("params", {})
                func = funcs.get(func_name)
                if func is not None:
                    func(**func_params)
        except websocket.WebSocketConnectionClosedException:
            break
        except Exception as e:
            print(f"{_COLOR_RED}[BOT] WebSocket receive error{_COLOR_RESET}", str(e))
            break


def _ping_loop() -> None:
    """Background thread to send keep-alive pings every 30 seconds"""
    while not _internal._ping_stop.wait(30):  # Wait 30 seconds or until stopped
        if _internal.ws_client is not None:
            try:
                with _internal.ws_lock:
                    _internal.ws_client.send(json.dumps({"type": "ping", "data": {}}))
            except Exception:
                break  # Connection closed, exit ping loop


def _connect_websocket() -> None:
    """Connect to the server via WebSocket"""
    # Build WebSocket URL
    session_id = f"bot-{bot_id}-{_generate_id()[:8]}"
    ws_scheme = "wss" if app_scheme == "https" else "ws"
    ws_url = f"{ws_scheme}://{app_host}/ws?sessionId={session_id}"

    print(f"{_COLOR_YELLOW}[BOT]{_COLOR_RESET} connecting to {app_host}...")

    # Connect with websocket-client (with timeout)
    _internal.ws_client = websocket.WebSocket()
    _internal.ws_client.connect(ws_url, timeout=10)
    _internal.ws_client.settimeout(60)

    # Send auth message (framework expects this as first message)
    auth_msg = json.dumps({"id": _generate_id(), "type": "auth", "payload": {"token": token}})
    _internal.ws_client.send(auth_msg)

    # Wait for auth response, then server ready signal
    while True:
        raw = _internal.ws_client.recv()
        if not raw:
            raise Exception("Connection closed before ready")
        msg = json.loads(raw)
        if msg.get("type") == "ready":
            break
        if msg.get("type") == "error":
            raise Exception(f"Auth failed: {msg.get('error', 'unknown')}")

    # Start receive loop in background thread
    receive_thread = threading.Thread(target=_ws_receive_loop, daemon=True)
    receive_thread.start()

    # Start keep-alive ping thread
    _internal._ping_stop.clear()
    _internal._ping_thread = threading.Thread(target=_ping_loop, daemon=True)
    _internal._ping_thread.start()


def start():
    """
    Start your bot, this runs the event loop so your bot can receive calls
    """
    _connect_websocket()

    print(f"{_COLOR_YELLOW}[BOT]{_COLOR_RESET} initialized")

    _internal.started = True
    calls = list(_internal.pending_calls)
    _internal.pending_calls.clear()
    for call in calls:
        _call_no_return(call.op, call.params)

    message_read_loop()


def run_call(message: str):
    msg = typing.cast(Any, json.loads(message))
    if verbose:
        print(f"{_COLOR_CYAN}[BOT]{_COLOR_RESET} message", msg)
    funcName = msg.get("func")
    funcParams = msg.get("params")
    func = funcs.get(funcName)
    if func is not None:
        arg_mapper = arg_map.get(funcName)
        if arg_mapper is not None:
            _internal.current_args = arg_mapper(funcParams)
        else:
            _internal.current_args = funcParams
        func(**_internal.current_args)


def start_nonblocking():
    """
    Start your bot, this start an async loop to handle future calls
    """
    _connect_websocket()

    print(f"{_COLOR_YELLOW}[BOT]{_COLOR_RESET} initialized")

    _internal.started = True
    calls = list(_internal.pending_calls)
    _internal.pending_calls.clear()
    for call in calls:
        _call_no_return(call.op, call.params)

    thread = threading.Thread(target=message_read_loop)
    thread.start()
    print(f"{_COLOR_YELLOW}[BOT]{_COLOR_RESET} start done", app_host)


def message_read_loop():
    while True:
        message = sys.stdin.readline()[:-1]
        if len(message) > 0:
            run_call(message)


# Additional functions not in generated_api (custom logic or not generated)


def live_user_get(id: str) -> Optional[LiveUser]:
    """
    Get live user
    """
    result = _call_return(
        "botCodeLiveUserGet",
        {
            "id": id,
        },
    )
    return LiveUser(**result) if result is not None else None


def bot_owners_get(id: str) -> List[str]:
    """
    Get owners of a bot
    """
    return _call_return(
        "botCodeBotOwnersGet",
        {"id": id},
    )


def message_continue(message: Message) -> None:
    """
    Continue a message to the active conversation, only used by moderator bots
    """

    return _call_no_return(
        "botCodeMessageContinue",
        {"message": message},
    )


def message_send_all(
    text: Optional[str] = None,
    image: Optional[Image] = None,
    images: Optional[List[Image]] = None,
    markdown: Optional[str] = None,
    lang: Optional[UserLang] = None,
    visibility: Optional[MessageVisibility] = None,
    color: Optional[MessageColor] = None,
    buttons: Optional[List[Button]] = None,
    mood: Optional[Mood] = None,
    files: Optional[List[File]] = None,
) -> None:
    """
    Send a message to all conversations
    """
    _call_no_return(
        "botCodeMessageSendAll",
        {
            "text": text,
            "markdown": markdown,
            "image": image,
            "images": images,
            "lang": lang,
            "visibility": visibility,
            "color": color,
            "buttons": buttons,
            "mood": mood,
            "file_ids": [file.id for file in files] if files is not None else None,
        },
    )


def user_post(
    file: File,
) -> None:
    """
    Post a file
    """
    _call_no_return(
        "botCodeUserPost",
        {
            "file_id": file.id,
        },
    )


def messages_to_text(
    messages: List[Message], strip_names: Optional[bool] = None
) -> str:
    """
    Convert a list of messages into string, useful if you need to add your conversation history to an LLM prompt
    """
    return _call_return(
        "botCodeMessagesToText",
        {
            "messages": messages,
            "strip_names": strip_names,
        },
    )


def query_news(
    query: Optional[str] = None,
    created: Optional[int] = None,
    limit: Optional[int] = None,
    categories: Optional[List[NewsCategory]] = None,
) -> List[NewsArticle]:
    """
    Get news based on semantic search using the query
    """
    result = _call_return(
        "botCodeQueryNews",
        {"query": query, "created": created, "limit": limit, "categories": categories},
    )

    return list(map(lambda m: NewsArticle(**m), result))


def image_upload(image: Optional[Image]) -> Optional[Image]:
    """
    Upload an image
    """
    result = _call_return(
        "botCodeImageUpload",
        {
            "image": image,
        },
    )
    return Image(**result) if result is not None else None


def conversation_cron_extend(
    end: Optional[int] = None,
) -> None:
    """
    Extends the end of cron jobs for this conversation
    """
    _call_no_return(
        "botCodeConversationCronExtend",
        {
            "end": end,
        },
    )


def conversation_buttons_show(
    user_id: Optional[str] = None, buttons: Optional[List[Button]] = None
) -> None:
    """
    Show buttons in the active conversation
    """
    _call_no_return(
        "botCodeConversationButtonsShow",
        {
            "user_id": user_id,
            "buttons": buttons,
        },
    )


def tool_conversation_show(
    session_id: str,
    video_call_enabled: Optional[bool] = None,
) -> None:
    """
    Open a conversation
    """
    _call_no_return(
        "botCodeToolConversationShow",
        {
            "session_id": session_id,
            "video_call_enabled": video_call_enabled,
        },
    )


def conversation_context_menu_set(
    user_id: Optional[str] = None, menu_items: Optional[List[MenuItem]] = None
) -> None:
    """
    Add context menu to the current web page when this conversation is active
    """
    _call_no_return(
        "botCodeConversationContextMenuSet",
        {
            "user_id": user_id,
            "menu_items": menu_items,
        },
    )


def file_to_text_gen_message(
    file: File,
    role: Optional[TextGenRole] = None,
    include_name: Optional[bool] = None,
    text: Optional[str] = None,
) -> TextGenMessage:
    """
    Convert a file to TextGenMessage, this is useful if you need to pass file into text_gen
    """
    return TextGenMessage(
        **_call_return(
            "botCodeFileToTextGenMessage",
            {
                "file": file,
                "role": role,
                "include_name": include_name,
                "text": text,
            },
        )
    )


def markdown_create_image(file_id: str, image: Image) -> str:
    """
    Convert an image into markdown syntax, this will upload the file if it is base64
    """
    return _call_return(
        "botCodeMarkdownCreateImage",
        {
            "file_id": file_id,
            "image": image,
        },
    )


def data_set(**kwargs) -> SimpleNamespace:
    """
    Set bot data for conversation
    """
    return SimpleNamespace(
        **_call_return(
            "botCodeDataSet",
            kwargs,
        )
    )


def data_get() -> SimpleNamespace:
    """
    Get bot data for conversation
    """
    return SimpleNamespace(
        **_call_return(
            "botCodeDataGet",
            {},
        )
    )


def user_data_set(user_id: str, **kwargs) -> SimpleNamespace:
    """
    Set bot data for specified user
    """
    return SimpleNamespace(
        **_call_return(
            "botCodeUserDataSet",
            {
                "user_id": user_id,
                "data": kwargs,
            },
        )
    )


def user_data_get(user_id: str) -> SimpleNamespace:
    """
    Get bot data for specified user
    """
    return SimpleNamespace(
        **_call_return(
            "botCodeUserDataGet",
            {
                "user_id": user_id,
            },
        )
    )


def web_page_get(session_id: str) -> WebPageData:
    """
    Get active web page, this only works when Ugly is being used a sidePanel in Chrome
    """
    result = _call_return(
        "botCodeWebPageGet",
        {"session_id": session_id},
    )

    return WebPageData(**result)


def log(
    *args: Any,
) -> None:
    """
    Log, this works the same as print
    """
    if is_dev_mode:
        print(*args)
    _call_no_return(
        "botCodeLog",
        {
            "type": "log",
            "args": args,
        },
    )


def error(
    *args: Any,
) -> None:
    """
    Log an error
    """
    if is_dev_mode:
        print(*args, file=sys.stderr)
    _call_no_return(
        "botCodeLog",
        {
            "type": "error",
            "args": args,
        },
    )


def kagi_summarize(url: Optional[str] = None, text: Optional[str] = None):
    """
    Kagi Summarize
    """
    return _call_return("kagiSummarize", {"url": url, "text": text})


def kagi_enrich_web(query: str):
    """
    Kagi Enrich Web
    """
    return KagiSearchOutput(**_call_return("kagiEnrichWeb", {"query": query}))


def kagi_enrich_news(query: str):
    """
    Kagi Enrich News
    """
    return KagiSearchOutput(
        **_call_return(
            "kagiEnrichNews",
            {
                "query": query,
            },
        )
    )


def kagi_search(query: str, limit: Optional[int] = None):
    """
    Kagi Search
    """
    return KagiSearchOutput(
        **_call_return("kagiSearch", {"query": query, "limit": limit})
    )
