"""Terminal animation primitives for the Keiro CLI.

Provides shimmer and spinner effects rendered via Rich. All timing uses
monotonic clocks. Falls back to static output in CI, non-TTY, or when
``KEIRO_NO_ANIMATION=1`` is set.
"""

from __future__ import annotations

import math
import os
import sys
import threading
import time

from rich.console import Console
from rich.live import Live
from rich.style import Style
from rich.text import Text

# Monotonic anchor for all animation phases.
_START_TIME = time.monotonic()

_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]


def animations_enabled() -> bool:
    """Return True if terminal animations should run."""
    if os.environ.get("KEIRO_NO_ANIMATION"):
        return False
    if os.environ.get("CI"):
        return False
    if not sys.stdout.isatty():
        return False
    return True


def _phase(duration: float) -> float:
    """Current animation phase in [0.0, 1.0)."""
    elapsed = time.monotonic() - _START_TIME
    return (elapsed % duration) / duration


# ---------------------------------------------------------------------------
# Shared shimmer brightness calculation
# ---------------------------------------------------------------------------


def _char_brightness(
    index: int,
    length: int,
    phase: float,
    band_width: float = 0.25,
) -> float:
    """Per-character brightness for the traveling highlight band.

    Returns a value in ``[0.0, 1.0]`` where 1.0 is brightest.
    Characters near *phase* are brighter; the band wraps at the edges.

    Args:
        index: Character position in the text.
        length: Total text length.
        phase: Sweep position in ``[0.0, 1.0)``.
        band_width: Width of the highlight band as a fraction.

    Returns:
        Brightness value.
    """
    n = max(length, 1)
    pos = index / n
    distance = abs(pos - phase)
    if distance > 0.5:
        distance = 1.0 - distance
    if distance < band_width:
        return 1.0 - distance / band_width
    return 0.0


# ---------------------------------------------------------------------------
# ANSI-based shimmer (for raw terminal output without Rich)
# ---------------------------------------------------------------------------

# ANSI escape codes for three brightness levels.
_ANSI_BRIGHT = "\033[1;97m"  # bold bright white
_ANSI_MID = "\033[0;37m"     # normal white
_ANSI_DIM = "\033[2;37m"     # dim white/gray
_ANSI_RESET = "\033[0m"


def _shimmer_ansi(
    text: str,
    phase: float,
    band_width: float = 0.25,
) -> str:
    """Apply a traveling highlight band to *text* using ANSI escapes.

    Same visual concept as :func:`shimmer` (Rich version) but produces a
    raw ANSI string suitable for direct ``sys.stdout.write()``.

    Args:
        text: Plain text to shimmer.
        phase: Current sweep position in ``[0.0, 1.0)``.
        band_width: Width of the highlight band as a fraction of text length.

    Returns:
        ANSI-escaped string with per-character brightness.
    """
    n = len(text)
    parts: list[str] = []
    prev_code = ""
    for i, char in enumerate(text):
        b = _char_brightness(i, n, phase, band_width)
        code = _ANSI_BRIGHT if b > 0.5 else (_ANSI_MID if b > 0 else _ANSI_DIM)
        if code != prev_code:
            parts.append(code)
            prev_code = code
        parts.append(char)
    parts.append(_ANSI_RESET)
    return "".join(parts)


# ---------------------------------------------------------------------------
# Rich-based shimmer (for standalone animated text)
# ---------------------------------------------------------------------------

def shimmer(text: str, phase: float, intensity: float = 0.8) -> Text:
    """Traveling highlight band across the string (Rich Text output)."""
    result = Text()
    n = len(text)
    for i, char in enumerate(text):
        b = _char_brightness(i, n, phase) * intensity
        if b > 0.5:
            style = Style(color="bright_white", bold=True)
        elif b > 0:
            style = Style(color="white")
        else:
            style = Style(color="bright_black")
        result.append(char, style=style)
    return result


def shimmer_once(
    console: Console,
    text: str,
    duration: float = 1.0,
    passes: int = 2,
) -> None:
    """Play shimmer passes across *text*, then print it statically."""
    if not animations_enabled():
        console.print(f"  {text}", style="bold")
        return

    per_pass = duration / passes
    steps_per_pass = max(int(per_pass / 0.035), 1)
    try:
        with Live(
            Text(f"  {text}", style="bright_black"),
            console=console,
            transient=True,
            refresh_per_second=30,
        ) as live:
            for p in range(passes):
                for step in range(steps_per_pass):
                    phase = step / steps_per_pass
                    if p % 2 == 1:
                        phase = 1.0 - phase
                    frame = Text("  ")
                    frame.append_text(shimmer(text, phase))
                    live.update(frame)
                    time.sleep(per_pass / steps_per_pass)
    except Exception:
        pass

    console.print(f"  [bold]{text}[/bold]")


def pulse_once(
    console: Console,
    text: str,
    duration: float = 0.8,
    cycles: int = 2,
) -> None:
    """Pulse brightness on *text*, then print it statically."""
    if not animations_enabled():
        console.print(f"  {text}", style="bold")
        return

    steps = max(int(duration / 0.035), 1)
    try:
        with Live(
            Text(f"  {text}", style="bold"),
            console=console,
            transient=True,
            refresh_per_second=30,
        ) as live:
            for step in range(steps):
                t = step / steps
                brightness = 0.5 + 0.5 * math.sin(t * cycles * 2 * math.pi)
                if brightness > 0.7:
                    style = Style(color="bright_white", bold=True)
                elif brightness > 0.3:
                    style = Style(color="white", bold=True)
                else:
                    style = Style(color="bright_black", bold=True)
                live.update(Text(f"  {text}", style=style))
                time.sleep(duration / steps)
    except Exception:
        pass

    console.print(f"  [bold]{text}[/bold]")


# ---------------------------------------------------------------------------
# Masked input (shows * per character, no OS secure-input indicator)
# ---------------------------------------------------------------------------

def _input_masked(prompt: str) -> str:
    """Read a line with echo disabled, printing ``*`` for each character.

    Uses ``termios`` directly to toggle the ECHO and ICANON flags on stdin.
    This avoids triggering macOS Secure Keyboard Entry (no lock icon).

    Args:
        prompt: Prompt string printed before reading.

    Returns:
        The raw string entered by the user.

    Raises:
        KeyboardInterrupt: On Ctrl+C.
        EOFError: On Ctrl+D.
    """
    try:
        import termios
    except ImportError:
        # Non-Unix fallback: read without masking.
        return input(prompt)

    fd = sys.stdin.fileno()
    old_attrs = termios.tcgetattr(fd)
    chars: list[str] = []

    try:
        # Disable echo and canonical (line-buffered) mode.
        # Keep output processing (OPOST) so \n still works normally.
        new_attrs = list(old_attrs)
        new_attrs[3] = new_attrs[3] & ~(termios.ECHO | termios.ICANON)
        new_attrs[6][termios.VMIN] = 1
        new_attrs[6][termios.VTIME] = 0
        termios.tcsetattr(fd, termios.TCSADRAIN, new_attrs)

        sys.stdout.write(prompt)
        sys.stdout.flush()

        while True:
            ch = sys.stdin.read(1)
            if ch in ("\r", "\n"):
                sys.stdout.write("\n")
                sys.stdout.flush()
                break
            elif ch in ("\x7f", "\x08"):  # Backspace / Delete
                if chars:
                    chars.pop()
                    sys.stdout.write("\b \b")
                    sys.stdout.flush()
            elif ch == "\x03":  # Ctrl+C
                raise KeyboardInterrupt
            elif ch == "\x04":  # Ctrl+D
                raise EOFError
            elif ch >= " ":  # Printable
                chars.append(ch)
                sys.stdout.write("*")
                sys.stdout.flush()
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)

    return "".join(chars)


# ---------------------------------------------------------------------------
# ANSI-based prompt pulse (runs alongside input())
# ---------------------------------------------------------------------------

def _prompt_pulse_loop(
    prompt: str,
    stop: threading.Event,
    cycle: float,
) -> None:
    """Background loop: pulse prompt brightness while ``input()`` waits.

    Uniform sine-wave brightness on the full prompt text — a gentle
    "waiting for you" beacon.
    """
    label = prompt.rstrip()
    while not stop.is_set():
        phase = _phase(cycle)
        brightness = 0.5 + 0.5 * math.sin(phase * 2 * math.pi)
        if brightness > 0.7:
            ansi = "\033[1;97m"
        elif brightness > 0.3:
            ansi = "\033[0;37m"
        else:
            ansi = "\033[0;90m"
        sys.stdout.write(f"\033[s\r{ansi}{label}\033[0m\033[u")
        sys.stdout.flush()
        stop.wait(0.07)


def prompt_with_live_header(
    console: Console,
    header: str,
    body_lines: list[str],
    prompt_text: str,
    shimmer_cycle: float = 2.5,
    secret: bool = False,
) -> str:
    """Shimmer-reveal *header*, print body, then pulse *prompt_text* while
    reading user input.

    Args:
        console: Rich Console for initial rendering.
        header: Text for the shimmer reveal (e.g. ``"Keiro"``).
        body_lines: Rich-markup lines printed between header and prompt.
        prompt_text: The input prompt string (pulses while waiting).
        shimmer_cycle: Seconds per full pulse cycle.
        secret: If True, suppress terminal echo (for API keys / passwords).

    Returns:
        The raw string entered by the user.
    """
    # 1. Shimmer reveal.
    console.print()
    shimmer_once(console, header)
    console.print()

    # 2. Print body.
    for line in body_lines:
        console.print(line)
    if body_lines:
        console.print()

    # 3. Start prompt pulse (prompt text gently pulses while awaiting input).
    # Skip pulse for secret inputs: _input_masked() manages its own prompt
    # rendering via termios, and a concurrent pulse thread corrupts cursor
    # positioning (produces duplicate prompt text).
    stop = threading.Event()
    thread = None
    if animations_enabled() and not secret:
        thread = threading.Thread(
            target=_prompt_pulse_loop,
            args=(prompt_text, stop, shimmer_cycle),
            daemon=True,
            name="keiro-prompt",
        )
        thread.start()

    # 4. Read input (prompt pulses while user types).
    try:
        if secret:
            value = _input_masked(prompt_text)
        else:
            value = input(prompt_text)
    except (EOFError, KeyboardInterrupt):
        stop.set()
        if thread:
            thread.join(timeout=0.5)
        print()
        sys.exit(1)

    # 5. Stop pulse, settle prompt to static bold.
    stop.set()
    if thread:
        thread.join(timeout=0.5)
        # Settle prompt to static bold (thread was started).
        sys.stdout.write(
            f"\033[s\033[1A\r\033[1m{prompt_text}\033[0m\033[u"
        )
        sys.stdout.flush()

    return value


# ---------------------------------------------------------------------------
# Spinner (for async operations like validation, account creation)
# ---------------------------------------------------------------------------

class Spinner:
    """Animated spinner with shimmer-styled message text.

    Usage::

        with Spinner(console, "Validating"):
            do_work()
    """

    _FRAME_INTERVAL = 0.08
    _SPINNER_CYCLE = 0.8
    _SHIMMER_CYCLE = 2.0

    def __init__(self, console: Console, message: str) -> None:
        self._console = console
        self._message = message
        self._live: Live | None = None
        self._thread: threading.Thread | None = None
        self._stop = threading.Event()
        self._lock = threading.Lock()
        self._disabled = not animations_enabled()

    def __enter__(self) -> Spinner:
        if self._disabled:
            self._console.print(f"  {self._message}...", end=" ", highlight=False)
            return self
        self._stop.clear()
        self._live = Live(
            self._render(),
            console=self._console,
            transient=True,
            refresh_per_second=12,
        )
        self._live.start()
        self._thread = threading.Thread(
            target=self._loop, daemon=True, name="keiro-spinner",
        )
        self._thread.start()
        return self

    def __exit__(self, exc_type: type | None, *_: object) -> bool:
        self._stop.set()
        if self._thread is not None:
            self._thread.join(timeout=1.0)
            self._thread = None
        with self._lock:
            if self._live is not None:
                try:
                    self._live.stop()
                except Exception:
                    pass
                self._live = None
        return False

    def _render(self) -> Text:
        spinner_phase = _phase(self._SPINNER_CYCLE)
        idx = int(spinner_phase * len(_SPINNER_FRAMES)) % len(_SPINNER_FRAMES)
        shimmer_phase = _phase(self._SHIMMER_CYCLE)

        frame = Text("  ")
        frame.append(_SPINNER_FRAMES[idx], style="bold")
        frame.append(" ")
        frame.append_text(shimmer(self._message, shimmer_phase, intensity=0.5))
        return frame

    def _loop(self) -> None:
        while not self._stop.is_set():
            with self._lock:
                if self._live is not None:
                    try:
                        self._live.update(self._render())
                    except Exception:
                        pass
            self._stop.wait(self._FRAME_INTERVAL)


# ---------------------------------------------------------------------------
# Tab toggle (for keiro admin on/off)
# ---------------------------------------------------------------------------

def toggle_switch(
    label: str,
    initial: bool,
    *,
    on_text: str = "on",
    off_text: str = "off",
) -> bool:
    """Interactive on/off toggle controlled by Tab, confirmed by Enter.

    Renders a single line with the label and a highlighted on/off state.
    Tab flips the state, Enter confirms. Ctrl+C aborts (returns initial).

    Args:
        label: Text label shown before the toggle (e.g. "Admin mode").
        initial: Starting state.
        on_text: Text shown when state is True.
        off_text: Text shown when state is False.

    Returns:
        The confirmed boolean state.
    """
    if not sys.stdout.isatty():
        return initial

    try:
        import termios
    except ImportError:
        return initial

    state = initial
    fd = sys.stdin.fileno()
    old_attrs = termios.tcgetattr(fd)

    def _render(value: bool) -> str:
        if value:
            on = f"\033[1;97m {on_text} \033[0m"
            off = f"\033[0;90m {off_text} \033[0m"
        else:
            on = f"\033[0;90m {on_text} \033[0m"
            off = f"\033[1;97m {off_text} \033[0m"
        hint = "\033[0;90m tab to toggle, enter to confirm\033[0m"
        return f"  {label}  {on} / {off}   {hint}"

    try:
        new_attrs = list(old_attrs)
        new_attrs[3] = new_attrs[3] & ~(termios.ECHO | termios.ICANON)
        new_attrs[6][termios.VMIN] = 1
        new_attrs[6][termios.VTIME] = 0
        termios.tcsetattr(fd, termios.TCSADRAIN, new_attrs)

        sys.stdout.write(_render(state))
        sys.stdout.flush()

        while True:
            ch = sys.stdin.read(1)
            if ch == "\t":
                state = not state
                sys.stdout.write(f"\r{_render(state)}")
                sys.stdout.flush()
            elif ch in ("\r", "\n"):
                sys.stdout.write("\n")
                sys.stdout.flush()
                return state
            elif ch == "\x03":  # Ctrl+C
                sys.stdout.write("\n")
                sys.stdout.flush()
                return initial
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
