"""LLM-guided codebase scan that extracts and saves relevant APIs to Maeris with review."""

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

from mcp.types import TextContent, Tool

from maeris_mcp.maeris.client import MaerisClient
from maeris_mcp.maeris.types import FieldPair, MaerisAPI
from maeris_mcp.scanners.code_reader import collect_scan_files, get_file_contents
from maeris_mcp.storage.api_store import APIStore
from maeris_mcp.tools.setup_automation import _api_output_schema, _build_flow_params, _extract_flow_id

_MAX_FILE_CHARS = 8_000


# ---------------------------------------------------------------------------
# Scanning instructions
# ---------------------------------------------------------------------------


def _scanning_instructions(functionality: str) -> str:
    return (
        f"create_apis_for scanning instructions (functionality: {functionality!r}):\n"
        "1. Analyze each file for HTTP API calls: endpoints, methods, headers, params, auth, payloads.\n"
        "2. Extract full details: request/response payloads, path params, query params, auth config, call sites.\n"
        "3. HEADERS — you MUST extract headers for every API. Look for:\n"
        "   - Content-Type (almost always 'application/json' for JSON APIs)\n"
        "   - Authorization / Bearer tokens\n"
        "   - Custom headers in fetch/axios options, shared HTTP client configs, interceptors\n"
        "   - If headers are set via `axios.create({headers: {...}})` or a shared config, include those\n"
        "   - At minimum, include `Content-Type: application/json` for any JSON API\n"
        "   - Submit headers as: [{field_name: 'Content-Type', field_value: 'application/json'}]\n"
        "4. REQUEST BODY — for POST/PUT/PATCH, extract the actual request body as a JSON object:\n"
        "   - Find the body/data passed to fetch/axios\n"
        "   - Submit as an example JSON object with realistic sample values\n"
        "   - e.g. payload: {\"name\": \"John Doe\", \"email\": \"john@example.com\"}\n"
        "   - NOT as a schema definition with field types\n"
        "5. URL FORMAT — use {{BASE_URL}} placeholder for the base URL:\n"
        "   - url: '{{BASE_URL}}/api/users' (not just '/api/users' or 'http://host:3000/api/users')\n"
        "   - If the backend URL uses an env var, set url to '{{BASE_URL}}/path'\n"
        "6. Submit ONLY the apis relevant to the requested functionality.\n"
        "7. Skip all APIs unrelated to the functionality — do not submit them.\n"
        "8. Call again with apis=[...found] and the next offset to continue scanning.\n"
        "9. On the final call (hasMore=false), also submit environments and flows:\n"
        "   - environments=[{name: 'Development', fields: [{field_name: 'BASE_URL', field_value: '{{BASE_URL}}'}]}]\n"
        "   - Include env var names found in source code (process.env.FOO, os.environ['FOO'])\n"
        "   - Use {{VARIABLE_NAME}} as field values — users fill in real values during review\n"
        "   - NEVER read .env files with real credentials\n"
        "   - flows=[...]: meaningful user-journey flows for the requested functionality.\n"
        "     Each flow: {name, description, steps: [{api_name, order, method, endpoint}]}\n"
        "     Name flows as user-facing actions: 'User Login', 'Browse Products'.\n"
        "     Steps should reflect real sequential data dependencies (e.g. login → get profile).\n"
        "     IMPORTANT: Each step's api_name MUST exactly match the name of an API you submitted\n"
        "     in this same scan session. Flows are built from the scanned APIs — do not reference\n"
        "     API names that were not submitted as part of this scan.\n"
        "     Only include flows relevant to the requested functionality.\n"
        "10. Set is_last=true to finalize the scan and transition to review.\n"
        "Note: The MCP tool stores your submissions; you must perform the analysis and filtering.\n\n"
        "PROXY / ROUTE HANDLER TRACING:\n"
        "If a file is a Next.js API route (`/pages/api/*`, `/app/api/*/route.ts`), Express route, "
        "or any server-side proxy handler, it forwards requests to a real backend.\n"
        "Look INSIDE the handler body for the outbound HTTP call (fetch, axios, http.request, etc.) "
        "to find the real backend URL.\n"
        "Example: `/pages/api/labels/labels.ts` containing "
        "`fetch(process.env.API_URL + '/labels', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({name, color}) })` "
        "should produce:\n"
        "  - url = `{{BASE_URL}}/labels`\n"
        "  - headers = [{field_name: 'Content-Type', field_value: 'application/json'}]\n"
        "  - payload = {\"name\": \"example\", \"color\": \"#ff0000\"}\n"
        "If the backend URL uses an env var, use {{BASE_URL}} as the base.\n"
        "If a handler delegates to a service module, note the import — look for that module in subsequent batches."
    )


# ---------------------------------------------------------------------------
# Tool definition
# ---------------------------------------------------------------------------


def create_apis_for_tool() -> Tool:
    """Return the tool definition for create_apis_for."""
    return Tool(
        name="create_apis_for",
        description=(
            "LLM-guided codebase scan that extracts and creates in Maeris "
            "only the APIs relevant to a given functionality. Use when the user asks to "
            "'create APIs for <feature>' or 'scan codebase for <feature> APIs'.\n\n"
            "Workflow:\n"
            "Phase 1 (Scan): Call with target_path + functionality + "
            "include_content=true. Analyze each file batch. Submit only the apis relevant "
            "to the functionality alongside the next offset. On the final batch also submit "
            "environments=[...] and flows=[...]. Repeat until hasMore=false, then set is_last=true.\n"
            "Phase 2 (Review): Inspect the proposed APIs, flows, and environments. "
            "To exclude items the user does not want saved, use the toggle parameters: "
            "exclude_apis, exclude_flows, exclude_flow_steps, exclude_environments, exclude_env_fields. "
            "To edit content use updated_apis / updated_flows / updated_environments. "
            "Repeat until the user confirms the selection is correct. "
            "IMPORTANT: ALL exclusions must be applied during this phase — NEVER call approve=true "
            "and then delete items from Maeris afterwards. Save-then-delete is always wrong here.\n"
            "Phase 3 (Save): Only call with collection_name + approve=true after the user has confirmed "
            "and all exclusions are applied. Only the selected items will be persisted to Maeris."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "target_path": {
                    "type": "string",
                    "description": "Absolute path to scan (first call only; defaults to server working directory)",
                },
                "session_id": {
                    "type": "string",
                    "description": "Existing session ID to continue/paginate",
                },
                "functionality": {
                    "type": "string",
                    "description": "Feature or domain to filter for (e.g. 'user authentication', 'order management')",
                },
                "include_content": {
                    "type": "boolean",
                    "description": "Include file contents for analysis. Default: false",
                    "default": False,
                },
                "offset": {
                    "type": "integer",
                    "description": "Skip first N files when including content. Default: 0",
                    "default": 0,
                },
                "limit": {
                    "type": "integer",
                    "description": "Max files to return content for (1-10). Default: 10",
                    "default": 10,
                },
                "apis": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": (
                        "APIs relevant to the functionality, extracted from the previous file batch. "
                        "Only include APIs that match the requested functionality."
                    ),
                },
                "environments": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Detected environments to submit on is_last=true call.",
                },
                "flows": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Proposed API flows to submit on is_last=true call.",
                },
                "is_last": {
                    "type": "boolean",
                    "description": "Set true on the final scanning call to finalize and transition to review.",
                },
                "updated_apis": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Review phase: full corrected APIs list (replaces current state).",
                },
                "updated_environments": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Review phase: full corrected environments list (replaces current state).",
                },
                "updated_flows": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "Review phase: full corrected flows list (replaces current state).",
                },
                "exclude_apis": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Review phase: API names to deselect (will not be saved to Maeris).",
                },
                "include_apis": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Review phase: API names to re-select (undo a previous exclusion).",
                },
                "exclude_flows": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Review phase: flow names to deselect.",
                },
                "include_flows": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Review phase: flow names to re-select.",
                },
                "exclude_flow_steps": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "flow_name": {"type": "string"},
                            "api_name": {"type": "string"},
                        },
                        "required": ["flow_name", "api_name"],
                    },
                    "description": "Review phase: individual flow steps to deselect.",
                },
                "include_flow_steps": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "flow_name": {"type": "string"},
                            "api_name": {"type": "string"},
                        },
                        "required": ["flow_name", "api_name"],
                    },
                    "description": "Review phase: individual flow steps to re-select.",
                },
                "exclude_environments": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Review phase: environment names to deselect (will not be saved).",
                },
                "include_environments": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Review phase: environment names to re-select.",
                },
                "exclude_env_fields": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "env_name": {"type": "string"},
                            "field_name": {"type": "string"},
                        },
                        "required": ["env_name", "field_name"],
                    },
                    "description": "Review phase: individual env var fields to deselect.",
                },
                "include_env_fields": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "env_name": {"type": "string"},
                            "field_name": {"type": "string"},
                        },
                        "required": ["env_name", "field_name"],
                    },
                    "description": "Review phase: individual env var fields to re-select.",
                },
                "set_env_field_values": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "env_name": {"type": "string"},
                            "field_name": {"type": "string"},
                            "field_value": {"type": "string"},
                        },
                        "required": ["env_name", "field_name", "field_value"],
                    },
                    "description": "Review phase: set actual values for specific env var fields (replaces {{PLACEHOLDER}}).",
                },
                "collection_name": {
                    "type": "string",
                    "description": "Save phase: name for the Maeris collection.",
                },
                "approve": {
                    "type": "boolean",
                    "description": "Save phase: set true (along with collection_name) to persist everything to Maeris.",
                },
                "verbose": {
                    "type": "boolean",
                    "description": "Review phase: include full api/environment lists in the response. Default: false.",
                    "default": False,
                },
            },
        },
    )


# ---------------------------------------------------------------------------
# Review response builder
# ---------------------------------------------------------------------------


def _pending_for_decision(
    selected_apis: int,
    total_apis: int,
    env_names: list[str],
    selected_flows: int = 0,
    total_flows: int = 0,
    selected_envs: int | None = None,
    total_envs: int | None = None,
    unfilled_env_vars: list[str] | None = None,
    existing_env_matches: list[dict] | None = None,
) -> dict:
    env_summary = f"{selected_envs} of {total_envs}" if selected_envs is not None else str(len(env_names))
    flow_part = f" and {selected_flows} of {total_flows} flows" if total_flows else ""
    result: dict[str, Any] = {
        "question": (
            f"{selected_apis} of {total_apis} APIs{flow_part} are selected. "
            f"Environments: {env_summary} selected ({', '.join(env_names) if env_names else 'none'}). "
            "Confirm the selection is correct before saving, or adjust it first."
        ),
        "warning": (
            "NEVER call approve=true and then delete items from Maeris to clean up. "
            "Apply ALL exclusions using the toggle parameters in this phase BEFORE approving. "
            "approve=true saves exactly the currently selected items — nothing more, nothing less."
        ),
        "hint": (
            "Exclude items: exclude_apis / exclude_flows / exclude_flow_steps / "
            "exclude_environments / exclude_env_fields. "
            "Re-include: include_apis / include_flows / include_flow_steps / "
            "include_environments / include_env_fields. "
            "Set env values: set_env_field_values=[{env_name, field_name, field_value}]. "
            "Edit content: updated_apis / updated_flows / updated_environments. "
            "Save only when ready: collection_name + approve=true."
        ),
    }
    result["userAction"] = (
        "Present the review summary to the user. Show them what APIs, flows, and environments "
        "are selected. Ask the user to confirm before saving, or tell you what to adjust. "
        "Do NOT call approve=true without explicit user confirmation."
    )
    if unfilled_env_vars:
        result["unfilledEnvVars"] = unfilled_env_vars
        result["envVarAction"] = (
            "Ask the user for actual values for these env vars, "
            "then call set_env_field_values=[{env_name, field_name, field_value}, ...]"
        )
    if existing_env_matches:
        result["existingEnvironmentMatches"] = existing_env_matches
        result["envMatchAction"] = (
            "Existing Maeris environments with a BASE_URL field were found. "
            "Ask the user: do you want to REUSE one of these existing environments "
            "(and skip creating new ones by using exclude_environments), "
            "or CREATE a new environment with the extracted fields? "
            "If reusing, also ask if the user wants to set the extracted BASE_URL "
            "field value to match the existing environment's value using set_env_field_values."
        )
    return result


def _build_for_review_response(
    api_store: APIStore,
    session_id: str,
    changes: list[str],
    verbose: bool = False,
) -> list[TextContent]:
    """Build a decorated review response with selected flags on every API/environment."""
    session = api_store.get_for_session(session_id)
    if not session:
        return [TextContent(type="text", text=f"Error: session lost: {session_id}")]

    env_names = [e.get("name", "") for e in session.environments]
    total_apis = len(session.apis)
    total_flows = len(session.flows)
    total_envs = len(session.environments)
    selected_apis = total_apis - len(session.excluded_api_names)
    selected_flows = total_flows - len(session.excluded_flow_names)
    selected_envs = total_envs - len(session.excluded_env_names)

    # Collect unfilled env vars
    unfilled: list[str] = []
    for env in session.environments:
        env_name = env.get("name", "")
        if env_name in session.excluded_env_names:
            continue
        for f in env.get("fields") or []:
            field_name = f.get("field_name", "")
            field_value = f.get("field_value", "")
            if (env_name, field_name) not in session.excluded_env_fields:
                if field_value.startswith("{{") and field_value.endswith("}}"):
                    unfilled.append(f"{env_name}.{field_name}")

    response: dict[str, Any] = {
        "sessionId": session_id,
        "functionality": session.functionality,
        "status": "review",
        "summary": {
            "totalApis": total_apis,
            "selectedApis": selected_apis,
            "totalFlows": total_flows,
            "selectedFlows": selected_flows,
            "totalEnvironments": total_envs,
            "selectedEnvironments": selected_envs,
            "environments": env_names,
        },
        "changesApplied": changes,
        "pendingDecision": _pending_for_decision(
            selected_apis, total_apis, env_names,
            selected_flows=selected_flows, total_flows=total_flows,
            selected_envs=selected_envs, total_envs=total_envs,
            unfilled_env_vars=unfilled if unfilled else None,
            existing_env_matches=session.existing_env_matches if session.existing_env_matches else None,
        ),
    }

    if verbose:
        apis_with_selection = [
            {**api, "selected": api.get("name") not in session.excluded_api_names}
            for api in session.apis
        ]

        flows_with_selection = []
        for flow in session.flows:
            flow_name = flow.get("name", "")
            steps = flow.get("steps") or []
            steps_with_selection = [
                {
                    **step,
                    "selected": (flow_name, step.get("api_name", "")) not in session.excluded_flow_steps,
                }
                for step in steps
            ]
            flows_with_selection.append(
                {**flow, "selected": flow_name not in session.excluded_flow_names, "steps": steps_with_selection}
            )

        envs_with_selection = []
        for env in session.environments:
            env_name = env.get("name", "")
            fields_with_selection = [
                {**f, "selected": (env_name, f.get("field_name", "")) not in session.excluded_env_fields}
                for f in env.get("fields") or []
            ]
            envs_with_selection.append({
                **env,
                "selected": env_name not in session.excluded_env_names,
                "fields": fields_with_selection,
            })

        response["apis"] = apis_with_selection
        response["flows"] = flows_with_selection
        response["environments"] = envs_with_selection
    else:
        response["hint"] = "Pass verbose=true to see the full API/flow/environment list."

    return [TextContent(type="text", text=json.dumps(response, indent=2))]


# ---------------------------------------------------------------------------
# Phase routing
# ---------------------------------------------------------------------------


async def handle_create_apis_for(api_store: APIStore, arguments: dict) -> list[TextContent]:
    """Handle the create_apis_for tool call."""
    session_id = arguments.get("session_id") or arguments.get("sessionId")

    # No session → start new scan
    if not session_id:
        return await _start_for_session(api_store, arguments)

    session = api_store.get_for_session(session_id)
    if not session:
        return [TextContent(type="text", text=f"Error: session not found: {session_id}")]

    if session.status == "scanning":
        return await _continue_for_scan(api_store, session_id, arguments)

    if session.status == "review":
        approve = bool(arguments.get("approve"))
        collection_name = arguments.get("collection_name")
        if approve and collection_name:
            return await _save_for_to_maeris(api_store, session_id, collection_name)
        return await _apply_for_feedback(api_store, session_id, arguments)

    # completed
    return [TextContent(
        type="text",
        text=json.dumps({"sessionId": session_id, "status": "completed"}, indent=2),
    )]


# ---------------------------------------------------------------------------
# Phase 1: Start session
# ---------------------------------------------------------------------------


async def _start_for_session(
    api_store: APIStore, arguments: dict
) -> list[TextContent]:
    target_path = arguments.get("target_path") or os.getcwd()
    functionality = arguments.get("functionality") or ""

    if not functionality:
        return [TextContent(type="text", text="Error: functionality is required on the first call")]

    scan_root = os.path.abspath(target_path)
    if not os.path.exists(scan_root):
        return [TextContent(type="text", text=f"Error: path does not exist: {target_path}")]

    path = Path(scan_root)
    project_root = path if path.is_dir() else path.parent
    base_for_rel = str(project_root.parent if project_root.parent != project_root else project_root)

    files_abs, _ = collect_scan_files(scan_root, api_signals_only=True)
    file_list = [os.path.relpath(p, base_for_rel) for p in files_abs]

    session_id = api_store.create_for_session(
        functionality=functionality,
        target_path=scan_root,
        scan_root=scan_root,
        base_for_rel=base_for_rel,
        file_list=file_list,
    )

    response: dict[str, Any] = {
        "sessionId": session_id,
        "functionality": functionality,
        "status": "scanning",
        "targetPath": scan_root,
        "filesTotal": len(file_list),
        "analysisInstructions": _scanning_instructions(functionality),
        "expectedApiSchema": _api_output_schema(),
        "nextStep": (
            "Call again with include_content=true, offset=0 to fetch file contents. "
            f"Submit only apis relevant to {functionality!r} alongside subsequent offsets. "
            "On the final call also submit environments=[...] and flows=[...], then set is_last=true."
        ),
    }
    return [TextContent(type="text", text=json.dumps(response, indent=2))]


# ---------------------------------------------------------------------------
# Phase 1: Continue scan
# ---------------------------------------------------------------------------


async def _continue_for_scan(
    api_store: APIStore, session_id: str, arguments: dict
) -> list[TextContent]:
    session = api_store.get_for_session(session_id)
    if not session:
        return [TextContent(type="text", text=f"Error: session not found: {session_id}")]

    include_content = bool(arguments.get("include_content", False))
    try:
        offset = int(arguments.get("offset", 0))
        limit = max(1, min(int(arguments.get("limit", 10)), 10))
    except (ValueError, TypeError):
        return [TextContent(type="text", text="Error: offset and limit must be integers")]

    raw_apis = arguments.get("apis") or []
    if isinstance(raw_apis, dict):
        raw_apis = [raw_apis]
    raw_environments = arguments.get("environments") or []
    is_last = bool(arguments.get("is_last") or arguments.get("isLast"))

    # Store submitted APIs
    warnings: list[str] = []
    apis_stats: dict[str, int] = {}
    if raw_apis and isinstance(raw_apis, list):
        dict_apis = [r for r in raw_apis if isinstance(r, dict)]
        dropped = len(raw_apis) - len(dict_apis)
        if dropped:
            warnings.append(f"{dropped} API item(s) skipped: not valid objects.")
        valid_apis = [
            a for a in dict_apis
            if (a.get("name") or "").strip() and (a.get("url") or a.get("endpoint") or "").strip()
        ]
        dropped_empty = len(dict_apis) - len(valid_apis)
        if dropped_empty:
            warnings.append(f"{dropped_empty} API item(s) skipped: missing name or url/endpoint.")
        apis_stats = api_store.add_for_apis(session_id, valid_apis)

    base_response: dict[str, Any] = {
        "sessionId": session_id,
        "functionality": session.functionality,
        "status": "scanning",
        "targetPath": session.target_path,
        "filesTotal": len(session.file_list),
        "apisAccumulatedTotal": len(session.apis),
    }
    if apis_stats:
        base_response["apisSubmitted"] = apis_stats
    if warnings:
        base_response["warnings"] = warnings

    # Finalize scan → transition to review
    if is_last:
        environments = [e for e in raw_environments if isinstance(e, dict)]
        raw_flows = arguments.get("flows") or []
        flows = [f for f in raw_flows if isinstance(f, dict)]

        finalized = api_store.finalize_for_session(session_id, environments, flows)
        if not finalized:
            return [TextContent(type="text", text=f"Error: could not finalize session: {session_id}")]

        if not finalized.apis:
            base_response["status"] = "review"
            base_response["warnings"] = base_response.get("warnings", []) + [
                "No APIs were collected. Nothing to review."
            ]
            return [TextContent(type="text", text=json.dumps(base_response, indent=2))]

        # Fetch existing Maeris environments that have a BASE_URL field
        existing_env_matches: list[dict] = []
        try:
            async with MaerisClient() as client:
                maeris_envs = await client.get_environments()
                for maeris_env in maeris_envs:
                    from maeris_mcp.tools.portal_api_tools import _env_id, _env_name
                    m_id = _env_id(maeris_env)
                    m_name = _env_name(maeris_env)
                    if not m_id:
                        continue
                    try:
                        env_data = await client.get_environment_fields(m_id)
                        base_url_value = None
                        if isinstance(env_data, dict):
                            for k, v in env_data.items():
                                if k.lower() in ("base_url", "baseurl") and isinstance(v, str) and v.strip():
                                    base_url_value = v
                                    break
                        elif isinstance(env_data, list):
                            for item in env_data:
                                if isinstance(item, dict):
                                    fk = (item.get("field_name") or "").lower()
                                    fv = item.get("field_value", "")
                                    if fk in ("base_url", "baseurl") and isinstance(fv, str) and fv.strip():
                                        base_url_value = fv
                                        break
                        if base_url_value:
                            existing_env_matches.append({
                                "existingEnvName": m_name,
                                "existingEnvId": m_id,
                                "baseUrlValue": base_url_value,
                            })
                    except Exception:
                        continue
        except Exception:
            pass

        finalized.existing_env_matches = existing_env_matches

        review_result = _build_for_review_response(api_store, session_id, [], verbose=True)
        if warnings and review_result:
            try:
                payload = json.loads(review_result[0].text)
                payload["warnings"] = warnings
                review_result = [TextContent(type="text", text=json.dumps(payload, indent=2))]
            except Exception:
                pass
        return review_result

    # Return next file batch
    if include_content:
        file_list = session.file_list
        if not file_list:
            base_response["warnings"] = base_response.get("warnings", []) + [
                "No scannable files found at the target path."
            ]
            base_response["nextStep"] = "Call with is_last=true to finalize the empty scan."
            return [TextContent(type="text", text=json.dumps(base_response, indent=2))]

        if offset >= len(file_list):
            return [TextContent(
                type="text",
                text=f"Error: offset {offset} is out of range — file list has {len(file_list)} file(s)",
            )]

        base_for_rel = session.base_for_rel
        paginated_rels = file_list[offset: offset + limit]
        abs_paths = [str(Path(base_for_rel) / p) for p in paginated_rels]
        contents = get_file_contents(abs_paths)

        has_more = offset + limit < len(file_list)
        base_response["pagination"] = {
            "offset": offset,
            "limit": limit,
            "returned": len(paginated_rels),
            "total": len(file_list),
            "hasMore": has_more,
            "nextOffset": offset + limit if has_more else None,
        }

        truncated = 0
        file_contents_map: dict[str, str] = {}
        for rel, abs_path in zip(paginated_rels, abs_paths):
            content = contents.get(abs_path, "")
            if len(content) > _MAX_FILE_CHARS:
                content = content[:_MAX_FILE_CHARS]
                truncated += 1
            file_contents_map[rel] = content
        base_response["fileContents"] = file_contents_map
        if truncated:
            base_response.setdefault("warnings", []).append(
                f"{truncated} file(s) truncated to {_MAX_FILE_CHARS} chars to stay within response limits."
            )

        if offset == 0:
            base_response["analysisInstructions"] = _scanning_instructions(session.functionality)
            base_response["expectedApiSchema"] = _api_output_schema()
    else:
        base_response["nextStep"] = (
            "Call again with include_content=true, offset=0 to fetch file contents. "
            f"Submit only apis relevant to {session.functionality!r} alongside subsequent offsets. "
            "On the final call also submit environments=[...] and flows=[...], then set is_last=true."
        )

    return [TextContent(type="text", text=json.dumps(base_response, indent=2))]


# ---------------------------------------------------------------------------
# Phase 2: Apply feedback (review loop)
# ---------------------------------------------------------------------------


async def _apply_for_feedback(
    api_store: APIStore, session_id: str, arguments: dict
) -> list[TextContent]:
    changes: list[str] = []

    # Toggle operations
    toggle_keys = [
        "exclude_apis", "include_apis",
        "exclude_flows", "include_flows",
        "exclude_flow_steps", "include_flow_steps",
        "exclude_environments", "include_environments",
        "exclude_env_fields", "include_env_fields",
    ]
    has_toggles = any(arguments.get(k) for k in toggle_keys)
    if has_toggles:
        toggle_changes, ok = api_store.apply_for_toggles(
            session_id,
            exclude_apis=arguments.get("exclude_apis"),
            include_apis=arguments.get("include_apis"),
            exclude_flows=arguments.get("exclude_flows"),
            include_flows=arguments.get("include_flows"),
            exclude_flow_steps=arguments.get("exclude_flow_steps"),
            include_flow_steps=arguments.get("include_flow_steps"),
            exclude_environments=arguments.get("exclude_environments"),
            include_environments=arguments.get("include_environments"),
            exclude_env_fields=arguments.get("exclude_env_fields"),
            include_env_fields=arguments.get("include_env_fields"),
        )
        if not ok:
            return [TextContent(type="text", text=f"Error: session not found: {session_id}")]
        changes.extend(toggle_changes)

    # Env field value setting
    edit_keys = ["updated_apis", "updated_flows", "updated_environments", "set_env_field_values"]
    set_env_field_values = arguments.get("set_env_field_values")
    has_edits = any(arguments.get(k) is not None for k in edit_keys)
    if set_env_field_values:
        value_changes, ok = api_store.set_for_env_field_values(session_id, set_env_field_values)
        if not ok:
            return [TextContent(type="text", text=f"Error: session not found: {session_id}")]
        changes.extend(value_changes)

    # Content edits
    content_edit_keys = ["updated_apis", "updated_flows", "updated_environments"]
    has_content_edits = any(arguments.get(k) is not None for k in content_edit_keys)
    if has_content_edits:
        edit_changes, ok = api_store.update_for_review(
            session_id,
            updated_apis=arguments.get("updated_apis"),
            updated_environments=arguments.get("updated_environments"),
            updated_flows=arguments.get("updated_flows"),
            feedback=str(arguments.get("feedback") or ""),
        )
        if not ok:
            return [TextContent(type="text", text=f"Error: session not found: {session_id}")]
        changes.extend(edit_changes)
    elif arguments.get("feedback"):
        api_store.update_for_review(session_id, None, None, feedback=str(arguments["feedback"]))

    if not changes:
        changes = ["No changes detected"]

    verbose = bool(arguments.get("verbose", False)) or has_edits
    return _build_for_review_response(api_store, session_id, changes, verbose=verbose)


# ---------------------------------------------------------------------------
# Phase 3: Save to Maeris
# ---------------------------------------------------------------------------


def _to_field_pairs(items: list) -> list[FieldPair]:
    """Convert raw header/param dicts to FieldPair list."""
    result: list[FieldPair] = []
    for item in items:
        if isinstance(item, dict):
            fn = item.get("field_name") or item.get("name") or ""
            fv = item.get("field_value") or item.get("value") or ""
            if fn:
                result.append(FieldPair(field_name=fn, field_value=str(fv)))
    return result


def _extract_env_id(result: dict) -> str:
    for key in ["environment_id", "api_environment_id", "env_id", "id", "_id"]:
        value = result.get(key)
        if value:
            return str(value)
    nested = result.get("data") or result.get("environment") or result.get("api_environment")
    if isinstance(nested, dict):
        for key in ["environment_id", "api_environment_id", "env_id", "id", "_id"]:
            value = nested.get(key)
            if value:
                return str(value)
    return ""


async def _save_for_to_maeris(
    api_store: APIStore, session_id: str, collection_name: str
) -> list[TextContent]:
    session = api_store.get_for_session(session_id)
    if not session:
        return [TextContent(type="text", text=f"Error: session not found: {session_id}")]

    apis_created: list[dict] = []
    apis_failed: list[str] = []
    envs_created: list[str] = []
    envs_failed: list[str] = []
    flows_created: list[str] = []
    flows_failed: list[str] = []

    async with MaerisClient() as client:
        # 1. Create collection
        try:
            collection_id = await client.create_collection(collection_name)
        except Exception as exc:
            return [TextContent(type="text", text=f"Error: failed to create collection '{collection_name}': {exc}")]

        if not collection_id:
            return [TextContent(type="text", text=f"Error: collection created but no ID returned for '{collection_name}'")]

        # 2. Create selected APIs and build name→id / name→def maps
        api_id_map: dict[str, str] = {}
        api_def_map: dict[str, dict] = {}
        selected_apis = [a for a in session.apis if a.get("name") not in session.excluded_api_names]
        for api_def in selected_apis:
            name = api_def.get("name") or ""
            method = api_def.get("method") or ""
            url = api_def.get("url") or api_def.get("endpoint") or ""
            if not name or not method or not url:
                apis_failed.append(f"Skipped (missing name/method/url): {api_def}")
                continue

            headers_fp = _to_field_pairs(api_def.get("headers") or [])
            params_fp = _to_field_pairs(api_def.get("params") or [])

            try:
                maeris_api = MaerisAPI(
                    name=name,
                    type=method.lower(),
                    url=url,
                    payload=api_def.get("payload") or "",
                    headers=headers_fp,
                    params=params_fp,
                    parent_collection_id=collection_id,
                )
                api_id = await client.create_api(maeris_api)
                api_id_map[name] = api_id
                api_def_map[name] = api_def
                apis_created.append({"id": api_id, "name": name, "method": method.upper(), "url": url})
            except Exception as exc:
                apis_failed.append(f"Failed to create '{name}': {exc}")

        # 3. Create selected environments with selected fields
        selected_envs = [e for e in session.environments if e.get("name") not in session.excluded_env_names]
        for env_def in selected_envs:
            env_name = env_def.get("name") or ""
            if not env_name:
                envs_failed.append(f"Skipped environment (no name): {env_def}")
                continue
            try:
                env_result = await client.create_environment(env_name)
                env_id = _extract_env_id(env_result)
                if env_id and env_def.get("fields"):
                    fields_payload = [
                        {"field_name": f.get("field_name", ""), "field_value": f.get("field_value", "")}
                        for f in env_def.get("fields", [])
                        if isinstance(f, dict) and f.get("field_name")
                        and (env_name, f.get("field_name", "")) not in session.excluded_env_fields
                    ]
                    if fields_payload:
                        await client.add_environment_fields(env_id, fields_payload)
                envs_created.append(env_name)
            except Exception as exc:
                envs_failed.append(f"Failed to create environment '{env_name}': {exc}")

        # 4. Create flows (only selected flows, with only selected steps)
        selected_flow_list = [f for f in session.flows if f.get("name") not in session.excluded_flow_names]
        for flow_def in selected_flow_list:
            flow_name = flow_def.get("name") or ""
            if not flow_name:
                flows_failed.append(f"Skipped flow (no name): {flow_def}")
                continue
            steps = flow_def.get("steps") or []
            try:
                flow_result = await client.create_api_flow(flow_name)
                flow_id = _extract_flow_id(flow_result)
                if flow_id and steps:
                    flow_map = []
                    prev_api_id: str | None = None
                    for flow_order, step in enumerate(steps):
                        step_api_name = step.get("api_name") or ""
                        step_excluded = (flow_name, step_api_name) in session.excluded_flow_steps
                        if not step_api_name or step_api_name not in api_id_map or step_excluded:
                            continue
                        step_api_id = api_id_map[step_api_name]
                        step_api_def = api_def_map.get(step_api_name, {})
                        flow_map.append({
                            "child_id": step_api_id,
                            "flow_order": flow_order,
                            "parent_id": prev_api_id,
                            "params": _build_flow_params(step_api_id, step_api_def),
                        })
                        prev_api_id = step_api_id
                    if flow_map:
                        await client.create_api_flow_map(flow_id, flow_map)
                flows_created.append(flow_name)
            except Exception as exc:
                flows_failed.append(f"Failed to create flow '{flow_name}': {exc}")

    api_store.mark_for_completed(session_id)

    summary: dict[str, Any] = {
        "sessionId": session_id,
        "status": "completed",
        "collectionName": collection_name,
        "collectionId": collection_id,
        "apisCreated": len(apis_created),
        "apisTotalScanned": len(session.apis),
        "apisFailed": len(apis_failed),
        "environmentsCreated": envs_created,
        "environmentsFailed": envs_failed,
        "flowsCreated": flows_created,
        "flowsFailed": flows_failed,
        "apis": apis_created,
    }
    if apis_failed:
        summary["apiErrors"] = apis_failed
    if envs_failed:
        summary["environmentErrors"] = envs_failed
    if flows_failed:
        summary["flowErrors"] = flows_failed

    return [TextContent(type="text", text=json.dumps(summary, indent=2))]
