"""FlowParser — load, validate, and resolve YAML flow definitions.

A flow YAML file looks like:

    name: login
    description: Log into the application
    include:
      - some_other_flow        # optional, prepended before this flow's steps
    params:
      test_email: admin@test.com
    steps:
      - action: navigate
        url: "{{app_url}}/login"
      - action: fill
        selector: "#email"
        value: "{{test_email}}"
    assertions:
      - type: url_contains
        value: "/dashboard"
      - "the success toast message should appear"   # NL assertion (AI-interpreted)
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel, Field

log = logging.getLogger("antiqa.flows")


class FlowStep(BaseModel):
    """A single action step in a flow."""
    action: str
    selector: str | None = None
    value: str | None = None
    url: str | None = None
    key: str | None = None
    name: str | None = None
    timeout_ms: int | None = None
    full_page: bool = False
    direction: str | None = None
    amount: int | None = None
    attribute: str | None = None
    wait_until: str | None = None
    description: str = ""


class FlowAssertion(BaseModel):
    """A post-flow assertion."""
    type: str
    selector: str | None = None
    value: str | None = None


class FlowDefinition(BaseModel):
    """Parsed representation of a flow YAML file."""
    name: str
    description: str = ""
    include: list[str] = Field(default_factory=list)
    params: dict[str, str] = Field(default_factory=dict)
    steps: list[FlowStep] = Field(default_factory=list)
    assertions: list[FlowAssertion] = Field(default_factory=list)
    source_file: str = ""


class FlowParser:
    """Load and validate YAML flows from a directory."""

    def __init__(self, flows_dir: str | Path = "flows/") -> None:
        self.flows_dir = Path(flows_dir)
        self._cache: dict[str, FlowDefinition] = {}

    def list_flows(self) -> list[FlowDefinition]:
        """Return all flow definitions found in the flows directory."""
        flows = []
        if not self.flows_dir.is_dir():
            return flows
        for path in sorted(self.flows_dir.glob("*.yaml")) + sorted(self.flows_dir.glob("*.yml")):
            try:
                flows.append(self.load(path.stem))
            except Exception as exc:
                log.warning("Skipping invalid flow %s: %s", path.name, exc)
        return flows

    def load(self, name: str) -> FlowDefinition:
        """Load a flow by name (without extension) from the flows directory."""
        if name in self._cache:
            return self._cache[name]

        path = self._find_file(name)
        if not path:
            raise FileNotFoundError(f"Flow '{name}' not found in {self.flows_dir}")

        raw = yaml.safe_load(path.read_text())
        if not isinstance(raw, dict):
            raise ValueError(f"Flow file {path.name} must be a YAML mapping")

        raw.setdefault("name", name)
        raw["source_file"] = str(path)

        # Handle NL assertions (plain strings in the assertions array)
        raw_assertions = raw.get("assertions", [])
        if raw_assertions:
            raw["assertions"] = self._normalize_assertions(raw_assertions)

        flow = FlowDefinition.model_validate(raw)

        self._cache[name] = flow
        return flow

    def _normalize_assertions(self, raw_assertions: list) -> list[dict[str, Any]]:
        """Convert a mix of structured and NL assertions into structured dicts."""
        normalized: list[dict[str, Any]] = []
        nl_assertions: list[str] = []

        for item in raw_assertions:
            if isinstance(item, str):
                nl_assertions.append(item)
            elif isinstance(item, dict) and "type" in item:
                normalized.append(item)
            elif isinstance(item, dict):
                # Dict without type — treat description as NL
                desc = item.get("description", item.get("text", ""))
                if desc:
                    nl_assertions.append(desc)

        if nl_assertions:
            try:
                from antiqa.agents.assertion_interpreter import AssertionInterpreterAgent
                from antiqa.config.manager import load_config
                config = load_config()
                interpreter = AssertionInterpreterAgent(config)
                for nl in nl_assertions:
                    interpreted = interpreter.interpret(nl)
                    normalized.extend(interpreted)
                    log.info("NL assertion '%s' → %d check(s)", nl[:40], len(interpreted))
            except Exception as exc:
                log.debug("NL assertion interpretation unavailable: %s", exc)
                for nl in nl_assertions:
                    normalized.append({"type": "element_visible", "selector": "body"})

        return normalized

    def resolve_with_includes(self, name: str) -> FlowDefinition:
        """Load a flow and prepend steps from any included flows (recursive)."""
        flow = self.load(name)
        if not flow.include:
            return flow

        combined_steps: list[FlowStep] = []
        combined_params: dict[str, str] = {}

        for inc_name in flow.include:
            inc_flow = self.resolve_with_includes(inc_name)
            combined_steps.extend(inc_flow.steps)
            combined_params.update(inc_flow.params)

        combined_steps.extend(flow.steps)
        combined_params.update(flow.params)

        return FlowDefinition(
            name=flow.name,
            description=flow.description,
            include=[],
            params=combined_params,
            steps=combined_steps,
            assertions=flow.assertions,
            source_file=flow.source_file,
        )

    def _find_file(self, name: str) -> Path | None:
        for ext in (".yaml", ".yml"):
            path = self.flows_dir / f"{name}{ext}"
            if path.is_file():
                return path
        return None
