"""Typer-based CLI for quick Tarot exploration with completions.

Run `python typer-test.py --help` for commands. Install shell completions with
`python typer-test.py --install-completion`.
"""

from __future__ import annotations

import ast
import inspect
import shlex
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Set

import typer
from rich.console import Console
from rich.table import Table

from letter import letter as letter_ns

try:
    from prompt_toolkit import PromptSession
    from prompt_toolkit.completion import Completer, Completion
except ImportError:  # pragma: no cover - optional at runtime
    PromptSession = None  # type: ignore
    Completer = object  # type: ignore
    Completion = object  # type: ignore
    WordCompleter = None  # type: ignore


# Ensure the src directory is on sys.path when running from repo root
ROOT = Path(__file__).resolve().parent
SRC_DIR = ROOT / "src"
if str(SRC_DIR) not in sys.path:
    sys.path.insert(0, str(SRC_DIR))

from cli_renderers import Renderer  # noqa: E402
from tarot import Card, Tarot, Tree, Cube  # noqa: E402
import tarot.ui as ui_mod  # noqa: E402
from tarot.ui import SpreadDisplay, display_cards as ui_display_cards  # noqa: E402


app = typer.Typer(add_completion=True, help="CLI playground for the Tarot API.")
tree_app = typer.Typer(help="Tree of Life commands. Examples: tree sephera 1; tree path 11.")
cube_app = typer.Typer(help="Cube of Space commands. Examples: cube wall North; cube direction North East.")
letter_app = typer.Typer(help="Letter/I Ching/Periodic commands. Examples: letter alphabet; letter cipher; letter char Aleph.")
console = Console()
renderer = Renderer(console)
_render_value = renderer.render_value
_print_kv_table = renderer.print_kv_table
_print_rows_table = renderer.print_rows_table
_print_cards_table = renderer.print_cards_table
EXPR_NO_MATCH = object()

SAFE_ROOTS: Dict[str, Any] = {
    "tarot": Tarot,
    "tree": Tree,
    "cube": Cube,
    "letter": letter_ns,
    "ui": ui_mod,
}

_MISSING_DOCS: Set[str] = set()


ROOT_COMMANDS = [
    "tarot",
    "search",
    "planet",
    "hexagram",
    "tree",
    "cube",
    "letter",
    "display-cards",
    "display-spread",
    "help",
    "quit",
    "exit",
]


def _base_keywords() -> List[str]:
    return list(ROOT_COMMANDS)

SUITS = ("Cups", "Pentacles", "Swords", "Wands")
ARCANA = ("Major", "Minor")


def _ensure_deck() -> None:
    """Make sure the shared deck is initialized before use."""

    Tarot.deck.card._ensure_initialized()  # type: ignore[attr-defined]


def _complete_suit(ctx: typer.Context, incomplete: str) -> List[str]:
    return [s for s in SUITS if s.lower().startswith(incomplete.lower())]


def _complete_arcana(ctx: typer.Context, incomplete: str) -> List[str]:
    return [a for a in ARCANA if a.lower().startswith(incomplete.lower())]


def _complete_planet_name(ctx: typer.Context, incomplete: str) -> List[str]:
    try:
        planets = Tarot.planet()  # type: ignore[assignment]
    except Exception:
        return []
    names = list(planets.keys()) if isinstance(planets, dict) else []
    return [n for n in names if n.lower().startswith(incomplete.lower())]


def _complete_hexagram(ctx: typer.Context, incomplete: str) -> List[str]:
    try:
        hexagrams = Tarot.hexagram()  # type: ignore[assignment]
    except Exception:
        return []
    nums = [str(k) for k in hexagrams.keys()] if isinstance(hexagrams, dict) else []
    return [n for n in nums if n.startswith(incomplete)]


def _planet_names() -> List[str]:
    try:
        planets = Tarot.planet()  # type: ignore[assignment]
    except Exception:
        return []
    return list(planets.keys()) if isinstance(planets, dict) else []


def _hexagram_numbers() -> List[str]:
    try:
        hexagrams = Tarot.hexagram()  # type: ignore[assignment]
    except Exception:
        return []
    return [str(k) for k in hexagrams.keys()] if isinstance(hexagrams, dict) else []


def _sephera_numbers() -> List[str]:
    try:
        from tarot import Tree

        seph = Tree.sephera()  # type: ignore[assignment]
    except Exception:
        return []
    return [str(k) for k in seph.keys()] if isinstance(seph, dict) else []


def _path_numbers() -> List[str]:
    try:
        from tarot import Tree

        paths = Tree.path()  # type: ignore[assignment]
    except Exception:
        return []
    return [str(k) for k in paths.keys()] if isinstance(paths, dict) else []


def _wall_names() -> List[str]:
    return ["North", "South", "East", "West", "Above", "Below"]


def _alphabet_names() -> List[str]:
    try:
        alph = letter_ns.alphabet.all()
    except Exception:
        return []
    return list(alph.keys()) if isinstance(alph, dict) else []


def _cipher_names() -> List[str]:
    try:
        ciphers = letter_ns.cipher.all()
    except Exception:
        return []
    return list(ciphers.keys()) if isinstance(ciphers, dict) else []


def _letter_names() -> List[str]:
    try:
        letters = letter_ns.letter.all()
    except Exception:
        return []
    return list(letters.keys()) if isinstance(letters, dict) else []


def _periodic_symbols() -> List[str]:
    try:
        periodic = letter_ns.periodic.all()
    except Exception:
        return []
    return list(periodic.keys()) if isinstance(periodic, dict) else []


def _spread_names() -> List[str]:
    return list(SpreadDisplay.LAYOUTS.keys())


def _current_fragment(text: str) -> str:
    stripped = text.rstrip()
    if not stripped:
        return ""
    last_token = stripped.split()[-1]
    if "." in last_token:
        return last_token.rsplit(".", 1)[-1]
    return last_token


def _completion_candidates(text: str) -> List[str]:
    stripped = text.rstrip()
    if not stripped:
        return _base_keywords()
    try:
        tokens = shlex.split(stripped)
    except ValueError:
        tokens = stripped.split()

    last_token = tokens[-1] if tokens else ""

    # Dotted path completion
    if "." in last_token:
        head, partial = last_token.rsplit(".", 1)
        target = _safe_eval_expr(head)
        if target is EXPR_NO_MATCH:
            return []
        partial_lower = partial.lower()
        if isinstance(target, dict):
            return [str(k) for k in target.keys() if str(k).lower().startswith(partial_lower)]
        return [name for name in dir(target) if not name.startswith("_") and name.lower().startswith(partial_lower)]

    fragment = last_token.lower() if last_token else ""

    # No tokens yet: suggest root commands
    if len(tokens) == 0:
        return [kw for kw in _base_keywords() if kw.lower().startswith(fragment)]

    cmd = tokens[0].lower()

    # First token: suggest matching commands
    if len(tokens) == 1:
        return [kw for kw in _base_keywords() if kw.lower().startswith(fragment)]

    # Contextual suggestions per command
    if cmd == "planet":
        return [p for p in _planet_names() if p.lower().startswith(fragment)]

    if cmd == "hexagram":
        return [n for n in _hexagram_numbers() if n.startswith(fragment)]

    if cmd == "tree":
        if len(tokens) == 2:
            return [sub for sub in ["sephera", "path"] if sub.startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() == "sephera":
            return [n for n in _sephera_numbers() if n.startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() == "path":
            return [n for n in _path_numbers() if n.startswith(fragment)]

    if cmd == "cube":
        if len(tokens) == 2:
            return [sub for sub in ["wall", "direction"] if sub.startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() == "wall":
            return [w for w in _wall_names() if w.lower().startswith(fragment)]
        if len(tokens) >= 3 and tokens[1].lower() == "direction":
            # suggest wall name at position 3
            if len(tokens) == 3:
                return [w for w in _wall_names() if w.lower().startswith(fragment)]
            # suggest direction names after wall
            return [d for d in _direction_names_for_wall(tokens[2]) if d.lower().startswith(fragment)]

    if cmd == "letter":
        if len(tokens) == 2:
            return [sub for sub in ["alphabet", "cipher", "char", "periodic"] if sub.startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() == "alphabet":
            return [a for a in _alphabet_names() if a.lower().startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() == "cipher":
            return [c for c in _cipher_names() if c.lower().startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() in {"char", "letter"}:
            return [l for l in _letter_names() if l.lower().startswith(fragment)]
        if len(tokens) == 3 and tokens[1].lower() == "periodic":
            return [s for s in _periodic_symbols() if s.lower().startswith(fragment)]

    if cmd == "display-spread":
        return [name for name in _spread_names() if name.lower().startswith(fragment)]

    if cmd == "search":
        # Simple heuristics: suggest option flags or option values based on prior flag
        if last_token.startswith("-"):
            return [opt for opt in ["--suit", "--arcana", "--pip", "-s", "-a", "-p"] if opt.startswith(last_token)]
        if any(tok in {"--suit", "-s"} for tok in tokens[:-1]):
            return [s for s in SUITS if s.lower().startswith(fragment)]
        if any(tok in {"--arcana", "-a"} for tok in tokens[:-1]):
            return [a for a in ARCANA if a.lower().startswith(fragment)]
        return []

    # Default: no suggestions
    return []


class DotPathCompleter(Completer):
    def get_completions(self, document, complete_event):  # type: ignore[override]
        text = document.text_before_cursor
        fragment = _current_fragment(text)
        for cand in _completion_candidates(text):
            yield Completion(cand, start_position=-len(fragment))


def _safe_eval_expr(expr: str) -> Any:
    expr = expr.strip()
    if not expr:
        return EXPR_NO_MATCH
    try:
        parsed = ast.parse(expr, mode="eval")
    except SyntaxError:
        return EXPR_NO_MATCH

    def _eval_node(node: ast.AST) -> Any:
        if isinstance(node, ast.Expression):
            return _eval_node(node.body)
        if isinstance(node, ast.Name):
            if node.id in SAFE_ROOTS:
                return SAFE_ROOTS[node.id]
            raise ValueError("Name not allowed")
        if isinstance(node, ast.Attribute):
            value = _eval_node(node.value)
            return getattr(value, node.attr)
        if isinstance(node, ast.Call):
            func = _eval_node(node.func)
            args = [_eval_node(arg) for arg in node.args]
            kwargs = {kw.arg: _eval_node(kw.value) for kw in node.keywords if kw.arg is not None}
            return func(*args, **kwargs)
        if isinstance(node, ast.Constant):
            return node.value
        raise ValueError("Expression not allowed")

    try:
        return _eval_node(parsed)
    except Exception:
        return EXPR_NO_MATCH


def _search_cards(suit: Optional[str], arcana: Optional[str], pip: Optional[int]) -> List[Card]:
    _ensure_deck()
    deck = Tarot.deck.card._deck  # type: ignore[attr-defined]
    if deck is None:
        return []

    cards: List[Card] = deck.cards
    if suit is not None:
        cards = [
            card
            for card in cards
            if getattr(getattr(card, "suit", None), "name", "").lower() == suit.lower()
        ]
    if arcana is not None:
        cards = [card for card in cards if getattr(card, "arcana", "").lower() == arcana.lower()]
    if pip is not None:
        cards = [card for card in cards if getattr(card, "pip", None) == pip]
    return cards
@app.command()
def search(
    suit: Optional[str] = typer.Option(
        None, "--suit", "-s", help="Filter by suit", autocompletion=_complete_suit
    ),
    arcana: Optional[str] = typer.Option(
        None, "--arcana", "-a", help="Filter by arcana", autocompletion=_complete_arcana
    ),
    pip: Optional[int] = typer.Option(
        None, "--pip", "-p", help="Filter by pip (1 for Ace, 2-10 for pips)"
    ),
) -> None:
    """Filter cards with autocompleting suit/arcana options."""

    cards = _search_cards(suit, arcana, pip)

    if not cards:
        typer.echo("No cards matched that filter.")
        raise typer.Exit(code=0)

def _doc(obj: Any, fallback: str) -> str:
    return inspect.getdoc(obj) or fallback


def _sync_doc(cmd_fn: Any, source_obj: Any) -> None:
    if cmd_fn is None or source_obj is None:
        return
    doc = _doc(source_obj, cmd_fn.__doc__ or "")
    if doc:
        cmd_fn.__doc__ = doc
    else:
        _MISSING_DOCS.add(cmd_fn.__name__)


def _report_missing_docs() -> None:
    if not _MISSING_DOCS:
        return
    console.print(
        f"[yellow]Docstring missing for: {', '.join(sorted(_MISSING_DOCS))}."
        " Update source API docstrings to sync help.[/yellow]"
    )


def _print_structure_table() -> None:
    rows = [
        ("tarot.deck.card(n)", "Access cards by deck position", "tarot.deck.card(1)"),
        (
            "tarot.deck.card.filter(...)",
            "Filter cards using attributes",
            "tarot.deck.card.filter(arcana='Major')",
        ),
        (
            "tarot.deck.card.spread(name)",
            "Draw a spread with random cards",
            "tarot.deck.card.spread('Celtic Cross')",
        ),
            ("ui.display_cards([card1, card2])", "Open GUI to view cards", "ui.display_cards([tarot.deck.card(1)])"),
            ("display-spread 'Celtic Cross'", "CLI: draw + show spread", "python typer-test.py display-spread 'Celtic Cross'"),
            ("display-cards 1 2 3", "CLI: show specific cards", "python typer-test.py display-cards 1 2 3"),
        ("tarot.planet()", "Planet correspondences as dict", "tarot.planet()['Venus']"),
        ("tarot.hexagram()", "I Ching hexagrams as dict", "tarot.hexagram()[1]"),
        ("tree.sephera(n)", "Kabbalistic sephiroth", "tree.sephera(1)"),
        ("tree.path(n)", "Paths on the Tree of Life", "tree.path(11)"),
        ("cube.wall(name)", "Cube of Space walls", "cube.wall('North')"),
        ("cube.direction(wall, dir)", "Directions on a wall", "cube.direction('North', 'East')"),
        ("letter.alphabet.all()", "Alphabet metadata", "letter.alphabet.all()['english']"),
        ("letter.cipher.all()", "Cipher definitions", "letter.cipher.all()['english_simple']"),
        ("letter.letter.all()", "Letters and properties", "letter.letter.all()['Aleph']"),
        ("letter.periodic.all()", "Periodic correspondences", "letter.periodic.all()['H']"),
        ("letter.iching.all()", "I Ching via letter namespace", "letter.iching.all()[1]"),
    ]

    table = Table(title="Tarot API Map", show_lines=True)
    table.add_column("Expression", style="cyan")
    table.add_column("Description")
    table.add_column("Example")
    for expr, desc, example in rows:
        table.add_row(expr, desc, example)
    console.print(table)
    console.print(
        "REPL: type expressions exactly as above; tab completion works per segment. "
        "CLI shortcuts that remain: search, planet, hexagram, tree, cube, letter."
    )


def _show_sephera(number: Optional[int]) -> None:
    getattr(Tree, "_ensure_initialized", lambda: None)()
    data = Tree.sephera(number)
    if data is None:
        typer.echo("Sephera not found.")
        return
    if isinstance(data, dict):
        rows: List[List[Any]] = []
        for num, obj in sorted(data.items()):
            rows.append([
                num,
                getattr(obj, "name", ""),
                getattr(obj, "hebrew_name", ""),
                getattr(obj, "planet", ""),
                getattr(obj, "element", ""),
            ])
        _print_rows_table("Sephiroth", ["#", "Name", "Hebrew", "Planet", "Element"], rows)
    else:
        attrs = data.__dict__ if hasattr(data, "__dict__") else {"value": data}
        _print_kv_table(f"Sephera: {getattr(data, 'name', number)}", attrs)


def _show_path(number: Optional[int]) -> None:
    getattr(Tree, "_ensure_initialized", lambda: None)()
    data = Tree.path(number)
    if data is None:
        typer.echo("Path not found.")
        return
    if isinstance(data, dict):
        rows: List[List[Any]] = []
        for num, obj in sorted(data.items()):
            rows.append([
                num,
                getattr(obj, "name", ""),
                getattr(obj, "hebrew_letter", getattr(obj, "hebrew", "")),
                getattr(obj, "planet", getattr(obj, "element", "")),
            ])
        _print_rows_table("Paths", ["#", "Name", "Hebrew", "Assoc"], rows)
    else:
        attrs = data.__dict__ if hasattr(data, "__dict__") else {"value": data}
        _print_kv_table(f"Path: {number}", attrs)


def _show_walls(name: Optional[str]) -> None:
    cube_walls = Cube.wall.all() if Cube.wall else []  # type: ignore[attr-defined]
    if name is None:
        rows = []
        for wall in cube_walls:
            rows.append([
                getattr(wall, "name", ""),
                getattr(wall, "element", ""),
                getattr(wall, "planet", ""),
                getattr(wall, "opposite", ""),
            ])
        _print_rows_table("Cube Walls", ["Wall", "Element", "Planet", "Opposite"], rows)
        return
    wall_obj = None
    for wall in cube_walls:
        if getattr(wall, "name", "").lower() == name.lower():
            wall_obj = wall
            break
    if wall_obj is None:
        typer.echo(f"Wall '{name}' not found.")
        return
    attrs = wall_obj.__dict__ if hasattr(wall_obj, "__dict__") else {"value": wall_obj}
    _print_kv_table(f"Wall: {getattr(wall_obj, 'name', name)}", attrs)


def _direction_names_for_wall(wall_name: str) -> List[str]:
    cube_walls = Cube.wall.all() if Cube.wall else []  # type: ignore[attr-defined]
    for wall in cube_walls:
        if getattr(wall, "name", "").lower() == wall_name.lower():
            return list(getattr(wall, "directions", {}).keys()) if hasattr(wall, "directions") else []
    return []


def _show_direction(wall_name: str, direction: Optional[str]) -> None:
    cube_walls = Cube.wall.all() if Cube.wall else []  # type: ignore[attr-defined]
    wall_obj = None
    for wall in cube_walls:
        if getattr(wall, "name", "").lower() == wall_name.lower():
            wall_obj = wall
            break
    if wall_obj is None:
        typer.echo(f"Wall '{wall_name}' not found.")
        return
    if direction is None:
        dirs = getattr(wall_obj, "directions", {}) if hasattr(wall_obj, "directions") else {}
        rows = []
        for name, dir_obj in dirs.items():
            rows.append([
                name,
                getattr(dir_obj, "element", ""),
                getattr(dir_obj, "planet", ""),
            ])
        _print_rows_table(
            f"Directions for {getattr(wall_obj, 'name', wall_name)}",
            ["Direction", "Element", "Planet"],
            rows,
        )
        return
    dirs = getattr(wall_obj, "directions", {}) if hasattr(wall_obj, "directions") else {}
    dir_obj = dirs.get(direction.capitalize()) if isinstance(dirs, dict) else None
    if dir_obj is None:
        typer.echo(f"Direction '{direction}' not found in wall '{wall_name}'.")
        return
    attrs = dir_obj.__dict__ if hasattr(dir_obj, "__dict__") else {"value": dir_obj}
    _print_kv_table(f"Direction: {direction} ({wall_name})", attrs)


def _show_alphabets(name: Optional[str]) -> None:
    alph = letter_ns.alphabet.all()
    if not isinstance(alph, dict):
        typer.echo("Alphabet data unavailable.")
        return
    if name is None:
        rows = [[k, v.get("name", "") if isinstance(v, dict) else ""] for k, v in alph.items()]
        _print_rows_table("Alphabets", ["Key", "Name"], rows)
        return
    obj = alph.get(name)
    if obj is None:
        typer.echo(f"Alphabet '{name}' not found.")
        return
    attrs = obj if isinstance(obj, dict) else {"value": obj}
    _print_kv_table(f"Alphabet: {name}", attrs)


def _show_ciphers(name: Optional[str]) -> None:
    ciphers = letter_ns.cipher.all()
    if not isinstance(ciphers, dict):
        typer.echo("Cipher data unavailable.")
        return
    if name is None:
        rows = [[k, ""] for k in ciphers.keys()]
        _print_rows_table("Ciphers", ["Key", ""], rows)
        return
    obj = ciphers.get(name)
    if obj is None:
        typer.echo(f"Cipher '{name}' not found.")
        return
    attrs = obj if isinstance(obj, dict) else {"value": obj}
    _print_kv_table(f"Cipher: {name}", attrs)


def _show_letter_char(name: Optional[str]) -> None:
    letters = letter_ns.letter.all()
    if not isinstance(letters, dict):
        typer.echo("Letter data unavailable.")
        return
    if name is None:
        rows = [[k, getattr(v, "hebrew_name", getattr(v, "name", ""))] for k, v in letters.items()]
        _print_rows_table("Letters", ["Key", "Name"], rows)
        return
    obj = letters.get(name)
    if obj is None:
        typer.echo(f"Letter '{name}' not found.")
        return
    attrs = obj.__dict__ if hasattr(obj, "__dict__") else {"value": obj}
    _print_kv_table(f"Letter: {name}", attrs)


def _show_periodic(symbol: Optional[str]) -> None:
    periodic = letter_ns.periodic.all()
    if not isinstance(periodic, dict):
        typer.echo("Periodic table data unavailable.")
        return
    if symbol is None:
        rows = [[k, getattr(v, "name", getattr(v, "element", ""))] for k, v in periodic.items()]
        _print_rows_table("Periodic Table", ["Symbol", "Name"], rows)
        return
    obj = periodic.get(symbol)
    if obj is None:
        typer.echo(f"Symbol '{symbol}' not found.")
        return
    attrs = obj.__dict__ if hasattr(obj, "__dict__") else {"value": obj}
    _print_kv_table(f"Element: {symbol}", attrs)


@app.command()
def planet(
    name: Optional[str] = typer.Argument(
        None, help="Planet name (omit to list all)", autocompletion=_complete_planet_name
    )
) -> None:
    """Show a planet or list all planet names."""

    Tarot._ensure_initialized()  # type: ignore[attr-defined]
    planets = Tarot.planet()  # type: ignore[assignment]
    if not isinstance(planets, dict):
        typer.echo("Planet data unavailable.")
        raise typer.Exit(code=1)

    if name is None:
        table = Table(title="Planets", show_header=True)
        table.add_column("Name")
        for planet_name in sorted(planets.keys()):
            table.add_row(planet_name)
        console.print(table)
        return

    planet_obj = planets.get(name)
    if planet_obj is None:
        typer.echo(f"Planet '{name}' not found.")
        raise typer.Exit(code=1)

    _render_value(planet_obj)


@app.command()
def hexagram(
    number: Optional[int] = typer.Argument(
        None, help="Hexagram number (1-64). Omit to list all.", autocompletion=_complete_hexagram
    )
) -> None:
    """Show an I Ching hexagram or list all numbers."""

    Tarot._ensure_initialized()  # type: ignore[attr-defined]
    hexagrams = Tarot.hexagram()  # type: ignore[assignment]
    if not isinstance(hexagrams, dict):
        typer.echo("Hexagram data unavailable.")
        raise typer.Exit(code=1)

    if number is None:
        table = Table(title="Hexagrams", show_header=True)
        table.add_column("Number")
        for num in sorted(hexagrams.keys()):
            table.add_row(str(num))
        console.print(table)
        return

    hex_obj = hexagrams.get(number)
    if hex_obj is None:
        typer.echo(f"Hexagram '{number}' not found.")
        raise typer.Exit(code=1)

    attrs = hex_obj.__dict__ if hasattr(hex_obj, "__dict__") else {"value": hex_obj}
    _print_kv_table(f"Hexagram: {number}", attrs)


# Keep CLI command docs in sync with API docstrings
_sync_doc(planet, getattr(Tarot, "planet", None))
_sync_doc(hexagram, getattr(Tarot, "hexagram", None))


app.add_typer(tree_app, name="tree")
app.add_typer(cube_app, name="cube")
app.add_typer(letter_app, name="letter")


@tree_app.command("sephera")
def tree_sephera(number: Optional[int] = typer.Argument(None, autocompletion=_sephera_numbers)) -> None:
    """List all sephiroth or show a specific one."""

    _show_sephera(number)


@tree_app.command("path")
def tree_path(number: Optional[int] = typer.Argument(None, autocompletion=_path_numbers)) -> None:
    """List all paths or show a specific one."""

    _show_path(number)


@cube_app.command("wall")
def cube_wall(name: Optional[str] = typer.Argument(None, autocompletion=_wall_names)) -> None:
    """List walls or show details for a wall."""

    _show_walls(name)


@cube_app.command("direction")
def cube_direction(
    wall_name: str = typer.Argument(..., autocompletion=_wall_names),
    direction: Optional[str] = typer.Argument(None),
) -> None:
    """List directions for a wall or show one."""

    _show_direction(wall_name, direction)


@letter_app.command("alphabet")
def letter_alphabet(name: Optional[str] = typer.Argument(None, autocompletion=_alphabet_names)) -> None:
    """List alphabets or show one."""

    _show_alphabets(name)


@letter_app.command("cipher")
def letter_cipher(name: Optional[str] = typer.Argument(None, autocompletion=_cipher_names)) -> None:
    """List ciphers or show one."""

    _show_ciphers(name)


@letter_app.command("char")
def letter_char(name: Optional[str] = typer.Argument(None, autocompletion=_letter_names)) -> None:
    """List letters or show one."""

    _show_letter_char(name)


@letter_app.command("periodic")
def letter_periodic(symbol: Optional[str] = typer.Argument(None, autocompletion=_periodic_symbols)) -> None:
    """List periodic elements or show one."""

    _show_periodic(symbol)


# Sync all command docstrings from source APIs (single source of truth)
_sync_doc(planet, getattr(Tarot, "planet", None))
_sync_doc(hexagram, getattr(Tarot, "hexagram", None))
_sync_doc(tree_sephera, getattr(Tree, "sephera", None))
_sync_doc(tree_path, getattr(Tree, "path", None))
_sync_doc(cube_wall, getattr(Cube, "wall", None))
_sync_doc(cube_direction, getattr(Cube, "direction", None))
_sync_doc(letter_alphabet, getattr(letter_ns, "alphabet", None))
_sync_doc(letter_cipher, getattr(letter_ns, "cipher", None))
_sync_doc(letter_char, getattr(letter_ns, "letter", None))
_sync_doc(letter_periodic, getattr(letter_ns, "periodic", None))
_report_missing_docs()


def _handle_repl_command(parts: List[str], raw_line: str = "") -> bool:
    if not parts:
        return True
    cmd = parts[0].lower()

    if cmd in {"quit", "exit"}:
        return False
    if cmd == "help":
        _print_structure_table()
        return True
    if cmd == "search":
        suit = None
        arcana = None
        pip = None
        tokens = parts[1:]
        i = 0
        while i < len(tokens):
            if tokens[i] in {"--suit", "-s"} and i + 1 < len(tokens):
                suit = tokens[i + 1]
                i += 2
            elif tokens[i] in {"--arcana", "-a"} and i + 1 < len(tokens):
                arcana = tokens[i + 1]
                i += 2
            elif tokens[i] in {"--pip", "-p"} and i + 1 < len(tokens):
                try:
                    pip = int(tokens[i + 1])
                except ValueError:
                    console.print("Invalid pip value.")
                    return True
                i += 2
            else:
                i += 1
        cards = _search_cards(suit, arcana, pip)
        if not cards:
            console.print("No cards matched that filter.")
        else:
            _print_cards_table(cards)


        @app.command("display-cards")
        def display_cards_cmd(
            numbers: List[int] = typer.Argument(..., help="Card numbers to display"),
            deck_name: str = typer.Option("default", help="Deck name for UI display"),
        ) -> None:
            """Open a GUI window showing the given card numbers."""

            _ensure_deck()
            cards: List[Card] = []
            for num in numbers:
                card = Tarot.deck.card(num)  # type: ignore[attr-defined]
                if card is None:
                    typer.echo(f"Card {num} not found.")
                    raise typer.Exit(code=1)
                cards.append(card)
            ui_display_cards(cards, deck_name)


        @app.command("display-spread")
        def display_spread_cmd(
            spread_name: str = typer.Argument("Celtic Cross", help="Spread name (e.g., 'Celtic Cross')"),
            deck_name: str = typer.Option("default", help="Deck name for UI display"),
        ) -> None:
            """Draw a spread and open the GUI display for it."""

            _ensure_deck()
            reading = Tarot.deck.card.spread(spread_name)  # type: ignore[attr-defined]
            if reading is None:
                typer.echo(f"Spread '{spread_name}' not found.")
                raise typer.Exit(code=1)
            ui_mod.display_spread(reading, deck_name)
        return True
    if cmd == "planet":
        planet_name = parts[1] if len(parts) > 1 else None
        planets = Tarot.planet()  # type: ignore[assignment]
        if not isinstance(planets, dict):
            console.print("Planet data unavailable.")
            return True
        if planet_name is None:
            console.print(", ".join(sorted(planets.keys())))
            return True
        planet_obj = planets.get(planet_name)
        if planet_obj is None:
            console.print(f"Planet '{planet_name}' not found.")
            return True
        _render_value(planet_obj)
        return True
    if cmd == "display-cards":
        if len(parts) < 2:
            console.print("Usage: display-cards <card numbers...>")
            console.print("Examples: display-cards 1 2 3")
            console.print("          display-cards 2,11,20")
            return True
        _ensure_deck()
        nums: List[int] = []
        
        # Handle both space-separated and comma-separated numbers
        tokens = []
        for tok in parts[1:]:
            # Split by comma if present
            if "," in tok:
                tokens.extend(tok.split(","))
            else:
                tokens.append(tok)
        
        for tok in tokens:
            tok = tok.strip()
            if not tok:
                continue
            try:
                nums.append(int(tok))
            except ValueError:
                console.print(f"Invalid card number: {tok}")
                return True
        
        cards: List[Card] = []
        for num in nums:
            card = Tarot.deck.card(num)  # type: ignore[attr-defined]
            if card is None:
                console.print(f"Card {num} not found.")
                return True
            cards.append(card)
        ui_display_cards(cards)
        return True
    if cmd == "display-spread":
        spread_name = " ".join(parts[1:]) if len(parts) > 1 else "Celtic Cross"
        _ensure_deck()
        reading = Tarot.deck.card.spread(spread_name)  # type: ignore[attr-defined]
        if reading is None:
            console.print(f"Spread '{spread_name}' not found.")
            return True
        ui_mod.display_spread(reading)
        return True
    if cmd == "hexagram":
        hex_num = None
        if len(parts) > 1 and parts[1].isdigit():
            hex_num = int(parts[1])
        hexagrams = Tarot.hexagram()  # type: ignore[assignment]
        if not isinstance(hexagrams, dict):
            console.print("Hexagram data unavailable.")
            return True
        if hex_num is None:
            console.print(", ".join(str(n) for n in sorted(hexagrams.keys())))
            return True
        hex_obj = hexagrams.get(hex_num)
        if hex_obj is None:
            console.print(f"Hexagram '{hex_num}' not found.")
            return True
        attrs = hex_obj.__dict__ if hasattr(hex_obj, "__dict__") else {"value": hex_obj}
        _print_kv_table(f"Hexagram: {hex_num}", attrs)
        return True
    if cmd == "tree" and len(parts) > 1:
        sub = parts[1].lower()
        arg = parts[2] if len(parts) > 2 else None
        if sub == "sephera":
            _show_sephera(int(arg)) if arg and arg.isdigit() else _show_sephera(None)
            return True
        if sub == "path":
            _show_path(int(arg)) if arg and arg.isdigit() else _show_path(None)
            return True
    if cmd == "cube" and len(parts) > 1:
        sub = parts[1].lower()
        arg = parts[2] if len(parts) > 2 else None
        if sub == "wall":
            _show_walls(arg)
            return True
        if sub == "direction" and len(parts) >= 3:
            direction = parts[3] if len(parts) > 3 else None
            _show_direction(parts[2], direction)
            return True
    if cmd == "letter" and len(parts) > 1:
        sub = parts[1].lower()
        arg = parts[2] if len(parts) > 2 else None
        if sub == "alphabet":
            _show_alphabets(arg)
            return True
        if sub == "cipher":
            _show_ciphers(arg)
            return True
        if sub in {"char", "letter"}:
            _show_letter_char(arg)
            return True
        if sub == "periodic":
            _show_periodic(arg)
            return True

    expr_result = _safe_eval_expr(raw_line or " ".join(parts))
    if expr_result is not EXPR_NO_MATCH:
        _render_value(expr_result)
        return True

    console.print("Unknown command. Type 'help' for the API map or use Python-style expressions.")
    return True


@app.command()
def repl() -> None:
    """Interactive loop for quick Tarot exploration."""

    console.print("Type 'help' for the API map, use Python-style expressions, 'quit' to exit.")

    # Build prompt-toolkit completer if available (supports dotted paths)
    session = None
    completer = None
    if PromptSession:
        completer = DotPathCompleter()
        session = PromptSession(completer=completer)

    while True:
        try:
            if session:
                line = session.prompt("tarot> ")
            else:
                line = input("tarot> ")
            line = line.strip()
        except (EOFError, KeyboardInterrupt):
            console.print("Bye.")
            break
        if not line:
            continue
        try:
            parts = shlex.split(line)
        except ValueError as exc:
            console.print(f"Parse error: {exc}")
            continue
        if not _handle_repl_command(parts, raw_line=line):
            break


if __name__ == "__main__":
    if len(sys.argv) == 2 and sys.argv[1] in {"-h", "--help"}:
        _print_structure_table()
        sys.exit(0)

    # If invoked without a subcommand, drop into the REPL for convenience.
    if len(sys.argv) == 1:
        repl()
    else:
        app()
