import copy
import re
from pathlib import Path


SETTING_IDS = {
    'Cavebot.Enable': '1092845226',
    'Cavebot.Waypoint': '1534554678',
    'Healer.Item.Low.Enable': '664726982',
    'Healer.Item.Low.HP': '-1257914102',
    'Healer.Item.Low.Item': '-1169183731',
    'Healer.Item.Low.Percent': '1705233088',
    'Healer.Spell.Low.Enable': '-2143817111',
    'Healer.Spell.Low.HP': '-112139308',
    'Healer.Spell.Low.Mana': '267068155',
    'Healer.Spell.Low.Percent': '-1958017710',
    'Healer.Spell.Low.Spell': '-1399506541',
    'Healer.Spell.Mid.Enable': '1393655302',
    'Healer.Spell.Mid.HP': '-12875631',
    'Healer.Spell.Mid.Mana': '1485121280',
    'Healer.Spell.Mid.Percent': '-1426447618',
    'Healer.Spell.Mid.Spell': '-2035450878',
    'Healer.Spell.High.Enable': '-1230307632',
    'Healer.Spell.High.HP': '347220298',
    'Healer.Spell.High.Mana': '-1684520157',
    'Healer.Spell.High.Percent': '-1246590074',
    'Healer.Spell.High.Spell': '1362722921',
    'Killer.Enable': '1398085848',
    'Killer.Targets': '20504739',
    'Looter.Looter.Enable': '646299023',
    'Looter.Looter.Items': '1414208239',
    'Looter.OpenCorpses.Enable': '1007361165',
    'Macros.AutoEnable': '1394395405',
    'Macros.Items': '-1028332407',
    'Variables': '1399504867',
}

ITEM_NAMES = {
    236: 'Strong Health Potion',
    239: 'Great Health Potion',
    266: 'Health Potion',
    268: 'Mana Potion',
    3031: 'gold coin',
    3357: 'Plate Armor',
    9689: 'bunch of troll hair',
    9692: 'lump of dirt',
    10275: 'carrion worm fang',
    11443: 'Girlish Hair Decoration',
    11444: 'Protective Charm',
}

LEGACY_HEALTH_ITEMS = {
    0: 7876,
    1: 266,
    2: 236,
    3: 239,
    6: 7643,
    7: 23375,
}

SHOVELS = {2973, 3457, 5710}
ROPES = {646, 3003}
CITIES = (
    'AbDendriel', 'Ankrahmun', 'Carlin', 'Darashia', 'Edron', 'Farmine',
    'Issavi', 'Kazoordon', 'LibertBay', 'PortHope', 'Rathleton', 'Svargrond',
    'Thais', 'Venore', 'Yalahar', 'Feyrist',
)


def parse_bbot(text):
    if '\0' in text or len(text) > 2_000_000:
        raise ValueError('Ungültige oder zu große BBot-Datei.')
    lines = text.splitlines()
    if not lines or lines[0].strip() != 'BBOT':
        raise ValueError('Die Datei ist kein gültiges BBot-Profil.')

    settings = {}
    index = 1
    while index < len(lines):
        line = lines[index]
        index += 1
        if '=' not in line:
            continue
        key, value = line.split('=', 1)
        key = key.strip()
        if not key:
            continue
        if value.startswith('@M:'):
            marker = value
            block = []
            while index < len(lines) and lines[index] != marker:
                block.append(lines[index])
                index += 1
            if index >= len(lines):
                raise ValueError(f'Nicht geschlossener BBot-Block: {key}')
            index += 1
            value = '\n'.join(block)
        settings[key] = value
    return settings


def import_bbot(path, existing_config):
    source = Path(path)
    if source.suffix.lower() != '.bbot':
        raise ValueError('Bitte eine .bbot-Datei auswählen.')
    text = source.read_text(encoding='latin-1')
    return convert_bbot(parse_bbot(text), existing_config, source.name, text)


def _translate_background_macros(macros, config, report):
    """Interval-Makros (nicht an Wegpunkte) in Bot-Features uebersetzen.

    - `Self.Say(utura)` mit HP-Bedingung → Utura-Upkeep aktivieren
    - `Creatures.OnScreen/Beside >= N` + `Self.Say(spell)` → Kombo-Eintrag
    - Ring-/Mount-Makros werden ehrlich als nicht uebernehmbar gemeldet
    """
    for name, macro in macros.items():
        code = macro['code']
        lowered = code.lower()
        if 'self.say(utura)' in lowered:
            utura = config.get('healing', {}).get('spells', {}).get('utura')
            if utura is not None:
                utura['enabled'] = True
                hp = re.search(r'Self\.Health(%?)<=!?\s*(\w+)!?', code)
                if hp and hp.group(2).isdigit():
                    if hp.group(1) == '%':
                        utura['hpPercentageLessThanOrEqual'] = int(hp.group(2))
                    else:
                        utura['hpLessThanOrEqual'] = int(hp.group(2))
                report['warnings'].append(
                    f'Background-Makro "{name}" → Utura-Upkeep aktiviert.')
        combo = re.search(
            r'Creatures\.(?:OnScreen|Beside)\(?\)?\s*>=\s*(\d+).*?'
            r'Self\.Say\(([^)]+)\)',
            code, re.IGNORECASE | re.DOTALL)
        if combo:
            spell = combo.group(2).strip().lower()
            combos = config.setdefault(
                'ng_comboSpells', {'enabled': True, 'items': []})
            combos['enabled'] = True
            combos['items'].append({
                'name': name,
                'enabled': True,
                'creatures': {
                    'compare': 'greaterThanOrEqual',
                    'value': int(combo.group(1)),
                },
                'spells': [{'name': spell, 'hotkey': None}],
                'currentSpellIndex': 0,
            })
            report['warnings'].append(
                f'Background-Makro "{name}" → Kombo "{spell}" '
                f'ab {combo.group(1)} Kreaturen.')
        if 'togglemount' in lowered:
            report['warnings'].append(
                f'Auto-Mount "{name}" nicht übernommen '
                '(Mount-Status visuell nicht erkennbar).')
        if 'self.equip.ring' in lowered or 'self.unequip.ring' in lowered:
            report['warnings'].append(
                f'Ring-Makro "{name}" nicht übernommen '
                '(Equipment-Slot unvermessen).')


def _apply_loot_rules(config, loot_items, report):
    """Erzeugt Loot-Router-Regeln aus der BBot-Lootliste.

    Jedes gelootete Item bekommt eine Regel ins Loot-Backpack; Icons
    werden bei Bedarf einmalig von TibiaWiki geladen. So sortiert der
    Router exakt das um, was das Script lootet — ohne manuelle
    Client-Einstellungen (die liegen ohnehin serverseitig).
    """
    lootBackpack = config.get('ng_backpacks', {}).get('loot')
    if not lootBackpack:
        if loot_items:
            report['warnings'].append(
                'Loot-Regeln nicht erzeugt: kein Loot-Backpack konfiguriert '
                '(Inventar-Seite oder Vollsetup).')
        return
    from src.repositories.actionBar.itemIcons import ensureItemIcon
    from src.wiki.items import itemName
    rules = config.setdefault('ng_lootRules', [])
    existing = {rule.get('itemId') for rule in rules}
    added = 0
    skipped = []
    for item in loot_items:
        item_id = item['id']
        if item_id in existing:
            continue
        if ensureItemIcon(item_id) is None:
            skipped.append(item['name'])
            continue
        rules.append({'itemId': item_id, 'to': lootBackpack})
        existing.add(item_id)
        added += 1
    if added:
        report['warnings'].append(
            f'{added} Loot-Regeln automatisch erzeugt (Item → {lootBackpack}).')
    if skipped:
        report['warnings'].append(
            'Ohne Icon (keine Regel): ' + ', '.join(sorted(set(skipped))))


def convert_bbot(settings, existing_config, source_name='import.bbot', source_text=''):
    config = copy.deepcopy(existing_config)
    report = {'warnings': [], 'unsupported': [], 'waypoints': 0}
    variables = _parse_variables(_setting(settings, 'Variables', ''))
    macros = _parse_macros(_setting(settings, 'Macros.Items', ''), variables)
    city = _detect_city(source_name + '\n' + source_text)
    waypoints = _parse_waypoints(
        _setting(settings, 'Cavebot.Waypoint', ''), variables, macros, city, report
    )
    if not waypoints:
        raise ValueError('Die BBot-Datei enthält keine importierbaren Wegpunkte.')
    _repair_label_references(waypoints, report)

    targets = _parse_targets(_setting(settings, 'Killer.Targets', ''))
    loot_items = _parse_loot_items(_setting(settings, 'Looter.Looter.Items', ''))
    bbot = {
        'source': source_name,
        'variables': variables,
        'macros': macros,
        'lootItems': loot_items,
        'itemSlots': {},
        'autoDropFlasks': _auto_drop_flasks(macros),
        'antiStuckSeconds': _anti_stuck_seconds(macros),
    }

    cave = config.setdefault('ng_cave', {})
    cave['enabled'] = _boolean(_setting(settings, 'Cavebot.Enable', '-1'))
    cave['runToCreatures'] = _boolean(_setting(settings, 'Killer.Enable', '-1'))
    cave.setdefault('waypoints', {})['items'] = waypoints

    config['ng_targeting'] = {'creatures': targets}
    config['ng_bbot'] = bbot
    _apply_healing(settings, config, bbot, report)
    _translate_background_macros(macros, config, report)

    report['waypoints'] = len(waypoints)
    report['targets'] = len(targets)
    report['lootItems'] = len(loot_items)
    report['city'] = city
    if loot_items and _boolean(_setting(settings, 'Looter.Looter.Enable', '0')):
        report['warnings'].append(
            'BBot-Lootliste übernommen — Quick-Loot lootet alles, der '
            'Loot-Router sortiert die Script-Items danach ins Loot-Backpack.'
        )
    _apply_loot_rules(config, loot_items, report)
    if not _boolean(_setting(settings, 'Looter.OpenCorpses.Enable', '0')):
        cave['runToCreatures'] = False
    return config, report


def _setting(settings, name, default=None):
    if name in settings:
        return settings[name]
    return settings.get(SETTING_IDS[name], default)


def _boolean(value):
    return str(value).strip().lower() not in {'', '0', 'false', 'no'}


def _integer(value, default=0):
    try:
        return int(str(value).strip())
    except (TypeError, ValueError):
        return default


def _parse_variables(block):
    variables = {}
    for line in block.splitlines():
        match = re.match(r'^([A-Za-z][\w-]*)\s*=\s*(-?\d+)\s*$', line.strip())
        if match:
            variables[match.group(1)] = int(match.group(2))
    return variables


def _substitute(value, variables):
    return re.sub(
        r'!([A-Za-z][\w-]*)!',
        lambda match: str(variables.get(match.group(1), match.group(0))),
        value,
    )


def _parse_macros(block, variables):
    macros = {}
    for line in block.splitlines():
        match = re.match(r'^\s*(\d+)\s+\{([^}]+)\}\s*(.*)$', line)
        if match:
            macros[match.group(2).strip()] = {
                'intervalMs': int(match.group(1)),
                'code': _substitute(match.group(3).strip(), variables),
            }
    return macros


def _parse_targets(block):
    targets = {}
    for line in block.splitlines():
        parts = line.rstrip('@').split(':')
        if len(parts) >= 2 and parts[0].strip():
            priority = _integer(parts[1], 1)
            if priority < 6:
                targets[parts[0].strip()] = {'ignore': False, 'priority': priority}
    return targets


def _parse_loot_items(block):
    items = []
    for line in block.splitlines():
        parts = line.split(':')
        if parts and parts[0].isdigit():
            item_id = int(parts[0])
            items.append({'id': item_id, 'name': ITEM_NAMES.get(item_id, str(item_id))})
    return items


def _detect_city(text):
    compact = text.replace(' ', '').lower()
    for city in CITIES:
        if city.replace(' ', '').lower() in compact:
            return city
    return 'Carlin'


def _waypoint(kind, coordinate, options=None, label=''):
    return {
        'label': label,
        'type': kind,
        'coordinate': list(coordinate),
        'options': options or {},
        'ignore': False,
        'passinho': False,
    }


def _parse_waypoints(block, variables, macros, city, report):
    waypoints = []
    for number, line in enumerate(block.splitlines(), start=1):
        if not line.strip():
            continue
        match = re.match(r'^([A-Za-z]+) \((\d+) (\d+) (\d+)(?::(.*))?\)$', line.strip())
        if not match:
            report['unsupported'].append(f'Wegpunktzeile {number}: {line.strip()}')
            continue
        kind = match.group(1)
        coordinate = tuple(map(int, match.group(2, 3, 4)))
        if coordinate[2] > 15:
            raise ValueError(f'Ungültige Etage in Wegpunktzeile {number}.')
        param = _substitute(match.group(5) or '', variables).strip()

        if kind in {'Point', 'Fixed'}:
            waypoints.append(_waypoint(
                'walk', coordinate, {'exact': kind == 'Fixed'},
            ))
        elif kind == 'Label':
            waypoints.append(_waypoint('bbotAction', coordinate, {'action': 'noop'}, param))
        elif kind in {'Say', 'NPCSay', 'SayNPC'}:
            waypoints.append(_waypoint('bbotSay', coordinate, {'text': param}))
        elif kind == 'Depositer':
            waypoints.append(_waypoint('depositItems', coordinate, {'city': city}))
        elif kind == 'ResetBackpacks':
            waypoints.append(_waypoint('bbotResetBackpacks', coordinate))
        elif kind == 'FullCheck':
            waypoints.append(_waypoint('bbotAction', coordinate, _full_check(param, report)))
        elif kind == 'Macro':
            waypoints.append(_waypoint('bbotAction', coordinate, _macro_action(param, macros, report)))
        elif kind == 'GoLabel':
            if param:
                waypoints.append(_waypoint('bbotAction', coordinate, {
                    'action': 'goLabel', 'label': param,
                }))
            else:
                report['unsupported'].append('GoLabel ohne Ziel')
        elif kind == 'Withdraw':
            items = []
            for item in param.split(';'):
                values = item.split()
                if len(values) == 3 and all(value.isdigit() for value in values):
                    items.append({
                        'id': int(values[0]),
                        'unitPrice': int(values[1]),
                        'target': int(values[2]),
                    })
            waypoints.append(_waypoint('bbotWithdraw', coordinate, {'items': items}))
        elif kind == 'Buy':
            values = param.split()
            if len(values) == 2 and all(value.isdigit() for value in values):
                item_id, target = map(int, values)
                waypoints.append(_waypoint('bbotBuy', coordinate, {
                    'id': item_id,
                    'item': ITEM_NAMES.get(item_id, str(item_id)),
                    'target': target,
                }))
            else:
                report['unsupported'].append(f'Buy: {param}')
        elif kind == 'Sell':
            if param.strip().isdigit():
                item_id = int(param.strip())
                waypoints.append(_waypoint('bbotSell', coordinate, {
                    'id': item_id,
                    'item': ITEM_NAMES.get(item_id, str(item_id)),
                }))
            else:
                report['unsupported'].append(f'Sell: {param}')
        elif kind == 'Delay':
            seconds = _integer(param, default=-1)
            if seconds >= 0:
                waypoints.append(_waypoint('bbotDelay', coordinate, {
                    'seconds': seconds,
                }))
            else:
                report['unsupported'].append(f'Delay: {param}')
        elif kind == 'MapTool':
            tool = re.search(r'Target:\s*(\d+)\s+Use:\s*(\d+)\s+Pos:\s*(\d+)\s+(\d+)\s+(\d+)', param)
            if not tool:
                report['unsupported'].append(f'MapTool: {param}')
                continue
            use_id = int(tool.group(2))
            target_coordinate = tuple(map(int, tool.group(3, 4, 5)))
            if use_id in SHOVELS:
                waypoint_type = 'useShovel'
            elif use_id in ROPES:
                waypoint_type = 'useRope'
            else:
                waypoint_type = 'rightClickUse'
            waypoints.append(_waypoint(waypoint_type, target_coordinate))
        else:
            report['unsupported'].append(f'{kind}: {param}')
    if len(waypoints) > 10_000:
        raise ValueError('BBot-Datei enthält zu viele Wegpunkte.')
    _convert_floor_transitions(waypoints)
    return waypoints


def _direction_from_map(coordinate):
    """Die Richtung des Etagenwechsels laut Karte, sonst None.

    Traege importiert: Der Import zieht die Kartenbilder nach, und die
    braucht ein reiner Skript-Import sonst nicht.
    """
    try:
        from src.repositories.radar.core import getFloorChangeDirection
    except Exception:
        return None
    return getFloorChangeDirection(tuple(coordinate))


def _convert_floor_transitions(waypoints):
    transitions = []
    for index, (current, following) in enumerate(zip(waypoints, waypoints[1:])):
        if current['type'] != 'walk' or following['type'] != 'walk':
            continue
        current_coordinate = current['coordinate']
        following_coordinate = following['coordinate']
        if current_coordinate[2] == following_coordinate[2]:
            continue
        # Die Karte kennt den Uebergang und ist damit die verlaesslichere
        # Quelle. Aus den blossen Koordinaten laesst sich die Richtung
        # naemlich nicht immer ableiten: Im Depot von Thais geht es von
        # (32349, 32231, 8) nach WESTEN hoch und man landet auf
        # (32348, 32230, 7) - eine Differenz von -1/-1, die genauso nach
        # Norden aussieht. Die Regel unten hat sich dort fuer 'north'
        # entschieden, und der Bot ist bis zur Wand gelaufen.
        direction = _direction_from_map(current_coordinate)
        if direction is None:
            dx = following_coordinate[0] - current_coordinate[0]
            dy = following_coordinate[1] - current_coordinate[1]
            if abs(dy) >= abs(dx) and dy:
                direction = 'south' if dy > 0 else 'north'
            elif dx:
                direction = 'east' if dx > 0 else 'west'
            else:
                continue
        transition_type = (
            'moveDown' if following_coordinate[2] > current_coordinate[2]
            else 'moveUp'
        )
        transitions.append((
            index + 1,
            _waypoint(
                transition_type,
                current_coordinate,
                {'direction': direction},
            ),
        ))
    for offset, (index, transition) in enumerate(transitions):
        waypoints.insert(index + offset, transition)


def _repair_label_references(waypoints, report):
    labels = {waypoint['label'].lower() for waypoint in waypoints if waypoint['label']}
    for waypoint in waypoints:
        options = waypoint['options']
        for key in ('trueLabel', 'falseLabel'):
            label = options.get(key)
            if not label or label.lower() in labels:
                continue
            repaired = label.replace('--', '-')
            if repaired.lower() in labels:
                options[key] = repaired
                report['warnings'].append(f'BBot-Label korrigiert: {label} → {repaired}')


def _full_check(param, report):
    match = re.match(r'^Full\s+(.+?)\s+Else\s+(.+?)\s+Code\s+(.+)$', param)
    if match:
        true_label, false_label, expression = match.groups()
    else:
        true_label, false_label, expression = '', '', param
    checks = _checks(expression)
    if not checks:
        report['unsupported'].append(f'FullCheck: {param}')
    return {
        'action': 'condition',
        'checks': checks,
        'trueLabel': true_label,
        'falseLabel': false_label,
    }


def _checks(expression):
    checks = []
    for part in expression.split(';'):
        match = re.match(r'^\s*([A-Za-z]+|-?\d+)\s*(<=|>=|==|<|>)\s*(-?\d+)\s*$', part)
        if not match:
            continue
        left, operator, right = match.groups()
        if left.lower() in {'cap', 'capacity'}:
            source, value = 'capacity', None
        elif left.lower() == 'stamina':
            source, value = 'stamina', None
        else:
            try:
                number = int(left)
            except ValueError:
                continue
            source, value = ('item', number) if number > 100 else ('literal', number)
        checks.append({'source': source, 'value': value, 'operator': operator, 'target': int(right)})
    return checks


def _macro_coordinate(parts):
    """(x, y, z) aus Makro-Argumenten; None bei Self-Position (!X! etc.)."""
    if len(parts) < 3:
        return None
    values = []
    for part in parts[:3]:
        if part.startswith('!'):
            return None
        values.append(_integer(part))
    return tuple(values)


def _macro_steps(code, report):
    """Zerlegt BBot-Makro-Code in ausfuehrbare Einzelschritte.

    Verstanden: Map.Use / Map.UseOn / Map.PickUpEx, NPC.Say, Macro.Wait,
    Self.Equip.Helmet / Self.UnEquip.Helmet, Self.Move[NESW],
    Cavebot.Stop, X:=Self.X()-Zuweisungen. !X!/!Y!/!Z! bedeutet
    "aktuelle Position" und wird zur Laufzeit aufgeloest.
    """
    steps = []
    # X:=Self.X()-Zuweisungen sind Positionsmarker, keine Befehle
    code = re.sub(r'[A-Za-z]+:=Self\.[A-Za-z]+\(\)', '', code)
    for match in re.finditer(r'([A-Za-z.]+)\(([^)]*)\)', code):
        command = match.group(1)
        args = [part.strip() for part in match.group(2).split(',')]
        if command == 'Map.UseOn':
            steps.append({
                'action': 'useItemOn', 'itemId': _integer(args[0]),
                'coordinate': _macro_coordinate(args[2:5]),
            })
        elif command == 'Map.Use':
            steps.append({
                'action': 'useMap',
                'coordinate': _macro_coordinate(args[1:4]),
            })
        elif command == 'Map.PickUpEx':
            steps.append({
                'action': 'pickup', 'itemId': _integer(args[0]),
                'coordinate': _macro_coordinate(args[3:6]),
            })
        elif command == 'NPC.Say':
            steps.append({'action': 'say', 'text': match.group(2).strip()})
        elif command == 'Self.Say':
            steps.append({'action': 'say', 'text': match.group(2).strip()})
        elif command == 'Macro.Wait':
            steps.append({'action': 'wait', 'ms': _integer(args[0])})
        elif command == 'Self.Equip.Helmet':
            steps.append({'action': 'equipHelmet', 'itemId': _integer(args[0])})
        elif command == 'Self.UnEquip.Helmet':
            steps.append({'action': 'unequipHelmet'})
        elif command == 'Cavebot.Stop':
            steps.append({'action': 'pause'})
        elif re.fullmatch(r'Self\.Move[NESW]', command):
            steps.append({'action': 'move', 'direction': command[-1]})
        elif re.fullmatch(r'Self\.Turn[NESW]', command):
            steps.append({'action': 'turn', 'direction': command[-1]})
        elif command == 'Tibia.Close':
            steps.append({'action': 'closeClient'})
        elif command == 'Self.Logout':
            steps.append({'action': 'logout'})
        elif command == 'Exit':
            # BBot Exit() beendet das Makro — Rest ignorieren
            break
        else:
            report['unsupported'].append(
                f'Makro-Befehl: {command}({match.group(2)[:40]})')
    leftover = re.sub(r'[A-Za-z.]+\([^)]*\)', '', code).strip()
    if leftover:
        report['unsupported'].append(f'Makro-Rest: {leftover[:60]}')
    return steps


def _macro_conditions(code):
    """Bedingungen aus Makro-Code als FullCheck-kompatible Checks."""
    checks = []
    stamina = re.search(r'Self\.StaminaMins\(\)\s*(<=|>=|==|<|>)\s*(\d+)', code)
    if stamina:
        checks.append({
            'source': 'stamina', 'value': None,
            'operator': stamina.group(1), 'target': int(stamina.group(2)),
        })
    for match in re.finditer(
            r'Misc\.ItemCountEx\((\d+)\)\s*(<=|>=|==|<|>)\s*(\d+)', code):
        checks.append({
            'source': 'item', 'value': int(match.group(1)),
            'operator': match.group(2), 'target': int(match.group(3)),
        })
    capacity = re.search(r'Self\.Capacity\(\)\s*(<=|>=|==|<|>)\s*(\d+)', code)
    if capacity:
        checks.append({
            'source': 'capacity', 'value': None,
            'operator': capacity.group(1), 'target': int(capacity.group(2)),
        })
    on_screen = re.search(
        r'Creatures\.OnScreen\(\)\s*(<=|>=|==|<|>)\s*(\d+)', code)
    if on_screen:
        checks.append({
            'source': 'creatures', 'value': None,
            'operator': on_screen.group(1), 'target': int(on_screen.group(2)),
        })
    return checks


def _macro_action(name, macros, report):
    macro = macros.get(name)
    if macro is None:
        report['unsupported'].append(f'Macro fehlt: {name}')
        return {'action': 'noop'}
    code = macro['code']
    checks = _macro_conditions(code)
    labels = re.findall(r'CaveBot\.GoLabel\(([^)]+)\)', code, re.IGNORECASE)
    if checks and (labels or 'Self.Logout()' in code or 'Cavebot.Stop()' in code):
        action = {
            'action': 'condition',
            'checks': checks,
            'trueLabel': labels[0] if labels else '',
            'falseLabel': labels[1] if len(labels) > 1 else None,
        }
        if 'Self.Logout()' in code:
            action['trueAction'] = 'logout'
        return action
    steps = _macro_steps(code, report)
    if steps:
        return {'action': 'macro', 'steps': steps}
    report['unsupported'].append(f'Macro-Ausführung: {name}')
    return {'action': 'noop'}


def _auto_drop_flasks(macros):
    for macro in macros.values():
        if 'Map.Thrown(285' in macro['code']:
            return {'enabled': True, 'intervalMs': max(1000, macro['intervalMs'])}
    return {'enabled': False, 'intervalMs': 3000}


def _anti_stuck_seconds(macros):
    for macro in macros.values():
        match = re.search(r'Misc\.StandTime\(\)>(\d+)', macro['code'])
        if match:
            return int(match.group(1))
    return 0


def _health_item_id(value):
    text = str(value).strip()
    if text.lower().startswith('@item'):
        return _integer(text.split()[-1])
    return LEGACY_HEALTH_ITEMS.get(_integer(text), _integer(text))


def _apply_healing(settings, config, bbot, report):
    healing = config.get('healing', {})
    potion = healing.get('potions', {}).get('firstHealthPotion')
    if potion is not None and _boolean(_setting(settings, 'Healer.Item.Low.Enable', '0')):
        item_id = _health_item_id(_setting(settings, 'Healer.Item.Low.Item', '1'))
        if _boolean(_setting(settings, 'Healer.Item.Low.Percent', '-1')):
            potion['enabled'] = True
            potion['hpLessThanOrEqual'] = None
            potion['hpPercentageLessThanOrEqual'] = _integer(
                _setting(settings, 'Healer.Item.Low.HP', '0')
            )
        else:
            potion['enabled'] = True
            potion['hpPercentageLessThanOrEqual'] = None
            potion['hpLessThanOrEqual'] = _integer(
                _setting(settings, 'Healer.Item.Low.HP', '0')
            )
            report['warnings'].append(
                'Heiltrank nutzt absolute HP-Schwelle (OCR-basiert, Prozent empfohlen).'
            )
        potion['item'] = item_id
        if potion.get('slot') is not None:
            bbot['itemSlots'][str(item_id)] = potion['slot']
        bbot['healthPotion'] = {'id': item_id, 'name': ITEM_NAMES.get(item_id, str(item_id))}

    spell_settings = []
    for level in ('Low', 'Mid', 'High'):
        spell_name = str(_setting(settings, f'Healer.Spell.{level}.Spell', '')).strip().lower()
        if not spell_name:
            continue
        # ponytail: BBot speichert Enable-Flags nicht zuverlässig (viele Scripts
        # haben Enable=0 trotz konfiguriertem Zauber) — Vorhandensein von
        # Zaubername gilt als aktiviert. Grenze: bewusst deaktivierte, aber
        # konfigurierte Zauber werden ebenfalls übernommen.
        if not _boolean(_setting(settings, f'Healer.Spell.{level}.Enable', '0')):
            report['warnings'].append(
                f'Heilzauber "{spell_name}" ({level}) ohne Enable-Flag übernommen.'
            )
        if not _boolean(_setting(settings, f'Healer.Spell.{level}.Percent', '-1')):
            report['warnings'].append(
                f'{level}-Heilzauber nutzt absolute HP-Schwelle (OCR-basiert, Prozent empfohlen).'
            )
            spell_settings.append({
                'spell': spell_name,
                'hpAbsolute': _integer(_setting(settings, f'Healer.Spell.{level}.HP', '0')),
                'mana': _integer(_setting(settings, f'Healer.Spell.{level}.Mana', '0')),
            })
            continue
        spell_settings.append({
            'spell': spell_name,
            'hp': _integer(_setting(settings, f'Healer.Spell.{level}.HP', '0')),
            'mana': _integer(_setting(settings, f'Healer.Spell.{level}.Mana', '0')),
        })
    # ponytail: absolute und prozentuale Schwellen gemischt sortieren —
    # hpAbsolute/10 als grobe Vergleichsskala (Upgrade-Trigger: getrennte
    # Sortierung, sobald Max-HP bekannt ist).
    spell_settings.sort(key=lambda item: item.get('hp', item.get('hpAbsolute', 0) / 10))
    for destination in ('criticalHealing', 'lightHealing', 'mediumHealing'):
        target = healing.get('spells', {}).get(destination)
        if target is not None:
            target['enabled'] = False
    if len(spell_settings) == 1:
        destinations = ('lightHealing',)
    elif len(spell_settings) == 2:
        destinations = ('criticalHealing', 'lightHealing')
    else:
        destinations = ('criticalHealing', 'lightHealing', 'mediumHealing')
    for destination, imported in zip(destinations, spell_settings[:3]):
        target = healing.get('spells', {}).get(destination)
        if target is None:
            continue
        target['enabled'] = True
        target['spell'] = imported['spell']
        if 'hpAbsolute' in imported:
            target['hpLessThanOrEqual'] = imported['hpAbsolute']
            target['hpPercentageLessThanOrEqual'] = None
        else:
            target['hpLessThanOrEqual'] = None
            target['hpPercentageLessThanOrEqual'] = imported['hp']
        target['manaPercentageGreaterThanOrEqual'] = imported['mana']
    if len(spell_settings) > 3:
        report['unsupported'].append('Mehr als drei aktive direkte Heilzauber')
