"""Chat-Antworten auf Local-Chat (Anti-Bot-Check-Abwehr).

Leute testen Bots, indem sie sie ansprechen ("jo", "wie lange huntest
du noch?"). Wer nie antwortet, faellt auf. Der Waechter beobachtet die
letzten Chat-Zeilen (billiger Hash-Vergleich); bei einer neuen Zeile
liest das lokale Vision-LLM (qwen2.5vl) die Zeile und formuliert eine
kurze, lockere Antwort. Der Bot bleibt kurz stehen, tippt menschlich
verzoegert und laeuft weiter.

Schutz gegen Unsinn:
- Ohne LLM-Endpunkt passiert nichts (kein OCR-Ratefall).
- LLM-Aufrufe gedrosselt (alle 15s max), globale Antwort-Cooldowns,
  Cooldown pro Ansprechpartner (2 Min).
- Eigene Zeilen (Bot-Says, Heil-Chat, eigene Antworten) werden dem LLM
  als "von dir" mitgegeben und nie beantwortet.

Config per Env:
  TIBIAPILOT_CHATREPLY=0  -> aus
"""
import base64
import json
import os
import random
import re
import urllib.request
from time import monotonic

import cv2

from src.gameplay.llmWorker import PENDING, poll, submit
from src.gameplay.monitoring import logEvent
from src.gameplay.recovery import _llmEndpoint
from src.repositories.chat.core import getChatMessagesContainerPosition
from src.utils import humanize
from src.utils.core import hashit
from src.utils.keyboard import press, write

CHATREPLY_ENV = 'TIBIAPILOT_CHATREPLY'
WATCH_INTERVAL_SECONDS = 2
LLM_INTERVAL_SECONDS = 15
GLOBAL_COOLDOWN_SECONDS = 20
SPEAKER_COOLDOWN_SECONDS = 120
_MAX_SPOKEN = 20

_PROMPT = (
    'You see the chat area of the MMORPG Tibia. Player messages look like '
    '"Playername: some text". Server messages have no name prefix. '
    'Find the LAST line that matches the player-message pattern and transcribe it. '
    'Detect its language. '
    'Answer ONLY with JSON: {"speaker": "<name>", "language": "<english|other>", '
    '"text": "<their message>", '
    '"reply": "<ONLY if language is english: a short natural answer, max 5 words, '
    'casual lowercase english, direct and plain — greetings and questions SHOULD '
    'be answered (silence looks suspicious). Good examples: \\"yo\\", \\"hey\\", '
    '\\"a few more rounds\\", \\"leaving soon\\", \\"lol yeah\\", \\"idk\\". '
    'Never invent facts, never answer with a question, never use phrases like '
    '\\"check map\\". If unsure, use \\"idk\\". '
    'Empty string if language is not english or no player message exists>"}'
)


def _sanitize(reply):
    text = re.sub(r'\s+', ' ', str(reply)).strip()
    if not text or len(text) > 60:
        return None
    return text


def _askLlm(baseUrl, model, region, spoken):
    ok, buffer = cv2.imencode('.png', region)
    if not ok:
        return None
    spokenNote = ''
    if spoken:
        spokenNote = (
            ' Lines written by the account itself (never reply to these): '
            + '; '.join(spoken[-5:]))
    payload = {
        'model': model,
        'stream': False,
        'format': 'json',
        'messages': [{
            'role': 'user',
            'content': _PROMPT + spokenNote,
            'images': [base64.b64encode(buffer).decode('ascii')],
        }],
    }
    try:
        request = urllib.request.Request(
            f'{baseUrl}/api/chat', data=json.dumps(payload).encode('utf-8'),
            headers={'Content-Type': 'application/json'})
        with urllib.request.urlopen(request, timeout=30) as response:
            data = json.loads(response.read().decode('utf-8'))
        return json.loads(data['message']['content'])
    except Exception:
        return None


def _respond(context, settings, hint, now):
    if str(hint.get('language', '')).lower() == 'other':
        # Fremdsprache → immer die klassische kurze Rückfrage
        reply = 'english?'
    else:
        reply = _sanitize(hint.get('reply'))
    if reply is None:
        return False
    speaker = str(hint.get('speaker') or '').strip().lower()
    speakers = settings.setdefault('speakers', {})
    if speaker and now - speakers.get(speaker, 0) < SPEAKER_COOLDOWN_SECONDS:
        return False
    # kurz stehen bleiben, menschlich verzoegert antworten, weiterlaufen
    context['ng_tasksOrchestrator'].reset()
    humanize.pause(random.uniform(1.5, 4.0))
    write(reply)
    press('enter')
    settings['lastReplyAt'] = now
    if speaker:
        speakers[speaker] = now
    spoken = settings.setdefault('spoken', [])
    spoken.append(reply.lower())
    del spoken[:max(0, len(spoken) - _MAX_SPOKEN)]
    logEvent('chatReply', {
        'speaker': speaker, 'text': str(hint.get('text', ''))[:120],
        'reply': reply,
    })
    return True


def watchChatReply(context):
    """Loop-Hook: erkennt Ansprache im Local-Chat und antwortet kurz."""
    if os.getenv(CHATREPLY_ENV, '1') == '0':
        return context
    if context['ng_pause']:
        return context
    settings = context.setdefault('ng_chatReply', {})
    now = monotonic()

    # Laufenden LLM-Job auswerten (blockiert nie den Loop)
    jobId = settings.get('llmJob')
    if jobId is not None:
        hint = poll(jobId)
        if hint is PENDING:
            return context
        settings['llmJob'] = None
        if hint and _respond(context, settings, hint, now):
            settings['lastHash'] = None
        return context

    if now - settings.get('lastScanAt', 0) < WATCH_INTERVAL_SECONDS:
        return context
    settings['lastScanAt'] = now
    screenshot = context['ng_screenshot']
    if screenshot is None:
        return context
    box = getChatMessagesContainerPosition(screenshot)
    if box is None:
        return context
    x, y, w, h = box
    if w <= 0 or h <= 0:
        return context
    bottom = screenshot[y + max(0, h - 66):y + h, x:x + w]
    if bottom.shape[0] < 10 or bottom.shape[1] < 10:
        return context
    regionHash = hashit(bottom)
    previous = settings.get('lastHash')
    settings['lastHash'] = regionHash
    if previous is None or previous == regionHash:
        return context
    if now - settings.get('lastReplyAt', 0) < GLOBAL_COOLDOWN_SECONDS:
        return context
    if now - settings.get('lastLlmAt', 0) < LLM_INTERVAL_SECONDS:
        return context
    baseUrl, model = _llmEndpoint()
    if not baseUrl:
        return context
    region = bottom.copy()
    spoken = list(settings.get('spoken', []))
    jobId = submit(lambda: _askLlm(baseUrl, model, region, spoken))
    if jobId is not None:
        settings['lastLlmAt'] = now
        settings['llmJob'] = jobId
    return context
