"""AR488VisaLibrary — pyvisa backend for AR488/Prologix GPIB adapters.

This is the main backend class that pyvisa loads when you use
``pyvisa.ResourceManager("@ar488")``. It manages sessions (one per
open instrument) and delegates I/O to a transport layer.

Two operating modes:
- **Direct mode** (standalone): opens serial/TCP directly
- **Bridge mode** (mcgpib): shares the existing BridgeConnection
"""

import logging
import random
from typing import Any

from pyvisa import constants, highlevel, rname
from pyvisa.constants import ResourceAttribute, StatusCode

from pyvisa_ar488.sessions import AR488InstrSession

logger = logging.getLogger(__name__)

# Type aliases matching pyvisa's convention
VISASession = int
VISARMSession = int
VISAEventContext = int


class AR488VisaLibrary(highlevel.VisaLibraryBase):
    """PyVISA backend for AR488/Prologix GPIB-USB adapters.

    Implements the VisaLibraryBase interface, routing read/write calls
    through AR488 ++ command sequences.
    """

    # --- Session registry ---
    # Maps integer session handles to AR488InstrSession instances
    _sessions: dict[int, AR488InstrSession]
    _rm_session: int | None
    _transport: Any  # AR488Transport (direct or bridge)

    def _init(self) -> None:
        """Called by VisaLibraryBase.__new__ after construction."""
        self._sessions = {}
        self._rm_session = None
        self._transport = None
        self._serial_port = None
        self._baudrate = 115200
        self._tcp_host = None
        self._tcp_port = 23
        self._read_timeout_ms = 3000

    # --- Factory class methods ---

    @classmethod
    def from_serial(
        cls, port: str = "/dev/ttyUSB0", baudrate: int = 115200, read_timeout_ms: int = 3000
    ) -> "AR488VisaLibrary":
        """Create a backend with direct serial transport."""
        lib = cls()
        lib._serial_port = port
        lib._baudrate = baudrate
        lib._read_timeout_ms = read_timeout_ms
        return lib

    @classmethod
    def from_tcp(
        cls, host: str, port: int = 23, read_timeout_ms: int = 3000
    ) -> "AR488VisaLibrary":
        """Create a backend with direct TCP transport."""
        lib = cls()
        lib._tcp_host = host
        lib._tcp_port = port
        lib._read_timeout_ms = read_timeout_ms
        return lib

    @classmethod
    def from_bridge(cls, bridge_connection, event_loop) -> "AR488VisaLibrary":
        """Create a backend wrapping mcgpib's BridgeConnection.

        This is the bridge mode — no new serial port is opened.
        The transport routes through the existing async bridge.
        """
        from pyvisa_ar488.bridge_transport import BridgeTransport

        lib = cls()
        lib._transport = BridgeTransport(bridge_connection, event_loop)
        return lib

    # --- Transport lifecycle ---

    def _ensure_transport(self) -> None:
        """Create and connect transport if not already done."""
        if self._transport is not None:
            if not self._transport.is_connected():
                self._transport.connect()
            return

        # Direct mode — create transport based on configuration
        if self._tcp_host:
            from pyvisa_ar488.direct_transport import DirectTcpTransport

            self._transport = DirectTcpTransport(
                host=self._tcp_host,
                port=self._tcp_port,
                read_timeout_ms=self._read_timeout_ms,
            )
        else:
            from pyvisa_ar488.direct_transport import DirectSerialTransport

            port = self._serial_port or "/dev/ttyUSB0"
            self._transport = DirectSerialTransport(
                port=port,
                baudrate=self._baudrate,
                read_timeout_ms=self._read_timeout_ms,
            )

        self._transport.connect()

    def _new_session_handle(self) -> int:
        """Generate a unique random session handle."""
        while True:
            handle = random.randint(1_000_000, 9_999_999)
            if handle not in self._sessions and handle != self._rm_session:
                return handle

    # --- VisaLibraryBase required methods ---

    @staticmethod
    def get_library_paths():
        return ("ar488",)

    @staticmethod
    def get_debug_info():
        from pyvisa_ar488 import __version__

        return {
            "Version": __version__,
            "Backend": "AR488/Prologix GPIB-USB",
            "Description": "Routes pyvisa calls through AR488 ++ command protocol",
        }

    def open_default_resource_manager(self) -> tuple[VISARMSession, StatusCode]:
        self._rm_session = self._new_session_handle()
        return self._rm_session, StatusCode.success

    def list_resources(
        self, session: VISARMSession, query: str = "?*::INSTR"
    ) -> tuple[str, ...]:
        """Scan the GPIB bus and return VISA resource names."""
        self._ensure_transport()
        try:
            addresses = self._transport.find_listeners()
        except Exception:
            logger.warning("Bus scan failed", exc_info=True)
            return ()

        resources = tuple(f"GPIB0::{addr}::INSTR" for addr in addresses)

        # Filter by query pattern if not wildcard
        if query and query != "?*::INSTR" and query != "?*":
            filtered = []
            for r in resources:
                if rname.filter(r, query):
                    filtered.append(r)
            return tuple(filtered)

        return resources

    def open(
        self,
        session: VISARMSession,
        resource_name: str,
        access_mode: constants.AccessModes = constants.AccessModes.no_lock,
        open_timeout: int = constants.VI_TMO_IMMEDIATE,
    ) -> tuple[VISASession, StatusCode]:
        """Open a GPIB resource, returning a session handle."""
        self._ensure_transport()

        # Parse the VISA resource name
        parsed = rname.parse_resource_name(resource_name)
        primary_address = int(parsed.primary_address)
        secondary_address = None
        if hasattr(parsed, "secondary_address") and parsed.secondary_address is not None:
            secondary_address = int(parsed.secondary_address)

        sess = AR488InstrSession(
            transport=self._transport,
            resource_name=resource_name,
            primary_address=primary_address,
            secondary_address=secondary_address,
            open_timeout=open_timeout,
        )

        handle = self._new_session_handle()
        self._sessions[handle] = sess
        return handle, StatusCode.success

    def close(
        self, session: VISASession | VISAEventContext | VISARMSession
    ) -> StatusCode:
        """Close a session."""
        if session == self._rm_session:
            # Closing the resource manager — close all sessions
            for sess in self._sessions.values():
                sess.close()
            self._sessions.clear()
            if self._transport:
                self._transport.disconnect()
                self._transport = None
            self._rm_session = None
            return StatusCode.success

        sess = self._sessions.pop(session, None)
        if sess:
            return sess.close()
        return StatusCode.error_invalid_object

    def read(self, session: VISASession, count: int) -> tuple[bytes, StatusCode]:
        sess = self._sessions.get(session)
        if not sess:
            return b"", StatusCode.error_invalid_object
        return sess.read(count)

    def write(self, session: VISASession, data: bytes) -> tuple[int, StatusCode]:
        sess = self._sessions.get(session)
        if not sess:
            return 0, StatusCode.error_invalid_object
        return sess.write(data)

    def get_attribute(
        self,
        session: VISASession | VISAEventContext | VISARMSession,
        attribute: constants.ResourceAttribute | constants.EventAttribute,
    ) -> tuple[Any, StatusCode]:
        # Resource manager attributes
        if session == self._rm_session:
            if attribute == ResourceAttribute.resource_name:
                return "AR488", StatusCode.success
            return 0, StatusCode.error_nonsupported_attribute

        sess = self._sessions.get(session)
        if not sess:
            return 0, StatusCode.error_invalid_object
        return sess.get_attribute(attribute)

    def set_attribute(
        self,
        session: VISASession,
        attribute: constants.ResourceAttribute,
        attribute_state: Any,
    ) -> StatusCode:
        if session == self._rm_session:
            return StatusCode.error_nonsupported_attribute

        sess = self._sessions.get(session)
        if not sess:
            return StatusCode.error_invalid_object
        return sess.set_attribute(attribute, attribute_state)

    # --- Device control ---

    def clear(self, session: VISASession) -> StatusCode:
        sess = self._sessions.get(session)
        if not sess:
            return StatusCode.error_invalid_object
        return sess.clear()

    def read_stb(self, session: VISASession) -> tuple[int, StatusCode]:
        sess = self._sessions.get(session)
        if not sess:
            return 0, StatusCode.error_invalid_object
        return sess.read_stb()

    def assert_trigger(
        self, session: VISASession, protocol: constants.TriggerProtocol
    ) -> StatusCode:
        sess = self._sessions.get(session)
        if not sess:
            return StatusCode.error_invalid_object
        return sess.assert_trigger(protocol)

    # --- GPIB-specific ---

    def gpib_send_ifc(self, session: VISASession) -> StatusCode:
        self._ensure_transport()
        self._transport.interface_clear()
        return StatusCode.success

    def gpib_control_ren(
        self, session: VISASession, mode: constants.RENLineOperation
    ) -> StatusCode:
        self._ensure_transport()
        if mode in (
            constants.RENLineOperation.assert_,
            constants.RENLineOperation.assert_address,
        ):
            self._transport.send_raw("++ren 1")
        elif mode == constants.RENLineOperation.deassert:
            self._transport.send_raw("++ren 0")
        return StatusCode.success

    def gpib_command(
        self, session: VISASession, data: bytes
    ) -> tuple[int, StatusCode]:
        # GPIB command bytes — not commonly used through pyvisa
        logger.debug("gpib_command: %r (not implemented for AR488)", data)
        return len(data), StatusCode.success

    # --- Unsupported but required stubs ---

    def flush(self, session: VISASession, mask: constants.BufferOperation) -> StatusCode:
        return StatusCode.success

    def lock(
        self, session: VISASession, lock_type: constants.Lock,
        timeout: int, requested_key: str | None = None,
    ) -> tuple[str, StatusCode]:
        return "ar488", StatusCode.success

    def unlock(self, session: VISASession) -> StatusCode:
        return StatusCode.success

    def gpib_control_atn(
        self, session: VISASession, mode: constants.ATNLineOperation
    ) -> StatusCode:
        return StatusCode.success

    def gpib_pass_control(
        self, session: VISASession, primary_address: int, secondary_address: int
    ) -> StatusCode:
        return StatusCode.error_nonsupported_operation

    def enable_event(self, session, event_type, mechanism, context=None):
        return StatusCode.success

    def disable_event(self, session, event_type, mechanism):
        return StatusCode.success

    def discard_events(self, session, event_type, mechanism):
        return StatusCode.success

    def wait_on_event(self, session, in_event_type, timeout):
        return in_event_type, 0, StatusCode.error_timeout

    def install_handler(self, session, event_type, handler, user_handle):
        return handler, user_handle, None, StatusCode.success

    def uninstall_handler(self, session, event_type, handler, user_handle=None):
        return StatusCode.success

    def parse_resource(self, session, resource_name):
        parsed = rname.parse_resource_name(resource_name)
        info = rname.ResourceInfo(
            interface_type=constants.InterfaceType.gpib,
            board_number=int(getattr(parsed, "board", 0)),
            resource_class=getattr(parsed, "resource_class", "INSTR"),
            resource_name=resource_name,
            alias=None,
        )
        return info, StatusCode.success

    def parse_resource_extended(self, session, resource_name):
        return self.parse_resource(session, resource_name)
