"""Generator for structured test step files (Mirabilis component format).

Each test case is written as a .json file containing an ordered array of
component-format steps.  These files are:
  1. Executed locally by maeris-mcp's in-process Playwright runner.
  2. Pushed to Mirabilis directly — no field translation needed.
"""

import json
import re
from pathlib import Path
from typing import Any


def generate_step_files(
    test_cases: list[dict],
    flow_name: str,
    base_url: str,
    output_dir: str,
    auth_config: dict | None = None,
    post_login_flow: list | None = None,
    credentials: dict | None = None,
    proven_paths: list[dict] | None = None,
) -> dict[str, Any]:
    """Write each test case as a JSON step file.

    Each test case dict must have:
        name  : str           — human-readable test name
        steps : list[dict]    — Mirabilis component-format step dicts

    Optional fields (stored but not used by the runner):
        description, priority, tags

    When auth_config is provided and a test does not already contain login
    steps, login steps are prepended automatically so that no test ever
    starts without authentication regardless of what the LLM generated.

    Returns a dict with generation metadata.
    """
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    # Build the default navigation preamble (used when no per-test proven path matches).
    # Prefer proven paths (from actual passing tests) over LLM-generated post_login_flow
    # because proven paths include every intermediate step (app selection, onboarding, etc.)
    # that the LLM frequently omits.
    _default_preamble = _build_preamble_from_proven(proven_paths or [], base_url, credentials or {})
    if not _default_preamble:
        _default_preamble = _build_login_prefix(auth_config or {}, post_login_flow or [], base_url, credentials or {})

    # Index proven paths by route for per-test matching
    _proven_by_route = _index_proven_paths(proven_paths or [], base_url, credentials or {})

    files_generated: list[str] = []
    test_file_count = 0

    for idx, test_case in enumerate(test_cases, start=1):
        name = test_case.get("name") or f"test_{idx}"
        slug = _slugify(name)
        filename = f"{slug}.json"
        file_path = output_path / filename

        # Resolve {{BASE_URL}} placeholders that the LLM may have left in mock_input
        steps = _resolve_base_url(test_case.get("steps") or [], base_url)

        # Replace $MAERIS_TEST_EMAIL / $MAERIS_TEST_PASSWORD with real credentials
        steps = _resolve_credentials(steps, credentials or {})

        # Try to find a proven preamble specific to this test's target route
        _preamble = _match_proven_preamble(steps, _proven_by_route) or _default_preamble

        # Inject preamble if the test doesn't already start with auth/navigation steps
        if _preamble and not _steps_have_login(steps):
            steps = _preamble + steps

        payload = {
            "name": name,
            "description": test_case.get("description", ""),
            "priority": test_case.get("priority", "medium"),
            "tags": test_case.get("tags") or [],
            "base_url": base_url or "",
            "steps": steps,
        }

        file_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
        files_generated.append(filename)
        test_file_count += 1

    return {
        "files_generated": files_generated,
        "test_files_count": test_file_count,
        "total_tests": test_file_count,
        "output_directory": output_dir,
    }


_LOGIN_KEYWORDS = re.compile(
    r"login|sign.?in|password|email|authenticate|credential",
    re.IGNORECASE,
)


def _steps_have_login(steps: list[dict]) -> bool:
    """Return True if steps already contain login/auth actions in the first 5 steps."""
    for step in steps[:5]:
        text = json.dumps(step)
        if _LOGIN_KEYWORDS.search(text):
            return True
    return False


def _build_preamble_from_proven(
    proven_paths: list[dict],
    base_url: str,
    credentials: dict,
) -> list[dict]:
    """Build a navigation preamble from proven_via_steps recorded by passing tests.

    Proven paths contain the exact step sequences that already passed in a
    real browser.  When available, this is strictly preferred over heuristic-
    based synthesis because it includes every intermediate step (login, app
    selection, onboarding, etc.) that the LLM frequently omits.

    The function picks the best proven path — preferring the root route (``/``)
    since it represents the most common entry journey — and converts its raw
    steps into the runner's ``action_taken`` format.  Credential placeholders
    in email/password fields are replaced with real values when provided.

    Returns an empty list if no usable proven path is found.
    """
    if not proven_paths:
        return []

    # Find the proven path for the root route — it typically contains the
    # full login → post-login journey that lands on the app's main view.
    best_entry: dict | None = None
    best_passes = -1
    fallback_entry: dict | None = None

    for pp in proven_paths:
        if not pp.get("proven_via_steps"):
            continue
        route = (pp.get("route") or "").rstrip("/") or "/"
        passes = pp.get("proven_passes", 1)
        if route == "/":
            if passes > best_passes:
                best_entry = pp
                best_passes = passes
        elif fallback_entry is None:
            fallback_entry = pp

    chosen = best_entry or fallback_entry
    if chosen is None:
        return []

    raw_steps = list(chosen["proven_via_steps"])

    # When using the root route as default preamble, the proven steps include
    # the full journey that originally reached `/` — including a final
    # feature-specific navigation click (e.g. "click sidebar-whats-new").
    # Strip that trailing feature-nav step so the preamble ends at the
    # "app is ready" state (after login + app selection) and each test can
    # then navigate to its own target feature.
    if best_entry is not None and len(raw_steps) > 3:
        # The common pattern is: go_to, send_keys(email), send_keys(pwd),
        # click(submit), click(app-select), click(feature-nav).
        # Find the last "boundary" step — app selection click — and trim after it.
        last_boundary = -1
        for i, step in enumerate(raw_steps):
            action = (step.get("action") or "").lower()
            sel = (step.get("css_selector") or "").lower()
            if action == "click" and (
                "appcard" in sel or "app-card" in sel or "appchoice" in sel
                or ("has-text" in sel and any(
                    kw in sel for kw in ("app", "dev", "staging", "prod", "test")
                ))
            ):
                last_boundary = i

        if last_boundary >= 0 and last_boundary < len(raw_steps) - 1:
            raw_steps = raw_steps[: last_boundary + 1]

    return _convert_proven_steps(raw_steps, base_url, credentials)


def _convert_proven_steps(
    raw_steps: list[dict],
    base_url: str,
    credentials: dict,
) -> list[dict]:
    """Convert codemap proven_via_steps into runner-format step dicts.

    Handles field name translation (``action`` → ``action_taken``), credential
    injection for email/password fields, and appropriate wait times.
    """
    if not raw_steps:
        return []

    email = (credentials.get("email") or credentials.get("username") or "").strip()
    password = (credentials.get("password") or "").strip()

    steps: list[dict] = []
    for raw in raw_steps:
        action = (raw.get("action") or "").lower()
        selector = (raw.get("css_selector") or "").strip()
        mock_input = (raw.get("mock_input") or "").strip()
        desc = raw.get("description", "")

        if action == "go_to":
            steps.append({
                "action_taken": "go_to",
                "mock_input": mock_input or base_url,
                "description": desc or "Navigate to app",
            })
        elif action == "send_keys" and selector:
            # Inject real credentials for email/password fields
            value = mock_input
            sel_lower = selector.lower()
            if email and ("email" in sel_lower or "you@example" in sel_lower):
                value = email
            elif password and ("password" in sel_lower):
                value = password
            steps.append({
                "css_selector": selector,
                "nth_appearance": 0,
                "action_taken": "send_keys",
                "mock_input": value,
                "description": desc or f"Type into {selector}",
                "wait_after": 500 if "password" in sel_lower else 0,
            })
        elif action in ("click", "hover", "check", "click_select") and selector:
            is_submit = "submit" in selector.lower() or "sign" in selector.lower()
            steps.append({
                "css_selector": selector,
                "nth_appearance": 0,
                "action_taken": action,
                "description": desc or f"{action} on {selector}",
                "wait_after": 2000 if is_submit else 1500,
            })

    # Only return if we have at least a go_to + one interaction
    if len(steps) >= 2:
        return steps
    return []


def _index_proven_paths(
    proven_paths: list[dict],
    base_url: str,
    credentials: dict,
) -> dict[str, list[dict]]:
    """Build a route → converted-steps index from all proven paths."""
    index: dict[str, list[dict]] = {}
    for pp in proven_paths:
        route = (pp.get("route") or "").rstrip("/") or "/"
        raw = pp.get("proven_via_steps")
        if not raw:
            continue
        converted = _convert_proven_steps(raw, base_url, credentials)
        if converted:
            index[route] = converted
    return index


def _match_proven_preamble(
    test_steps: list[dict],
    proven_by_route: dict[str, list[dict]],
) -> list[dict] | None:
    """Find a proven preamble that matches the test's target route.

    Scans the test's first few steps for go_to URLs or assertion URLs that
    indicate which route the test targets.  If a matching proven path exists,
    return its steps so the test uses an exact proven journey.
    """
    if not proven_by_route or not test_steps:
        return None

    from urllib.parse import urlparse

    # Collect candidate routes from the test's steps
    candidate_routes: list[str] = []
    for step in test_steps[:10]:
        action = (step.get("action_taken") or "").lower()
        mock_input = (step.get("mock_input") or "").strip()
        assertion_value = (step.get("assertion_value") or "").strip()

        # go_to URL → extract path
        if action == "go_to" and mock_input:
            try:
                path = urlparse(mock_input).path.rstrip("/") or "/"
                candidate_routes.append(path)
            except Exception:
                pass

        # URL assertion → extract the asserted path fragment
        a_type = (step.get("assertion_type") or "").lower()
        if assertion_value and ("url" in a_type or "page" == (step.get("assertion_nature") or "").lower()):
            # assertion_value could be a full URL or a path fragment like "/notebook"
            frag = assertion_value.strip().rstrip("/") or "/"
            if frag.startswith("/"):
                candidate_routes.append(frag)
            elif frag.startswith("http"):
                try:
                    candidate_routes.append(urlparse(frag).path.rstrip("/") or "/")
                except Exception:
                    pass

    # Match against proven routes (exact match, then prefix match)
    for route in candidate_routes:
        if route in proven_by_route:
            return proven_by_route[route]
    # Prefix match: if test targets /notebook/settings, match /notebook
    for route in candidate_routes:
        for proven_route, proven_steps in proven_by_route.items():
            if route.startswith(proven_route) and proven_route != "/":
                return proven_steps

    return None


def _build_login_prefix(
    auth_config: dict,
    post_login_flow: list,
    base_url: str,
    credentials: dict,
) -> list[dict]:
    """Build the mandatory login + post-login steps from scan_notes.auth.

    Returns an empty list if there is no auth config to work from.
    """
    if not isinstance(auth_config, dict):
        return []

    login_url = (auth_config.get("login_url") or "").strip()
    email_sel = (auth_config.get("email_selector") or '[type="email"]').strip()
    password_sel = (auth_config.get("password_selector") or '[type="password"]').strip()
    submit_sel = (auth_config.get("submit_selector") or 'button:has-text("Sign in")').strip()

    # No auth info at all → do not inject
    if not any([login_url, email_sel != '[type="email"]', submit_sel != 'button:has-text("Sign in")']):
        # Only skip if truly no auth detected
        if not auth_config.get("has_auth") and not auth_config.get("login_url"):
            return []

    # Resolve login URL to absolute
    if login_url and not login_url.startswith("http"):
        login_url = (base_url or "").rstrip("/") + "/" + login_url.lstrip("/")
    elif not login_url:
        login_url = (base_url or "").rstrip("/") + "/login"

    email = (credentials.get("email") or credentials.get("username") or "TEST_EMAIL").strip()
    password = (credentials.get("password") or "TEST_PASSWORD").strip()

    steps: list[dict] = [
        {
            "action_taken": "go_to",
            "mock_input": login_url,
            "description": "Navigate to login page",
        },
        {
            "css_selector": email_sel,
            "nth_appearance": 0,
            "action_taken": "send_keys",
            "mock_input": email,
            "description": "Enter email address",
        },
        {
            "css_selector": password_sel,
            "nth_appearance": 0,
            "action_taken": "send_keys",
            "mock_input": password,
            "description": "Enter password",
            "wait_after": 500,
        },
        {
            "css_selector": submit_sel,
            "nth_appearance": 0,
            "action_taken": "click",
            "description": "Click Sign in button",
            "wait_after": 2000,
        },
    ]

    # Inject post-login steps derived from post_login_flow scan notes
    post_steps = _build_post_login_steps(post_login_flow, base_url)
    steps.extend(post_steps)

    return steps


def _build_post_login_steps(post_login_flow: list, base_url: str) -> list[dict]:
    """Convert post_login_flow scan note entries into executable steps.

    Handles two patterns:
    - Step-like dicts already in the scan notes (passthrough)
    - String descriptions mentioning app/application selection → synthesized click step
    """
    if not post_login_flow or not isinstance(post_login_flow, list):
        return []

    steps: list[dict] = []
    app_selection_injected = False

    for item in post_login_flow:
        if isinstance(item, dict) and item.get("action_taken"):
            # Already a runnable step dict from scan notes
            steps.append(item)
            continue

        text = item if isinstance(item, str) else (
            " ".join(str(v) for v in item.values()) if isinstance(item, dict) else ""
        )
        text_lower = text.lower()

        if not app_selection_injected and (
            ("select" in text_lower or "choose" in text_lower or "pick" in text_lower)
            and ("app" in text_lower or "application" in text_lower or "workspace" in text_lower)
        ):
            # Synthesize an app selection click using domain hint from base_url
            app_hint = _extract_app_name_hint(base_url)
            css = f'button:has-text("{app_hint}")' if app_hint else "button"
            steps.append({
                "css_selector": css,
                "nth_appearance": 0,
                "action_taken": "click",
                "description": f"Select application{f' ({app_hint})' if app_hint else ''}",
                "wait_after": 1500,
            })
            app_selection_injected = True

    return steps


def _extract_app_name_hint(base_url: str) -> str:
    """Extract a short app name from base_url to use in button:has-text().

    E.g. https://light-dev.up.railway.app → 'light'
         https://myapp.staging.example.com → 'myapp'
         https://light-dev.up.railway.app             → ''  (can't determine)
    """
    if not base_url:
        return ""
    try:
        from urllib.parse import urlparse
        host = urlparse(base_url).hostname or ""
        if not host or host in ("localhost", "127.0.0.1"):
            return ""
        # Take the leftmost subdomain part, strip env suffixes like -dev/-staging/-prod
        first_part = host.split(".")[0]
        # Remove trailing -dev, -staging, -prod, -test, -local, -uat
        cleaned = re.sub(r"-(dev|staging|prod|test|local|uat|qa|demo|preview)$", "", first_part, flags=re.IGNORECASE)
        return cleaned if len(cleaned) >= 2 else first_part
    except Exception:
        return ""


def _resolve_base_url(steps: list[dict], base_url: str) -> list[dict]:
    """Replace {{BASE_URL}} in mock_input fields with the real base URL."""
    if not base_url:
        return steps
    resolved = []
    for step in steps:
        s = dict(step)
        if s.get("mock_input") and "{{BASE_URL}}" in s["mock_input"]:
            s["mock_input"] = s["mock_input"].replace("{{BASE_URL}}", base_url)
        resolved.append(s)
    return resolved


def _resolve_credentials(steps: list[dict], credentials: dict) -> list[dict]:
    """Replace $MAERIS_TEST_EMAIL / $MAERIS_TEST_PASSWORD placeholders with real credentials."""
    if not credentials:
        return steps
    email = (credentials.get("email") or credentials.get("username") or "").strip()
    password = (credentials.get("password") or "").strip()
    if not email and not password:
        return steps
    resolved = []
    for step in steps:
        s = dict(step)
        mock = s.get("mock_input") or ""
        if mock == "$MAERIS_TEST_EMAIL" and email:
            s["mock_input"] = email
        elif mock == "$MAERIS_TEST_PASSWORD" and password:
            s["mock_input"] = password
        resolved.append(s)
    return resolved


def _slugify(value: str) -> str:
    """Convert a string to a safe filename (lowercase, underscored)."""
    cleaned = "".join(ch.lower() if ch.isalnum() or ch.isspace() else "_" for ch in value)
    parts = [p for p in cleaned.split() if p]
    return "_".join(parts) or "test"
