"""
OCP Schema Discovery

Provides automatic API discovery and tool generation from various API specification
formats (OpenAPI, Postman, GraphQL, etc.), enabling context-aware API interactions 
with zero infrastructure requirements.
"""

import json
import requests
import logging
import yaml
from typing import Dict, List, Any, Optional
from pathlib import Path

from .errors import SchemaDiscoveryError
from .parsers import ParserRegistry
from .parsers.base import OCPTool, OCPAPISpec

logger = logging.getLogger(__name__)

# Configuration constants
DEFAULT_SPEC_TIMEOUT = 30

# Re-export for backward compatibility
__all__ = ['OCPSchemaDiscovery', 'OCPTool', 'OCPAPISpec']

class OCPSchemaDiscovery:
    """
    Automatic API discovery and tool generation from various API specification formats.
    
    This enables automatic API discovery while maintaining OCP's zero-infrastructure
    approach by parsing API specifications directly.
    
    Supported formats:
    - OpenAPI 2.0 (Swagger)
    - OpenAPI 3.0.x, 3.1.x, 3.2.x
    - Future: Postman Collections, GraphQL schemas, Google Discovery format
    """
    
    def __init__(self, parser_registry: Optional[ParserRegistry] = None):
        """
        Initialize schema discovery.
        
        Args:
            parser_registry: Optional custom parser registry. If None, uses default
                           registry with built-in parsers.
        """
        self.cached_specs: Dict[str, OCPAPISpec] = {}
        self.parser_registry = parser_registry or ParserRegistry()
    
    def discover_api(self, spec_path: str, base_url: Optional[str] = None, include_resources: Optional[List[str]] = None, path_prefix: Optional[str] = None) -> OCPAPISpec:
        """
        Discover API capabilities from various API specification formats.
        
        Args:
            spec_path: URL or file path to API specification (JSON or YAML)
            base_url: Optional override for API base URL
            include_resources: Optional list of resource names to filter tools by (case-insensitive, first resource segment matching)
            path_prefix: Optional path prefix to strip before filtering (e.g., '/v1', '/api/v2')
            
        Returns:
            OCPAPISpec with discovered tools and capabilities
            
        Raises:
            SchemaDiscoveryError: If the specification format is unsupported or parsing fails
        """
        # Normalize cache key (absolute path for files, URL as-is)
        cache_key = self._normalize_cache_key(spec_path)
        
        # Check cache first
        if cache_key in self.cached_specs:
            cached_spec = self.cached_specs[cache_key]
            
            # If filters are specified, apply them to cached spec
            if include_resources:
                # Get the appropriate parser to apply filtering
                parser = self.parser_registry.find_parser(cached_spec.raw_spec)
                if parser and hasattr(parser, '_filter_tools_by_resources'):
                    filtered_tools = parser._filter_tools_by_resources(
                        cached_spec.tools, include_resources, path_prefix
                    )
                    return OCPAPISpec(
                        base_url=cached_spec.base_url,
                        title=cached_spec.title,
                        version=cached_spec.version,
                        description=cached_spec.description,
                        tools=filtered_tools,
                        raw_spec=cached_spec.raw_spec
                    )
            
            return cached_spec

        try:
            # Fetch specification data
            spec_data = self._fetch_spec(spec_path)
            
            # Find appropriate parser
            parser = self.parser_registry.find_parser(spec_data)
            if not parser:
                supported_formats = self.parser_registry.get_supported_formats()
                raise SchemaDiscoveryError(
                    f"Unsupported API specification format. "
                    f"Supported formats: {', '.join(supported_formats)}"
                )
            
            # Parse using the appropriate parser
            parsed_spec = parser.parse(spec_data, base_url, include_resources, path_prefix)
            
            # Cache for future use (cache the unfiltered version)
            if not include_resources:
                self.cached_specs[cache_key] = parsed_spec
            else:
                # Cache the original without filters
                unfiltered_spec = parser.parse(spec_data, base_url, None, None)
                self.cached_specs[cache_key] = unfiltered_spec
            
            return parsed_spec
            
        except SchemaDiscoveryError:
            raise
        except Exception as e:
            raise SchemaDiscoveryError(f"Failed to discover API: {e}")
    
    def _normalize_cache_key(self, spec_path: str) -> str:
        """Normalize cache key: URLs as-is, file paths to absolute."""
        if spec_path.startswith(('http://', 'https://')):
            return spec_path
        return str(Path(spec_path).expanduser().resolve())
    
    def _fetch_spec(self, spec_path: str) -> Dict[str, Any]:
        """Fetch OpenAPI spec from URL or local file."""
        if spec_path.startswith(('http://', 'https://')):
            return self._fetch_from_url(spec_path)
        else:
            return self._fetch_from_file(spec_path)
    
    def _fetch_from_url(self, url: str) -> Dict[str, Any]:
        """Fetch OpenAPI specification from URL"""
        try:
            response = requests.get(url, timeout=DEFAULT_SPEC_TIMEOUT)
            response.raise_for_status()
            return response.json()
        except Exception as e:
            raise SchemaDiscoveryError(f"Failed to fetch OpenAPI spec from {url}: {e}")
    
    def _fetch_from_file(self, file_path: str) -> Dict[str, Any]:
        """Load OpenAPI specification from local JSON or YAML file."""
        try:
            # Expand ~ and resolve path
            path = Path(file_path).expanduser().resolve()
            
            # Check file exists
            if not path.exists():
                raise SchemaDiscoveryError(f"File not found: {file_path}")
            
            # Validate extension
            ext = path.suffix.lower()
            if ext not in ['.json', '.yaml', '.yml']:
                raise SchemaDiscoveryError(
                    f"Unsupported file format: {ext}. Supported formats: .json, .yaml, .yml"
                )
            
            # Read and parse based on format
            with open(path, 'r', encoding='utf-8') as f:
                if ext == '.json':
                    return json.load(f)
                else:  # .yaml or .yml
                    return yaml.safe_load(f)
                    
        except yaml.YAMLError as e:
            raise SchemaDiscoveryError(f"Invalid YAML in file {file_path}: {e}")
        except json.JSONDecodeError as e:
            raise SchemaDiscoveryError(f"Invalid JSON in file {file_path}: {e}")
        except SchemaDiscoveryError:
            raise
        except Exception as e:
            raise SchemaDiscoveryError(f"Failed to load spec from {file_path}: {e}")
    
    def get_tools_by_tag(self, api_spec: OCPAPISpec, tag: str) -> List[OCPTool]:
        """Get tools filtered by tag"""
        return [tool for tool in api_spec.tools if tag in (tool.tags or [])]
    
    def search_tools(self, api_spec: OCPAPISpec, query: str) -> List[OCPTool]:
        """Search tools by name or description"""
        query_lower = query.lower()
        matches = []
        
        for tool in api_spec.tools:
            if (query_lower in tool.name.lower() or 
                query_lower in tool.description.lower()):
                matches.append(tool)
        
        return matches
    
    def generate_tool_documentation(self, tool: OCPTool) -> str:
        """Generate human-readable documentation for a tool"""
        doc_lines = [
            f"## {tool.name}",
            f"**Method:** {tool.method}",
            f"**Path:** {tool.path}",
            f"**Description:** {tool.description}",
            ""
        ]
        
        if tool.parameters:
            doc_lines.append("### Parameters:")
            for param_name, param_info in tool.parameters.items():
                required = " (required)" if param_info.get('required') else " (optional)"
                location = f" [{param_info.get('location', 'query')}]"
                doc_lines.append(f"- **{param_name}**{required}{location}: {param_info.get('description', '')}")
            doc_lines.append("")
        
        if tool.tags:
            doc_lines.append(f"**Tags:** {', '.join(tool.tags)}")
            doc_lines.append("")
        
        return "\n".join(doc_lines)
    
    def clear_cache(self):
        """Clear cached API specifications"""
        self.cached_specs.clear()
    
    def get_supported_formats(self) -> List[str]:
        """
        Get list of supported API specification formats.
        
        Returns:
            List of format names (e.g., ['OpenAPI', 'Postman Collection'])
        """
        return self.parser_registry.get_supported_formats()
    
    def register_parser(self, parser) -> None:
        """
        Register a custom parser for additional API specification formats.
        
        Args:
            parser: An instance of APISpecParser
            
        Example:
            from ocp_agent.parsers.base import APISpecParser
            
            class MyCustomParser(APISpecParser):
                def can_parse(self, spec_data):
                    return 'myformat' in spec_data
                    
                def parse(self, spec_data, base_url_override=None, **kwargs):
                    # Parse logic here
                    pass
                    
                def get_format_name(self):
                    return "My Custom Format"
            
            discovery = OCPSchemaDiscovery()
            discovery.register_parser(MyCustomParser())
        """
        self.parser_registry.register(parser)