#!/usr/bin/env python3
"""
API Linter - Analyzes OpenAPI/Swagger specifications for REST conventions and best practices.

This script validates API designs against established conventions including:
- Resource naming conventions (kebab-case resources, camelCase fields)
- HTTP method usage patterns
- URL structure consistency
- Error response format standards
- Documentation completeness
- Pagination patterns
- Versioning compliance

Supports both OpenAPI JSON specifications and raw endpoint definition JSON.
"""

import argparse
import json
import re
import sys
from typing import Any, Dict, List, Tuple, Optional, Set
from urllib.parse import urlparse
from dataclasses import dataclass, field


@dataclass
class LintIssue:
    """Represents a linting issue found in the API specification."""
    severity: str  # 'error', 'warning', 'info'
    category: str
    message: str
    path: str
    suggestion: str = ""
    line_number: Optional[int] = None


@dataclass
class LintReport:
    """Complete linting report with issues and statistics."""
    issues: List[LintIssue] = field(default_factory=list)
    total_endpoints: int = 0
    endpoints_with_issues: int = 0
    score: float = 0.0
    
    def add_issue(self, issue: LintIssue) -> None:
        """Add an issue to the report."""
        self.issues.append(issue)
    
    def get_issues_by_severity(self) -> Dict[str, List[LintIssue]]:
        """Group issues by severity level."""
        grouped = {'error': [], 'warning': [], 'info': []}
        for issue in self.issues:
            if issue.severity in grouped:
                grouped[issue.severity].append(issue)
        return grouped
    
    def calculate_score(self) -> float:
        """Calculate overall API quality score (0-100)."""
        if self.total_endpoints == 0:
            return 100.0
        
        error_penalty = len([i for i in self.issues if i.severity == 'error']) * 10
        warning_penalty = len([i for i in self.issues if i.severity == 'warning']) * 3
        info_penalty = len([i for i in self.issues if i.severity == 'info']) * 1
        
        total_penalty = error_penalty + warning_penalty + info_penalty
        base_score = 100.0
        
        # Penalty per endpoint to normalize across API sizes
        penalty_per_endpoint = total_penalty / self.total_endpoints if self.total_endpoints > 0 else total_penalty
        
        self.score = max(0.0, base_score - penalty_per_endpoint)
        return self.score


class APILinter:
    """Main API linting engine."""
    
    def __init__(self):
        self.report = LintReport()
        self.openapi_spec: Optional[Dict] = None
        self.raw_endpoints: Optional[Dict] = None
        
        # Regex patterns for naming conventions
        self.kebab_case_pattern = re.compile(r'^[a-z]+(?:-[a-z0-9]+)*$')
        self.camel_case_pattern = re.compile(r'^[a-z][a-zA-Z0-9]*$')
        self.snake_case_pattern = re.compile(r'^[a-z]+(?:_[a-z0-9]+)*$')
        self.pascal_case_pattern = re.compile(r'^[A-Z][a-zA-Z0-9]*$')
        
        # Standard HTTP methods
        self.http_methods = {'GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'}
        
        # Standard HTTP status codes by method
        self.standard_status_codes = {
            'GET': {200, 304, 404},
            'POST': {200, 201, 400, 409, 422},
            'PUT': {200, 204, 400, 404, 409},
            'PATCH': {200, 204, 400, 404, 409},
            'DELETE': {200, 204, 404},
            'HEAD': {200, 404},
            'OPTIONS': {200}
        }
        
        # Common error status codes
        self.common_error_codes = {400, 401, 403, 404, 405, 409, 422, 429, 500, 502, 503}

    def lint_openapi_spec(self, spec: Dict[str, Any]) -> LintReport:
        """Lint an OpenAPI/Swagger specification."""
        self.openapi_spec = spec
        self.report = LintReport()
        
        # Basic structure validation
        self._validate_openapi_structure()
        
        # Info section validation
        self._validate_info_section()
        
        # Server section validation
        self._validate_servers_section()
        
        # Paths validation (main linting logic)
        self._validate_paths_section()
        
        # Components validation
        self._validate_components_section()
        
        # Security validation
        self._validate_security_section()
        
        # Calculate final score
        self.report.calculate_score()
        
        return self.report

    def lint_raw_endpoints(self, endpoints: Dict[str, Any]) -> LintReport:
        """Lint raw endpoint definitions."""
        self.raw_endpoints = endpoints
        self.report = LintReport()
        
        # Validate raw endpoint structure
        self._validate_raw_endpoint_structure()
        
        # Lint each endpoint
        for endpoint_path, endpoint_data in endpoints.get('endpoints', {}).items():
            self._lint_raw_endpoint(endpoint_path, endpoint_data)
        
        self.report.calculate_score()
        return self.report

    def _validate_openapi_structure(self) -> None:
        """Validate basic OpenAPI document structure."""
        required_fields = ['openapi', 'info', 'paths']
        
        for field in required_fields:
            if field not in self.openapi_spec:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='structure',
                    message=f"Missing required field: {field}",
                    path=f"/{field}",
                    suggestion=f"Add the '{field}' field to the root of your OpenAPI specification"
                ))

    def _validate_info_section(self) -> None:
        """Validate the info section of OpenAPI spec."""
        if 'info' not in self.openapi_spec:
            return
            
        info = self.openapi_spec['info']
        required_info_fields = ['title', 'version']
        recommended_info_fields = ['description', 'contact']
        
        for field in required_info_fields:
            if field not in info:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='documentation',
                    message=f"Missing required info field: {field}",
                    path=f"/info/{field}",
                    suggestion=f"Add a '{field}' field to the info section"
                ))
        
        for field in recommended_info_fields:
            if field not in info:
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='documentation',
                    message=f"Missing recommended info field: {field}",
                    path=f"/info/{field}",
                    suggestion=f"Consider adding a '{field}' field to improve API documentation"
                ))
        
        # Validate version format
        if 'version' in info:
            version = info['version']
            if not re.match(r'^\d+\.\d+(\.\d+)?(-\w+)?$', version):
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='versioning',
                    message=f"Version format '{version}' doesn't follow semantic versioning",
                    path="/info/version",
                    suggestion="Use semantic versioning format (e.g., '1.0.0', '2.1.3-beta')"
                ))

    def _validate_servers_section(self) -> None:
        """Validate the servers section."""
        if 'servers' not in self.openapi_spec:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='configuration',
                message="Missing servers section",
                path="/servers",
                suggestion="Add a servers section to specify API base URLs"
            ))
            return
        
        servers = self.openapi_spec['servers']
        if not isinstance(servers, list) or len(servers) == 0:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='configuration',
                message="Empty servers section",
                path="/servers",
                suggestion="Add at least one server URL"
            ))

    def _validate_paths_section(self) -> None:
        """Validate all API paths and operations."""
        if 'paths' not in self.openapi_spec:
            return
            
        paths = self.openapi_spec['paths']
        if not paths:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message="No paths defined in API specification",
                path="/paths",
                suggestion="Define at least one API endpoint"
            ))
            return
        
        self.report.total_endpoints = sum(
            len([method for method in path_obj.keys() if method.upper() in self.http_methods])
            for path_obj in paths.values() if isinstance(path_obj, dict)
        )
        
        endpoints_with_issues = set()
        
        for path, path_obj in paths.items():
            if not isinstance(path_obj, dict):
                continue
                
            # Validate path structure
            path_issues = self._validate_path_structure(path)
            if path_issues:
                endpoints_with_issues.add(path)
            
            # Validate each operation in the path
            for method, operation in path_obj.items():
                if method.upper() not in self.http_methods:
                    continue
                    
                operation_issues = self._validate_operation(path, method.upper(), operation)
                if operation_issues:
                    endpoints_with_issues.add(path)
        
        self.report.endpoints_with_issues = len(endpoints_with_issues)

    def _validate_path_structure(self, path: str) -> bool:
        """Validate REST path structure and naming conventions."""
        has_issues = False
        
        # Check if path starts with slash
        if not path.startswith('/'):
            self.report.add_issue(LintIssue(
                severity='error',
                category='url_structure',
                message=f"Path must start with '/' character: {path}",
                path=f"/paths/{path}",
                suggestion=f"Change '{path}' to '/{path.lstrip('/')}'"
            ))
            has_issues = True
        
        # Split path into segments
        segments = [seg for seg in path.split('/') if seg]
        
        # Check for empty segments (double slashes)
        if '//' in path:
            self.report.add_issue(LintIssue(
                severity='error',
                category='url_structure',
                message=f"Path contains empty segments: {path}",
                path=f"/paths/{path}",
                suggestion="Remove double slashes from the path"
            ))
            has_issues = True
        
        # Validate each segment
        for i, segment in enumerate(segments):
            # Skip parameter segments
            if segment.startswith('{') and segment.endswith('}'):
                # Validate parameter naming
                param_name = segment[1:-1]
                if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Path parameter '{param_name}' should use camelCase or kebab-case",
                        path=f"/paths/{path}",
                        suggestion=f"Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')"
                    ))
                    has_issues = True
                continue
            
            # Check for resource naming conventions
            if not self.kebab_case_pattern.match(segment):
                # Allow version segments like 'v1', 'v2'
                if not re.match(r'^v\d+$', segment):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Resource segment '{segment}' should use kebab-case",
                        path=f"/paths/{path}",
                        suggestion=f"Use kebab-case for '{segment}' (e.g., 'user-profiles', 'order-items')"
                    ))
                    has_issues = True
            
            # Check for verb usage in URLs (anti-pattern)
            common_verbs = {'get', 'post', 'put', 'delete', 'create', 'update', 'remove', 'add'}
            if segment.lower() in common_verbs:
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='rest_conventions',
                    message=f"Avoid verbs in URLs: '{segment}' in {path}",
                    path=f"/paths/{path}",
                    suggestion="Use HTTP methods instead of verbs in URLs. Use nouns for resources."
                ))
                has_issues = True
        
        # Check path depth (avoid over-nesting)
        if len(segments) > 6:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='url_structure',
                message=f"Path has excessive nesting ({len(segments)} levels): {path}",
                path=f"/paths/{path}",
                suggestion="Consider flattening the resource hierarchy or using query parameters"
            ))
            has_issues = True
        
        # Check for consistent versioning
        if any('v' + str(i) in segments for i in range(1, 10)):
            version_segments = [seg for seg in segments if re.match(r'^v\d+$', seg)]
            if len(version_segments) > 1:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='versioning',
                    message=f"Multiple version segments in path: {path}",
                    path=f"/paths/{path}",
                    suggestion="Use only one version segment per path"
                ))
                has_issues = True
        
        return has_issues

    def _validate_operation(self, path: str, method: str, operation: Dict[str, Any]) -> bool:
        """Validate individual operation (HTTP method + path combination)."""
        has_issues = False
        operation_path = f"/paths/{path}/{method.lower()}"
        
        # Check for required operation fields
        if 'responses' not in operation:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message=f"Missing responses section for {method} {path}",
                path=f"{operation_path}/responses",
                suggestion="Define expected responses for this operation"
            ))
            has_issues = True
        
        # Check for operation documentation
        if 'summary' not in operation:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='documentation',
                message=f"Missing summary for {method} {path}",
                path=f"{operation_path}/summary",
                suggestion="Add a brief summary describing what this operation does"
            ))
            has_issues = True
        
        if 'description' not in operation:
            self.report.add_issue(LintIssue(
                severity='info',
                category='documentation',
                message=f"Missing description for {method} {path}",
                path=f"{operation_path}/description",
                suggestion="Add a detailed description for better API documentation"
            ))
            has_issues = True
        
        # Validate HTTP method usage patterns
        method_issues = self._validate_http_method_usage(path, method, operation)
        if method_issues:
            has_issues = True
        
        # Validate responses
        if 'responses' in operation:
            response_issues = self._validate_responses(path, method, operation['responses'])
            if response_issues:
                has_issues = True
        
        # Validate parameters
        if 'parameters' in operation:
            param_issues = self._validate_parameters(path, method, operation['parameters'])
            if param_issues:
                has_issues = True
        
        # Validate request body
        if 'requestBody' in operation:
            body_issues = self._validate_request_body(path, method, operation['requestBody'])
            if body_issues:
                has_issues = True
        
        return has_issues

    def _validate_http_method_usage(self, path: str, method: str, operation: Dict[str, Any]) -> bool:
        """Validate proper HTTP method usage patterns."""
        has_issues = False
        
        # GET requests should not have request body
        if method == 'GET' and 'requestBody' in operation:
            self.report.add_issue(LintIssue(
                severity='error',
                category='rest_conventions',
                message=f"GET request should not have request body: {method} {path}",
                path=f"/paths/{path}/{method.lower()}/requestBody",
                suggestion="Remove requestBody from GET request or use POST if body is needed"
            ))
            has_issues = True
        
        # DELETE requests typically should not have request body
        if method == 'DELETE' and 'requestBody' in operation:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='rest_conventions',
                message=f"DELETE request typically should not have request body: {method} {path}",
                path=f"/paths/{path}/{method.lower()}/requestBody",
                suggestion="Consider using query parameters or path parameters instead"
            ))
            has_issues = True
        
        # POST/PUT/PATCH should typically have request body (except for actions)
        if method in ['POST', 'PUT', 'PATCH'] and 'requestBody' not in operation:
            # Check if this is an action endpoint
            if not any(action in path.lower() for action in ['activate', 'deactivate', 'reset', 'confirm']):
                self.report.add_issue(LintIssue(
                    severity='info',
                    category='rest_conventions',
                    message=f"{method} request typically should have request body: {method} {path}",
                    path=f"/paths/{path}/{method.lower()}",
                    suggestion=f"Consider adding requestBody for {method} operation or use GET if no data is being sent"
                ))
                has_issues = True
        
        return has_issues

    def _validate_responses(self, path: str, method: str, responses: Dict[str, Any]) -> bool:
        """Validate response definitions."""
        has_issues = False
        
        # Check for success response
        success_codes = {'200', '201', '202', '204'}
        has_success = any(code in responses for code in success_codes)
        
        if not has_success:
            self.report.add_issue(LintIssue(
                severity='error',
                category='responses',
                message=f"Missing success response for {method} {path}",
                path=f"/paths/{path}/{method.lower()}/responses",
                suggestion="Define at least one success response (200, 201, 202, or 204)"
            ))
            has_issues = True
        
        # Check for error responses
        has_error_responses = any(code.startswith('4') or code.startswith('5') for code in responses.keys())
        
        if not has_error_responses:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='responses',
                message=f"Missing error responses for {method} {path}",
                path=f"/paths/{path}/{method.lower()}/responses",
                suggestion="Define common error responses (400, 404, 500, etc.)"
            ))
            has_issues = True
        
        # Validate individual response codes
        for status_code, response in responses.items():
            if status_code == 'default':
                continue
                
            try:
                code_int = int(status_code)
            except ValueError:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='responses',
                    message=f"Invalid status code '{status_code}' for {method} {path}",
                    path=f"/paths/{path}/{method.lower()}/responses/{status_code}",
                    suggestion="Use valid HTTP status codes (e.g., 200, 404, 500)"
                ))
                has_issues = True
                continue
            
            # Check if status code is appropriate for the method
            expected_codes = self.standard_status_codes.get(method, set())
            common_codes = {400, 401, 403, 404, 429, 500}  # Always acceptable
            
            if expected_codes and code_int not in expected_codes and code_int not in common_codes:
                self.report.add_issue(LintIssue(
                    severity='info',
                    category='responses',
                    message=f"Uncommon status code {status_code} for {method} {path}",
                    path=f"/paths/{path}/{method.lower()}/responses/{status_code}",
                    suggestion=f"Consider using standard codes for {method}: {sorted(expected_codes)}"
                ))
                has_issues = True
        
        return has_issues

    def _validate_parameters(self, path: str, method: str, parameters: List[Dict[str, Any]]) -> bool:
        """Validate parameter definitions."""
        has_issues = False
        
        for i, param in enumerate(parameters):
            param_path = f"/paths/{path}/{method.lower()}/parameters[{i}]"
            
            # Check required fields
            if 'name' not in param:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='parameters',
                    message=f"Parameter missing name field in {method} {path}",
                    path=f"{param_path}/name",
                    suggestion="Add a name field to the parameter"
                ))
                has_issues = True
                continue
            
            if 'in' not in param:
                self.report.add_issue(LintIssue(
                    severity='error',
                    category='parameters',
                    message=f"Parameter '{param['name']}' missing 'in' field in {method} {path}",
                    path=f"{param_path}/in",
                    suggestion="Specify parameter location (query, path, header, cookie)"
                ))
                has_issues = True
            
            # Validate parameter naming
            param_name = param['name']
            param_location = param.get('in', '')
            
            if param_location == 'query':
                # Query parameters should use camelCase or kebab-case
                if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Query parameter '{param_name}' should use camelCase or kebab-case in {method} {path}",
                        path=f"{param_path}/name",
                        suggestion="Use camelCase (e.g., 'pageSize') or kebab-case (e.g., 'page-size')"
                    ))
                    has_issues = True
            
            elif param_location == 'path':
                # Path parameters should use camelCase or kebab-case
                if not self.camel_case_pattern.match(param_name) and not self.kebab_case_pattern.match(param_name):
                    self.report.add_issue(LintIssue(
                        severity='warning',
                        category='naming',
                        message=f"Path parameter '{param_name}' should use camelCase or kebab-case in {method} {path}",
                        path=f"{param_path}/name",
                        suggestion="Use camelCase (e.g., 'userId') or kebab-case (e.g., 'user-id')"
                    ))
                    has_issues = True
                
                # Path parameters must be required
                if not param.get('required', False):
                    self.report.add_issue(LintIssue(
                        severity='error',
                        category='parameters',
                        message=f"Path parameter '{param_name}' must be required in {method} {path}",
                        path=f"{param_path}/required",
                        suggestion="Set required: true for path parameters"
                    ))
                    has_issues = True
        
        return has_issues

    def _validate_request_body(self, path: str, method: str, request_body: Dict[str, Any]) -> bool:
        """Validate request body definition."""
        has_issues = False
        
        if 'content' not in request_body:
            self.report.add_issue(LintIssue(
                severity='error',
                category='request_body',
                message=f"Request body missing content for {method} {path}",
                path=f"/paths/{path}/{method.lower()}/requestBody/content",
                suggestion="Define content types for the request body"
            ))
            has_issues = True
        
        return has_issues

    def _validate_components_section(self) -> None:
        """Validate the components section."""
        if 'components' not in self.openapi_spec:
            self.report.add_issue(LintIssue(
                severity='info',
                category='structure',
                message="Missing components section",
                path="/components",
                suggestion="Consider defining reusable components (schemas, responses, parameters)"
            ))
            return
        
        components = self.openapi_spec['components']
        
        # Validate schemas
        if 'schemas' in components:
            self._validate_schemas(components['schemas'])

    def _validate_schemas(self, schemas: Dict[str, Any]) -> None:
        """Validate schema definitions."""
        for schema_name, schema in schemas.items():
            # Check schema naming (should be PascalCase)
            if not self.pascal_case_pattern.match(schema_name):
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='naming',
                    message=f"Schema name '{schema_name}' should use PascalCase",
                    path=f"/components/schemas/{schema_name}",
                    suggestion=f"Use PascalCase for schema names (e.g., 'UserProfile', 'OrderItem')"
                ))
            
            # Validate schema properties
            if isinstance(schema, dict) and 'properties' in schema:
                self._validate_schema_properties(schema_name, schema['properties'])

    def _validate_schema_properties(self, schema_name: str, properties: Dict[str, Any]) -> None:
        """Validate schema property naming."""
        for prop_name, prop_def in properties.items():
            # Properties should use camelCase
            if not self.camel_case_pattern.match(prop_name):
                self.report.add_issue(LintIssue(
                    severity='warning',
                    category='naming',
                    message=f"Property '{prop_name}' in schema '{schema_name}' should use camelCase",
                    path=f"/components/schemas/{schema_name}/properties/{prop_name}",
                    suggestion="Use camelCase for property names (e.g., 'firstName', 'createdAt')"
                ))

    def _validate_security_section(self) -> None:
        """Validate security definitions."""
        if 'security' not in self.openapi_spec and 'components' not in self.openapi_spec:
            self.report.add_issue(LintIssue(
                severity='warning',
                category='security',
                message="No security configuration found",
                path="/security",
                suggestion="Define security schemes and apply them to operations"
            ))

    def _validate_raw_endpoint_structure(self) -> None:
        """Validate structure of raw endpoint definitions."""
        if 'endpoints' not in self.raw_endpoints:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message="Missing 'endpoints' field in raw endpoint definition",
                path="/endpoints",
                suggestion="Provide an 'endpoints' object containing endpoint definitions"
            ))
            return
        
        endpoints = self.raw_endpoints['endpoints']
        self.report.total_endpoints = len(endpoints)

    def _lint_raw_endpoint(self, path: str, endpoint_data: Dict[str, Any]) -> None:
        """Lint individual raw endpoint definition."""
        # Validate path structure
        self._validate_path_structure(path)
        
        # Check for required fields
        if 'method' not in endpoint_data:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message=f"Missing method field for endpoint {path}",
                path=f"/endpoints/{path}/method",
                suggestion="Specify HTTP method (GET, POST, PUT, PATCH, DELETE)"
            ))
            return
        
        method = endpoint_data['method'].upper()
        if method not in self.http_methods:
            self.report.add_issue(LintIssue(
                severity='error',
                category='structure',
                message=f"Invalid HTTP method '{method}' for endpoint {path}",
                path=f"/endpoints/{path}/method",
                suggestion=f"Use valid HTTP methods: {', '.join(sorted(self.http_methods))}"
            ))

    def generate_json_report(self) -> str:
        """Generate JSON format report."""
        issues_by_severity = self.report.get_issues_by_severity()
        
        report_data = {
            "summary": {
                "total_endpoints": self.report.total_endpoints,
                "endpoints_with_issues": self.report.endpoints_with_issues,
                "total_issues": len(self.report.issues),
                "errors": len(issues_by_severity['error']),
                "warnings": len(issues_by_severity['warning']),
                "info": len(issues_by_severity['info']),
                "score": round(self.report.score, 2)
            },
            "issues": []
        }
        
        for issue in self.report.issues:
            report_data["issues"].append({
                "severity": issue.severity,
                "category": issue.category,
                "message": issue.message,
                "path": issue.path,
                "suggestion": issue.suggestion
            })
        
        return json.dumps(report_data, indent=2)

    def generate_text_report(self) -> str:
        """Generate human-readable text report."""
        issues_by_severity = self.report.get_issues_by_severity()
        
        report_lines = [
            "═══════════════════════════════════════════════════════════════",
            "                      API LINTING REPORT",
            "═══════════════════════════════════════════════════════════════",
            "",
            "SUMMARY:",
            f"  Total Endpoints: {self.report.total_endpoints}",
            f"  Endpoints with Issues: {self.report.endpoints_with_issues}",
            f"  Overall Score: {self.report.score:.1f}/100.0",
            "",
            "ISSUE BREAKDOWN:",
            f"  🔴 Errors: {len(issues_by_severity['error'])}",
            f"  🟡 Warnings: {len(issues_by_severity['warning'])}",
            f"  ℹ️  Info: {len(issues_by_severity['info'])}",
            "",
        ]
        
        if not self.report.issues:
            report_lines.extend([
                "🎉 Congratulations! No issues found in your API specification.",
                ""
            ])
        else:
            # Group issues by category
            issues_by_category = {}
            for issue in self.report.issues:
                if issue.category not in issues_by_category:
                    issues_by_category[issue.category] = []
                issues_by_category[issue.category].append(issue)
            
            for category, issues in issues_by_category.items():
                report_lines.append(f"{'═' * 60}")
                report_lines.append(f"CATEGORY: {category.upper().replace('_', ' ')}")
                report_lines.append(f"{'═' * 60}")
                
                for issue in issues:
                    severity_icon = {"error": "🔴", "warning": "🟡", "info": "ℹ️"}[issue.severity]
                    
                    report_lines.extend([
                        f"{severity_icon} {issue.severity.upper()}: {issue.message}",
                        f"   Path: {issue.path}",
                    ])
                    
                    if issue.suggestion:
                        report_lines.append(f"   💡 Suggestion: {issue.suggestion}")
                    
                    report_lines.append("")
        
        # Add scoring breakdown
        report_lines.extend([
            "═══════════════════════════════════════════════════════════════",
            "SCORING DETAILS:",
            "═══════════════════════════════════════════════════════════════",
            f"Base Score: 100.0",
            f"Errors Penalty: -{len(issues_by_severity['error']) * 10} (10 points per error)",
            f"Warnings Penalty: -{len(issues_by_severity['warning']) * 3} (3 points per warning)",
            f"Info Penalty: -{len(issues_by_severity['info']) * 1} (1 point per info)",
            f"Final Score: {self.report.score:.1f}/100.0",
            ""
        ])
        
        # Add recommendations based on score
        if self.report.score >= 90:
            report_lines.append("🏆 Excellent! Your API design follows best practices.")
        elif self.report.score >= 80:
            report_lines.append("✅ Good API design with minor areas for improvement.")
        elif self.report.score >= 70:
            report_lines.append("⚠️  Fair API design. Consider addressing warnings and errors.")
        elif self.report.score >= 50:
            report_lines.append("❌ Poor API design. Multiple issues need attention.")
        else:
            report_lines.append("🚨 Critical API design issues. Immediate attention required.")
        
        return "\n".join(report_lines)


def main():
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        description="Analyze OpenAPI/Swagger specifications for REST conventions and best practices",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  python api_linter.py openapi.json
  python api_linter.py --format json openapi.json > report.json
  python api_linter.py --raw-endpoints endpoints.json
        """
    )
    
    parser.add_argument(
        'input_file',
        help='Input file: OpenAPI/Swagger JSON file or raw endpoints JSON'
    )
    
    parser.add_argument(
        '--format',
        choices=['text', 'json'],
        default='text',
        help='Output format (default: text)'
    )
    
    parser.add_argument(
        '--raw-endpoints',
        action='store_true',
        help='Treat input as raw endpoint definitions instead of OpenAPI spec'
    )
    
    parser.add_argument(
        '--output',
        help='Output file (default: stdout)'
    )
    
    args = parser.parse_args()
    
    # Load input file
    try:
        with open(args.input_file, 'r') as f:
            input_data = json.load(f)
    except FileNotFoundError:
        print(f"Error: Input file '{args.input_file}' not found.", file=sys.stderr)
        return 1
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON in '{args.input_file}': {e}", file=sys.stderr)
        return 1
    
    # Initialize linter and run analysis
    linter = APILinter()
    
    try:
        if args.raw_endpoints:
            report = linter.lint_raw_endpoints(input_data)
        else:
            report = linter.lint_openapi_spec(input_data)
    except Exception as e:
        print(f"Error during linting: {e}", file=sys.stderr)
        return 1
    
    # Generate report
    if args.format == 'json':
        output = linter.generate_json_report()
    else:
        output = linter.generate_text_report()
    
    # Write output
    if args.output:
        try:
            with open(args.output, 'w') as f:
                f.write(output)
            print(f"Report written to {args.output}")
        except IOError as e:
            print(f"Error writing to '{args.output}': {e}", file=sys.stderr)
            return 1
    else:
        print(output)
    
    # Return appropriate exit code
    error_count = len([i for i in report.issues if i.severity == 'error'])
    return 1 if error_count > 0 else 0


if __name__ == '__main__':
    sys.exit(main())