"""SweetCheat Engine — One-Click Memory & Savegame Runtime."""
import os
import re
import sys
import json
import shutil
import struct
import logging
import threading
import time

logger = logging.getLogger('SweetCheat.CheatEngine')

try:
    import pymem
    import pymem.process
except ImportError:
    pymem = None


class CheatEngine:
    def __init__(self):
        self.active_process = None
        self.pm = None
        self._freeze_threads = {}
        self._freeze_running = {}

    def attach(self, process_name):
        if not pymem:
            return False, "pymem nicht verfügbar"
        try:
            self.pm = pymem.Pymem(process_name)
            self.active_process = process_name
            return True, f"Attached to {process_name}"
        except Exception as e:
            return False, f"Could not attach: {e}"

    def attach_by_pid(self, pid):
        if not pymem:
            return False, "pymem nicht verfügbar"
        try:
            self.pm = pymem.Pymem(pid)
            self.active_process = pid
            return True, f"Attached to PID {pid}"
        except Exception as e:
            return False, f"Could not attach: {e}"

    def _value_to_bytes(self, value, value_type='int32'):
        size_map = {'int8': 1, 'int16': 2, 'int32': 4, 'int64': 8, 'float': 4, 'double': 8}
        size = size_map.get(value_type, 4)
        if value_type == 'float':
            return struct.pack('<f', float(value))
        elif value_type == 'double':
            return struct.pack('<d', float(value))
        else:
            return int(value).to_bytes(size, 'little', signed=True)

    def write_memory_value(self, address, value, value_type='int32'):
        if not self.pm:
            return False, "Not attached"
        try:
            size_map = {'int8': 1, 'int16': 2, 'int32': 4, 'int64': 8, 'float': 4, 'double': 8}
            size = size_map.get(value_type, 4)
            if value_type == 'float':
                self.pm.write_float(address, float(value))
            elif value_type == 'double':
                self.pm.write_double(address, float(value))
            elif size == 1:
                self.pm.write_bytes(address, int(value).to_bytes(1, 'little'), 1)
            elif size == 2:
                self.pm.write_short(address, int(value))
            elif size == 4:
                self.pm.write_int(address, int(value))
            elif size == 8:
                self.pm.write_longlong(address, int(value))
            return True, "Value written"
        except Exception as e:
            return False, str(e)

    def read_memory_value(self, address, value_type='int32'):
        if not self.pm:
            return None
        try:
            if value_type == 'float':
                return self.pm.read_float(address)
            elif value_type == 'double':
                return self.pm.read_double(address)
            size_map = {'int8': 1, 'int16': 2, 'int32': 4, 'int64': 8}
            size = size_map.get(value_type, 4)
            if size == 1:
                return self.pm.read_bytes(address, 1)[0]
            elif size == 2:
                return self.pm.read_short(address)
            elif size == 4:
                return self.pm.read_int(address)
            elif size == 8:
                return self.pm.read_longlong(address)
        except Exception:
            return None

    def _aob_to_bytes(self, pattern):
        """Convert '48 89 5C 24 ?? 48' style pattern to bytes + mask."""
        parts = pattern.split()
        sig = []
        mask = []
        for p in parts:
            if p == '??' or p == '?':
                sig.append(0)
                mask.append(0)
            else:
                sig.append(int(p, 16))
                mask.append(1)
        return bytes(sig), bytes(mask)

    def scan_aob(self, pattern):
        """Scan all modules for AoB pattern. Returns first address or None."""
        if not self.pm:
            return None
        sig, mask = self._aob_to_bytes(pattern)
        try:
            address = self.pm.pattern_scan_all(sig, mask)
            if address and address != 0:
                return address
        except Exception as e:
            logger.error(f"AoB scan error: {e}")
        return None

    def find_and_patch(self, pattern, offset, value, value_type='int32'):
        addr = self.scan_aob(pattern)
        if not addr:
            return False, "Pattern nicht gefunden"
        return self.write_memory_value(addr + offset, value, value_type)

    def freeze_value(self, address, value, value_type='int32'):
        self._freeze_running[address] = True
        def worker():
            while self._freeze_running.get(address):
                self.write_memory_value(address, value, value_type)
                time.sleep(0.05)
        t = threading.Thread(target=worker, daemon=True)
        self._freeze_threads[address] = t
        t.start()
        return True, f"Freeze aktiv"

    def stop_freeze(self, address):
        self._freeze_running.pop(address, None)
        return True, "Freeze gestoppt"

    def toggle_cheat(self, game_meta, pattern_info, enabled):
        """One-click toggle. If enabled: attach, find pattern, write value, freeze if configured."""
        if not pymem:
            return False, "pymem nicht verfügbar"
        process_name = game_meta.get('process_name') or game_meta.get('exe')
        if not process_name:
            return False, "Keine Prozessinfo"
        if not self.pm or self.active_process != process_name:
            ok, msg = self.attach(process_name)
            if not ok:
                return False, msg
        addr = self.scan_aob(pattern_info['aob_pattern'])
        if not addr:
            return False, "Cheat-Pattern nicht gefunden (Spielversion möglicherweise nicht unterstützt)"
        target = addr + int(pattern_info.get('offset', 0))
        value = pattern_info['default_value']
        value_type = pattern_info.get('value_type', 'int32')
        if enabled:
            ok, msg = self.write_memory_value(target, value, value_type)
            if not ok:
                return False, msg
            if int(pattern_info.get('freeze', 1)):
                self.freeze_value(target, value, value_type)
            return True, f"{pattern_info['cheat_name']} aktiviert"
        else:
            self.stop_freeze(target)
            return True, f"{pattern_info['cheat_name']} deaktiviert"

    # ---------- Live Scanning (Fallback) ----------
    def scan_value(self, value, value_type='int32'):
        if not self.pm:
            return []
        try:
            val_bytes = self._value_to_bytes(value, value_type)
        except Exception as e:
            logger.error(f"scan_value convert error: {e}")
            return []
        hits = []
        for mbi in pymem.process.list_process_modules(self.pm.process_handle):
            base = mbi.BaseAddress
            region = mbi.RegionSize
            protect = mbi.Protect
            if protect not in (0x04, 0x40, 0x02):
                continue
            try:
                data = self.pm.read_bytes(base, region)
            except Exception:
                continue
            start = 0
            while True:
                i = data.find(val_bytes, start)
                if i == -1:
                    break
                hits.append(base + i)
                if len(hits) > 20000:
                    return hits
                start = i + 1
        return hits

    def rescan_value(self, addresses, value, value_type='int32'):
        if not self.pm:
            return []
        val_bytes = self._value_to_bytes(value, value_type)
        hits = []
        for addr in addresses:
            try:
                size = len(val_bytes)
                data = self.pm.read_bytes(addr, size)
                if data == val_bytes:
                    hits.append(addr)
            except Exception:
                pass
        return hits

    def patch(self, address, value, value_type='int32'):
        return self.write_memory_value(address, value, value_type)

    # ---------- Savegame Edit ----------
    def edit_savegame(self, file_path, search_key, new_value):
        if not os.path.exists(file_path):
            return False, "Save file not found"
        try:
            backup_path = file_path + ".bak"
            shutil.copy2(file_path, backup_path)
            with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()
            pattern = rf'("{re.escape(search_key)}"\s*:\s*)(-?\d+(?:\.\d+)?)'
            new_content = re.sub(pattern, rf'\1{new_value}', content, count=1)
            if new_content == content:
                pattern = rf'(<{re.escape(search_key)}>)([^\u003c]+)(</{re.escape(search_key)}>)'
                new_content = re.sub(pattern, rf'\g<1>{new_value}\g<3>', content, count=1)
            if new_content == content:
                return False, "Value key not found in savegame"
            with open(file_path, 'w', encoding='utf-8') as f:
                f.write(new_content)
            return True, "Savegame patched successfully"
        except Exception as e:
            return False, str(e)

    def detach(self):
        for addr in list(self._freeze_running.keys()):
            self.stop_freeze(addr)
        self.pm = None
        self.active_process = None

    # ---------- Stardew Valley Bridge ----------
    def activate_command(self, command, params=None):
        if command.startswith('stardew_'):
            try:
                from stardew_bridge import apply_cheat, list_save_files
                saves = list_save_files()
                if not saves:
                    return False, "Keine Stardew Valley Savegames gefunden"
                cheat_id = command.replace('stardew_', '')
                return apply_cheat(saves[0], cheat_id, params)
            except Exception as e:
                logger.exception("Stardew cheat failed")
                return False, f"Stardew cheat error: {e}"
        return False, f"Unknown command: {command}"
