"""
Schema-Payload Validator

Validates that frontend API payloads match backend Pydantic request schemas.
"""

import re
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from pydantic import BaseModel
from dataclasses import dataclass


@dataclass
class SchemaField:
    """Represents a field in a Pydantic schema"""
    name: str
    type: str
    required: bool
    default: Optional[str] = None


@dataclass
class SchemaInfo:
    """Represents a Pydantic schema"""
    name: str
    file: str
    fields: Dict[str, SchemaField]


@dataclass
class PayloadField:
    """Represents a field in a frontend payload"""
    name: str
    type: str  # inferred from TypeScript/JavaScript
    optional: bool


@dataclass
class PayloadInfo:
    """Represents a frontend payload"""
    endpoint: str
    method: str
    file: str
    line: int
    fields: Dict[str, PayloadField]


class ValidationIssue(BaseModel):
    """Represents a validation issue"""
    endpoint: str
    method: str
    issue_type: str  # "extra_field", "missing_required_field", "missing_interface", "orphan_interface"
    field_name: str
    expected: str
    actual: str
    frontend_file: str
    frontend_line: int


class SchemaPayloadValidator:
    """Validates frontend payloads against backend schemas"""
    
    def __init__(self, project_dir: str):
        self.project_dir = Path(project_dir)
        self.backend_api_dir = self.project_dir / "backend" / "app" / "api"
        self.backend_schemas_dir = self.project_dir / "backend" / "app" / "schemas"
        self.frontend_src_dir = self.project_dir / "frontend" / "src"
    
    def extract_backend_schemas(self) -> Dict[str, SchemaInfo]:
        """Extract all Pydantic schemas from backend"""
        schemas = {}
        
        if not self.backend_schemas_dir.exists():
            return schemas
        
        for schema_file in self.backend_schemas_dir.glob("*.py"):
            if schema_file.name == "__init__.py":
                continue
            
            file_schemas = self._extract_schema_fields(schema_file)
            for schema in file_schemas:
                schemas[schema.name] = schema
        
        return schemas
    
    def _extract_schema_fields(self, schema_file: Path) -> List[SchemaInfo]:
        """Extract schema fields from a schema file"""
        schemas = []
        
        try:
            with open(schema_file, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Find all class definitions that inherit from BaseModel
            class_pattern = r'class\s+(\w+)\s*\(.*?BaseModel.*?\):'
            
            for class_match in re.finditer(class_pattern, content):
                schema_name = class_match.group(1)
                class_start = class_match.start()
                class_end = class_match.end()
                
                # Extract fields from this class
                lines = content[class_end:].split('\n')
                class_content = []
                indent_level = None
                
                for line in lines:
                    if line.strip():
                        line_indent = len(line) - len(line.lstrip())
                        if indent_level is None:
                            indent_level = line_indent
                        if line_indent == indent_level:
                            class_content.append(line)
                        elif line_indent < indent_level:
                            break
                    else:
                        class_content.append(line)
                
                class_content_str = '\n'.join(class_content)
                fields = self._parse_schema_fields(class_content_str)
                
                schemas.append(SchemaInfo(
                    name=schema_name,
                    file=str(schema_file.relative_to(self.project_dir)),
                    fields=fields
                ))
            
            return schemas
            
        except Exception as e:
            print(f"Error analyzing {schema_file}: {e}")
            return []
    
    def _parse_schema_fields(self, class_content: str) -> Dict[str, SchemaField]:
        """Parse Pydantic schema fields from class content"""
        fields = {}

        for line in class_content.split('\n'):
            line = line.strip()
            if not line or line.startswith('#') or line.startswith('@'):
                continue

            # Skip class definitions and methods
            if line.startswith('class ') or line.startswith('def '):
                continue

            # Simple pattern: field_name: type
            # Handle: field_name: type = Field(...) or field_name: type = value
            parts = line.split('=', 1)
            if len(parts) == 2:
                # Has default value
                declaration = parts[0].strip()
                default_value = parts[1].strip()
            else:
                # No default value
                declaration = line.strip()
                default_value = None

            # Extract field name and type from declaration
            if ':' in declaration:
                field_name, field_type = declaration.split(':', 1)
                field_name = field_name.strip()
                field_type = field_type.strip()

                # Skip if field_name contains parentheses (method call)
                if '(' in field_name or ')' in field_name:
                    continue

                # Check if required (no default value or default is None or ...)
                required = default_value is None or default_value in ["None", "Ellipsis", "..."]

                fields[field_name] = SchemaField(
                    name=field_name,
                    type=field_type,
                    required=required,
                    default=default_value
                )

        return fields
    
    def extract_endpoint_schemas(self) -> Dict[str, Dict[str, str]]:
        """Extract request schemas for each endpoint"""
        endpoint_schemas = {}
        
        if not self.backend_api_dir.exists():
            return endpoint_schemas
        
        for api_file in self.backend_api_dir.glob("*.py"):
            if api_file.name == "__init__.py":
                continue
            
            file_schemas = self._extract_endpoint_schemas(api_file)
            endpoint_schemas.update(file_schemas)
        
        return endpoint_schemas
    
    def _extract_endpoint_schemas(self, api_file: Path) -> Dict[str, Dict[str, str]]:
        """Extract request schemas from an API file using naming conventions"""
        endpoint_schemas = {}

        try:
            with open(api_file, 'r', encoding='utf-8') as f:
                content = f.read()

            # Extract router prefix to determine resource name
            prefix_pattern = r'router\s*=\s*APIRouter\(.*?prefix\s*=\s*["\']([^"\']+)["\']'
            prefix_match = re.search(prefix_pattern, content)
            prefix = prefix_match.group(1) if prefix_match else ""

            # Extract resource name from prefix (e.g., /api/streams → streams)
            resource_name = prefix.split('/')[-1] if prefix else ""

            # Find endpoint definitions
            endpoint_pattern = r'@router\.(get|post|put|delete|patch)\s*\(\s*["\']([^"\']*)["\'][^)]*\)'

            for endpoint_match in re.finditer(endpoint_pattern, content):
                method = endpoint_match.group(1).upper()
                path = endpoint_match.group(2)
                full_path = f"{prefix}{path}" if prefix else path

                # Infer schema name based on method and resource
                # POST → {Resource}Create
                # PUT → {Resource}Update
                # PATCH → {Resource}Update
                # GET/DELETE → No request schema
                if method == 'POST':
                    schema_name = f"{resource_name.capitalize()}Create" if resource_name else None
                elif method in ['PUT', 'PATCH']:
                    schema_name = f"{resource_name.capitalize()}Update" if resource_name else None
                else:
                    schema_name = None

                if schema_name:
                    endpoint_key = f"{method}:{full_path}"
                    endpoint_schemas[endpoint_key] = {
                        "method": method,
                        "path": full_path,
                        "schema": schema_name,
                        "file": str(api_file.relative_to(self.project_dir))
                    }

            return endpoint_schemas

        except Exception as e:
            print(f"Error analyzing {api_file}: {e}")
            return {}
    
    def extract_frontend_payloads(self) -> List[PayloadInfo]:
        """Extract frontend payloads from API calls"""
        payloads = []
        
        if not self.frontend_src_dir.exists():
            return payloads
        
        for frontend_file in self.frontend_src_dir.rglob("*.ts"):
            if frontend_file.name.endswith(".d.ts"):
                continue
            
            file_payloads = self._extract_payloads(frontend_file)
            payloads.extend(file_payloads)
        
        return payloads
    
    def extract_typescript_interfaces(self) -> Dict[str, Dict[str, PayloadField]]:
        """Extract TypeScript interfaces from frontend"""
        interfaces = {}

        if not self.frontend_src_dir.exists():
            return interfaces

        for frontend_file in self.frontend_src_dir.rglob("*.ts"):
            if frontend_file.name.endswith(".d.ts"):
                continue

            file_interfaces = self._extract_typescript_interfaces(frontend_file)
            interfaces.update(file_interfaces)

        return interfaces

    def _extract_typescript_interfaces(self, frontend_file: Path) -> Dict[str, Dict[str, PayloadField]]:
        """Extract TypeScript interfaces from a frontend file"""
        interfaces = {}

        try:
            with open(frontend_file, 'r', encoding='utf-8') as f:
                content = f.read()

            # Find interface definitions
            # Pattern: export interface InterfaceName { ... }
            # Using a simple approach: find interface declaration, then balance braces
            interface_decl_pattern = r'export\s+interface\s+(\w+)\s*\{'
            
            for match in re.finditer(interface_decl_pattern, content):
                interface_name = match.group(1)
                start_pos = match.end() - 1  # Position of opening brace
                
                # Extract body by balancing braces
                brace_count = 1
                end_pos = start_pos + 1
                while brace_count > 0 and end_pos < len(content):
                    if content[end_pos] == '{':
                        brace_count += 1
                    elif content[end_pos] == '}':
                        brace_count -= 1
                    end_pos += 1
                
                if brace_count == 0:
                    interface_body = content[start_pos + 1:end_pos - 1]
                    fields = self._parse_interface_fields(interface_body)
                    if fields:  # Only add if we found valid fields
                        interfaces[interface_name] = fields

            return interfaces

        except Exception as e:
            print(f"Error analyzing {frontend_file}: {e}")
            return {}

    def _parse_interface_fields(self, interface_body: str) -> Dict[str, PayloadField]:
        """Parse fields from TypeScript interface body"""
        fields = {}

        for line in interface_body.split('\n'):
            line = line.strip()
            if not line or line.startswith('//') or line.startswith('*'):
                continue

            # Pattern: fieldName: type or fieldName?: type (optional)
            field_pattern = r'(\w+)(\?)?:\s+([^\n,;]+)'
            match = re.match(field_pattern, line)

            if match:
                field_name = match.group(1)
                is_optional = match.group(2) == '?'
                field_type = match.group(3).strip()

                fields[field_name] = PayloadField(
                    name=field_name,
                    type=field_type,
                    optional=is_optional
                )

        return fields
    
    def _extract_payloads(self, frontend_file: Path) -> List[PayloadInfo]:
        """Extract payloads from a frontend file"""
        payloads = []
        
        try:
            with open(frontend_file, 'r', encoding='utf-8') as f:
                content = f.read()
            
            # Find API calls with payloads
            # Pattern: api.post("/path", { ... }) or api.put("/path", { ... })
            payload_pattern = r'(?:axios|api)\.(post|put|patch)\s*\(\s*["\']([^"\']+)["\'][^,]*,\s*({[^}]+})'
            
            for match in re.finditer(payload_pattern, content, re.DOTALL):
                method = match.group(1).upper()
                path = match.group(2)
                payload_str = match.group(3)
                line_num = content[:match.start()].count('\n') + 1
                
                # Parse payload fields
                fields = self._parse_payload_fields(payload_str)
                
                payloads.append(PayloadInfo(
                    endpoint=path,
                    method=method,
                    file=str(frontend_file.relative_to(self.project_dir)),
                    line=line_num,
                    fields=fields
                ))
            
            return payloads
            
        except Exception as e:
            print(f"Error analyzing {frontend_file}: {e}")
            return []
    
    def _parse_payload_fields(self, payload_str: str) -> Dict[str, PayloadField]:
        """Parse payload fields from a JavaScript object string"""
        fields = {}
        
        # Simple parsing - extract field names
        # Pattern: "fieldName": value or fieldName: value
        field_pattern = r'["\']?(\w+)["\']?\s*:'
        
        for match in re.finditer(field_pattern, payload_str):
            field_name = match.group(1)
            fields[field_name] = PayloadField(
                name=field_name,
                type="unknown",  # Would need TypeScript parsing
                optional=False
            )
        
        return fields
    
    def validate_payloads(self) -> List[ValidationIssue]:
        """Validate all frontend payloads against backend schemas"""
        issues = []

        # Extract backend schemas and endpoint mappings
        schemas = self.extract_backend_schemas()
        endpoint_schemas = self.extract_endpoint_schemas()

        # Extract TypeScript interfaces
        interfaces = self.extract_typescript_interfaces()

        # Compare schemas with matching interfaces
        for schema_name, schema in schemas.items():
            # Check if there's a corresponding TypeScript interface
            if schema_name not in interfaces:
                # No TypeScript interface found - this might be OK if no frontend usage
                # Check if this schema is used in any endpoint
                used_in_endpoints = [k for k, v in endpoint_schemas.items() if v["schema"] == schema_name]
                if used_in_endpoints:
                    # Schema is used but no interface exists
                    for endpoint_key in used_in_endpoints:
                        endpoint_info = endpoint_schemas[endpoint_key]
                        issues.append(ValidationIssue(
                            endpoint=endpoint_info["path"],
                            method=endpoint_info["method"],
                            issue_type="missing_interface",
                            field_name="",
                            expected=f"interface {schema_name}",
                            actual="not found in frontend",
                            frontend_file="",
                            frontend_line=0
                        ))
                continue

            interface = interfaces[schema_name]

            # Compare interface fields with schema fields
            for field_name, interface_field in interface.items():
                if field_name not in schema.fields:
                    # Extra field in interface
                    issues.append(ValidationIssue(
                        endpoint="",
                        method="",
                        issue_type="extra_field",
                        field_name=field_name,
                        expected="",
                        actual=f"present in {schema_name} interface",
                        frontend_file="",
                        frontend_line=0
                    ))

            for field_name, schema_field in schema.fields.items():
                if schema_field.required and field_name not in interface:
                    # Missing required field in interface
                    issues.append(ValidationIssue(
                        endpoint="",
                        method="",
                        issue_type="missing_required_field",
                        field_name=field_name,
                        expected=schema_field.type,
                        actual=f"missing from {schema_name} interface",
                        frontend_file="",
                        frontend_line=0
                    ))

        # Also check the reverse: fields in interfaces that should be in schemas
        # This catches cases where frontend has fields that backend doesn't support
        for interface_name, interface in interfaces.items():
            if interface_name in schemas:
                schema = schemas[interface_name]
                for field_name in interface.keys():
                    if field_name not in schema.fields:
                        # Field in interface but not in schema - this is a problem
                        # Frontend expects this field but backend won't provide it
                        issues.append(ValidationIssue(
                            endpoint="",
                            method="",
                            issue_type="missing_in_backend",
                            field_name=field_name,
                            expected=f"field in {interface_name} schema",
                            actual=f"field missing from backend schema",
                            frontend_file="",
                            frontend_line=0
                        ))

        # Check for orphan interfaces (interfaces without backend schemas)
        for interface_name, interface in interfaces.items():
            if interface_name not in schemas:
                # Interface exists but no corresponding backend schema
                # Report all interfaces - they should all have corresponding schemas (request or response)
                issues.append(ValidationIssue(
                    endpoint="",
                    method="",
                    issue_type="orphan_interface",
                    field_name="",
                    expected=f"schema {interface_name}",
                    actual="not found in backend",
                    frontend_file="",
                    frontend_line=0
                ))

        return issues

    def _find_interface_usage(self) -> Dict[str, Dict]:
        """Find which interfaces are used for which endpoints"""
        usage = {}

        if not self.frontend_src_dir.exists():
            return usage

        for frontend_file in self.frontend_src_dir.rglob("*.ts"):
            if frontend_file.name.endswith(".d.ts"):
                continue

            file_usage = self._extract_interface_usage(frontend_file)
            usage.update(file_usage)

        return usage

    def _extract_interface_usage(self, frontend_file: Path) -> Dict[str, Dict]:
        """Extract interface usage from a frontend file"""
        usage = {}

        try:
            with open(frontend_file, 'r', encoding='utf-8') as f:
                content = f.read()

            # Find API calls with interface variables
            # Pattern: api.post("/path", variable: InterfaceName) or api.put("/path", variable: InterfaceName)
            pattern = r'(?:axios|api)\.(post|put|patch)\s*\(\s*["\']([^"\']+)["\'][^,]*,\s*(\w+)\s*:\s*(\w+)'

            for match in re.finditer(pattern, content):
                method = match.group(1).upper()
                path = match.group(2)
                variable_name = match.group(3)
                interface_name = match.group(4)
                line_num = content[:match.start()].count('\n') + 1

                endpoint_key = f"{method}:{path}"
                usage[endpoint_key] = {
                    "interface": interface_name,
                    "variable": variable_name,
                    "file": str(frontend_file.relative_to(self.project_dir)),
                    "line": line_num
                }

            return usage

        except Exception as e:
            print(f"Error analyzing {frontend_file}: {e}")
            return {}


if __name__ == "__main__":
    validator = SchemaPayloadValidator(".")

    print("=" * 60)
    print("Schema-Payload Validation Report")
    print("=" * 60)

    # Debug: Show what was extracted
    schemas = validator.extract_backend_schemas()
    print(f"\nBackend Schemas found: {len(schemas)}")
    for schema_name in list(schemas.keys())[:5]:
        print(f"  - {schema_name}: {len(schemas[schema_name].fields)} fields")

    endpoint_schemas = validator.extract_endpoint_schemas()
    print(f"\nEndpoint-Schema mappings found: {len(endpoint_schemas)}")
    for endpoint_key in list(endpoint_schemas.keys())[:5]:
        print(f"  - {endpoint_key}: {endpoint_schemas[endpoint_key]['schema']}")

    interfaces = validator.extract_typescript_interfaces()
    print(f"\nTypeScript Interfaces found: {len(interfaces)}")
    for interface_name in list(interfaces.keys())[:5]:
        print(f"  - {interface_name}: {len(interfaces[interface_name])} fields")

    interface_usage = validator._find_interface_usage()
    print(f"\nInterface usage found: {len(interface_usage)}")
    for endpoint_key in list(interface_usage.keys())[:5]:
        print(f"  - {endpoint_key}: {interface_usage[endpoint_key]['interface']}")

    issues = validator.validate_payloads()

    print(f"\nValidation Issues found: {len(issues)}")

    if not issues:
        print("✓ No validation issues found")
    else:
        print(f"Found {len(issues)} validation issue(s):\n")

        for issue in issues:
            print(f"  [{issue.issue_type.upper()}] {issue.method} {issue.endpoint}")
            print(f"    Field: {issue.field_name}")
            print(f"    Expected: {issue.expected}")
            print(f"    Actual: {issue.actual}")
            print(f"    File: {issue.frontend_file}:{issue.frontend_line}")
            print()
