# src/ctf/decoding.py
from chepy import Chepy
import re
from typing import Dict, Set
from ctf.helpers import check_for_flag, is_valid_flag
# }}}

# {{{ attempt_decode
def attempt_decode(val: str, name: str, pattern: str, method_name: str, len_check=None, validator=None) -> str | None:
    """Helper to run regex matching, dynamic Chepy decoding, and string printability checks."""
    # Find all matches sequentially and try decoding
    for match in re.finditer(pattern, val):
        matched_val = match.group(0)
        if len_check and not len_check(matched_val):
            continue
            
        try:
            chepy_inst = Chepy(matched_val)
            method = getattr(chepy_inst, method_name)
            decoded = method().state
            if isinstance(decoded, bytes):
                dec_str = decoded.decode("utf-8")
            elif isinstance(decoded, str):
                dec_str = decoded
            else:
                dec_str = ""
                
            if dec_str.strip() and all(32 <= ord(c) < 127 or c in "\r\n\t" for c in dec_str):
                if validator and not validator(dec_str, val):
                    continue
                return dec_str
        except Exception:
            pass
    return None
# }}}

# {{{ try_decode_metadata
def try_decode_metadata(val: str, current_chain: str = "", max_depth: int = 10, seen: Set[str] = None) -> Dict[str, str]:
    """Attempts to decode a metadata value using defined formats recursively."""
    if seen is None:
        seen = set()
        
    results = {}
    if not isinstance(val, str) or not val.strip() or max_depth <= 0:
        return results
        
    cleaned_val = val.strip()
    if cleaned_val in seen:
        return results
    seen.add(cleaned_val)
    
    # We define our decoders here using unanchored patterns. Note: for rot13 and reversed, we only allow them
    # if they produce a valid flag to avoid spamming everyday metadata fields.
    formats = [
        ("hex", r"[0-9a-fA-F]{4,}", "from_hex", lambda s: len(s) % 2 == 0, None),
        ("base64", r"[A-Za-z0-9+/=]{4,}", "from_base64", lambda s: len(s) % 4 == 0, None),
        ("base32", r"[A-Za-z2-7=]{4,}", "from_base32", lambda s: len(s) % 8 == 0, None),
        ("binary", r"[01]{8,}", "from_binary", lambda s: len(s) % 8 == 0, None),
        ("url", r"(?:%[0-9a-fA-F]{2})+", "from_url_encoding", None, None),
        ("rot13", r"[\x20-\x7E\s]{4,}", "rot_13", None, is_valid_flag),
        ("reversed", r"[\x20-\x7E\s]{4,}", "reverse", None, is_valid_flag),
    ]
    
    for name, pattern, method, len_check, validator in formats:
        decoded_val = attempt_decode(cleaned_val, name, pattern, method, len_check, validator)
        if decoded_val is not None:
            chain_name = f"{current_chain}->{name}" if current_chain else name
            results[chain_name] = decoded_val
            
            # Recurse with copy of seen to allow different branching paths to process same strings
            nested_results = try_decode_metadata(
                decoded_val,
                current_chain=chain_name,
                max_depth=max_depth - 1,
                seen=set(seen)
            )
            results.update(nested_results)
            
    return results
# }}}
