"""OpenAPI/Swagger specification parser.

Parses OpenAPI 3.0+/3.1 and Swagger 2.0 specs from:
  - JSON or YAML files (local path)
  - JSON or YAML URLs (remote fetch)
  - Raw JSON/YAML strings

Extracts operations, parameters, request bodies, auth schemes,
and organises them into ApiSpec/ApiOperation models ready for
MCP tool generation.
"""

from __future__ import annotations

import json
import logging
from pathlib import Path
from typing import Any
from urllib.parse import urljoin

import yaml

from mcp_bridge.openapi.models import (
    ApiKeyLocation,
    ApiOperation,
    ApiParameter,
    ApiSpec,
    AuthScheme,
    AuthSchemeType,
    ParameterLocation,
    RequestBody,
)

logger = logging.getLogger(__name__)


class OpenAPIParser:
    """Parse OpenAPI/Swagger specs into ApiSpec models."""

    def __init__(self) -> None:
        self._raw: dict[str, Any] = {}

    # ------------------------------------------------------------------
    # Public entry points
    # ------------------------------------------------------------------

    async def parse_url(self, url: str) -> ApiSpec:
        """Fetch and parse a spec from a URL.

        Supports both JSON and YAML content types.
        """
        import httpx

        async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
            resp = await client.get(url)
            resp.raise_for_status()

            content_type = resp.headers.get("content-type", "")
            text = resp.text

            if "yaml" in content_type or "yml" in content_type or url.endswith((".yaml", ".yml")):
                self._raw = yaml.safe_load(text)
            else:
                self._raw = json.loads(text)

        logger.info("Parsed spec from URL: %s", url)
        return self._build_spec()

    def parse_file(self, path: str | Path) -> ApiSpec:
        """Parse a spec from a local file."""
        file_path = Path(path)
        text = file_path.read_text(encoding="utf-8")

        if file_path.suffix in (".yaml", ".yml"):
            self._raw = yaml.safe_load(text)
        else:
            self._raw = json.loads(text)

        logger.info("Parsed spec from file: %s", file_path)
        return self._build_spec()

    def parse_string(self, content: str) -> ApiSpec:
        """Parse a spec from a raw JSON or YAML string."""
        try:
            self._raw = json.loads(content)
        except json.JSONDecodeError:
            self._raw = yaml.safe_load(content)

        logger.info("Parsed spec from string (%d chars)", len(content))
        return self._build_spec()

    # ------------------------------------------------------------------
    # Spec builder
    # ------------------------------------------------------------------

    def _build_spec(self) -> ApiSpec:
        """Build an ApiSpec from the parsed raw dict."""
        is_swagger_2 = self._raw.get("swagger", "").startswith("2")
        is_openapi_3 = self._raw.get("openapi", "").startswith("3")

        if not is_swagger_2 and not is_openapi_3:
            raise ValueError(
                "Unsupported spec format. Expected OpenAPI 3.x or Swagger 2.0. "
                f"Found: swagger={self._raw.get('swagger')}, openapi={self._raw.get('openapi')}"
            )

        # Extract metadata
        info = self._raw.get("info", {})
        title = info.get("title", "Untitled API")
        description = info.get("description", "")
        version = info.get("version", "")

        # Extract base URL
        base_url = self._extract_base_url(is_swagger_2)

        # Extract auth schemes
        auth_schemes = self._extract_auth_schemes(is_swagger_2)

        # Extract operations
        operations = self._extract_operations(is_swagger_2, auth_schemes)

        logger.info(
            "Spec parsed: title=%r, operations=%d, auth_schemes=%d",
            title,
            len(operations),
            len(auth_schemes),
        )

        return ApiSpec(
            title=title,
            description=description,
            version=version,
            base_url=base_url,
            operations=operations,
            auth_schemes=auth_schemes,
        )

    # ------------------------------------------------------------------
    # Base URL extraction
    # ------------------------------------------------------------------

    def _extract_base_url(self, is_swagger_2: bool) -> str:
        """Extract the base URL from the spec."""
        if is_swagger_2:
            host = self._raw.get("host", "")
            base_path = self._raw.get("basePath", "/")
            schemes = self._raw.get("schemes", ["https"])
            scheme = schemes[0] if schemes else "https"
            if host:
                return f"{scheme}://{host}{base_path}".rstrip("/")
            return base_path.rstrip("/")

        # OpenAPI 3.x
        servers = self._raw.get("servers", [])
        if servers:
            return servers[0].get("url", "").rstrip("/")
        return ""

    # ------------------------------------------------------------------
    # Auth scheme extraction
    # ------------------------------------------------------------------

    def _extract_auth_schemes(self, is_swagger_2: bool) -> list[AuthScheme]:
        """Extract security schemes from the spec."""
        schemes: list[AuthScheme] = []

        if is_swagger_2:
            raw_defs = self._raw.get("securityDefinitions", {})
        else:
            components = self._raw.get("components", {})
            raw_defs = components.get("securitySchemes", {})

        for name, defn in raw_defs.items():
            scheme_type = defn.get("type", "").lower()

            if scheme_type == "apikey":
                schemes.append(
                    AuthScheme(
                        name=name,
                        type=AuthSchemeType.API_KEY,
                        api_key_name=defn.get("name", ""),
                        api_key_location=ApiKeyLocation(defn.get("in", "header")),
                    )
                )
            elif scheme_type == "http":
                http_scheme = defn.get("scheme", "").lower()
                if http_scheme == "bearer":
                    schemes.append(
                        AuthScheme(
                            name=name,
                            type=AuthSchemeType.BEARER,
                            bearer_format=defn.get("bearerFormat"),
                        )
                    )
                elif http_scheme == "basic":
                    schemes.append(AuthScheme(name=name, type=AuthSchemeType.BASIC))
            elif scheme_type == "oauth2":
                schemes.append(
                    AuthScheme(
                        name=name,
                        type=AuthSchemeType.OAUTH2,
                        oauth2_flows=defn.get("flows", defn.get("flow")),
                    )
                )
            elif scheme_type == "basic":  # Swagger 2.0
                schemes.append(AuthScheme(name=name, type=AuthSchemeType.BASIC))

        return schemes

    # ------------------------------------------------------------------
    # Operation extraction
    # ------------------------------------------------------------------

    def _extract_operations(
        self,
        is_swagger_2: bool,
        auth_schemes: list[AuthScheme],
    ) -> list[ApiOperation]:
        """Extract all operations from the spec's paths."""
        operations: list[ApiOperation] = []
        paths = self._raw.get("paths", {})
        http_methods = {"get", "post", "put", "patch", "delete", "head", "options"}
        op_counter = 0

        for path, path_item in paths.items():
            if not isinstance(path_item, dict):
                continue

            # Path-level parameters (shared by all methods)
            path_level_params = self._parse_parameters(path_item.get("parameters", []))

            for method in http_methods:
                if method not in path_item:
                    continue

                op = path_item[method]
                if not isinstance(op, dict):
                    continue

                op_counter += 1

                # Operation ID
                operation_id = op.get("operationId", f"{method}_{path.strip('/').replace('/', '_')}_{op_counter}")
                # Clean operation_id for use as tool name
                operation_id = operation_id.replace("-", "_").replace(".", "_")

                # Parameters (merge path-level + operation-level)
                op_params = self._parse_parameters(op.get("parameters", []))
                all_params = {p.name: p for p in path_level_params}
                for p in op_params:
                    all_params[p.name] = p  # operation-level overrides path-level

                # Request body
                request_body = None
                if is_swagger_2:
                    # Swagger 2.0: body params in parameters list
                    body_params = [p for p in op.get("parameters", []) if p.get("in") == "body"]
                    if body_params:
                        request_body = self._parse_swagger2_body(body_params[0])
                        # Remove body param from the regular params
                        all_params.pop("body", None)
                else:
                    raw_body = op.get("requestBody")
                    if raw_body:
                        request_body = self._parse_openapi3_body(raw_body)

                # Security requirements
                security_reqs = op.get("security", self._raw.get("security", []))
                security_names = []
                for req in security_reqs:
                    if isinstance(req, dict):
                        security_names.extend(req.keys())

                # Response description
                responses = op.get("responses", {})
                resp_desc = ""
                for code in ["200", "201", "202", "204"]:
                    if code in responses:
                        resp_desc = responses[code].get("description", "")
                        break

                operations.append(
                    ApiOperation(
                        operation_id=operation_id,
                        method=method,
                        path=path,
                        summary=op.get("summary", ""),
                        description=op.get("description", ""),
                        tags=op.get("tags", []),
                        parameters=list(all_params.values()),
                        request_body=request_body,
                        response_description=resp_desc,
                        deprecated=op.get("deprecated", False),
                        security=security_names,
                    )
                )

        return operations

    # ------------------------------------------------------------------
    # Parameter parsing
    # ------------------------------------------------------------------

    def _parse_parameters(self, raw_params: list[dict[str, Any]]) -> list[ApiParameter]:
        """Parse parameters from the spec."""
        params: list[ApiParameter] = []

        for raw in raw_params:
            location = raw.get("in", "query")
            if location == "body":
                continue  # Handled separately for Swagger 2.0

            # Resolve $ref if present
            if "$ref" in raw:
                raw = self._resolve_ref(raw["$ref"])

            schema = raw.get("schema", raw)
            schema_type = schema.get("type", "string")

            params.append(
                ApiParameter(
                    name=raw.get("name", ""),
                    location=ParameterLocation(location),
                    description=raw.get("description", ""),
                    required=raw.get("required", location == "path"),
                    schema_type=schema_type,
                    default=schema.get("default"),
                    enum_values=schema.get("enum"),
                    example=raw.get("example", schema.get("example")),
                )
            )

        return params

    # ------------------------------------------------------------------
    # Request body parsing
    # ------------------------------------------------------------------

    def _parse_swagger2_body(self, body_param: dict[str, Any]) -> RequestBody:
        """Parse Swagger 2.0 body parameter."""
        schema = body_param.get("schema", {})
        return self._schema_to_request_body(
            schema=schema,
            required=body_param.get("required", False),
            description=body_param.get("description", ""),
        )

    def _parse_openapi3_body(self, raw_body: dict[str, Any]) -> RequestBody:
        """Parse OpenAPI 3.x requestBody."""
        # Resolve $ref
        if "$ref" in raw_body:
            raw_body = self._resolve_ref(raw_body["$ref"])

        content = raw_body.get("content", {})
        required = raw_body.get("required", False)
        description = raw_body.get("description", "")

        # Prefer JSON, fall back to any content type
        for ct in ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"]:
            if ct in content:
                schema = content[ct].get("schema", {})
                return self._schema_to_request_body(
                    schema=schema,
                    required=required,
                    description=description,
                    content_type=ct,
                )

        # Fallback to first content type
        if content:
            ct, ct_obj = next(iter(content.items()))
            schema = ct_obj.get("schema", {})
            return self._schema_to_request_body(
                schema=schema,
                required=required,
                description=description,
                content_type=ct,
            )

        return RequestBody(required=required, description=description)

    def _schema_to_request_body(
        self,
        schema: dict[str, Any],
        required: bool = False,
        description: str = "",
        content_type: str = "application/json",
    ) -> RequestBody:
        """Convert a JSON Schema object into a RequestBody model."""
        # Resolve $ref
        if "$ref" in schema:
            schema = self._resolve_ref(schema["$ref"])

        properties: dict[str, dict[str, Any]] = {}
        required_props: list[str] = schema.get("required", [])

        for prop_name, prop_schema in schema.get("properties", {}).items():
            # Resolve nested $ref
            if "$ref" in prop_schema:
                prop_schema = self._resolve_ref(prop_schema["$ref"])

            properties[prop_name] = {
                "type": prop_schema.get("type", "string"),
                "description": prop_schema.get("description", ""),
                "enum": prop_schema.get("enum"),
                "example": prop_schema.get("example"),
            }

        return RequestBody(
            content_type=content_type,
            required=required,
            schema_properties=properties,
            required_properties=required_props,
            description=description,
            example=schema.get("example"),
        )

    # ------------------------------------------------------------------
    # $ref resolver
    # ------------------------------------------------------------------

    def _resolve_ref(self, ref: str) -> dict[str, Any]:
        """Resolve a JSON $ref pointer within the spec."""
        if not ref.startswith("#/"):
            logger.warning("External $ref not supported: %s", ref)
            return {}

        parts = ref[2:].split("/")
        current: Any = self._raw

        for part in parts:
            # Handle URL-encoded characters
            part = part.replace("~1", "/").replace("~0", "~")
            if isinstance(current, dict):
                current = current.get(part, {})
            else:
                return {}

        return current if isinstance(current, dict) else {}
