#!/usr/bin/env python3
"""
Interactive keyspace enumerator.

Features
- Ask for number of positions (N)
- Ask for the global set of possible values (for example: 1,2,3,4,5,6 or u,v,w,x,y,z)
- For each position, ask for allowed values (examples: 5  OR  3,6  OR  1-6  OR  * for all)
- Calculates total keyspace size
- Lists combinations in the format:
  Combination 1:  5,3,4,1,6,4,3

Safety controls
- If combinations > 1024, abort.
- If combinations > 128, ask user if they want to continue.
"""

from __future__ import annotations

import itertools
import math
import re
import sys
from typing import List, Sequence


def parse_global_values(raw: str) -> List[str]:
    """
    Parse a comma/space separated list into a list of tokens.
    Example inputs:
      "1,2,3,4,5,6"
      "u v w x y z"
      "u,v,w,x,y,z"
    """
    tokens = [t.strip() for t in re.split(r"[,\s]+", raw.strip()) if t.strip()]
    if not tokens:
        raise ValueError("No values provided.")
    return tokens


def parse_position_values(raw: str, global_values: Sequence[str]) -> List[str]:
    """
    Parse allowed values for one position.

    Supported formats:
      "*"                -> all global values
      "5"                -> single value
      "3,6" or "3 6"     -> list
      "1-6"              -> numeric range (only if global values are numeric tokens)
      "u-z"              -> NOT supported as alpha range, provide explicit list instead

    The function validates that all chosen values exist in global_values.
    """
    s = raw.strip()
    if not s:
        raise ValueError("Empty input.")

    if s == "*":
        return list(global_values)

    # Allow "OR" in the input, treat it as a delimiter
    s = re.sub(r"\bOR\b", ",", s, flags=re.IGNORECASE)

    parts = [p.strip() for p in re.split(r"[,\s]+", s) if p.strip()]
    if not parts:
        raise ValueError("No values parsed from input.")

    # Handle a single numeric range like "1-6"
    if len(parts) == 1 and re.fullmatch(r"-?\d+\s*-\s*-?\d+", parts[0]):
        a_str, b_str = [x.strip() for x in parts[0].split("-", 1)]
        a = int(a_str)
        b = int(b_str)
        lo, hi = (a, b) if a <= b else (b, a)

        # Ensure globals are numeric
        try:
            global_nums = [int(v) for v in global_values]
        except ValueError:
            raise ValueError("Range syntax like '1-6' requires numeric global values.")

        # Build set of allowed numbers in the inclusive range, then map back to token strings
        allowed_nums = set(range(lo, hi + 1))
        allowed_tokens = [v for v in global_values if int(v) in allowed_nums]
        if not allowed_tokens:
            raise ValueError("Range produced no values from the provided global set.")
        return allowed_tokens

    # Otherwise treat as explicit tokens
    allowed = parts

    # Validate membership in global values
    global_set = set(global_values)
    bad = [v for v in allowed if v not in global_set]
    if bad:
        raise ValueError(
            f"These values are not in the global set {list(global_values)}: {bad}"
        )

    # Deduplicate but preserve order
    seen = set()
    deduped = []
    for v in allowed:
        if v not in seen:
            seen.add(v)
            deduped.append(v)
    return deduped


def prompt_int(prompt: str, min_value: int = 1) -> int:
    while True:
        raw = input(prompt).strip()
        try:
            n = int(raw)
            if n < min_value:
                print(f"Please enter an integer >= {min_value}.")
                continue
            return n
        except ValueError:
            print("Please enter a valid integer.")


def main() -> None:
    print("Keyspace enumerator")
    print()

    n_positions = prompt_int("A) Number of positions to solve for: ", min_value=1)

    while True:
        raw_globals = input(
            "B) Global possible values (comma or space separated, example: 1,2,3,4,5,6): "
        ).strip()
        try:
            global_values = parse_global_values(raw_globals)
            break
        except ValueError as e:
            print(f"Error: {e}")

    print()
    print("C) For each position, enter allowed values.")
    print("   Examples: 5   or   3,6   or   1-6 (numeric only)   or   * (all values)")
    print()

    per_position: List[List[str]] = []
    for i in range(1, n_positions + 1):
        while True:
            raw = input(f"Position #{i} allowed values: ").strip()
            try:
                vals = parse_position_values(raw, global_values)
                per_position.append(vals)
                break
            except ValueError as e:
                print(f"Error: {e}")

    # 1) Calculate keyspace
    sizes = [len(vs) for vs in per_position]
    keyspace = math.prod(sizes)

    print()
    print(f"Possible combinations (keyspace): {keyspace}")

    # Abort / confirm rules
    if keyspace > 1024:
        print("Keyspace exceeds 1024, aborting without listing combinations.")
        sys.exit(0)

    if keyspace > 128:
        ans = input("Keyspace exceeds 128, list all combinations anyway? (y/N): ").strip().lower()
        if ans not in ("y", "yes"):
            print("Not listing combinations.")
            sys.exit(0)

    print()
    # 2) List keyspace
    for idx, combo in enumerate(itertools.product(*per_position), start=1):
        print(f"Combination {idx}:  " + ",".join(combo))


if __name__ == "__main__":
    main()

