"""Bildquelle ueber OBS statt ueber die Grafikkarte (dxcam).

Warum: Der Bot spielt auf einem virtuellen Bildschirm (usbmmidd), damit
kein echter Monitor belegt wird. dxcam und die Windows-Bildschirmaufnahme
koennen von diesem Treiber aber KEIN Bild lesen - sie liefern nur Schwarz.
OBS greift das Tibia-Fenster fensterbasiert ab (Game Capture) und liefert
das Bild zuverlaessig, egal auf welchem Bildschirm.

Diese Klasse haelt eine offene WebSocket-Verbindung zu OBS und fordert pro
Frame ein Bild der Tibia-Quelle an. Die Verbindung wird bei einem Abbruch
automatisch neu aufgebaut.
"""
import base64
import hashlib
import json
import os
from pathlib import Path

import cv2
import numpy as np
import websocket

from src.shared.typings import GrayImage

# Bildformat fuer die OBS-Anfrage. Nur verlustfreie Formate sind
# zulaessig - siehe _screenshotRequest(). Umstellbar nur fuer Messungen.
FORMAT = os.getenv('TIBIAPILOT_OBS_FORMAT', 'png')
QUALITY = int(os.getenv('TIBIAPILOT_OBS_QUALITY', '-1'))

_CONFIG = (Path.home()
           / 'AppData/Roaming/obs-studio/plugin_config'
           / 'obs-websocket/config.json')


class ObsCamera:
    def __init__(self, width: int = 1920, height: int = 1080):
        self._width = width
        self._height = height
        self._ws = None
        self._source = None

    # -- Verbindung --------------------------------------------------------
    def _receive(self, expected_op):
        while True:
            message = json.loads(self._ws.recv())
            if message.get('op') == expected_op:
                return message['d']

    def _request(self, request_type, request_data=None):
        self._ws.send(json.dumps({
            'op': 6,
            'd': {
                'requestType': request_type,
                'requestId': request_type,
                'requestData': request_data or {},
            },
        }))
        response = self._receive(7)
        status = response['requestStatus']
        if not status['result']:
            raise RuntimeError(f"{request_type}: {status.get('comment', 'failed')}")
        return response.get('responseData', {})

    def _connect(self):
        config = json.loads(_CONFIG.read_text(encoding='utf-8-sig'))
        port = config.get('server_port', 4455)
        self._ws = websocket.create_connection(
            f'ws://127.0.0.1:{port}', timeout=5)
        hello = self._receive(0)
        identify = {'rpcVersion': hello['rpcVersion']}
        authentication = hello.get('authentication')
        if authentication:
            secret = base64.b64encode(hashlib.sha256(
                (config['server_password'] + authentication['salt']).encode()
            ).digest()).decode()
            identify['authentication'] = base64.b64encode(hashlib.sha256(
                (secret + authentication['challenge']).encode()
            ).digest()).decode()
        self._ws.send(json.dumps({'op': 1, 'd': identify}))
        self._receive(2)
        self._source = self._pick_source()

    def _pick_source(self):
        inputs = self._request('GetInputList')['inputs']
        namen = sorted(
            (item['inputName'] for item in inputs),
            key=lambda name: ('tibia' not in name.lower(), name.lower()),
        )
        if not namen:
            raise RuntimeError('OBS kennt keine Eingabequelle.')
        return namen[0]

    def close(self):
        if self._ws is not None:
            try:
                self._ws.close()
            except Exception:
                pass
            self._ws = None

    # -- Bild --------------------------------------------------------------
    def _screenshotRequest(self):
        """Anfrage fuer ein Bild - bewusst VERLUSTFREI.

        Hier stand einmal JPEG mit Qualitaet 50, weil das rund sechsmal
        schneller ist. Das war ein schwerer Fehler: Die Auswertung des
        Bots vergleicht EXAKTE Grauwerte - der Lebensbalken zaehlt
        Pixel gegen eine feste Farbliste, Zahlen und Kreaturen werden
        ueber Pruefsummen erkannt. JPEG verschiebt jeden Wert um ein
        paar Stufen. Gemessen am vollen Lebensbalken: verlustfrei
        stimmten 94 von 94 Pixeln (100 %), mit JPEG genau eines (1 %).
        Der Bot hielt sich fuer todgeweiht und heilte ununterbrochen.

        Die Mustersuche (Vorlagen) haette die Unschaerfe verkraftet -
        alles andere nicht.
        """
        return {
            'sourceName': self._source,
            'imageFormat': FORMAT,
            'imageWidth': self._width,
            'imageHeight': self._height,
            'imageCompressionQuality': QUALITY,
        }

    def grab(self) -> GrayImage:
        if self._ws is None:
            self._connect()
        try:
            data = self._request(
                'GetSourceScreenshot', self._screenshotRequest())['imageData']
        except Exception:
            # Verbindung neu aufbauen und ein einziges Mal erneut versuchen.
            self.close()
            self._connect()
            data = self._request(
                'GetSourceScreenshot', self._screenshotRequest())['imageData']
        raw = base64.b64decode(data.split(',', 1)[1])
        buffer = np.frombuffer(raw, dtype=np.uint8)
        color = cv2.imdecode(buffer, cv2.IMREAD_COLOR)
        if color is None:
            raise RuntimeError('OBS lieferte kein lesbares Bild.')
        return cv2.cvtColor(color, cv2.COLOR_BGR2GRAY)
