import asyncio
import dataclasses
import json
import logging
import re
import sys
import threading
import traceback
from concurrent.futures import ThreadPoolExecutor
from typing import Dict
from typing import Type
from typing import TypeVar

from .handlers import SphinxHandler
from .util import send_error

HANDLERS = SphinxHandler()
logger = logging.getLogger(__name__)


T = TypeVar("T")


def parse_message(obj: Dict, cls: Type[T]) -> T:
    """Convert a raw dict into the given type"""

    if dataclasses.is_dataclass(cls):
        kwargs = {}
        fields = {f.name: f for f in dataclasses.fields(cls)}

        for key, value in obj.items():
            kwargs[key] = parse_message(value, fields[key].type)

        return cls(**kwargs)  # type: ignore[return-value]

    return obj  # type: ignore[return-value]


def handle_message(data: bytes):
    message = json.loads(data.decode("utf8"))

    method = message.get("method", None)
    if not method:
        # TODO: Return a valid JSON-RPC error
        raise TypeError("Invalid message")

    result = HANDLERS.get(method)
    if result is None:
        # TODO: Return a valid JSON-RPC error
        raise TypeError(f"Unknown method: '{method}'")

    type_, handler = result
    obj = parse_message(message, type_)
    try:
        handler(obj)
    except Exception as e:
        msg_id = message.get("id", None)
        if msg_id is not None:
            send_error(
                id=msg_id,
                code=-32602,
                message=f"{e}",
                data=dict(traceback=traceback.format_exc()),
            )


async def main_loop(loop, executor, stop_event, rfile, proxy):
    """Originally taken from ``pygls``"""

    CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")

    # Initialize message buffer
    content_length = 0

    while not stop_event.is_set() and not rfile.closed:
        # Read a header line
        header = await loop.run_in_executor(executor, rfile.readline)
        if not header:
            break

        # Extract content length if possible
        if not content_length:
            match = CONTENT_LENGTH_PATTERN.fullmatch(header)
            if match:
                content_length = int(match.group(1))
                logger.debug("Content length: %s", content_length)

        # Check if all headers have been read (as indicated by an empty line \r\n)
        if content_length and not header.strip():
            # Read body
            body = await loop.run_in_executor(executor, rfile.read, content_length)
            if not body:
                break

            proxy(body)

            # Reset the buffer
            content_length = 0


async def main():
    loop = asyncio.get_running_loop()
    event = threading.Event()
    executor = ThreadPoolExecutor(max_workers=2)

    await main_loop(loop, executor, event, sys.stdin.buffer, handle_message)
