# Internal module for bot call infrastructure
# This module is separated to avoid circular imports between bot.py and generated_api.py

import json
import os
import sys
import threading
import uuid
from dataclasses import asdict, is_dataclass
from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Optional

import websocket
from pydantic.dataclasses import dataclass

# Global state
current_args: Dict[str, Any] = {}

app_host = os.environ.get("APP_HOST", "localhost:3000")
app_scheme = os.environ.get("APP_SCHEME", "http")
is_dev_mode = os.environ.get("UGLY_BOT_DEV", "").lower() in ("1", "true", "yes")
verbose = os.environ.get("UGLY_BOT_VERBOSE", "").lower() in ("1", "true", "yes")

# ANSI color codes for terminal output
_use_color = sys.stdout.isatty()
_COLOR_RESET = "\033[0m" if _use_color else ""
_COLOR_RED = "\033[91m" if _use_color else ""
_COLOR_YELLOW = "\033[93m" if _use_color else ""
_COLOR_CYAN = "\033[96m" if _use_color else ""

ws_client: Optional[websocket.WebSocket] = None
ws_lock = threading.Lock()
pending_responses: Dict[str, threading.Event] = {}
response_data: Dict[str, Any] = {}
_ping_thread: Optional[threading.Thread] = None
_ping_stop = threading.Event()

bot_port = int(sys.argv[2]) if len(sys.argv) > 1 and sys.argv[1] == "bot" else None
token = sys.argv[3] if len(sys.argv) > 1 and sys.argv[1] == "bot" else None
json_data = (
    json.loads(sys.argv[4]) if len(sys.argv) > 1 and sys.argv[1] == "bot" else None
)
started = False


@dataclass
class Call:
    op: str
    params: dict


pending_calls: List[Call] = []

bot_params = (
    SimpleNamespace(**(json_data.get("params") or {}))
    if json_data is not None
    else None
)
"""Bot Params"""

bot_id = json_data["botId"] if json_data is not None else None
"""Bot ID"""

conversation_id = json_data["conversationId"] if json_data is not None else None
"""Conversation ID"""

bot_context = (
    {
        "botId": json_data["botId"],
        "botCodeId": json_data["botCodeId"],
        "conversationId": json_data["conversationId"],
        "chargeUserIds": json_data["chargeUserIds"],
    }
    if json_data is not None
    else None
)


def _generate_id() -> str:
    """Generate a unique message ID"""
    return str(uuid.uuid4())


def _ws_send(message: dict) -> None:
    """Send a message over the WebSocket connection"""
    global ws_client
    if ws_client is None:
        raise Exception("WebSocket not connected")
    with ws_lock:
        ws_client.send(json.dumps(message))


def convert_to_dict(data: Any) -> Any:
    if is_dataclass(data):
        return convert_to_dict(asdict(data))  # type: ignore
    elif isinstance(data, SimpleNamespace):
        return convert_to_dict(data.__dict__)
    elif isinstance(data, dict):
        return dict(map(lambda kv: (kv[0], convert_to_dict(kv[1])), data.items()))
    elif isinstance(data, list) or isinstance(data, tuple):
        return list(map(lambda v: convert_to_dict(v), data))
    else:
        return data


def _call_return(op: str, params: dict) -> Any:
    """Make a call to the server and return the result"""
    global started, pending_calls

    if not started:
        raise Exception(
            "You cannot call bot functions that require a return value until after start()"
        )

    converted = convert_to_dict(params)
    msg_id = _generate_id()

    # Create event to wait for response
    event = threading.Event()
    pending_responses[msg_id] = event

    # Send the call message
    _ws_send(
        {
            "type": "call",
            "id": msg_id,
            "data": {
                "op": op,
                "input": {
                    "context": bot_context,
                    "params": converted,
                },
            },
        }
    )

    # Wait for response with timeout
    if not event.wait(timeout=300):
        del pending_responses[msg_id]
        raise Exception("Call timed out")

    # Get and clean up response
    result = response_data.pop(msg_id, None)
    del pending_responses[msg_id]

    if result is None:
        raise Exception("Invalid response")

    error = result.get("error")
    if error is not None:
        raise Exception(error)

    return result.get("data")


def _call_no_return(op: str, params: Any) -> None:
    """Make a call to the server without expecting a return value"""
    global started, pending_calls

    if not started:
        pending_calls.append(Call(op=op, params=params))
        return

    converted = convert_to_dict(params)
    msg_id = _generate_id()

    # Create event to wait for response
    event = threading.Event()
    pending_responses[msg_id] = event

    # Send the call message
    _ws_send(
        {
            "type": "call",
            "id": msg_id,
            "data": {
                "op": op,
                "input": {
                    "context": bot_context,
                    "params": converted,
                },
            },
        }
    )

    # Wait for response with timeout
    if not event.wait(timeout=300):
        del pending_responses[msg_id]
        raise Exception("Call timed out")

    # Get and clean up response
    result = response_data.pop(msg_id, None)
    del pending_responses[msg_id]

    if result is None:
        raise Exception("Invalid response")

    error = result.get("error")
    if error is not None:
        raise Exception(error)
