"""
Decode a polylan_mod_log.txt.

Hash format: sha1(seed|timestamp|id)  — timestamp is bound into the hash so
it cannot be altered without breaking the signature.

Seed-less entries (INIT, DEBUG, mod checks) use sha1(id) with no seed or
timestamp — those are resolved via a static lookup.

Usage:
    python decode_log.py [path/to/polylan_mod_log.txt]

Default path: ~/.local/share/Steam/steamapps/common/Noita/polylan_mod_log.txt
"""

import hashlib
import re
from functools import cache
from noita.services.spells import ALL_SPELLS, ALL_PERKS


SEED_POOL = [
    # General good seeds
    3154823,
    3718311,
    10064758,
    123156801,
    1024089369,
    1026967166,
    # Pacifist seeds
    177795258,
    520542929,
    10600249,
    25300740,
    21875589,
    24085389,
    59775105,
    44190726,
    1039649471,
    1072607354,
    # Perk combo seeds
    839747651,
    839844768,
    840909713,
    839959129,
    840016192,
    840039886,
    840398606,
    840439045,
    840457463,
    840492754,
    840507802,
    840513742,
    840542079,
    840574169,
    840610974,
    840626894,
    840872436,
    840894605,
    841221188,
]

DEATH_PENALTY = 1

# All scoreable events: base 1 pt for every spell and perk, overrides below.
POINTS = {
    # ── Spells ───────────────────────────────────────────────────────────────
    **{sid: 3 for sid in ALL_SPELLS},
    "ADD_TRIGGER": 10,
    "ADD_TIMER": 10,
    "ADD_DEATH_TRIGGER": 10,
    "NOLLA": 10,
    "CHAOTIC_TRANSMUTATION": 10,
    "DUPLICATE": 5,
    "BURST_2": 10,
    "BURST_3": 15,
    "BURST_4": 20,
    "BURST_8": 20,
    "BURST_X": 20,
    "HEAL_BULLET": 5,
    "ANTIHEAL": 5,
    "NUKE": 5,
    "NUKE_GIGA": 5,
    "TELEPORT_PROJECTILE": 5,
    "TELEPORT_PROJECTILE_SHORT": 5,
    "TOUCH_BLOOD": 100,
    "TOUCH_GOLD": 100,
    "TOUCH_PISS": 100,
    "TOUCH_GRASS": 100,
    "TOUCH_OIL": 100,
    "TOUCH_SMOKE": 100,
    "TOUCH_ALCOHOL": 100,
    "TOUCH_WATER": 100,
    "SPELLS_TO_POWER": 10,
    "ALL_SPELLS": 100,
    "DIVIDE_2": 10,
    "DIVIDE_3": 15,
    "DIVIDE_4": 20,
    "DIVIDE_10": 50,
    "ALPHA": 50,
    "GAMMA": 50,
    "MU": 50,
    "OMEGA": 50,
    "PHI": 50,
    "SIGMA": 50,
    "TAU": 50,
    "ZETA": 50,
    "DISC_BULLET_BIGGER": 20,
    "SUMMON_WANDGHOST": 50,
    "ALL_BLACKHOLES": 20,
    "ALL_DEATHCROSSES": 20,
    "ALL_ROCKETS": 20,
    "ALL_NUKES": 20,
    "ALL_DISCS": 20,
    # ── Perks ─────────────────────────────────────────────────────────────────
    **{pid: 15 for pid in ALL_PERKS},
    "PROTECTION_FIRE": 30,
    "PROTECTION_RADIOACTIVITY": 30,
    "PROTECTION_EXPLOSION": 30,
    "PROTECTION_MELEE": 30,
    "PROTECTION_ELECTRICITY": 30,
    # ── Spell combos ─────────────────────────────────────────────────────────
    "PING_PONG_DRILL": 40,
    "HEAVY_SHOT_DISC": 40,
    "TWO_ARC_MODIFIERS": 50,
    "TWO_TRAIL_MODIFIERS": 50,
    # ── Perk combos ──────────────────────────────────────────────────────────
    "CRIMSON_ALCHEMIST": 50,
    "GREEDY_GOBLIN_KING": 50,
    "STORM_TOUCHED_ASCENDANT": 50,
    "ARCHMAGE_OF_CONTROL": 50,
    "HAUNTED_MAGE": 50,
    "GLASS_CANNON_MESSIAH": 50,
    "INFINITE_ENGINE": 50,
    "PERFECT_ACCURACY_LOOP": 50,
    "HOMING_DEATH_SWARM": 50,
    "UNTOUCHABLE_FIELD": 50,
    "ELECTRIC_SUSTAIN_LOOP": 50,
    "IMMORTAL_LEECH_CORE": 50,
    "PROJECTILE_OVERLOAD": 50,
    "CLOSE_RANGE_DEATH_MACHINE": 50,
    "CRITICAL_MASS": 50,
    "HOLY_MOUNTAIN_ABUSER": 50,
    "DEFLECTOR_MATRIX": 50,
    "STORMBORNE_LEVITATOR": 50,
    # ── Objectives ───────────────────────────────────────────────────────────
    "HP_200": 50,
    "HP_500": 50,
    "HP_2000": 100,
    "HP_5000": 500,
    "GOLD_1000": 5,
    "GOLD_10000": 50,
    "GOLD_100000": 100,
    "GOLD_1000000": 200,
    **{f"ORB_{i}": 25 for i in range(34)},
    **{f"BOSS_KILL_{i}": 75 * i for i in range(34)},
    "WAND_MANA_500": 10,
    "WAND_MANA_1000": 10,
    "WAND_MANA_1500": 10,
    "WAND_CAPACITY_10": 10,
    "WAND_CAPACITY_20": 10,
    "BOSS_KILL": 100,
    "CRIMSON_ALCHEMIST-BOSS_KILL": 100,
    "GREEDY_GOBLIN_KING-BOSS_KILL": 100,
    "STORM_TOUCHED_ASCENDANT-BOSS_KILL": 100,
    "ARCHMAGE_OF_CONTROL-BOSS_KILL": 100,
    "HAUNTED_MAGE-BOSS_KILL": 100,
    "GLASS_CANNON_MESSIAH-BOSS_KILL": 100,
    "INFINITE_ENGINE-BOSS_KILL": 100,
    "PERFECT_ACCURACY_LOOP-BOSS_KILL": 100,
    "HOMING_DEATH_SWARM-BOSS_KILL": 100,
    "UNTOUCHABLE_FIELD-BOSS_KILL": 100,
    "ELECTRIC_SUSTAIN_LOOP-BOSS_KILL": 100,
    "IMMORTAL_LEECH_CORE-BOSS_KILL": 100,
    "PROJECTILE_OVERLOAD-BOSS_KILL": 100,
    "CLOSE_RANGE_DEATH_MACHINE-BOSS_KILL": 100,
    "CRITICAL_MASS-BOSS_KILL": 100,
    "HOLY_MOUNTAIN_ABUSER-BOSS_KILL": 100,
    "DEFLECTOR_MATRIX-BOSS_KILL": 100,
    "STORMBORNE_LEVITATOR-BOSS_KILL": 100,
}

# Perk-combo IDs — used to award a per-combo boss-kill bonus.
PERK_COMBO_IDS = {
    "CRIMSON_ALCHEMIST",
    "GREEDY_GOBLIN_KING",
    "STORM_TOUCHED_ASCENDANT",
    "ARCHMAGE_OF_CONTROL",
    "HAUNTED_MAGE",
    "GLASS_CANNON_MESSIAH",
    "INFINITE_ENGINE",
    "PERFECT_ACCURACY_LOOP",
    "HOMING_DEATH_SWARM",
    "UNTOUCHABLE_FIELD",
    "ELECTRIC_SUSTAIN_LOOP",
    "IMMORTAL_LEECH_CORE",
    "PROJECTILE_OVERLOAD",
    "CLOSE_RANGE_DEATH_MACHINE",
    "CRITICAL_MASS",
    "HOLY_MOUNTAIN_ABUSER",
    "DEFLECTOR_MATRIX",
    "STORMBORNE_LEVITATOR",
}

BOSS_KILL_COMBO_BONUS = 100  # default bonus per active perk combo on boss kill

# IDs tried with the seed+timestamp scheme.
_ALL_IDS = list(POINTS.keys()) + ["DEATH"]

# Seed-less hashes (sha1(id) only, no seed, no timestamp).
_STATIC_LOOKUP = {
    hashlib.sha1(k.encode()).hexdigest(): k
    for k in ["-", "DEBUG", "polylan-mod", "cheatgui", "alchemy_recipes_display"]
}


def _sha1(text: str) -> str:
    return hashlib.sha1(text.encode("utf-8")).hexdigest()


@cache
def resolve(
    hash_val: str, ts: str, preferred_seed: int | None = None
) -> tuple[str | None, int | None]:
    """Return (name, seed) or (None, None). preferred_seed is tried first."""
    if hash_val in _STATIC_LOOKUP:
        return _STATIC_LOOKUP[hash_val], None

    seeds = (
        [preferred_seed] + [s for s in SEED_POOL if s != preferred_seed]
        if preferred_seed is not None
        else SEED_POOL
    )

    for seed in seeds:
        prefix = f"{seed}|{ts}|"
        for name in _ALL_IDS:
            if _sha1(prefix + name) == hash_val:
                return name, seed

    return None, None


def parse_log(file) -> list:
    entries = []
    for il, line in enumerate(file.split("\n")):
        m = re.match(r"\[(.+?)\] ([0-9a-f]{40})", line.rstrip())
        if m:
            entries.append({"ts": m.group(1), "hash": m.group(2)})
            continue

        print(f"Unable to parse line number {il:>4}: {line.strip()}??")

    return entries


def decode(file) -> None:
    entries = parse_log(file)

    if not entries:
        print("No entries found in log.")
        return

    seen = set()
    total = 0
    deaths = 0
    have_init = False
    have_debug = False
    known_seed = None  # cached once first seeded entry is resolved
    current_run_perk_combos = set()  # perk combos seen since the last "-" entry

    for entry in entries:
        name, seed = resolve(entry["hash"], entry["ts"], known_seed)

        if name is None:
            print(f"  ????  (unresolved) {entry['hash']}  ts: {entry['ts']}")
            continue

        if seed is not None and known_seed is None:
            known_seed = seed

        if name == "-":
            have_init = True
            current_run_perk_combos = set()
            continue

        if name == "DEBUG":
            have_debug = True
            continue

        if name == "DEATH":
            deaths += 1
            continue

        # skip mod-presence entries
        if name not in POINTS:
            continue

        if name in seen:
            continue

        if name not in {"BOSS_KILL"}:
            seen.add(name)

        if name in PERK_COMBO_IDS:
            current_run_perk_combos.add(name)

        pts = POINTS.get(name, 0)
        if pts:
            print(f"  +{pts:>4}  {name:<40}  first seen: {entry['ts']}")
            total += pts

        if name == "BOSS_KILL":
            for combo_id in sorted(current_run_perk_combos):
                bonus_key = f"{combo_id}-BOSS_KILL"
                if bonus_key not in seen:
                    seen.add(bonus_key)
                    bonus_pts = POINTS.get(bonus_key, BOSS_KILL_COMBO_BONUS)
                    print(f"  +{bonus_pts:>4}  {bonus_key:<40}  combo bonus")
                    total += bonus_pts

    death_deduction = deaths * DEATH_PENALTY
    if deaths:
        print(
            f"  -{death_deduction:>4}  ({deaths} death{'s' if deaths > 1 else ''} × {DEATH_PENALTY} pts)"
        )

    if have_init:
        print(
            f"\nTotal: {total - death_deduction} pts  ({total} earned − {death_deduction} penalty)"
        )

    if have_debug:
        print("Note: DEBUG mode was active during at least one session.")
