"""
DSL Models - Test Step and Action Definitions
"""

from enum import Enum
from typing import Annotated, Any, Dict, List, Literal, Optional, Union

from pydantic import BaseModel, Field


class ActionType(str, Enum):
    """Test action types."""
    # Common actions
    LAUNCH = "launch"
    WAIT = "wait"
    SCREENSHOT = "screenshot"
    ASSERT = "assert"

    # GUI actions
    CLICK = "click"
    DOUBLE_CLICK = "double_click"
    RIGHT_CLICK = "right_click"
    TYPE = "type"
    HOTKEY = "hotkey"
    PRESS = "press"
    DRAG = "drag"
    SCROLL = "scroll"
    MOVE = "move"

    # TUI actions
    SEND_KEYS = "send_keys"
    WAIT_OUTPUT = "wait_output"

    # CLI actions
    EXEC = "exec"

    # Control flow
    IF = "if"
    LOOP = "loop"
    FOR_EACH = "for_each"
    CALL = "call"


class FailureAction(str, Enum):
    """Action to take on failure."""
    IGNORE = "ignore"
    FAIL = "fail"
    RETRY = "retry"
    FALLBACK = "fallback"


class WaitConditionType(str, Enum):
    """Wait condition types."""
    DURATION = "duration"
    ELEMENT_VISIBLE = "element_visible"
    OUTPUT_CONTAINS = "output_contains"
    SCREEN_STABLE = "screen_stable"


class RetryConfig(BaseModel):
    """Retry configuration."""
    count: int = Field(default=3)
    interval: float = Field(default=2.0, description="Interval in seconds")
    backoff: Literal["fixed", "exponential"] = "fixed"


# ============== Selectors ==============

class GUISelector(BaseModel):
    """GUI element selector using AI vision or coordinates."""
    type: Literal["description", "coordinate"] = "description"
    description: Optional[str] = Field(default=None, description="Natural language description for AI vision")
    cached: Optional[Dict[str, Any]] = Field(default=None, description="Cached coordinate from previous run")


class TUISelector(BaseModel):
    """TUI element selector using terminal buffer."""
    type: Literal["text", "cursor"] = "text"
    content: Optional[str] = Field(default=None, description="Text to find in terminal")
    row: Optional[int] = None
    col: Optional[int] = None


class CLISelector(BaseModel):
    """CLI output selector."""
    type: Literal["stdout", "stderr", "exit_code"] = "stdout"
    pattern: Optional[str] = Field(default=None, description="Pattern to match")


Selector = Union[GUISelector, TUISelector, CLISelector]


# ============== Conditions ==============

class WaitCondition(BaseModel):
    """Wait condition for wait action."""
    model_config = {"extra": "allow"}
    type: Union[WaitConditionType, str]  # Allow any string for flexibility
    value: Optional[Any] = None  # duration seconds, or element description
    target: Optional[GUISelector] = None  # for element_visible
    pattern: Optional[str] = None  # for output_contains
    timeout: Union[float, str] = 10.0  # Allow string for variable syntax


class IfConditionType(str, Enum):
    """Condition types for if action."""
    OUTPUT_CONTAINS = "output_contains"
    EXIT_CODE = "exit_code"
    VAR_EQUALS = "var_equals"
    VAR_NOT_EQUALS = "var_not_equals"
    VAR_CONTAINS = "var_contains"
    VAR_EMPTY = "var_empty"
    VAR_NOT_EMPTY = "var_not_empty"


class IfCondition(BaseModel):
    """Condition for if action."""
    model_config = {"extra": "allow"}
    type: IfConditionType
    # For output_contains - check last step output
    pattern: Optional[str] = None
    # For exit_code - check last step exit code
    expected_code: Optional[int] = None
    # For var_equals, var_not_equals, var_contains
    variable: Optional[str] = None
    expected: Optional[Any] = None
    # For backward compatibility
    value: Optional[Any] = None


# ============== Assertions ==============

class GUIAssertionType(str, Enum):
    ELEMENT_EXISTS = "element_exists"
    VISUAL_STATE = "visual_state"


class TUIAssertionType(str, Enum):
    SCREEN_CONTAINS = "screen_contains"
    CURSOR_AT = "cursor_at"
    SCREEN_STABLE = "screen_stable"


class CLIAssertionType(str, Enum):
    EXIT_CODE = "exit_code"
    STDOUT_CONTAINS = "stdout_contains"
    STDERR_CONTAINS = "stderr_contains"
    STDOUT_MATCHES = "stdout_matches"
    STDERR_EMPTY = "stderr_empty"


class GUIAssertion(BaseModel):
    """GUI assertion."""
    model_config = {"extra": "allow"}
    type: Literal["element_exists", "visual_state"]
    target: Optional[GUISelector] = None
    expected: str = Field(..., description="Natural language description of expected state")
    timeout: float = 10.0


class TUIAssertion(BaseModel):
    """TUI assertion."""
    model_config = {"extra": "allow"}
    type: Literal["screen_contains", "cursor_at", "screen_stable"]
    text: Optional[str] = None
    row: Optional[int] = None
    col: Optional[int] = None
    timeout: float = 5.0


class CLIAssertion(BaseModel):
    """CLI assertion."""
    model_config = {"extra": "allow"}
    type: Literal["exit_code", "stdout_contains", "stderr_contains", "stdout_matches", "stderr_empty"]
    expected: Union[int, str] = Field(..., description="Expected exit code or pattern")
    timeout: float = 30.0


# Tagged union discriminator for assertions
Assertion = Union[
    Annotated[GUIAssertion, Field(discriminator="type")],
    Annotated[TUIAssertion, Field(discriminator="type")],
    Annotated[CLIAssertion, Field(discriminator="type")],
]


# ============== Test Steps ==============

class TestStep(BaseModel):
    """Test step definition."""
    model_config = {"extra": "allow"}

    name: Optional[str] = Field(default=None, description="Step name for display")
    action: ActionType

    # Common parameters - allow float or str for variable syntax like ${vars.timeout}
    timeout: Optional[Union[float, str]] = None
    on_failure: FailureAction = FailureAction.FAIL
    retry: Optional[RetryConfig] = None

    # GUI action parameters
    target: Optional[GUISelector] = None
    text: Optional[str] = None  # for type action
    secret: bool = False  # mask in logs
    clear: bool = False  # clear before type
    interval: float = 0.01  # key interval for type
    keys: Optional[List[str]] = None  # for hotkey/press
    key: Optional[str] = None  # single key for press
    from_target: Optional[GUISelector] = None  # for drag
    to_target: Optional[GUISelector] = None  # for drag
    direction: Optional[Literal["up", "down", "left", "right"]] = None  # for scroll
    amount: Optional[int] = None  # for scroll
    duration: Optional[float] = None  # for drag/move animation

    # TUI action parameters
    sequence: Optional[str] = None  # key sequence

    # CLI action parameters
    command: Optional[str] = None
    working_dir: Optional[str] = None
    env: Optional[Dict[str, str]] = None
    capture: Optional[List[str]] = None  # stdout, stderr, exit_code

    # Wait action parameters - allow dict for flexibility with variable syntax
    condition: Optional[Dict[str, Any]] = None  # Used by 'wait' action

    # Assert action parameters - use discriminator for tagged union
    assertion: Optional[Annotated[Assertion, Field(discriminator="type")]] = None

    # Screenshot action parameters
    screenshot_name: Optional[str] = Field(default=None, alias="name")

    # Control flow parameters - use dict for flexibility
    then_steps: Optional[List["TestStep"]] = Field(default=None, alias="then")
    else_steps: Optional[List["TestStep"]] = Field(default=None, alias="else")
    max_iterations: Optional[int] = 10
    loop_interval: Optional[float] = 1.0
    for_each_var: Optional[str] = Field(default=None, alias="var")
    for_each_items: Optional[Union[List[str], str]] = Field(default=None, alias="in")
    for_each_body: Optional[List["TestStep"]] = Field(default=None, alias="body")
    call_flow: Optional[str] = Field(default=None, alias="flow")
    call_args: Optional[Dict[str, Any]] = Field(default=None, alias="args")

    # Fallback steps
    fallback: Optional[List["TestStep"]] = None


# ============== Control Flow Steps ==============

class IfStep(TestStep):
    """If condition step - conditionally execute steps."""
    model_config = {"extra": "allow"}
    action: Literal["if"]
    condition: IfCondition
    # Use then_steps/else_steps from parent TestStep


class LoopStep(TestStep):
    """Loop step - repeatedly execute body while condition is true."""
    model_config = {"extra": "allow"}
    action: Literal["loop"]
    max_iterations: int = Field(default=10, ge=1, le=1000)
    condition: IfCondition
    # Use for_each_body from parent TestStep as body


class ForEachStep(TestStep):
    """For each step - iterate over items."""
    model_config = {"extra": "allow"}
    action: Literal["for_each"]
    items: Union[List[str], str] = Field(default_factory=list)
    var_name: str = Field(default="item", alias="var")
    # Use for_each_body from parent TestStep as steps


# Forward reference resolution
TestStep.model_rebuild()
IfStep.model_rebuild()
LoopStep.model_rebuild()
ForEachStep.model_rebuild()


# ============== DSL Script ==============

class Variable(BaseModel):
    """Variable definition in DSL."""
    name: str
    value: Union[str, int, float, bool, List, Dict]
    source: Literal["literal", "env", "now", "random", "file"] = "literal"
    format: Optional[str] = None  # for 'now' datetime format


class SubFlow(BaseModel):
    """Reusable sub-flow definition."""
    name: str
    params: List[str] = Field(default_factory=list)
    steps: List[TestStep]


class DSLScript(BaseModel):
    """Complete DSL script."""
    version: str = "1.0"

    meta: Dict[str, Any] = Field(default_factory=dict)

    # Variable definitions
    vars: Dict[str, Any] = Field(default_factory=dict)

    # Sub-flow definitions
    define: Dict[str, SubFlow] = Field(default_factory=dict)

    # Main test steps
    steps: List[TestStep]
