from __future__ import annotations

import asyncio
import contextlib
import copy
import inspect
import json
import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from enum import Enum
from typing import Any

from pydantic import BaseModel, ValidationError

from .exceptions import RequestError
from .task import (
    DefaultMessageDispatcher,
    InMemoryMessageQueue,
    InMemoryMessageStateStore,
    MessageDispatcher,
    MessageQueue,
    MessageSender,
    MessageStateStore,
    NotificationRunner,
    RequestRunner,
    RpcTask,
    RpcTaskKind,
    SenderFactory,
    TaskSupervisor,
)
from .telemetry import span_context

JsonValue = Any
MethodHandler = Callable[[str, JsonValue | None, bool], Awaitable[JsonValue | None]]


__all__ = ["Connection", "JsonValue", "MethodHandler", "StreamDirection", "StreamEvent"]


DispatcherFactory = Callable[
    [MessageQueue, TaskSupervisor, MessageStateStore, RequestRunner, NotificationRunner],
    MessageDispatcher,
]


class StreamDirection(str, Enum):
    INCOMING = "incoming"
    OUTGOING = "outgoing"


@dataclass(frozen=True, slots=True)
class StreamEvent:
    direction: StreamDirection
    message: dict[str, Any]


StreamObserver = Callable[[StreamEvent], Awaitable[None] | None]


class Connection:
    """Minimal JSON-RPC 2.0 connection over newline-delimited JSON frames."""

    def __init__(
        self,
        handler: MethodHandler,
        writer: asyncio.StreamWriter,
        reader: asyncio.StreamReader,
        *,
        queue: MessageQueue | None = None,
        state_store: MessageStateStore | None = None,
        dispatcher_factory: DispatcherFactory | None = None,
        sender_factory: SenderFactory | None = None,
        observers: list[StreamObserver] | None = None,
        listening: bool = True,
        receive_timeout: float | None = None,
    ) -> None:
        self._handler = handler
        self._writer = writer
        self._reader = reader
        self._next_request_id = 0
        self._state = state_store or InMemoryMessageStateStore()
        self._tasks = TaskSupervisor(source="acp.Connection")
        self._tasks.add_error_handler(self._on_task_error)
        self._queue = queue or InMemoryMessageQueue()
        self._closed = False
        self._disconnected = False
        self._sender = (sender_factory or self._default_sender_factory)(self._writer, self._tasks)
        self._observers: list[StreamObserver] = list(observers or [])
        self._receive_timeout = receive_timeout
        if listening:
            self._recv_task = self._tasks.create(
                self._receive_loop(),
                name="acp.Connection.receive",
                on_error=self._on_receive_error,
            )
        else:
            self._recv_task = None
        dispatcher_factory = dispatcher_factory or self._default_dispatcher_factory
        self._dispatcher = dispatcher_factory(
            self._queue,
            self._tasks,
            self._state,
            self._run_request,
            self._run_notification,
        )
        self._dispatcher.start()

    async def close(self) -> None:
        """Stop the receive loop and cancel any in-flight handler tasks."""
        if self._closed:
            return
        self._closed = True
        await self._dispatcher.stop()
        await self._sender.close()
        await self._tasks.shutdown()
        self._state.reject_all_outgoing(ConnectionError("Connection closed"))

    async def main_loop(self) -> None:
        try:
            await self._receive_loop()
        except Exception as exc:
            logging.exception("Connection main loop failed", exc_info=exc)
            self._on_receive_error(None, exc)  # type: ignore[arg-type]
            raise

    async def __aenter__(self) -> Connection:
        return self

    async def __aexit__(self, exc_type, exc, tb) -> None:
        await self.close()

    def add_observer(self, observer: StreamObserver) -> None:
        """Register a callback that receives every raw JSON-RPC message."""
        self._observers.append(observer)

    async def send_request(self, method: str, params: JsonValue | None = None) -> Any:
        self._raise_if_unavailable()
        request_id = self._next_request_id
        self._next_request_id += 1
        future = self._state.register_outgoing(request_id, method)
        payload = {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params}
        await self._sender.send(payload)
        self._notify_observers(StreamDirection.OUTGOING, payload)
        return await future

    async def send_notification(self, method: str, params: JsonValue | None = None) -> None:
        self._raise_if_unavailable()
        payload = {"jsonrpc": "2.0", "method": method, "params": params}
        await self._sender.send(payload)
        self._notify_observers(StreamDirection.OUTGOING, payload)

    async def _receive_loop(self) -> None:
        try:
            while True:
                line = await asyncio.wait_for(self._reader.readline(), timeout=self._receive_timeout)
                if not line:
                    break
                line = line.strip()
                if not line:
                    continue
                try:
                    message: dict[str, Any] = json.loads(line)
                except Exception:
                    logging.exception("Error parsing JSON-RPC message")
                    continue
                self._notify_observers(StreamDirection.INCOMING, message)
                await self._process_message(message)
        except asyncio.CancelledError:
            return
        except asyncio.TimeoutError:
            raise RequestError.internal_error({"details": "Agent timeout"}) from None
        self._disconnect()

    async def _process_message(self, message: dict[str, Any]) -> None:
        method = message.get("method")
        has_id = "id" in message
        if method is not None and has_id:
            await self._queue.publish(RpcTask(RpcTaskKind.REQUEST, message))
            return
        if method is not None and not has_id:
            await self._queue.publish(RpcTask(RpcTaskKind.NOTIFICATION, message))
            return
        if has_id:
            await self._handle_response(message)

    def _notify_observers(self, direction: StreamDirection, message: dict[str, Any]) -> None:
        if not self._observers:
            return
        snapshot = copy.deepcopy(message)
        event = StreamEvent(direction, snapshot)
        for observer in list(self._observers):
            try:
                result = observer(event)
            except Exception:
                logging.exception("Stream observer failed", exc_info=True)
                continue
            if inspect.isawaitable(result):
                self._tasks.create(
                    result,
                    name=f"acp.Connection.observer.{direction.value}",
                    on_error=self._on_observer_error,
                )

    def _on_observer_error(self, task: asyncio.Task[Any], exc: BaseException) -> None:
        logging.exception("Stream observer coroutine failed", exc_info=exc)

    async def _run_request(self, message: dict[str, Any]) -> Any:
        payload: dict[str, Any] = {"jsonrpc": "2.0", "id": message["id"]}
        method = message["method"]
        with span_context(
            "acp.request",
            attributes={"method": method},
        ):
            try:
                result = await self._handler(method, message.get("params"), False)
                if isinstance(result, BaseModel):
                    result = result.model_dump(
                        mode="json",
                        by_alias=True,
                        exclude_none=True,
                        exclude_unset=True,
                    )
                payload["result"] = result if result is not None else None
                await self._sender.send(payload)
                self._notify_observers(StreamDirection.OUTGOING, payload)
                return payload.get("result")
            except RequestError as exc:
                payload["error"] = exc.to_error_obj()
                await self._sender.send(payload)
                self._notify_observers(StreamDirection.OUTGOING, payload)
                raise
            except ValidationError as exc:
                err = RequestError.invalid_params({"errors": exc.errors()})
                payload["error"] = err.to_error_obj()
                await self._sender.send(payload)
                self._notify_observers(StreamDirection.OUTGOING, payload)
                raise err from None
            except Exception as exc:
                try:
                    data = json.loads(str(exc))
                except Exception:
                    data = {"details": str(exc)}
                err = RequestError.internal_error(data)
                payload["error"] = err.to_error_obj()
                await self._sender.send(payload)
                self._notify_observers(StreamDirection.OUTGOING, payload)
                raise err from None

    async def _run_notification(self, message: dict[str, Any]) -> None:
        method = message["method"]
        with span_context("acp.notification", attributes={"method": method}), contextlib.suppress(Exception):
            await self._handler(method, message.get("params"), True)

    async def _handle_response(self, message: dict[str, Any]) -> None:
        request_id = message["id"]
        result = message.get("result")
        if "result" in message:
            self._state.resolve_outgoing(request_id, result)
            return
        if "error" in message:
            error_obj = message.get("error") or {}
            self._state.reject_outgoing(
                request_id,
                RequestError(
                    error_obj.get("code", -32603),
                    error_obj.get("message", "Error"),
                    error_obj.get("data"),
                ),
            )
            return
        self._state.resolve_outgoing(request_id, None)

    def _on_receive_error(self, task: asyncio.Task[Any], exc: BaseException) -> None:
        logging.exception("Receive loop failed", exc_info=exc)
        self._disconnect()

    def _on_task_error(self, task: asyncio.Task[Any], exc: BaseException) -> None:
        logging.exception("Background task failed", exc_info=exc)

    def _default_dispatcher_factory(
        self,
        queue: MessageQueue,
        supervisor: TaskSupervisor,
        state: MessageStateStore,
        request_runner: RequestRunner,
        notification_runner: NotificationRunner,
    ) -> MessageDispatcher:
        return DefaultMessageDispatcher(
            queue=queue,
            supervisor=supervisor,
            store=state,
            request_runner=request_runner,
            notification_runner=notification_runner,
        )

    def _default_sender_factory(self, writer: asyncio.StreamWriter, supervisor: TaskSupervisor) -> MessageSender:
        return MessageSender(writer, supervisor)

    def _disconnect(self) -> None:
        if self._disconnected:
            return
        self._disconnected = True
        self._state.reject_all_outgoing(ConnectionError("Connection closed"))

    def _raise_if_unavailable(self) -> None:
        if self._disconnected or self._closed:
            raise ConnectionError("Connection closed")
