"""
Phase 101: OpenAPI Bridge — Native integration with APIs.guru

This module allows Brynq to dynamically ingest OpenAPI 3.0/Swagger specifications
from APIs.guru (or any generic URL), automatically translate their endpoints into
OpenAI-compatible tool schemas, and register them directly into the live Swarm
using dynamic_tools.py.
"""

import json
import logging
import urllib.error
import urllib.request
from typing import Dict, Any, Optional

from brynq.dynamic_tools import register_tool

logger = logging.getLogger("brynq.openapi_bridge")


def fetch_apis_guru_directory() -> dict:
    """Fetch the full list of available APIs from APIs.guru."""
    url = "https://api.apis.guru/v2/list.json"
    req = urllib.request.Request(url, headers={
        "Accept": "application/json",
        "User-Agent": "Brynq-Agent/1.0 (https://brynq.ai)"
    })
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        logger.error(f"Failed to fetch APIs.guru directory: {e}")
        return {}


def fetch_openapi_spec(spec_url: str) -> dict:
    """Download and parse an OpenAPI specification from a URL."""
    req = urllib.request.Request(spec_url, headers={
        "Accept": "application/json",
        "User-Agent": "Brynq-Agent/1.0 (https://brynq.ai)"
    })
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        logger.error(f"Failed to fetch OpenAPI spec from {spec_url}: {e}")
        return {}


def _create_handler_for_endpoint(
    base_url: str,
    path: str,
    method: str,
    operation_id: str
) -> callable:
    """Create a generic HTTP dispatcher function for an OpenAPI endpoint."""
    def _handler(args: dict) -> dict:
        import urllib.request
        import json
        
        # Replace path parameters (e.g. /users/{id} -> /users/123)
        actual_path = path
        for key, val in args.items():
            actual_path = actual_path.replace(f"{{{key}}}", urllib.request.quote(str(val)))
            
        url = f"{base_url.rstrip('/')}{actual_path}"
        
        # Build query params
        query_params = {k: v for k, v in args.items() if f"{{{k}}}" not in path and method.upper() == "GET"}
        if query_params:
            qs = "&".join(f"{k}={urllib.request.quote(str(v))}" for k, v in query_params.items())
            url = f"{url}?{qs}"
            
        # Build body for POST/PUT
        body = None
        if method.upper() in ("POST", "PUT", "PATCH"):
            body_dict = {k: v for k, v in args.items() if f"{{{k}}}" not in path}
            if body_dict:
                body = json.dumps(body_dict).encode("utf-8")
                
        req = urllib.request.Request(
            url, 
            data=body, 
            method=method.upper(),
            headers={"Content-Type": "application/json"} if body else {}
        )
        
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                data = resp.read().decode("utf-8")
                try:
                    return json.loads(data)
                except json.JSONDecodeError:
                    return {"text_response": data}
        except Exception as e:
            return {"error": str(e)}

    return _handler


def ingest_openapi_spec(name_prefix: str, spec: dict) -> int:
    """
    Parse an OpenAPI spec and register its endpoints as Brynq tools.
    Returns the number of tools successfully registered.
    """
    registered_count = 0
    servers = spec.get("servers", [])
    base_url = servers[0].get("url", "") if servers else ""
    
    if not base_url:
        logger.warning(f"No base URL found in OpenAPI spec for {name_prefix}")
        return 0

    paths = spec.get("paths", {})
    for path, methods in paths.items():
        for method, details in methods.items():
            if method.lower() not in ("get", "post", "put", "delete", "patch"):
                continue
                
            operation_id = details.get("operationId")
            if not operation_id:
                # Fallback if operationId is missing
                clean_path = path.replace("/", "_").replace("{", "").replace("}", "")
                operation_id = f"{method}_{clean_path}".strip("_")
                
            tool_name = f"{name_prefix}_{operation_id}".replace("-", "_").lower()
            description = details.get("summary", details.get("description", f"Execute {method.upper()} {path}"))
            
            # Very basic parameter extraction (could be expanded significantly)
            parameters = {
                "type": "object",
                "properties": {},
                "required": []
            }
            
            for param in details.get("parameters", []):
                param_name = param.get("name")
                if not param_name:
                    continue
                parameters["properties"][param_name] = {
                    "type": "string", # default fallback
                    "description": param.get("description", "")
                }
                if param.get("required"):
                    parameters["required"].append(param_name)
            
            # Register it into the live Brynq Swarm
            handler = _create_handler_for_endpoint(base_url, path, method, operation_id)
            try:
                register_tool(
                    name=tool_name,
                    description=description[:512], # Keep descriptions tight for LLMs
                    handler_fn=handler,
                    parameters=parameters,
                    category=f"api_{name_prefix}"
                )
                registered_count += 1
            except Exception as e:
                logger.error(f"Failed to register tool {tool_name}: {e}")
                
    return registered_count


def _handle_ingest_apis_guru(args: dict) -> dict:
    """MCP Tool Handler to pull an API directly from APIs.guru."""
    api_name = args.get("api_name")
    if not api_name:
        return {"error": "api_name is required"}
        
    directory = fetch_apis_guru_directory()
    if api_name not in directory:
        return {"error": f"API '{api_name}' not found in APIs.guru directory."}
        
    versions = directory[api_name].get("versions", {})
    if not versions:
        return {"error": "No versions found for this API."}
        
    # Get the preferred version or just the first one
    preferred = directory[api_name].get("preferred")
    version_data = versions.get(preferred) if preferred else list(versions.values())[0]
    
    swagger_url = version_data.get("swaggerUrl")
    if not swagger_url:
        return {"error": "No swagger/OpenAPI URL available."}
        
    spec = fetch_openapi_spec(swagger_url)
    if not spec:
        return {"error": "Failed to download or parse the OpenAPI spec."}
        
    prefix = api_name.split(".")[0].replace("-", "_")
    count = ingest_openapi_spec(prefix, spec)
    
    return {"status": "success", "tools_registered": count, "prefix": prefix}

# Automatically register the ingestion tool itself when this module is loaded
register_tool(
    name="broker_ingest_openapi",
    description="Search APIs.guru and dynamically ingest an entire REST API as Brynq tools.",
    handler_fn=_handle_ingest_apis_guru,
    parameters={
        "type": "object",
        "properties": {
            "api_name": {
                "type": "string",
                "description": "The exact name of the API on APIs.guru (e.g., 'stripe.com', 'github.com', 'xkcd.com')."
            }
        },
        "required": ["api_name"]
    },
    category="system"
)
