import json
from pathlib import Path

import cv2
from time import sleep, time
import traceback
import sys
from src.utils.keyboard import releaseAll
from src.gameplay.cavebot import resolveCavebotTasks, shouldAskForCavebotTasks
from src.gameplay.combo import comboSpells
from src.gameplay.bbot import handleBBotAutomation, handleBBotKeepAlive
from src.gameplay.core.middlewares.battleList import setBattleListMiddleware
from src.gameplay.core.middlewares.chat import setChatTabsMiddleware
from src.gameplay.core.middlewares.gameWindow import setDirectionMiddleware, setGameWindowCreaturesMiddleware, setGameWindowMiddleware, setHandleLootMiddleware
from src.gameplay.core.middlewares.playerStatus import setMapPlayerStatusMiddleware
from src.gameplay.core.middlewares.statsBar import setMapStatsBarMiddleware
from src.gameplay.core.middlewares.radar import setRadarMiddleware, setWaypointIndexMiddleware
from src.gameplay.core.middlewares.screenshot import setScreenshotMiddleware
from src.gameplay.core.middlewares.tasks import setCleanUpTasksMiddleware
from src.gameplay.core.tasks.collectDeadCorpse import ATTEMPT_LIMIT
from src.gameplay.core.tasks.lootCorpse import LootCorpseTask
from src.gameplay.resolvers import resolveTasksByWaypoint
from src.gameplay.failsafe import PlayerFailsafe
from src.gameplay.monitoring import logEvent
from src.gameplay.recovery import smartRecovery
from src.gameplay.session import SessionManager
from src.gameplay.smartSync import autoSyncActionBar
from src.gameplay.stats import stats as sessionStats
from src.gameplay.supplies import watchSupplies
from src.gameplay.sessionRhythm import sessionRhythm
from src.gameplay.captureWatch import watchCapture
from src.gameplay.chatReply import watchChatReply
from src.gameplay.chatWatch import watchChat
from src.gameplay.relogin import watchRelogin
from src.gameplay.lootRouter import routeLoot
from src.gameplay.backpackWatch import ensureBackpacksOpen
from src.gameplay.recorder import recordTick
from src.gameplay.vitals import watchVitals
from src.gameplay.death import watchDeathRecovery
from src.gameplay.webcommands import consumeWebCommand
from src.gameplay.healing.observers.eatFood import eatFood
from src.gameplay.healing.observers.autoHur import autoHur
from src.gameplay.healing.observers.clearPoison import clearPoison
from src.gameplay.healing.observers.healingBySpells import healingBySpells
from src.gameplay.healing.observers.healingByPotions import healingByPotions
from src.gameplay.healing.observers.healingByMana import healingByMana
from src.gameplay.healing.observers.swapAmulet import swapAmulet
from src.gameplay.healing.observers.swapRing import swapRing
from src.gameplay.targeting import getAttackableCreatures, hasCreaturesToAttack
from src.repositories.gameWindow.creatures import getBestCreature, getTargetCreature
from src.utils.ino import ArduinoUnavailableError

class PilotNGThread:
    # TODO: add typings
    def __init__(self, context):
        self.context = context
        self.lastStateAt = 0
        self.session = SessionManager()
        self.failsafe = PlayerFailsafe()

    def mainloop(self):
        while True:
            try:
                self.context.context = consumeWebCommand(self.context)
                self.context.context = self.session.tick(self.context.context)
                if self.context.context['ng_pause']:
                    self.context.context = sessionRhythm(self.context.context)
                    sessionState = (self.context.context.get('ng_session') or {}).get('state')
                    if sessionState not in ('logout', 'offline', 'relogin'):
                        self.context.context = handleBBotKeepAlive(
                            self.context.context)
                    sleep(1)
                    continue
                startTime = time()
                self.context.context = self.handleGameData(
                    self.context.context)
                self.context.context = self.failsafe.tick(self.context.context)
                if self.context.context['ng_pause']:
                    sleep(1)
                    continue
                sessionStats.tick(
                    self.context.context['ng_radar']['coordinate'])
                if self.context.context['ng_radar']['coordinate'] is None:
                    self.context.context = watchRelogin(self.context.context)
                    sessionState = (self.context.context.get('ng_session') or {}).get('state')
                    if sessionState not in ('logout', 'offline', 'relogin') \
                            and not (self.context.context.get('ng_relogin') or {}).get('waiting'):
                        self.context.context = handleBBotKeepAlive(
                            self.context.context)
                    context = self.context.context
                    captureRequest = Path('.pilotng-capture-request')
                    if captureRequest.exists():
                        cv2.imwrite(
                            '.pilotng-live.png', context['ng_screenshot'],
                        )
                        captureRequest.unlink()
                    Path('.pilotng-state.json').write_text(json.dumps({
                        'pause': context['ng_pause'],
                        'waypointIndex': context['ng_cave']['waypoints']['currentIndex'],
                        'coordinate': None,
                        'task': 'waitingForCoordinate',
                    }), encoding='utf-8')
                    sleep(0.1)
                    continue
                captureRequest = Path('.pilotng-capture-request')
                if captureRequest.exists():
                    cv2.imwrite(
                        '.pilotng-live.png',
                        self.context.context['ng_screenshot'],
                    )
                    captureRequest.unlink()
                self.context.context = self.handleGameplayTasks(
                    self.context.context)
                self.context.context = self.context.context['ng_tasksOrchestrator'].do(
                    self.context.context)
                self.context.context = handleBBotAutomation(self.context.context)
                self.context.context['ng_radar']['lastCoordinateVisited'] = self.context.context['ng_radar']['coordinate']
                self.context.context = autoSyncActionBar(self.context.context)
                self.context.context = watchSupplies(self.context.context)
                self.context.context = smartRecovery(self.context.context)
                self.context.context = watchVitals(self.context.context)
                self.context.context = watchDeathRecovery(self.context.context)
                self.context.context = watchChat(self.context.context)
                self.context.context = watchChatReply(self.context.context)
                self.context.context = sessionRhythm(self.context.context)
                self.context.context = watchCapture(self.context.context)
                self.context.context = routeLoot(self.context.context)
                self.context.context = ensureBackpacksOpen(self.context.context)
                self.context.context = recordTick(self.context.context)
                status = self.context.context['ng_statusBar']
                if (status['hpPercentage'] is not None and
                        status['manaPercentage'] is not None):
                    healingByPotions(self.context.context)
                    healingByMana(self.context.context)
                    healingBySpells(self.context.context)
                    swapAmulet(self.context.context)
                    swapRing(self.context.context)
                comboSpells(self.context.context)
                clearPoison(self.context.context)
                autoHur(self.context.context)
                eatFood(self.context.context)
                if time() - self.lastStateAt >= 1:
                    context = self.context.context
                    currentTask = context['ng_tasksOrchestrator'].getCurrentTask(
                        context,
                    )
                    taskPath = []
                    task = currentTask
                    while task is not None:
                        taskPath.append({
                            'name': task.name,
                            'status': task.status,
                            'trapped': getattr(task, 'isTrapped', False),
                            'walkpoint': getattr(task, 'walkpoint', None),
                        })
                        task = task.parentTask
                    captureRequest = Path('.pilotng-capture-request')
                    if captureRequest.exists():
                        cv2.imwrite(
                            '.pilotng-live.png', context['ng_screenshot'],
                        )
                        captureRequest.unlink()
                    Path('.pilotng-state.json').write_text(json.dumps({
                        'pause': context['ng_pause'],
                        'waypointIndex': context['ng_cave']['waypoints']['currentIndex'],
                        'coordinate': context['ng_radar']['coordinate'],
                        'task': context['ng_tasksOrchestrator'].getCurrentTaskName(context),
                        'taskPath': taskPath,
                        'hpPercentage': context['ng_statusBar'].get('hpPercentage'),
                        'manaPercentage': context['ng_statusBar'].get('manaPercentage'),
                        'cavebotEnabled': context['ng_cave']['enabled'],
                        'runToCreatures': context['ng_cave']['runToCreatures'],
                        'comboEnabled': context['ng_comboSpells']['enabled'],
                    }), encoding='utf-8')
                    Path('.pilotng-last-coordinate.json').write_text(
                        json.dumps(context['ng_radar']['coordinate']), encoding='utf-8',
                    )
                    self.lastStateAt = time()
                endTime = time()
                diff = endTime - startTime
                sleep(max(0.045 - diff, 0))
            except KeyboardInterrupt:
                sys.exit()
            except ArduinoUnavailableError:
                self.context.context['ng_pause'] = True
                logEvent('arduinoUnavailable')
                Path('.pilotng-runtime.log').write_text(
                    traceback.format_exc(), encoding='utf-8',
                )
                sleep(1)
            except Exception as e:
                print(f"An exception occurred: {e}")
                print(traceback.format_exc())
                Path('.pilotng-runtime.log').write_text(traceback.format_exc(), encoding='utf-8')
                # Nach einer Panne koennte eine Taste physisch gedrueckt
                # geblieben sein (z. B. Shift beim Boxen-Loot). Sicher alles
                # loslassen - ein dauergedrueckter Zustand ist verdaechtig.
                try:
                    releaseAll()
                except Exception:
                    pass
                # Nicht ungebremst weiterrasen: sonst Dauer-Screenshot und
                # Dauer-OCR, bis der Fehler von selbst weggeht.
                sleep(0.5)

    def handleGameData(self, context):
        if context['ng_pause']:
            return context
        context = setScreenshotMiddleware(context)
        context = setRadarMiddleware(context)
        if context['ng_radar']['coordinate'] is None:
            return context
        context = setChatTabsMiddleware(context)
        context = setBattleListMiddleware(context)
        context = setGameWindowMiddleware(context)
        context = setDirectionMiddleware(context)
        context = setGameWindowCreaturesMiddleware(context)
        if context['ng_cave']['enabled'] and context['ng_cave']['runToCreatures'] == True:
            context = setHandleLootMiddleware(context)          
        else:
            context['ng_cave']['targetCreature'] = getTargetCreature(context['gameWindow']['monsters'])
        context = setWaypointIndexMiddleware(context)
        context = setMapPlayerStatusMiddleware(context)
        context = setMapStatsBarMiddleware(context)
        context = setCleanUpTasksMiddleware(context)
        return context

    def handleGameplayTasks(self, context):
        # TODO: func to check if coord is none
        if context['ng_radar']['coordinate'] is None:
            return context
        if any(coord is None for coord in context['ng_radar']['coordinate']):
            return context
        context['ng_cave']['closestCreature'] = getBestCreature(
            getAttackableCreatures(context), context['ng_radar']['coordinate'],
            {name: creature.get('priority', 1)
             for name, creature in context['ng_targeting'].get('creatures', {}).items()},
            context['ng_battleList'].get('creaturesHp'))
        currentTask = context['ng_tasksOrchestrator'].getCurrentTask(context)
        if currentTask is not None and currentTask.name == 'selectChatTab':
            return context
        if len(context['loot']['corpsesToLoot']) > 0 and context['ng_cave']['runToCreatures'] == True and context['ng_cave']['enabled']:
            from src.repositories.skills.core import getCapacity
            capacity = getCapacity(context['ng_screenshot'])
            if capacity is not None and capacity < 50:
                # Voller Rucksack: Refill/Waypoints gehen vor, nicht looten.
                context['way'] = 'waypoint'
            else:
                context['loot']['corpsesToLoot'] = [
                    corpse for corpse in context['loot']['corpsesToLoot']
                    if corpse.get('lootAttempts', 0) < ATTEMPT_LIMIT
                ]
                if len(context['loot']['corpsesToLoot']) > 0:
                    context['way'] = 'lootCorpses'
                    if currentTask is not None and currentTask.rootTask is not None and currentTask.rootTask.name != 'lootCorpse':
                        context['ng_tasksOrchestrator'].setRootTask(context, None)
                    if context['ng_tasksOrchestrator'].getCurrentTask(context) is None:
                        firstDeadCorpse = context['loot']['corpsesToLoot'][0]
                        context['ng_tasksOrchestrator'].setRootTask(
                            context, LootCorpseTask(firstDeadCorpse))
                    context['gameWindow']['previousMonsters'] = context['gameWindow']['monsters']
                    return context
        if context['ng_cave']['runToCreatures'] == True and context['ng_cave']['enabled']:
            hasCreaturesToAttackAfterCheck = hasCreaturesToAttack(context)
            if hasCreaturesToAttackAfterCheck:
                if context['ng_cave']['closestCreature'] is not None:
                    context['way'] = 'ng_cave'
                else:
                    context['way'] = 'waypoint'
            else:
                context['way'] = 'waypoint'
            if hasCreaturesToAttackAfterCheck and shouldAskForCavebotTasks(context):
                currentRootTask = currentTask.rootTask if currentTask is not None else None
                isTryingToAttackClosestCreature = currentRootTask is not None and (
                    currentRootTask.name == 'attackClosestCreature')
                if not isTryingToAttackClosestCreature:
                    context = resolveCavebotTasks(context)
            elif context['way'] == 'waypoint':
                if context['ng_tasksOrchestrator'].getCurrentTask(context) is None:
                    currentWaypointIndex = context['ng_cave']['waypoints']['currentIndex']
                    if currentWaypointIndex is None:
                        return context
                    currentWaypoint = context['ng_cave']['waypoints']['items'][currentWaypointIndex]
                    context['ng_tasksOrchestrator'].setRootTask(
                        context, resolveTasksByWaypoint(currentWaypoint))
        elif context['ng_cave']['enabled'] and context['ng_tasksOrchestrator'].getCurrentTask(context) is None:
                currentWaypointIndex = context['ng_cave']['waypoints']['currentIndex']
                if currentWaypointIndex is not None:
                    currentWaypoint = context['ng_cave']['waypoints']['items'][currentWaypointIndex]
                    context['ng_tasksOrchestrator'].setRootTask(
                        context, resolveTasksByWaypoint(currentWaypoint))

        context['gameWindow']['previousMonsters'] = context['gameWindow']['monsters']
        return context
