"""LLM-guided API extraction scanning tool for MCP server."""

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

from mcp.types import TextContent, Tool

from maeris_mcp.maeris.types import FieldPair
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.process_data import _parse_extracted_api
from maeris_mcp.types.api import ExtractedAPI


def api_scan_tool() -> Tool:
    """Return the tool definition for api_scan."""
    return Tool(
        name="api_scan",
        description=(
            "LLM-guided API extraction scan. "
            "Fetches file batches and accepts extracted APIs in the same call. "
            "Pass apis=[...] alongside offset to submit APIs from the previous batch while fetching the next. "
            "Set is_last=true on the final call to finalize the scan and receive the full API report. "
            "Use the returned apis list with add_apis_bulk to add them to an existing Maeris collection."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "target_path": {
                    "type": "string",
                    "description": "Absolute path to scan (optional, defaults to server working directory)",
                },
                "scan_id": {
                    "type": "string",
                    "description": "Existing session ID to continue/paginate",
                },
                "project_name": {
                    "type": "string",
                    "description": "Name for the API collection (e.g. 'MyApp'). Used on the first call.",
                },
                "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: 5",
                    "default": 5,
                },
                "apis": {
                    "type": "array",
                    "items": {"type": "object"},
                    "description": "APIs extracted from the previous file batch. Submit alongside the next batch offset.",
                },
                "http_clients": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "HTTP client libraries detected (e.g. ['axios', 'fetch'])",
                },
                "is_last": {
                    "type": "boolean",
                    "description": "Set true on the final call to finalize the scan and get the full API report.",
                },
            },
        },
    )


def _extract_environment_from_apis(apis: list[ExtractedAPI]) -> list[FieldPair]:
    """Extract environment variables from a list of APIs.

    Scans APIs for base URLs, common auth config, and env-var references
    to build a Postman-style environment.
    """
    fields: list[FieldPair] = []
    seen_keys: set[str] = set()

    def _add_field(name: str, value: str) -> None:
        if name not in seen_keys and value:
            seen_keys.add(name)
            fields.append(FieldPair(field_name=name, field_value=value))

    for api in apis:
        if api.base_url:
            _add_field("base_url", api.base_url)

    for api in apis:
        if api.auth:
            if api.auth.token_source:
                _add_field("auth_token_source", api.auth.token_source)
            if api.auth.header_name:
                _add_field("auth_header", api.auth.header_name)

    for api in apis:
        for header in api.headers or []:
            if header.source and header.source.lower() in ("env", "environment"):
                _add_field(header.name, header.value or "")

    return fields


def _analysis_instructions() -> str:
    return (
        "LLM-guided API extraction instructions:\n"
        "1) Call api_scan with include_content=true and offset=0 to get the first file batch.\n"
        "2) Analyze each file for HTTP API calls: endpoints, methods, headers, params, auth, payloads.\n"
        "3) Call api_scan again with apis=[...found], http_clients=[...] and the next offset.\n"
        "4) Repeat until hasMore=false, then call with apis=[...] and is_last=true to finalize.\n"
        "5) The final response includes the full API report, extracted apis list, and extractedEnvironment fields.\n"
        "6) Call add_apis_bulk with the returned apis and set auto_environment=true to automatically:\n"
        "   - Check for an existing environment matching the project name\n"
        "   - Create one if it doesn't exist\n"
        "   - Populate it with the extractedEnvironment fields (base_url, auth config, etc.)\n"
        "   - Parameterize all API URLs as {{base_url}}/path\n"
        "   - Select the environment\n"
        "   Pass extracted_environment from the scan response and project_name.\n"
        "Note: MCP does not perform detection; the LLM must analyze and produce API findings."
    )


def _expected_output_schema() -> dict[str, Any]:
    return {
        "type": "object",
        "properties": {
            "name": {"type": "string", "description": "Descriptive name for the API (e.g. 'Create Label')"},
            "endpoint": {
                "type": "string",
                "description": (
                    "The API endpoint path (e.g. '/labels'). "
                    "For proxy/route handlers, use the REAL backend path, not the proxy path."
                ),
            },
            "baseUrl": {
                "type": "string",
                "description": (
                    "The base URL of the backend service (e.g. 'https://api.example.com' or "
                    "'process.env.API_URL'). IMPORTANT: always include this when the code "
                    "constructs URLs from a base + path."
                ),
            },
            "method": {
                "type": "string",
                "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
            },
            "headers": {
                "type": "array",
                "description": (
                    "IMPORTANT: Always include headers found in the code. "
                    "Look for Content-Type, Authorization, Accept, and custom headers "
                    "in fetch/axios options, shared HTTP client configs, and middleware. "
                    "Even if only Content-Type is found, include it."
                ),
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string", "description": "Header name (e.g. 'Content-Type')"},
                        "value": {"type": "string", "description": "Header value (e.g. 'application/json')"},
                        "source": {"type": "string"},
                        "isRequired": {"type": "boolean"},
                    },
                },
            },
            "queryParams": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "type": {"type": "string"},
                        "required": {"type": "boolean"},
                        "description": {"type": "string"},
                    },
                },
            },
            "pathParams": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "type": {"type": "string"},
                        "description": {"type": "string"},
                    },
                },
            },
            "requestPayload": {
                "type": "object",
                "description": (
                    "The request body structure. IMPORTANT: for POST/PUT/PATCH methods, "
                    "always extract the payload fields from the code."
                ),
                "properties": {
                    "contentType": {"type": "string", "description": "e.g. 'application/json'"},
                    "typeName": {"type": "string"},
                    "isArray": {"type": "boolean"},
                    "fields": {
                        "type": "array",
                        "description": "Fields in the request body with name, type, required, description",
                    },
                },
            },
            "responsePayload": {
                "type": "object",
                "properties": {
                    "contentType": {"type": "string"},
                    "typeName": {"type": "string"},
                    "isArray": {"type": "boolean"},
                    "fields": {"type": "array"},
                },
            },
            "auth": {
                "type": "object",
                "properties": {
                    "type": {
                        "type": "string",
                        "enum": ["none", "bearer", "basic", "api_key", "oauth", "custom"],
                    },
                    "headerName": {"type": "string"},
                    "tokenSource": {"type": "string"},
                },
            },
            "callSites": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "filePath": {"type": "string"},
                        "componentName": {"type": "string"},
                        "functionName": {"type": "string"},
                        "lineNumber": {"type": "integer"},
                    },
                },
            },
            "description": {"type": "string"},
        },
        "required": ["name", "endpoint", "method"],
    }


def _normalize_api(raw: dict[str, Any]):
    """Normalize a raw API dict using the shared parser from process_data."""
    return _parse_extracted_api(raw)


def _build_api_report(session, collection) -> str:
    """Build a human-readable summary for a finalized API scan."""
    summary = collection.summary
    by_method = summary.by_method or {}
    http_clients = sorted(session.http_clients) if session.http_clients else []

    method_parts = "  ".join(f"{m}: {c}" for m, c in sorted(by_method.items()))
    lines = [
        "API Extraction Complete",
        f"Project: {session.project_name}  Target: {session.target_path}",
        f"Files analyzed: {len(session.file_list)}  Total APIs found: {summary.total_apis}",
    ]
    if method_parts:
        lines.append(f"By method: {method_parts}")
    if http_clients:
        lines.append(f"HTTP clients: {', '.join(http_clients)}")
    return "\n".join(lines)


async def handle_api_scan(api_store: APIStore, arguments: dict) -> list[TextContent]:
    """Handle the api_scan tool call."""
    target_path = arguments.get("target_path") or os.getcwd()
    scan_id = arguments.get("scan_id") or arguments.get("scanId")
    include_content = arguments.get("include_content", False)
    try:
        offset = int(arguments.get("offset", 0))
        limit = max(1, min(int(arguments.get("limit", 5)), 10))
    except (ValueError, TypeError):
        return [TextContent(type="text", text="Error: offset and limit must be integers")]
    project_name = arguments.get("project_name") or arguments.get("projectName") or "UnnamedProject"
    raw_apis = arguments.get("apis") or []
    if isinstance(raw_apis, dict):
        raw_apis = [raw_apis]
    raw_http_clients = arguments.get("http_clients") or arguments.get("httpClients") or []
    is_last = bool(arguments.get("is_last") or arguments.get("isLast"))

    # --- Resolve or create session ---
    if scan_id:
        session = api_store.get_session(scan_id)
        if not session:
            return [TextContent(type="text", text=f"Error: scan session not found: {scan_id}")]
    else:
        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, info = collect_scan_files(scan_root, api_signals_only=True)
        file_list = [os.path.relpath(p, base_for_rel) for p in files_abs]

        scan_id = api_store.create_scan_session(
            project_name=project_name,
            target_path=scan_root,
            scan_root=scan_root,
            base_for_rel=base_for_rel,
            file_list=file_list,
        )
        session = api_store.get_session(scan_id)
        if not session:
            return [TextContent(type="text", text="Error: failed to initialize scan session")]

    # --- 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_invalid = len(raw_apis) - len(dict_apis)
        if dropped_invalid:
            warnings.append(f"{dropped_invalid} API item(s) skipped: not valid objects.")
        normalized = [_normalize_api(r) for r in dict_apis]
        valid_apis = [a for a in normalized if a.name.strip() and a.endpoint.strip()]
        dropped_empty = len(normalized) - len(valid_apis)
        if dropped_empty:
            warnings.append(f"{dropped_empty} API item(s) skipped: missing name or endpoint.")
        apis_stats = api_store.add_apis(scan_id, valid_apis)

    # --- Store submitted HTTP clients ---
    if raw_http_clients and isinstance(raw_http_clients, list):
        valid_clients = [c for c in raw_http_clients if isinstance(c, str) and c.strip()]
        if valid_clients:
            api_store.add_http_clients(scan_id, valid_clients)

    base_response: dict[str, Any] = {
        "scanId": scan_id,
        "projectName": session.project_name,
        "targetPath": session.target_path,
        "filesTotal": len(session.file_list),
        "status": session.status,
        "analysisInstructions": _analysis_instructions(),
        "expectedOutputSchema": _expected_output_schema(),
    }

    if apis_stats:
        base_response["apisSubmitted"] = apis_stats
    if warnings:
        base_response["warnings"] = warnings

    # --- Finalize ---
    if is_last:
        total_apis = len(session.apis)
        if total_apis == 0:
            base_response["warnings"] = base_response.get("warnings", []) + [
                "Finalizing with 0 APIs. No APIs were submitted during this scan."
            ]
        result = api_store.finalize_session(scan_id)
        if not result:
            return [TextContent(type="text", text=f"Error: could not finalize session: {scan_id}")]
        storage_id, collection = result
        base_response["status"] = "completed"
        base_response["storageId"] = storage_id
        base_response["summary"] = collection.summary.model_dump(by_alias=True)
        base_response["apis"] = [a.model_dump(by_alias=True) for a in collection.apis]
        base_response["apiReport"] = _build_api_report(session, collection)

        env_fields = _extract_environment_from_apis(collection.apis)
        if env_fields:
            base_response["extractedEnvironment"] = [
                {"field_name": f.field_name, "field_value": f.field_value} for f in env_fields
            ]

        base_response["nextStep"] = (
            "Scan finalized. Use the returned apis list with add_apis_bulk to add them to a collection. "
            "Set auto_environment=true and pass extracted_environment and project_name to automatically "
            "set up an environment, parameterize API URLs as {{base_url}}/path, and select the environment."
        )
        return [TextContent(type="text", text=json.dumps(base_response, indent=2))]

    # --- Paginate file contents ---
    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. Verify the path contains supported source files."
            ]
            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,
        }
        base_response["fileContents"] = {
            rel: contents.get(abs_path, "")
            for rel, abs_path in zip(paginated_rels, abs_paths)
        }
    else:
        base_response["nextStep"] = (
            "Call again with include_content=true, offset=0 to fetch file contents. "
            "Pass apis=[...] alongside subsequent offsets to submit extracted APIs as you go. "
            "Set is_last=true on the final call to finalize."
        )

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