import ctypes
import json
import os
import socket
from ctypes import wintypes
from pathlib import Path
from threading import Lock
from time import sleep

import serial
from serial.tools import list_ports

from src.utils import humanize


BAUD_RATE = 115200

# Eingabe-Methoden (in den Bot-Einstellungen umschaltbar).
INPUT_METHOD_MAC_BT = 'mac_bt'
INPUT_METHOD_USB = 'usb'
INPUT_METHODS = (INPUT_METHOD_MAC_BT, INPUT_METHOD_USB)
# ponytail: USB is the installer default until the Mac-Bluetooth route can be
# verified on a target PC; switch explicitly in settings when that changes.
DEFAULT_INPUT_METHOD = INPUT_METHOD_USB

INPUT_METHOD_ENV = 'TIBIAPILOT_INPUT_METHOD'
BRIDGE_ADDRESS_ENV = 'TIBIAPILOT_HID_BRIDGE'
BRIDGE_TOKEN_FILE_ENV = 'TIBIAPILOT_HID_TOKEN_FILE'
ARDUINO_PORT_ENV = 'TIBIAPILOT_ARDUINO_PORT'

_REPO_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_BRIDGE_TOKEN_FILE = _REPO_ROOT / '.hid-bridge-token'
INPUT_METHOD_FILE = _REPO_ROOT / '.input-method'
USB_IDENTITY_FILE = _REPO_ROOT / 'arduino' / 'usb-identity.json'


def _getarnteVid():
    """Kennung, unter der sich der geflashte Chip meldet.

    Das Board traegt bewusst nicht mehr die Arduino-Kennung, sondern die
    eines gewoehnlichen Eingabegeraets (siehe arduino/usb-identity.json).
    Der Bot muss es trotzdem finden - darum kommt sie hier zu den
    bekannten Kennungen dazu.
    """
    try:
        return int(json.loads(
            USB_IDENTITY_FILE.read_text(encoding='utf-8'))['vid'], 16)
    except Exception:
        return None


# Echte Arduino-Kennungen (ungeflasht und im Bootloader) plus die
# getarnte Kennung des geflashten Chips.
KNOWN_ARDUINO_VIDS = {0x2341, 0x2A03, 0x1B4F, 0x239A}
_getarnt = _getarnteVid()
if _getarnt is not None:
    KNOWN_ARDUINO_VIDS.add(_getarnt)

_lock = Lock()
_method = None
_serial = None


class ArduinoUnavailableError(RuntimeError):
    pass


class POINT(ctypes.Structure):
    _fields_ = [('x', wintypes.LONG), ('y', wintypes.LONG)]


def _cursorPosition():
    point = POINT()
    if not ctypes.windll.user32.GetCursorPos(ctypes.byref(point)):
        raise ctypes.WinError()
    return point.x, point.y


# ---------------------------------------------------------------------------
# Auswahl der Eingabe-Methode
# ---------------------------------------------------------------------------
def _normaliseMethod(value):
    name = (value or '').strip().lower()
    if name in ('mac', 'mac_bt', 'macbt', 'bluetooth', 'bt'):
        return INPUT_METHOD_MAC_BT
    if name in ('usb', 'arduino', 'serial', 'leonardo', 'usb_arduino'):
        return INPUT_METHOD_USB
    return None


def getInputMethod():
    override = _normaliseMethod(os.getenv(INPUT_METHOD_ENV))
    if override:
        return override
    if _method:
        return _method
    try:
        stored = _normaliseMethod(INPUT_METHOD_FILE.read_text(encoding='ascii'))
        if stored:
            return stored
    except OSError:
        pass
    return DEFAULT_INPUT_METHOD


def setInputMethod(method):
    global _method, _serial
    normalised = _normaliseMethod(method)
    if normalised is None:
        raise ValueError(f'Unbekannte Eingabe-Methode: {method}')
    with _lock:
        if normalised != _method and _serial is not None:
            # Beim Wechsel weg vom USB-Board den COM-Port sofort freigeben.
            try:
                _serial.close()
            except serial.SerialException:
                pass
            _serial = None
        _method = normalised
        try:
            INPUT_METHOD_FILE.write_text(normalised, encoding='ascii')
        except OSError:
            pass
    return normalised


# ---------------------------------------------------------------------------
# USB-Transport (physischer Arduino 32u4 am Windows-PC)
# ---------------------------------------------------------------------------
def detectArduinoPort():
    """Board am VID (oder Namen) erkennen — unabhaengig von der Nummer."""
    for port in list_ports.comports():
        description = ' '.join(filter(None, (
            port.description, port.manufacturer, port.product,
        ))).lower()
        if (port.vid in KNOWN_ARDUINO_VIDS or
                any(name in description for name in (
                    'arduino leonardo', 'arduino micro', 'pro micro',
                ))):
            return port.device
    return None


def getArduinoPort():
    """Bevorzugt den eingestellten Port — faellt aber zurueck, wenn der
    nicht mehr existiert.

    Ein 32u4 wechselt nach dem Flashen die COM-Nummer (z. B. COM3 -> COM5).
    Eine veraltete Vorgabe darf den Bot deshalb nicht dauerhaft blockieren.
    """
    configured = os.getenv(ARDUINO_PORT_ENV)
    if configured:
        available = {port.device for port in list_ports.comports()}
        if configured in available:
            return configured
    return detectArduinoPort()


def _serialResponse(connection):
    return connection.readline().decode('ascii', errors='replace').strip()


def _responseTimeout(command):
    """Wie lange das Board fuer diesen Befehl braucht (Sekunden).

    Die Firmware tippt bewusst menschlich: 80-270 ms je Zeichen, nach
    Leerzeichen zusaetzlich bis 450 ms. Erst danach bestaetigt sie.
    Ein festes 1-Sekunden-Fenster laesst deshalb jedes laengere
    writeHex (z. B. das Passwort beim Login) fehlschlagen.
    """
    if command.startswith('writeHex,'):
        characters = len(command.split(',', 1)[1]) // 2
        return 5 + characters * 0.8
    return 2


def _serialWrite(connection, command):
    if not command or '\n' in command or '\r' in command or len(command) > 4096:
        raise ValueError('Ungültiger Arduino-Befehl.')
    connection.write(f'{command}\n'.encode('utf-8'))
    connection.flush()
    connection.timeout = _responseTimeout(command)
    response = _serialResponse(connection)
    if not response.startswith('OK'):
        raise ArduinoUnavailableError(
            f'Arduino hat den Befehl nicht bestätigt: {response or "keine Antwort"}'
        )


def _closeSerial():
    global _serial
    if _serial is not None:
        try:
            _serial.close()
        except serial.SerialException:
            pass
    _serial = None


def _openSerial():
    global _serial
    if _serial is not None and _serial.is_open:
        return _serial
    port = getArduinoPort()
    if port is None:
        raise ArduinoUnavailableError(
            'Kein Arduino Leonardo/Pro Micro (32u4) erkannt.'
        )
    try:
        _serial = serial.Serial(port, BAUD_RATE, timeout=1, write_timeout=1)
        sleep(2)
        _serial.reset_input_buffer()
        _serialWrite(_serial, 'health')
        return _serial
    except (OSError, serial.SerialException, ArduinoUnavailableError) as error:
        _closeSerial()
        raise ArduinoUnavailableError(
            'Arduino erkannt, aber PilotNG-HID antwortet nicht.'
        ) from error


def _usbSend(command):
    last_error = None
    for _ in range(2):
        try:
            _serialWrite(_openSerial(), command)
            return True
        except (OSError, serial.SerialException, ArduinoUnavailableError) as error:
            last_error = error
            _closeSerial()
    raise ArduinoUnavailableError('Arduino-Verbindung abgebrochen.') from last_error


def _usbHealthy():
    try:
        _usbSend('health')
        return True
    except ArduinoUnavailableError:
        return False


# ---------------------------------------------------------------------------
# Mac-Bluetooth-Transport (Mac wirkt per Bluetooth als HID-Tastatur/Maus)
# ---------------------------------------------------------------------------
def _bridgeAddress():
    configured = os.getenv(BRIDGE_ADDRESS_ENV)
    if not configured:
        return None
    try:
        host, port = configured.rsplit(':', 1)
        port = int(port)
    except ValueError:
        return None
    if not host or not 1 <= port <= 65535:
        return None
    return host, port


def _bridgeToken():
    token_file = Path(os.getenv(BRIDGE_TOKEN_FILE_ENV, DEFAULT_BRIDGE_TOKEN_FILE))
    try:
        token = token_file.read_text(encoding='ascii').strip()
    except OSError:
        return None
    return token if 32 <= len(token) <= 256 else None


def _bridgeCommand(command):
    """Den Arduino-Wire-Dialekt in den BTRemote-Dialekt übersetzen."""
    if command.startswith('writeHex,'):
        try:
            text = bytes.fromhex(command[len('writeHex,'):]).decode('utf-8')
        except (ValueError, UnicodeDecodeError) as error:
            raise ArduinoUnavailableError(
                'Ungültiger Text für Mac-Bluetooth-Eingabe.'
            ) from error
        text = text.replace('\n', ' ').replace('\r', ' ')
        return f'write,{text}'
    if command.startswith('scroll,'):
        parts = command.split(',')
        if len(parts) == 2:
            return f'scroll,0,0,{parts[1]}'
    return command


def _bridgeRequest(command, address, token, timeout=1.5):
    try:
        with socket.create_connection(address, timeout=timeout) as connection:
            connection.sendall(f'{token}\t{command}\n'.encode('utf-8'))
            response = bytearray()
            while len(response) < 128 and b'\n' not in response:
                chunk = connection.recv(128 - len(response))
                if not chunk:
                    break
                response.extend(chunk)
        return bytes(response).split(b'\n', 1)[0].decode('ascii', errors='replace')
    except OSError:
        return None


def _bridgeConfig():
    address = _bridgeAddress()
    if address is None:
        raise ArduinoUnavailableError(
            'Mac-Bluetooth-Bridge nicht konfiguriert (TIBIAPILOT_HID_BRIDGE fehlt).'
        )
    token = _bridgeToken()
    if token is None:
        raise ArduinoUnavailableError(
            'Mac-Bluetooth-Bridge-Token fehlt (.hid-bridge-token).'
        )
    return address, token


def _macSend(command):
    address, token = _bridgeConfig()
    wire = _bridgeCommand(command)
    last = None
    for _ in range(2):
        response = _bridgeRequest(wire, address, token)
        if response and response.startswith('OK'):
            return True
        last = response
    if last is None:
        raise ArduinoUnavailableError(
            'Mac-Bluetooth-Bridge nicht erreichbar.'
        )
    if last == 'ERR disconnected':
        raise ArduinoUnavailableError(
            'Mac ist nicht per Bluetooth mit diesem PC gekoppelt.'
        )
    raise ArduinoUnavailableError(
        f'Mac-Bluetooth-Bridge lehnte Befehl ab: {last}'
    )


def _macHealthy():
    address = _bridgeAddress()
    token = _bridgeToken()
    if address is None or token is None:
        return False
    return _bridgeRequest('health', address, token) == 'OK connected'


# ---------------------------------------------------------------------------
# Router (backend-neutral) – öffentliche API bleibt stabil
# ---------------------------------------------------------------------------
def _send(command):
    if getInputMethod() == INPUT_METHOD_MAC_BT:
        return _macSend(command)
    return _usbSend(command)


def _moveTo(x, y):
    x = humanize.jitterCoordinate(x)
    y = humanize.jitterCoordinate(y)
    for _ in range(120):
        current_x, current_y = _cursorPosition()
        dx, dy = x - current_x, y - current_y
        if abs(dx) <= 2 and abs(dy) <= 2:
            humanize.pause(0.03)
            return True
        if abs(dx) > 100 or abs(dy) > 100:
            # Die Windows-Zeigerbeschleunigung verstaerkt schnelle
            # HID-Moves (~2,5x). Grosse Distanzen darum kompensiert
            # annaehern, der Restweg bleibt dann exakt steuerbar.
            step_x = max(-100, min(100, int(dx * 0.4)))
            step_y = max(-100, min(100, int(dy * 0.4)))
            pause = 0.05
        else:
            # Nah am Ziel: kleine, langsame Schritte bleiben 1:1.
            step_x = max(-10, min(10, dx))
            step_y = max(-10, min(10, dy))
            pause = 0.1
        _send(f'move,{step_x},{step_y}')
        humanize.pause(pause)
    raise ArduinoUnavailableError('Maus konnte das Ziel nicht erreichen.')


def sendCommandArduino(command):
    with _lock:
        if command.startswith('moveTo,'):
            try:
                _, x, y = command.split(',')
                return _moveTo(int(x), int(y))
            except ValueError as error:
                raise ValueError('Ungültige Mauskoordinate.') from error
        return _send(command)


def requireArduino():
    with _lock:
        if getInputMethod() == INPUT_METHOD_MAC_BT:
            address, token = _bridgeConfig()
            if _bridgeRequest('health', address, token) != 'OK connected':
                raise ArduinoUnavailableError(
                    'Mac ist nicht per Bluetooth mit diesem PC gekoppelt.'
                )
            return True
        _usbSend('health')
        return True


def isArduinoAvailable():
    with _lock:
        if getInputMethod() == INPUT_METHOD_MAC_BT:
            return _macHealthy()
        return _usbHealthy()
