"""Resource CRUD CLI commands: api, collection, env, flow, assertion."""

import click

from ._core import (
    api_group,
    api_param_group,
    app_group,
    assertion_group,
    collection_group,
    env_group,
    env_data_group,
    flow_group,
    flow_map_group,
    flow_param_group,
)
from ._utils import (
    _claude_interactive,
    _claude_run,
    _INTERACTIVE_SYSTEM_PROMPT,
    _is_token_expired,
    _next_steps,
    _not_found_error,
    _parse_json_input,
    _print_json,
    _print_record,
    _print_result,
    _print_table,
    _refresh_client,
    _resolve_ai_mode,
    _resolve_api,
    _resolve_collection,
    _resolve_environment,
    _resolve_flow,
    _resolve_or_create_collection,
    _run_async,
    _run_claude_capture,
    _run_direct_api_capture,
    _run_via_direct_api,
)


# ---------------------------------------------------------------------------
# list subcommands
# ---------------------------------------------------------------------------


def _with_parent_collection_names(
    rows: list[dict],
    collections: list[dict],
) -> list[dict]:
    """Attach parent collection names to collection rows for CLI display."""
    if not isinstance(rows, list):
        return []
    if not isinstance(collections, list):
        collections = []
    name_by_id: dict[str, str] = {}
    for item in collections:
        if not isinstance(item, dict):
            continue
        cid = (
            item.get("_id")
            or item.get("api_collection_id")
            or item.get("collection_id")
            or item.get("id")
            or ""
        )
        if cid:
            name_by_id[str(cid)] = str(item.get("name") or "")

    return [
        {
            **row,
            "parent_collection_name": name_by_id.get(
                str(row.get("parent_collection_id") or ""),
                "",
            ),
        }
        for row in rows
    ]


def _with_actor_names(record: dict, app_users: list[dict]) -> dict:
    """Replace user IDs with readable names for non-JSON CLI output."""
    name_by_uid: dict[str, str] = {}
    for user in app_users:
        if not isinstance(user, dict):
            continue
        uid = str(user.get("uid") or user.get("user_id") or user.get("id") or "").strip()
        if not uid:
            continue
        display_name = (
            str(user.get("full_name") or "").strip()
            or str(user.get("name") or "").strip()
            or str(user.get("displayName") or "").strip()
            or str(user.get("email") or "").strip()
            or uid
        )
        name_by_uid[uid] = display_name

    return {
        **record,
        "created_by": name_by_uid.get(str(record.get("created_by") or ""), record.get("created_by")),
        "updated_by": name_by_uid.get(str(record.get("updated_by") or ""), record.get("updated_by")),
    }


@collection_group.command("list")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def list_collection(output_json: bool) -> None:
    """List all collections."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            data = await client.get_collections()
            if output_json:
                _print_json(data)
            else:
                rows = _with_parent_collection_names(data, data)
                _print_table(rows, ["_id", "name", "parent_collection_name"])

    _run_async(_inner())


async def _print_collection_children(collection: str) -> None:
    from maeris_mcp.maeris.client import MaerisClient

    async with MaerisClient() as client:
        cid = await _resolve_collection(client, collection)
        children = await client.get_collection_children([cid])
        collections = await client.get_collections()
        rows: list[dict] = []
        if isinstance(children, list):
            rows = [row for row in children if isinstance(row, dict)]
        elif isinstance(children, dict):
            if isinstance(children.get("data"), dict):
                rows = children.get("data", {}).get(cid, {}).get("api_collections", []) or []
            elif isinstance(children.get(cid), dict):
                rows = children.get(cid, {}).get("api_collections", []) or []
        rows = [
            {
                **row,
                "_id": row.get("_id") or row.get("api_collection_id") or row.get("collection_id") or row.get("id") or "",
            }
            for row in rows
        ]
        rows = _with_parent_collection_names(rows, collections)
        _print_table(rows, ["_id", "name", "parent_collection_name"])


@collection_group.command("children")
@click.argument("collection")
def list_collection_children(collection: str) -> None:
    """List child collections of a collection (name or ID)."""
    _run_async(_print_collection_children(collection))


@collection_group.command("subs", hidden=True)
@click.argument("collection")
def list_subcollection_alias(collection: str) -> None:
    """Hidden backward-compatible alias for collection children."""
    _run_async(_print_collection_children(collection))


@env_group.command("list")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def list_environment(output_json: bool) -> None:
    """List all environments."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            data = await client.get_environments()
            if output_json:
                _print_json(data)
            else:
                _print_table(data, ["env_id", "name"])

    _run_async(_inner())


@assertion_group.command("list")
@click.option("--api", "-a", default=None, help="Filter by API name or ID")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def list_assertion(api: str | None, output_json: bool) -> None:
    """List assertions, optionally filtered by API name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            if api:
                api_id = await _resolve_api(client, api)
                data = await client.get_assertions_for_api(api_id)
            else:
                data = await client.get_assertions()
            if output_json:
                _print_json(data)
            else:
                _print_table(data, ["_id", "api_id", "assertion_nature", "assertion_type", "assertion_value"])

    _run_async(_inner())


@flow_group.command("list")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def list_flow(output_json: bool) -> None:
    """List all API flows."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            data = await client.get_api_flows()
            if output_json:
                _print_json(data)
            else:
                _print_table(data, ["flow_id", "flow_name"])

    _run_async(_inner())


@api_group.command("list")
@click.option("--collection", "-c", default=None, help="Filter by collection name or ID.")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def list_api(collection: str | None, output_json: bool) -> None:
    """List all APIs for the connected app, grouped by collection."""
    import re as _re
    from maeris_mcp.maeris.client import MaerisClient
    from maeris_mcp.auth.token_store import TokenStore

    async def _inner() -> None:
        async with MaerisClient() as client:
            apis = await client.get_all_apis()
            if collection:
                col_id = await _resolve_collection(client, collection)
                apis = [a for a in apis if a.get("collection_id") == col_id or a.get("parent_collection_id") == col_id]
            if not apis:
                click.echo("No APIs found.")
                return
            if output_json:
                _print_json(apis)
                return

            # Resolve selected environment and its variables
            store = TokenStore()
            env_id = store.get_environment_id()
            env_name = store.get_environment_name()
            env_vars: dict[str, str] = {}

            if not env_id:
                environments = await client.get_environments()
                click.echo("")
                click.secho("  No environment selected.", fg="yellow")
                if environments:
                    click.echo("  Available environments:")
                    for env in environments:
                        eid = env.get("env_id") or env.get("_id") or "-"
                        click.echo(f"    • {env.get('name', '-')}  ({eid})")
                click.echo("  Run:  maeris env select <name>  to resolve environment variables in URLs.")
                click.echo("")
            else:
                try:
                    fields = await client.get_environment_fields(env_id)
                    field_list = fields if isinstance(fields, list) else fields.get("fields", fields.get("data", []))
                    for f in field_list:
                        if isinstance(f, dict) and f.get("field_name"):
                            env_vars[f["field_name"]] = f.get("field_value", "")
                except Exception:
                    pass

            def _resolve_url(url: str) -> str:
                return _re.sub(r"\{\{(\w+)\}\}", lambda m: env_vars.get(m.group(1), m.group(0)), url)

            # Fetch collection names
            collections = await client.get_collections()
            col_name_by_id: dict[str, str] = {}
            for c in collections:
                cid = c.get("_id") or c.get("api_collection_id") or c.get("collection_id") or c.get("id") or ""
                if cid:
                    col_name_by_id[str(cid)] = c.get("name", "-")

            # Group APIs by collection
            from collections import defaultdict as _defaultdict
            grouped: dict[str, list[dict]] = _defaultdict(list)
            for a in apis:
                cid = str(a.get("collection_id") or a.get("parent_collection_id") or "")
                grouped[cid].append(a)

            if env_id:
                click.secho(f"  Environment: {env_name or env_id}", fg="green")
            click.echo("")

            for cid, col_apis in grouped.items():
                col_display = col_name_by_id.get(cid, cid or "No Collection")
                click.secho(f"  {col_display}", bold=True, fg="cyan")
                click.echo("  " + "─" * max(len(col_display), 1))
                for a in col_apis:
                    method = (a.get("type") or a.get("method", "GET")).upper()
                    url = _resolve_url(a.get("url", "-"))
                    name = a.get("name", "-")
                    click.echo(f"    {method:<7}  {name}")
                    click.echo(f"             {url}")
                click.echo("")

    _run_async(_inner())


@app_group.command("links")
def list_links_cmd() -> None:
    """List repositories linked as context sources for test generation."""
    import os as _os
    from maeris_mcp.auth.linked_repo_store import LinkedRepoStore

    store = LinkedRepoStore()
    links = store.list_all()

    if not links:
        click.echo("No linked repos configured for this repository.")
        click.echo("Use:  maeris link <path>  to add one.")
        return

    click.echo(f"Linked repos for: {_os.getcwd()}")
    click.echo()
    for lk in links:
        status = click.style("●", fg="green") if lk.get("exists") else click.style("FAIL", fg="red")
        added = lk.get("added", "")[:10]
        label = lk["label"]
        path = lk["path"]
        exists_note = "" if lk.get("exists") else "  (path not found)"
        click.echo(f"  {status}  {label:<22}  {path}{exists_note}  (linked {added})")

    valid = sum(1 for lk in links if lk.get("exists"))
    click.echo()
    click.echo(f"{valid}/{len(links)} linked repos available.")


# ---------------------------------------------------------------------------
# get subcommands
# ---------------------------------------------------------------------------


@api_group.command("get")
@click.argument("api")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def get_api(api: str, output_json: bool) -> None:
    """Get one API by name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            result = await client.get_api(api_id)
            if output_json:
                _print_json(result)
            else:
                _print_api_detail(result)

    _run_async(_inner())


def _format_datetime(raw: str) -> str:
    """Format an ISO datetime string to a readable local format."""
    if not raw or raw == "-":
        return raw
    try:
        from datetime import datetime, timezone
        dt = datetime.fromisoformat(raw.replace("Z", "+00:00"))
        local = dt.astimezone()
        return local.strftime("%d %b %Y  %H:%M %Z")
    except Exception:
        return raw


def _print_api_detail(result: dict) -> None:
    """Print API details in a human-readable format."""
    import json as _json

    api = result.get("api", result)
    param_headers = result.get("param_headers") or api.get("param_headers", [])
    hdrs = [ph for ph in param_headers if (ph.get("type") or "").upper() in ("HEADERS", "HEADER", "")]
    params = [ph for ph in param_headers if (ph.get("type") or "").upper() in ("PARAMS", "PARAM")]
    # If type field is missing/empty treat all as headers (legacy)
    if not any((ph.get("type") or "") for ph in param_headers):
        hdrs = param_headers
        params = []

    click.echo("")
    click.secho("  " + api.get("name", "Unknown"), fg="cyan", bold=True)
    click.echo("  ID:      " + api.get("api_id", "-"))
    click.echo("  Method:  " + (api.get("type") or api.get("method", "GET")).upper())
    click.echo("  URL:     " + api.get("url", "-"))
    click.echo("  Created: " + _format_datetime(api.get("created_at", "-")))

    payload = api.get("payload", "")
    if payload:
        click.echo("  Payload: " + payload)

    verification = api.get("verification", "")
    if verification:
        try:
            v = _json.loads(verification) if isinstance(verification, str) else verification
            click.echo("  Schema:  " + ", ".join(k + ": " + t for k, t in v.items()))
        except Exception:
            click.echo("  Schema:  " + str(verification))

    click.echo("")
    click.secho("  Headers", bold=True)
    if hdrs:
        for h in hdrs:
            click.echo("    " + h.get("key", "?") + ": " + h.get("value", ""))
    else:
        click.echo("    (none)")

    click.echo("")
    click.secho("  Query Params", bold=True)
    if params:
        for p in params:
            click.echo("    " + p.get("key", "?") + ": " + p.get("value", ""))
    else:
        click.echo("    (none)")

    trends = api.get("api_trends")
    if trends:
        click.echo("")
        click.secho("  Performance", bold=True)
        tier = trends.get("performance_tier", "-")
        is_slow = trends.get("is_slow", False)
        click.echo("    Tier:    ", nl=False)
        click.secho(tier, fg="red" if is_slow else "green")
        summary = (trends.get("analysis") or {}).get("slowness_reason_summary", "")
        if summary:
            click.echo("    Summary: " + summary)
        suggested = trends.get("suggested_assertions", [])
        if suggested:
            click.echo("")
            click.secho("  Suggested Assertions", bold=True)
            for s in suggested:
                val = s.get("assertion_value", {})
                val_str = ", ".join(str(k) + "=" + str(v) for k, v in val.items()) if isinstance(val, dict) else str(val)
                click.echo("    [" + s.get("assertion_nature", "") + "] " + s.get("assertion_type", "") + ": " + val_str)

    click.echo("")


@collection_group.command("get")
@click.argument("collection")
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
def get_collection(collection: str, output_json: bool) -> None:
    """Get a collection by name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            cid = await _resolve_collection(client, collection)
            data = await client.get_collection(cid)
            if output_json:
                _print_json(data)
            else:
                try:
                    app_users = await client.get_app_users()
                except Exception:
                    app_users = []
                _print_record(_with_actor_names(data, app_users))

    _run_async(_inner())


@env_data_group.command("list")
@click.argument("environment")
def get_env_data(environment: str) -> None:
    """Get environment key/value fields by environment name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            env_id = await _resolve_environment(client, environment)
            data = await client.get_environment_fields(env_id)
        rows = data if isinstance(data, list) else [data]
        _print_table(rows, ["field_name", "field_value"])

    _run_async(_inner())


@flow_map_group.command("get")
@click.argument("flow")
def get_flow_map(flow: str) -> None:
    """Get the flow map for a flow (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            data = await client.get_api_flow_map(flow_id)
        rows = data if isinstance(data, list) else [data]
        _print_table(rows, ["api_id", "order", "name"])

    _run_async(_inner())


@flow_param_group.command("get")
@click.argument("flow")
def get_flow_params(flow: str) -> None:
    """Get params for a flow (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            data = await client.get_api_flow_params(flow_id)
        rows = data if isinstance(data, list) else [data]
        _print_table(rows, ["param_name", "param_value", "api_id"])

    _run_async(_inner())


# ---------------------------------------------------------------------------
# create subcommands
# ---------------------------------------------------------------------------


def _create_api_for(for_description: str, collection: str | None, source: str | None, model: str) -> None:
    """Scan the codebase for APIs matching for_description and create them in Maeris."""
    import os
    from pathlib import Path

    claude_bin, ai_mode = _resolve_ai_mode()

    target_path = str(Path(source).resolve()) if source else os.getcwd()
    coll_name = collection or Path(target_path).name

    click.echo(
        f"\n  Scanning for: {for_description!r}\n"
        f"  Target:       {target_path}\n"
        f"  Collection:   {coll_name}\n"
    )

    prompt = (
        f"Use the create_apis_for MCP tool to find and create APIs related to: {for_description!r}\n\n"
        f"Parameters for the first call:\n"
        f"  target_path    = {target_path!r}\n"
        f"  functionality  = {for_description!r}\n"
        f"  collection_name = {coll_name!r}\n"
        f"  include_content = true\n"
        f"  limit          = 5\n\n"
        "Follow the tool's workflow instructions exactly.\n"
        "On each batch: submit only the apis relevant to the functionality alongside the next offset.\n"
        "On the final batch: pass is_last=true to finalize the scan and enter review.\n\n"
        "After scanning completes, the tool enters review phase with a pendingDecision.\n"
        "Present a clear summary to the user showing:\n"
        "  - APIs found (names, methods, endpoints)\n"
        "  - Flows proposed (names and steps)\n"
        "  - Environments detected (names and fields)\n"
        "Wait for the user to confirm or request adjustments.\n"
        f"When the user approves, call with collection_name={coll_name!r} and approve=true to save.\n"
        "Do not call any other tools."
    )

    if claude_bin:
        click.echo("  Starting interactive session — review and approve results when prompted.\n")
        rc = _claude_interactive(
            claude_bin, prompt, model, target_path,
            allowed_tools=["mcp__maeris__create_apis_for"],
            system_prompt=_INTERACTIVE_SYSTEM_PROMPT,
        )
    else:
        rc = _run_via_direct_api(prompt, target_path, spinner_message="Scanning for APIs…")
    if rc != 0:
        click.secho(f"\nFAIL Failed (exit code {rc}).", fg="red")
        raise SystemExit(rc)


@api_group.command("create")
@click.option("--name", "-n", default=None, help="API display name")
@click.option("--method", "-m", default=None, help="HTTP method")
@click.option("--url", "-u", "api_url", default=None, help="API endpoint URL")
@click.option("--payload", "-p", default="", help="Payload string or JSON")
@click.option("--collection", "-c", default=None, help="Collection name or ID to assign the API to")
@click.option(
    "--for", "for_description", default=None,
    help="Scan codebase for APIs matching this description and create them automatically.",
)
@click.option(
    "--source", "-s", default=None,
    help="Directory to scan. Only used with --for. Default: cwd.",
)
@click.option(
    "--model", default="claude-sonnet-4-6", show_default=True,
    help="AI model. Only used with --for.",
)
def create_api(
    name: str | None,
    method: str | None,
    api_url: str | None,
    payload: str,
    collection: str | None,
    for_description: str | None,
    source: str | None,
    model: str,
) -> None:
    """Create a new API entry.

    \b
    Manual:  --name, --method, --url
    Scan:    --for "functionality description"
    """
    _VALID_METHODS = {"GET", "POST", "PUT", "DELETE", "PATCH"}

    if for_description:
        _create_api_for(for_description, collection, source, model)
        return

    missing = [
        flag for flag, val in [
            ("--name", name),
            ("--method", method),
            ("--url", api_url),
            ("--collection", collection),
        ]
        if not (val and str(val).strip())
    ]
    if missing:
        raise click.UsageError(
            f"Missing required: {', '.join(missing)}. Use --name/--method/--url/--collection or --for."
        )

    # Validate field values are non-empty after stripping
    if not name.strip():
        raise click.UsageError("--name cannot be empty.")
    if not api_url.strip():
        raise click.UsageError("--url cannot be empty.")

    method_upper = method.strip().upper()
    if method_upper not in _VALID_METHODS:
        raise click.UsageError(
            f"Invalid method '{method}'. Must be one of: {', '.join(sorted(_VALID_METHODS))}."
        )

    from maeris_mcp.maeris.client import MaerisClient
    from maeris_mcp.maeris.types import MaerisAPI

    async def _inner() -> None:
        async with MaerisClient() as client:
            collection_id = await _resolve_or_create_collection(client, collection)
            api_id = await client.create_api(
                MaerisAPI(name=name.strip(), type=method_upper.lower(), url=api_url.strip(), payload=payload, parent_collection_id=collection_id)
            )
        _print_record({"api_id": api_id, "name": name.strip(), "method": method_upper, "url": api_url.strip(), "collection_id": collection_id})
        _next_steps(
            "maeris api param add <api> --headers '[...]'   Add headers",
            "maeris assertion create --api <api> ...        Add assertions",
        )

    _run_async(_inner())


def _detect_import_format(suffix: str, content: str) -> str:
    """Return 'csv', 'openapi', 'postman', or 'ai'."""
    import json as _json

    if suffix == ".csv":
        return "csv"

    if suffix in (".json", ".yaml", ".yml"):
        parsed: dict = {}
        if suffix == ".json":
            try:
                parsed = _json.loads(content)
            except _json.JSONDecodeError:
                pass
        else:
            try:
                import yaml as _yaml  # type: ignore[import]
                parsed = _yaml.safe_load(content) or {}
            except Exception:
                pass

        if isinstance(parsed, dict):
            # OpenAPI / Swagger
            if any(k in parsed for k in ("openapi", "swagger")):
                return "openapi"
            # Postman collection
            info = parsed.get("info") or {}
            if isinstance(info, dict) and "schema" in info and "item" in parsed:
                return "postman"

    return "ai"


def _parse_csv_apis(content: str, default_collection: str) -> list[dict]:
    """Parse a CSV file where each row is an API.

    Expected columns (case-insensitive): name, method, url/endpoint, collection, payload.
    """
    import csv
    import io

    reader = csv.DictReader(io.StringIO(content))
    apis: list[dict] = []
    for row in reader:
        # Normalise keys to lowercase
        row_lower = {k.lower().strip(): v for k, v in row.items() if k}
        name = row_lower.get("name", "").strip()
        method = (row_lower.get("method", "GET") or "GET").strip().upper()
        url = (row_lower.get("url") or row_lower.get("endpoint", "")).strip()
        col = (row_lower.get("collection") or default_collection).strip()
        payload = row_lower.get("payload", "")
        if not name or not url:
            continue
        apis.append({
            "name": name,
            "method": method,
            "url": url,
            "collection": col,
            "payload": payload,
        })
    return apis


def _parse_openapi_apis(content: str, suffix: str, default_collection: str) -> list[dict]:
    """Parse an OpenAPI 2/3 spec and return normalised API dicts."""
    import json as _json

    parsed: dict = {}
    if suffix == ".json":
        parsed = _json.loads(content)
    else:
        import yaml as _yaml  # type: ignore[import]
        parsed = _yaml.safe_load(content) or {}

    # Resolve a human-readable tag/collection for a path+operation
    def _collection_for(op: dict, path: str) -> str:
        tags = op.get("tags") or []
        if tags:
            return str(tags[0])
        # Fall back to first path segment
        parts = [p for p in path.strip("/").split("/") if p and not p.startswith("{")]
        return parts[0].capitalize() if parts else default_collection

    apis: list[dict] = []
    paths: dict = parsed.get("paths") or {}
    for path, path_item in paths.items():
        if not isinstance(path_item, dict):
            continue
        for method in ("get", "post", "put", "patch", "delete", "head", "options"):
            op = path_item.get(method)
            if not isinstance(op, dict):
                continue
            summary = op.get("summary") or op.get("operationId") or f"{method.upper()} {path}"
            col = _collection_for(op, path)

            # Build a minimal payload example from requestBody if present
            payload: object = None
            req_body = op.get("requestBody") or {}
            content_map = req_body.get("content") or {}
            for media_type in ("application/json", "application/x-www-form-urlencoded"):
                media = content_map.get(media_type) or {}
                schema = media.get("schema") or {}
                example = media.get("example") or schema.get("example")
                if example:
                    payload = example
                    break
                # Build a skeleton from schema properties
                props = schema.get("properties") or {}
                if props:
                    payload = {k: "" for k in props}
                    break

            apis.append({
                "name": str(summary),
                "method": method.upper(),
                "url": path,
                "collection": col,
                "payload": payload or "",
            })
    return apis


def _parse_postman_apis(content: str, default_collection: str) -> list[dict]:
    """Parse a Postman collection v2/v2.1 and return normalised API dicts."""
    import json as _json

    parsed: dict = _json.loads(content)
    col_name = (parsed.get("info") or {}).get("name") or default_collection

    apis: list[dict] = []

    def _walk(items: list, parent_name: str) -> None:
        for item in items:
            if not isinstance(item, dict):
                continue
            # Folder
            if "item" in item:
                folder_name = item.get("name") or parent_name
                _walk(item["item"], folder_name)
                continue
            # Request
            request = item.get("request") or {}
            if not isinstance(request, dict):
                continue
            raw_url = request.get("url") or {}
            if isinstance(raw_url, str):
                url = raw_url
            else:
                url = raw_url.get("raw") or "/".join(
                    str(p) for p in (raw_url.get("path") or [])
                )
            method = str(request.get("method") or "GET").upper()
            name = item.get("name") or f"{method} {url}"

            # Payload from body
            body = request.get("body") or {}
            payload: object = ""
            if body.get("mode") == "raw":
                raw_body = body.get("raw") or ""
                try:
                    payload = _json.loads(raw_body)
                except Exception:
                    payload = raw_body
            elif body.get("mode") == "urlencoded":
                fields = body.get("urlencoded") or []
                payload = {f.get("key", ""): f.get("value", "") for f in fields if f.get("key")}

            apis.append({
                "name": name,
                "method": method,
                "url": url,
                "collection": parent_name or col_name,
                "payload": payload,
            })

    _walk(parsed.get("item") or [], col_name)
    return apis


@api_group.command("import")
@click.option(
    "--from", "source_file",
    required=True,
    type=click.Path(exists=True, dir_okay=False, readable=True),
    help="Source file to extract APIs from. Any language (.py, .js, .ts, .java, .go, .json, …).",
)
@click.option(
    "--collection", "-c", default=None,
    help="Assign all imported APIs to this collection (name or ID). Created if it doesn't exist.",
)
def create_apis_from_file(source_file: str, collection: str | None) -> None:
    """Extract API endpoints from any source file and create them in Maeris.

    Structured formats (CSV, OpenAPI/Swagger JSON/YAML, Postman collections)
    are parsed directly without AI. All other files (Python, TypeScript, Java,
    Go, etc.) use AI extraction.

    \b
    Examples:
      maeris api import --from routes.py
      maeris api import --from src/api/routes.ts --collection "User Service"
      maeris api import --from openapi.json
      maeris api import --from postman_collection.json
      maeris api import --from apis.csv
    """
    import json as _json
    import re as _re
    from pathlib import Path as _Path

    source_path = _Path(source_file)
    try:
        file_content = source_path.read_text(encoding="utf-8", errors="replace")
    except OSError as e:
        click.echo(f"Error: cannot read {source_file}: {e}", err=True)
        raise SystemExit(1)

    if not file_content.strip():
        click.echo(f"Error: file is empty: {source_file}", err=True)
        raise SystemExit(1)

    suffix = source_path.suffix.lower()
    default_collection = collection or source_path.stem
    fmt = _detect_import_format(suffix, file_content)

    if fmt == "csv":
        click.echo(f"\nDetected CSV — parsing {source_path.name} directly (no AI)…")
        try:
            data: list[dict] = _parse_csv_apis(file_content, default_collection)
        except Exception as exc:
            click.echo(f"Error: failed to parse CSV: {exc}", err=True)
            raise SystemExit(1)

    elif fmt == "openapi":
        click.echo(f"\nDetected OpenAPI/Swagger — parsing {source_path.name} directly (no AI)…")
        try:
            data = _parse_openapi_apis(file_content, suffix, default_collection)
        except Exception as exc:
            click.echo(f"Error: failed to parse OpenAPI spec: {exc}", err=True)
            raise SystemExit(1)

    elif fmt == "postman":
        click.echo(f"\nDetected Postman collection — parsing {source_path.name} directly (no AI)…")
        try:
            data = _parse_postman_apis(file_content, default_collection)
        except Exception as exc:
            click.echo(f"Error: failed to parse Postman collection: {exc}", err=True)
            raise SystemExit(1)

    else:
        # AI extraction path — unchanged
        claude_bin, _ai_mode = _resolve_ai_mode()

        prompt = f"""Analyze the following source code and extract every API endpoint definition.

For each endpoint, produce one JSON object with these fields:
  name        string   — short descriptive name, e.g. "List Users", "Create Order"
  method      string   — HTTP method in uppercase: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
  url         string   — the URL path or full URL as written in the code, e.g. "/api/users" or "https://api.example.com/users"
  payload     object|null — example request body as a JSON object if the method has a body, otherwise null
  description string   — one-sentence description of what the endpoint does
  collection  string   — logical group/resource name for this endpoint, e.g. "Users", "Orders", "Auth", "Products". Group related endpoints together under the same collection name. Use the router prefix, controller name, or resource name as the collection.

Rules:
- Include EVERY route/endpoint defined in the file, even if there are many.
- If the file is an OpenAPI/Swagger spec, extract all paths and operations.
- If the file is a Postman collection, extract all requests.
- Use the exact URL path string from the code — do not invent or shorten paths.
- For payload, use real field names from the code (request body schema, Pydantic model, DTO, etc.).
- Every endpoint MUST have a collection field — never leave it blank.
- Output ONLY a raw JSON array — no markdown, no code fences, no explanation.

Source file: {source_path.name}

```
{file_content}
```

Output (raw JSON array only):"""

        click.echo(f"\nAnalysing {source_path.name} with AI…")
        if claude_bin:
            raw_output, _rc = _run_claude_capture(
                claude_bin, prompt, "claude-sonnet-4-6", label=f"extracting APIs from {source_path.name}"
            )
        else:
            raw_output, _rc = _run_direct_api_capture(
                prompt, "claude-sonnet-4-6", label=f"extracting APIs from {source_path.name}"
            )
        raw_output = raw_output.strip()
        if not raw_output:
            click.echo("Error: AI returned no output.", err=True)
            raise SystemExit(1)

        json_str = raw_output
        fenced = _re.search(r"```(?:json)?\s*([\s\S]+?)```", json_str)
        if fenced:
            json_str = fenced.group(1).strip()
        array_match = _re.search(r"\[\s*\{[\s\S]*\}\s*\]", json_str)
        if array_match:
            json_str = array_match.group(0)

        try:
            data = _json.loads(json_str)
        except _json.JSONDecodeError:
            click.echo("Error: AI output could not be parsed as JSON.", err=True)
            click.echo("Raw output:", err=True)
            click.echo(raw_output[:2000], err=True)
            raise SystemExit(1)

        if isinstance(data, dict):
            data = data.get("apis") or data.get("endpoints") or data.get("routes") or []
        if not isinstance(data, list) or not data:
            click.echo("No API endpoints found in the file.", err=True)
            raise SystemExit(1)

    if not data:
        click.echo("No API endpoints found in the file.", err=True)
        raise SystemExit(1)

    click.echo(f"\nFound {len(data)} API(s):\n")
    col_w = 8
    name_w = max((len(str(e.get("name", ""))) for e in data), default=20) + 2
    grp_w = max((len(str(e.get("collection", ""))) for e in data), default=12) + 2
    click.echo(f"  {'METHOD':<{col_w}}  {'COLLECTION':<{grp_w}}  {'NAME':<{name_w}}  URL")
    click.echo(f"  {'-'*col_w}  {'-'*grp_w}  {'-'*name_w}  {'-'*40}")
    for entry in data:
        method = str(entry.get("method", "?")).upper()
        name = str(entry.get("name", ""))
        url = str(entry.get("url", ""))
        grp = str(entry.get("collection", ""))
        click.echo(f"  {method:<{col_w}}  {grp:<{grp_w}}  {name:<{name_w}}  {url}")

    click.echo()
    if not click.confirm(f"Create these {len(data)} API(s) in Maeris?"):
        click.echo("Aborted.")
        raise SystemExit(0)

    async def _inner() -> None:
        from maeris_mcp.maeris.client import MaerisClient
        from maeris_mcp.maeris.types import MaerisAPI

        client = MaerisClient()

        forced_collection_id = ""
        if collection:
            forced_collection_id = await _resolve_or_create_collection(client, collection)
            click.echo(f"  Collection: {collection!r}  (id={forced_collection_id})")

        collection_cache: dict[str, str] = {}
        if not collection:
            unique_names = {
                str(e["collection"]).strip()
                for e in data
                if e.get("collection") and str(e.get("collection", "")).strip()
            }
            if unique_names:
                click.echo(f"\n  Resolving {len(unique_names)} collection(s)…")
                for col_name in sorted(unique_names):
                    try:
                        col_id = await _resolve_or_create_collection(client, col_name)
                        collection_cache[col_name] = col_id
                        click.echo(f"    OK {col_name!r}  (id={col_id})")
                    except Exception as exc:
                        click.secho(f"    FAIL {col_name!r}: {exc}", fg="yellow")
                        collection_cache[col_name] = ""
                click.echo()

        created: list[dict] = []
        failed: list[dict] = []
        total = len(data)

        for i, entry in enumerate(data, 1):
            click.echo(f"  [{i}/{total}] {entry.get('name', '?')[:50]}", err=True)

            if forced_collection_id:
                entry_col_id = forced_collection_id
            else:
                col_name = str(entry.get("collection", "")).strip()
                entry_col_id = collection_cache.get(col_name, "")

            api_obj = MaerisAPI(
                name=str(entry["name"]),
                type=str(entry.get("method", entry.get("type", "GET"))).upper(),
                url=str(entry["url"]),
                payload=entry.get("payload", ""),
                headers=entry.get("headers", []) or [],
                params=entry.get("params", []) or [],
                parent_collection_id=entry_col_id,
            )

            try:
                api_id = await client.create_api(api_obj)
            except Exception as exc:
                if _is_token_expired(exc):
                    client = await _refresh_client()
                    click.echo(f"  Retrying {entry.get('name', '?')}…", err=True)
                    try:
                        api_id = await client.create_api(api_obj)
                    except Exception as exc2:
                        failed.append({"name": entry.get("name", "?"), "error": str(exc2)})
                        continue
                else:
                    failed.append({"name": entry.get("name", "?"), "error": str(exc)})
                    continue

            created.append({
                "api_id": api_id,
                "name": entry["name"],
                "method": entry.get("method", "GET").upper(),
                "url": entry["url"],
                "collection": entry.get("collection", ""),
            })

        await client.close()

        click.echo()
        if created:
            _print_table(created, ["collection", "method", "name", "url"])
        if failed:
            click.echo()
            click.secho(f"{len(failed)} failed:", fg="red")
            for f in failed:
                click.echo(f"  FAIL {f['name']}: {f['error']}")
        click.echo()
        click.secho(f"OK {len(created)} created", fg="green", nl=False)
        if failed:
            click.secho(f"  FAIL {len(failed)} failed", fg="red")
        else:
            click.echo()

        _next_steps(
            "maeris api list              View imported APIs",
            "maeris flow create --name <flow>   Chain them into a flow",
        )

    _run_async(_inner())


@api_param_group.command("add")
@click.argument("api")
@click.option("--headers", default="[]",
              help="JSON array of header objects. Format: '[{\"key\":\"name\",\"value\":\"val\"}]'")
@click.option("--params", default="[]",
              help="JSON array of param objects. Format: '[{\"key\":\"name\",\"value\":\"val\"}]'")
def create_api_param(api: str, headers: str, params: str) -> None:
    """Add param/header records to an API (name or ID).

    \b
    Both --headers and --params expect a JSON array of {"key","value"} objects.
    Always wrap the JSON in single quotes to prevent shell parsing errors.

    \b
    Examples:
      maeris api param add "My API" --headers '[{"key":"Authorization","value":"Bearer tok"}]'
      maeris api param add "My API" --params  '[{"key":"page","value":"1"}]'
    """
    from maeris_mcp.maeris.client import MaerisClient

    import click as _click
    _ps_hint = (
        "\n\n"
        "  PowerShell tip: assign JSON to a variable first to avoid quoting issues:\n\n"
        "    $h = '[{\"key\": \"Authorization\", \"value\": \"Bearer token\"}]'\n"
        "    maeris api param add \"My API\" --headers $h\n\n"
        "  For --params use the same pattern with $p instead."
    )
    _fmt = (
        "  Format:  '[{\"key\": \"<name>\", \"value\": \"<val>\"}]'\n"
        "  Example: '[{\"key\": \"Authorization\", \"value\": \"Bearer token\"}]'"
    )
    try:
        parsed_headers = _parse_json_input(headers, "--headers")
    except _click.BadParameter:
        raise _click.BadParameter(
            "--headers must be a JSON array of objects.\n\n" + _fmt + _ps_hint
        )
    try:
        parsed_params = _parse_json_input(params, "--params")
    except _click.BadParameter:
        raise _click.BadParameter(
            "--params must be a JSON array of objects.\n\n" + _fmt + _ps_hint
        )
    if not isinstance(parsed_headers, list):
        raise click.BadParameter(
            "--headers must be a JSON array (not a plain object).\n\n" + _fmt + _ps_hint
        )
    if not isinstance(parsed_params, list):
        raise click.BadParameter(
            "--params must be a JSON array (not a plain object).\n\n" + _fmt + _ps_hint
        )

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            _print_record(await client.add_param_header(api_id, parsed_headers, parsed_params))

    _run_async(_inner())


@collection_group.command("create")
@click.option("--name", "-n", required=True)
@click.option("--parent", "-p", default="", help="Optional parent collection name or ID")
def create_collection(name: str, parent: str) -> None:
    """Create a new collection."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            parent_id = await _resolve_collection(client, parent) if parent else ""
            new_id = await client.create_collection(name=name, parent_collection_id=parent_id)
        _print_record({"collection_id": new_id, "name": name, "parent_collection_id": parent_id})
        _next_steps(
            f"maeris api create --collection {name!r} ...   Add APIs to this collection",
            f"maeris scan api --project-name {name!r}       Auto-extract APIs into it",
        )

    _run_async(_inner())


@env_group.command("create")
@click.option("--name", "-n", required=True)
def create_environment(name: str) -> None:
    """Create a new environment."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            _print_record(await client.create_environment(name))
        _next_steps(
            f"maeris env data add {name!r} --fields '[...]'   Add variables (base_url, tokens, …)",
            "maeris env list                                  View all environments",
        )

    _run_async(_inner())


@env_data_group.command("add")
@click.argument("environment")
@click.option("--fields", required=True, help="JSON array: [{field_name, field_value}]")
def create_env_data(environment: str, fields: str) -> None:
    """Add key/value fields to an environment (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    parsed = _parse_json_input(fields, "--fields")
    if not isinstance(parsed, list):
        raise click.BadParameter("--fields must be a JSON array")

    async def _inner() -> None:
        async with MaerisClient() as client:
            env_id = await _resolve_environment(client, environment)
            _print_result(await client.add_environment_fields(env_id, parsed))

    _run_async(_inner())


@assertion_group.command("create")
@click.option("--api", required=True, help="API name or ID")
@click.option("--nature", required=True)
@click.option("--type", "assertion_type", required=True)
@click.option("--value", required=True, help="JSON object assertion value")
def create_assertion(api: str, nature: str, assertion_type: str, value: str) -> None:
    """Create a new API assertion."""
    from maeris_mcp.maeris.client import MaerisClient

    parsed = _parse_json_input(value, "--value")
    if not isinstance(parsed, dict):
        raise click.BadParameter("--value must be a JSON object")

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            _print_record(await client.add_assertion(api_id, nature, assertion_type, parsed))

    _run_async(_inner())


@flow_group.command("create")
@click.option("--name", required=True)
def create_flow(name: str) -> None:
    """Create a new API flow."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            _print_record(await client.create_api_flow(name))
        _next_steps(
            f"maeris flow map set {name!r} --flow-map '[...]'   Define the step sequence",
            "maeris flow list                                   View all flows",
        )

    _run_async(_inner())


@flow_param_group.command("add")
@click.argument("flow")
@click.option("--params", required=True, help="JSON array")
def create_flow_params(flow: str, params: str) -> None:
    """Add params to a flow (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    parsed = _parse_json_input(params, "--params")
    if not isinstance(parsed, list):
        raise click.BadParameter("--params must be a JSON array")

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            _print_result(await client.add_api_flow_params(flow_id, parsed))

    _run_async(_inner())


# ---------------------------------------------------------------------------
# update subcommands
# ---------------------------------------------------------------------------


@api_group.command("update")
@click.argument("api")
@click.option("--name", default=None, help="New name for the API.")
@click.option("--url", default=None, help="New endpoint URL.")
@click.option("--type", "method", default=None,
              type=click.Choice(["get", "post", "put", "patch", "delete"], case_sensitive=False),
              help="HTTP method.")
@click.option("--payload", default=None, help="Request body / payload string.")
@click.option("--data", default=None, hidden=True,
              help="Raw JSON object (deprecated; use named flags instead).")
def update_api(api: str, name, url, method, payload, data) -> None:
    """Update an API's fields by name or ID.

    \b
    Examples:
      maeris api update "Beer - ales" --url https://api.example.com/beers
      maeris api update "Beer - ales" --name "Beer - Ales v2" --type get
      maeris api update "Beer - ales" --data '{"url": "https://api.example.com"}'
    """
    from maeris_mcp.maeris.client import MaerisClient

    if data is not None:
        parsed = _parse_json_input(data, "--data")
        if not isinstance(parsed, dict):
            raise click.BadParameter("--data must be a JSON object")
    else:
        parsed = {}
        if name is not None:
            parsed["name"] = name
        if url is not None:
            parsed["url"] = url
        if method is not None:
            parsed["type"] = method.lower()
        if payload is not None:
            parsed["payload"] = payload
        if not parsed:
            raise click.UsageError(
                "Provide at least one field to update.\n\n"
                "Named flags:\n"
                "  --name TEXT     New name\n"
                "  --url TEXT      New endpoint URL\n"
                "  --type TEXT     HTTP method  (get | post | put | patch | delete)\n"
                "  --payload TEXT  Request body\n\n"
                "Or pass all fields as a JSON object:\n"
                "  --data TEXT     e.g. '{\"url\": \"https://...\", \"type\": \"post\"}'"
            )

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            _print_record(await client.update_api(api_id, parsed))

    _run_async(_inner())


@api_param_group.command("update")
@click.argument("api")
@click.option("--field-name", required=True)
@click.option("--field-value", required=True)
@click.option("--field-type", required=True, type=click.Choice(["param", "header"]))
def update_api_param(api: str, field_name: str, field_value: str, field_type: str) -> None:
    """Update an API param or header field (API name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            _print_record(
                await client.update_param_header(api_id, field_name, field_value, field_type)
            )

    _run_async(_inner())


@env_group.command("rename")
@click.argument("environment")
@click.option("--name", required=True)
def update_environment(environment: str, name: str) -> None:
    """Update an environment's name (environment name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            env_id = await _resolve_environment(client, environment)
            _print_result(await client.update_environment(env_id, name))

    _run_async(_inner())


@env_data_group.command("update")
@click.argument("environment")
@click.option("--field-name", required=True)
@click.option("--field-value", required=True)
def update_env_data(environment: str, field_name: str, field_value: str) -> None:
    """Update an environment key/value field (environment name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            env_id = await _resolve_environment(client, environment)
            _print_result(
                await client.update_environment_field(env_id, field_name, field_value)
            )

    _run_async(_inner())


@assertion_group.command("update")
@click.argument("assertion_id")
@click.option("--value", required=True, help="JSON object assertion value")
def update_assertion(assertion_id: str, value: str) -> None:
    """Update an assertion by ID."""
    from maeris_mcp.maeris.client import MaerisClient

    parsed = _parse_json_input(value, "--value")
    if not isinstance(parsed, dict):
        raise click.BadParameter("--value must be a JSON object")

    async def _inner() -> None:
        async with MaerisClient() as client:
            _print_record(await client.update_assertion(assertion_id, parsed))

    _run_async(_inner())


@flow_group.command("rename")
@click.argument("flow")
@click.option("--name", required=True)
def update_flow(flow: str, name: str) -> None:
    """Update a flow's name (flow name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            result = await client.update_api_flow(flow_id, name)
        if isinstance(result, list):
            _print_table(result, ["flow_id", "flow_name"])
        else:
            _print_record(result)

    _run_async(_inner())


@flow_param_group.command("update")
@click.argument("flow")
@click.option("--params", required=True, help="JSON array")
def update_flow_params(flow: str, params: str) -> None:
    """Update params for a flow (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    parsed = _parse_json_input(params, "--params")
    if not isinstance(parsed, list):
        raise click.BadParameter("--params must be a JSON array")

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            _print_result(await client.update_api_flow_params(flow_id, parsed))

    _run_async(_inner())


# ---------------------------------------------------------------------------
# delete subcommands (resource only — test/folder deletes are in test_cmds.py)
# ---------------------------------------------------------------------------


@api_group.command("delete")
@click.argument("api")
def delete_api(api: str) -> None:
    """Delete an API by name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            await client.delete_api(api_id)
        click.secho(f"Deleted API: {api}", fg="green")

    _run_async(_inner())


@api_param_group.command("delete")
@click.argument("api")
@click.option("--field-name", required=True)
def delete_api_param(api: str, field_name: str) -> None:
    """Delete an API param or header field (API name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            await client.delete_param_header(api_id, field_name)
        click.secho(f"Deleted field '{field_name}' from API '{api}'", fg="green")

    _run_async(_inner())


@collection_group.command("delete")
@click.argument("collection")
def delete_collection(collection: str) -> None:
    """Delete a collection by name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            cid = await _resolve_collection(client, collection)
            await client.delete_collection(cid)
        click.secho(f"Deleted collection: {collection}", fg="green")

    _run_async(_inner())


@env_group.command("delete")
@click.argument("environment")
def delete_environment(environment: str) -> None:
    """Delete an environment by name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            env_id = await _resolve_environment(client, environment)
            await client.delete_environment(env_id)
        click.secho(f"Deleted environment: {environment}", fg="green")

    _run_async(_inner())


@env_data_group.command("delete")
@click.argument("environment")
@click.option("--field-name", required=True)
def delete_env_data(environment: str, field_name: str) -> None:
    """Delete an environment key/value field (environment name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            env_id = await _resolve_environment(client, environment)
            await client.delete_environment_field(env_id, field_name)
        click.secho(f"Deleted field '{field_name}' from environment '{environment}'", fg="green")

    _run_async(_inner())


@assertion_group.command("delete")
@click.argument("assertion_id")
def delete_assertion(assertion_id: str) -> None:
    """Delete an assertion by ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            await client.delete_assertion(assertion_id)
        click.secho(f"Deleted assertion: {assertion_id}", fg="green")

    _run_async(_inner())


@flow_group.command("delete")
@click.argument("flow")
def delete_flow(flow: str) -> None:
    """Delete a flow by name or ID."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            await client.delete_api_flow(flow_id)
        click.secho(f"Deleted flow: {flow}", fg="green")

    _run_async(_inner())


@flow_param_group.command("delete")
@click.argument("flow")
def delete_flow_params(flow: str) -> None:
    """Delete params for a flow (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            await client.delete_api_flow_params(flow_id)
        click.secho(f"Deleted params for flow: {flow}", fg="green")

    _run_async(_inner())


# ---------------------------------------------------------------------------
# move / duplicate / rename / copy / set subcommands
# ---------------------------------------------------------------------------

# --- Shared helpers for move / clone (module-level so both commands can use them) ---

def _extract_collection_id(item: dict) -> str:
    return str(
        item.get("api_collection_id")
        or item.get("collection_id")
        or item.get("id")
        or item.get("_id")
        or ""
    )


def _extract_api_ids(item: dict) -> set[str]:
    ids = {
        str(item.get("api_id") or ""),
        str(item.get("_id") or ""),
        str(item.get("id") or ""),
        str(item.get("apiId") or ""),
        str(item.get("child_id") or ""),
        str(item.get("childId") or ""),
    }
    return {v for v in ids if v}


def _extract_children_payload(children_map: object, collection_id: str) -> dict:
    if isinstance(children_map, dict):
        by_id = children_map.get(collection_id)
        if isinstance(by_id, dict):
            return by_id
        data = children_map.get("data")
        if isinstance(data, dict):
            by_id = data.get(collection_id)
            if isinstance(by_id, dict):
                return by_id
    return {}


def _iter_child_apis(children: dict) -> list[dict]:
    apis = []
    for key in ("apis", "api", "requests"):
        value = children.get(key)
        if isinstance(value, list):
            apis.extend([v for v in value if isinstance(v, dict)])
    mixed = children.get("children")
    if isinstance(mixed, list):
        for entry in mixed:
            if not isinstance(entry, dict):
                continue
            e_type = str(entry.get("type") or "").upper()
            if e_type in {"REQUEST", "API"}:
                apis.append(entry)
    return apis


def _iter_child_collections(children: dict) -> list[dict]:
    cols = []
    for key in ("api_collections", "collections", "sub_collections", "subCollections"):
        value = children.get(key)
        if isinstance(value, list):
            cols.extend([v for v in value if isinstance(v, dict)])
    mixed = children.get("children")
    if isinstance(mixed, list):
        for entry in mixed:
            if not isinstance(entry, dict):
                continue
            e_type = str(entry.get("type") or "").upper()
            if e_type in {"COLLECTION", "FOLDER"}:
                cols.append(entry)
    return cols


async def _resolve_collection_any(client: "MaerisClient", query: str) -> str:
    from maeris_mcp.tools.collection_tools import _find_collection_by_name, _find_collection_by_path
    query = query.strip()
    try:
        return await _resolve_collection(client, query)
    except Exception:
        pass
    found = (
        await _find_collection_by_path(client, query)
        if ">" in query
        else await _find_collection_by_name(client, query)
    )
    if found:
        cid = _extract_collection_id(found)
        if cid:
            return cid
    raise _not_found_error("Collection", query, "maeris collection list")


async def _collect_api_identity_set(client: "MaerisClient", api_id: str) -> set[str]:
    ids = {api_id}
    try:
        items = await client.get_all_apis()
        for item in items:
            if api_id in _extract_api_ids(item):
                ids.update(_extract_api_ids(item))
                break
    except Exception:
        pass
    try:
        detail = await client.get_api(api_id)
        if isinstance(detail, dict):
            candidates: list[dict] = [detail]
            api_obj = detail.get("api")
            if isinstance(api_obj, dict):
                candidates.insert(0, api_obj)
            for obj in candidates:
                ids.update(_extract_api_ids(obj))
    except Exception:
        pass
    return {v for v in ids if v}


async def _detect_source_collection_ids(client: "MaerisClient", identity_ids: set[str]) -> set[str]:
    source_ids: set[str] = set()
    collection_keys = (
        "collection_id", "parent_collection_id", "api_collection_id",
        "collectionId", "parentCollectionId",
    )
    try:
        items = await client.get_all_apis()
        for item in items:
            if not identity_ids.intersection(_extract_api_ids(item)):
                continue
            for key in collection_keys:
                value = item.get(key)
                if value:
                    source_ids.add(str(value))
    except Exception:
        pass
    try:
        api_id = next(iter(identity_ids))
        detail = await client.get_api(api_id)
        if isinstance(detail, dict):
            candidates: list[dict] = [detail]
            api_obj = detail.get("api")
            if isinstance(api_obj, dict):
                candidates.insert(0, api_obj)
            for obj in candidates:
                for key in collection_keys:
                    value = obj.get(key)
                    if value:
                        source_ids.add(str(value))
    except Exception:
        pass
    try:
        roots = await client.get_collections()
        queue: list[str] = []
        visited: set[str] = set()
        for col in roots:
            cid = _extract_collection_id(col)
            if cid:
                queue.append(cid)
        while queue:
            current = queue.pop(0)
            if current in visited:
                continue
            visited.add(current)
            children_map = await client.get_collection_children([current])
            children = _extract_children_payload(children_map, current)
            for child_api in _iter_child_apis(children):
                if identity_ids.intersection(_extract_api_ids(child_api)):
                    source_ids.add(current)
            for sub in _iter_child_collections(children):
                sub_id = _extract_collection_id(sub)
                if sub_id and sub_id not in visited:
                    queue.append(sub_id)
    except Exception:
        pass
    return source_ids


@api_group.command("move")
@click.argument("api")
@click.option("--to-collection", "-c", required=True, help="Destination collection name or ID")
@click.option(
    "--from-collection",
    "-f",
    default=None,
    help="Optional source collection name or ID (useful when auto-detection is ambiguous).",
)
def move_api(api: str, to_collection: str, from_collection: str | None) -> None:
    """Move an API to another collection (both accept name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            cid = await _resolve_collection_any(client, to_collection)
            identity_ids = await _collect_api_identity_set(client, api_id)

            if from_collection:
                source_ids = {await _resolve_collection_any(client, from_collection)}
            else:
                source_ids = await _detect_source_collection_ids(client, identity_ids)

            if cid in source_ids:
                click.secho(
                    f"Already in {to_collection} collection",
                    fg="yellow",
                )
                return
            try:
                # Prefer "attach to target, then detach from sources" to avoid accidental orphaning.
                await client.move_api_to_collection(cid, api_id)
            except Exception as exc:
                # Some deployments only allow attaching to the target after detaching from the source.
                removed_sources: list[str] = []
                for source_cid in sorted(source_ids):
                    if not source_cid or source_cid == cid:
                        continue
                    try:
                        await client.remove_api_from_collection(source_cid, api_id)
                        removed_sources.append(source_cid)
                    except Exception as detach_exc:
                        click.secho(
                            f"Warning: could not detach from source collection {source_cid}: {detach_exc}",
                            fg="yellow",
                        )

                try:
                    await client.move_api_to_collection(cid, api_id)
                except Exception as exc2:
                    # Roll back: best-effort re-attach to any detached sources.
                    for source_cid in removed_sources:
                        try:
                            await client.move_api_to_collection(source_cid, api_id)
                        except Exception:
                            try:
                                await client.update_api(api_id, {"collection_id": source_cid})
                            except Exception:
                                pass
                    raise click.ClickException(f"Move failed: {exc2}") from exc2

                # Attach succeeded after detaching sources — nothing else to detach.
                source_ids.clear()

            # Attach succeeded — now detach from old source collections.
            for source_cid in sorted(source_ids):
                if not source_cid or source_cid == cid:
                    continue
                try:
                    await client.remove_api_from_collection(source_cid, api_id)
                except Exception as exc:
                    click.secho(
                        f"Warning: could not detach from source collection {source_cid}: {exc}",
                        fg="yellow",
                    )

        click.secho(f"Moved '{api}' to collection '{to_collection}'", fg="green")

    _run_async(_inner())


@api_group.command("clone")
@click.argument("api")
@click.option("--new-name", "-n", default=None, help="Name for the cloned API. Defaults to 'Copy of <name>'.")
@click.option("--collection", "-c", default=None, help="Collection to clone into. Defaults to the source API's collection.")
def duplicate_api(api: str, new_name: str | None, collection: str | None) -> None:
    """Clone an API by name or ID.

    By default the clone is placed in the same collection as the source.
    Use --collection to clone into a different collection.

    \b
    Examples:
      maeris api clone "Login"
      maeris api clone "Login" --new-name "Login v2"
      maeris api clone "Login" --collection "Auth Staging"
    """
    from maeris_mcp.maeris.client import MaerisClient
    from maeris_mcp.maeris.types import MaerisAPI

    async def _inner() -> None:
        async with MaerisClient() as client:
            api_id = await _resolve_api(client, api)
            detail = await client.get_api(api_id)
            src = detail.get("api", detail)
            param_headers = detail.get("param_headers", [])

            # Resolve target collection
            if collection:
                target_col_id = await _resolve_or_create_collection(client, collection)
                target_col_name = collection
            else:
                identity_ids = await _collect_api_identity_set(client, api_id)
                source_ids = await _detect_source_collection_ids(client, identity_ids)
                target_col_id = next(iter(source_ids), "")
                # Resolve collection name for display
                target_col_name = ""
                if target_col_id:
                    try:
                        col_detail = await client.get_collection(target_col_id)
                        target_col_name = str(
                            col_detail.get("name")
                            or col_detail.get("collection_name")
                            or col_detail.get("api_collection_name")
                            or target_col_id
                        )
                    except Exception:
                        target_col_name = target_col_id

            clone_name = new_name or f"Copy of {src.get('name', api)}"
            cloned = MaerisAPI(
                name=clone_name,
                type=str(src.get("type", "get")),
                url=str(src.get("url", "")),
                payload=src.get("payload", ""),
                verification=src.get("verification", ""),
                parent_collection_id=target_col_id,
            )
            clone_id = await client.create_api(cloned)

            # Copy headers and params
            if param_headers:
                copy_headers = [ph for ph in param_headers if (ph.get("type") or "").upper() == "HEADERS"]
                copy_params = [ph for ph in param_headers if (ph.get("type") or "").upper() == "PARAMS"]
                if copy_headers or copy_params:
                    h = [{"field_name": ph["field_name"], "field_value": ph.get("field_value", "")} for ph in copy_headers]
                    p = [{"field_name": ph["field_name"], "field_value": ph.get("field_value", "")} for ph in copy_params]
                    await client.add_param_header(clone_id, h, p)

        click.secho(f"Cloned '{api}' as '{clone_name}' in collection '{target_col_name}'", fg="green")

    _run_async(_inner())


@collection_group.command("rename")
@click.argument("collection")
@click.option("--name", "-n", required=True)
def rename_collection(collection: str, name: str) -> None:
    """Rename a collection (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            cid = await _resolve_collection(client, collection)
            _print_record(await client.rename_collection(cid, name))

    _run_async(_inner())


@collection_group.command("clone")
@click.argument("collection")
@click.option("--name", "-n", "new_name", required=True, help="Name for copied collection")
def copy_collection(collection: str, new_name: str) -> None:
    """Copy a collection (name or ID) into a new collection."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            cid = await _resolve_collection(client, collection)
            original = await client.get_collection(cid)
            parent_id = str(original.get("parent_collection_id", ""))
            copied_id = await client.create_collection(
                name=new_name,
                parent_collection_id=parent_id,
            )
        _print_record({"source": collection, "copied_collection_id": copied_id})

    _run_async(_inner())


@flow_map_group.command("set")
@click.argument("flow")
@click.option("--flow-map", required=True, help="JSON array")
def set_flow_map(flow: str, flow_map: str) -> None:
    """Set the flow map for a flow (name or ID)."""
    from maeris_mcp.maeris.client import MaerisClient

    parsed = _parse_json_input(flow_map, "--flow-map")
    if not isinstance(parsed, list):
        raise click.BadParameter("--flow-map must be a JSON array")

    async def _inner() -> None:
        async with MaerisClient() as client:
            flow_id = await _resolve_flow(client, flow)
            _print_record(await client.create_api_flow_map(flow_id, parsed))

    _run_async(_inner())


# ---------------------------------------------------------------------------
# flow run
# ---------------------------------------------------------------------------


@flow_group.command("run")
@click.argument("flow")
@click.option("--env", "-e", required=True, help="Environment name or ID to run the flow against.")
@click.option("--json", "output_json", is_flag=True, default=False, help="Output result as JSON.")
def flow_run_cmd(flow: str, env: str, output_json: bool) -> None:
    """Run an API flow against an environment."""
    from maeris_mcp.maeris.client import MaerisClient

    async def _inner() -> None:
        async with MaerisClient() as client:
            fid = await _resolve_flow(client, flow)
            eid = await _resolve_environment(client, env)
            result = await client.run_flow(fid, eid)
            _print_result(result)

    _run_async(_inner())
