"""
Technology stack detector for identifying project frameworks.
"""

import re
import json
from pathlib import Path
from dataclasses import dataclass, field
from typing import Optional, Dict, List
from ..utils import log_info


@dataclass
class TechStack:
    """Represents detected technology stack information."""
    framework_id: str
    name: str
    language: str
    version: Optional[str] = None
    confidence: float = 0.0
    indicators: List[str] = field(default_factory=list)


class TechDetector:
    """Detects technology stack and framework from project structure."""
    
    def __init__(self, repo_root: Path, verbose: bool = False):
        self.repo_root = Path(repo_root)
        self.verbose = verbose
        
        # Framework detection rules
        self.detection_rules = {
            'java_spring': {
                'name': 'Java Spring Boot',
                'language': 'java',
                'files': ['pom.xml', 'build.gradle', 'build.gradle.kts'],
                'patterns': [
                    (r'<artifactId>spring-boot', 'pom.xml'),
                    (r'org\.springframework\.boot', 'build.gradle'),
                    (r'@SpringBootApplication', '**/*.java'),
                    (r'@RestController', '**/*.java'),
                ],
                'structure': ['src/main/java', 'src/main/resources']
            },
            'nodejs_express': {
                'name': 'Node.js Express',
                'language': 'javascript',
                'files': ['package.json'],
                'patterns': [
                    (r'"express":', 'package.json'),
                    (r'require\([\'"]express[\'"]\)', '**/*.js'),
                    (r'app\.(get|post|put|delete)', '**/*.js'),
                ],
                'exclude_patterns': [
                    (r'"@nestjs/core":', 'package.json'),  # Exclude if NestJS
                ]
            },
            'nodejs_nestjs': {
                'name': 'NestJS',
                'language': 'typescript',
                'files': ['package.json', 'nest-cli.json'],
                'patterns': [
                    (r'"@nestjs/core":', 'package.json'),
                    (r'@Controller\(', '**/*.ts'),
                    (r'@Module\(', '**/*.ts'),
                ],
                'structure': ['src']
            },
            'python_django': {
                'name': 'Python Django',
                'language': 'python',
                'files': ['manage.py', 'requirements.txt', 'pyproject.toml'],
                'patterns': [
                    (r'django', 'requirements.txt'),
                    (r'from django', '**/*.py'),
                    (r'django-admin', 'manage.py'),
                    (r'DJANGO_SETTINGS_MODULE', '**/*.py'),
                ],
                'structure': ['*/settings.py', '*/urls.py']
            },
            'python_flask': {
                'name': 'Python Flask',
                'language': 'python',
                'files': ['requirements.txt', 'pyproject.toml'],
                'patterns': [
                    (r'flask', 'requirements.txt'),
                    (r'from flask import', '**/*.py'),
                    (r'Flask\(__name__\)', '**/*.py'),
                    (r'@app\.route', '**/*.py'),
                ],
                'exclude_patterns': [
                    (r'django', 'requirements.txt'),  # Exclude if Django
                    (r'fastapi', 'requirements.txt'),  # Exclude if FastAPI
                ]
            },
            'python_fastapi': {
                'name': 'Python FastAPI',
                'language': 'python',
                'files': ['requirements.txt', 'pyproject.toml'],
                'patterns': [
                    (r'fastapi', 'requirements.txt'),
                    (r'from fastapi import', '**/*.py'),
                    (r'FastAPI\(', '**/*.py'),
                    (r'@app\.(get|post|put|delete)', '**/*.py'),
                ],
                'structure': ['app']
            },
            'dotnet': {
                'name': 'ASP.NET Core',
                'language': 'csharp',
                'files': ['*.csproj', '*.sln'],
                'patterns': [
                    (r'<Project Sdk="Microsoft\.NET\.Sdk\.Web">', '*.csproj'),
                    (r'\[ApiController\]', '**/*.cs'),
                    (r'\[Route\(', '**/*.cs'),
                    (r'using Microsoft\.AspNetCore', '**/*.cs'),
                ],
                'structure': ['Controllers', 'Models']
            },
            'ruby_rails': {
                'name': 'Ruby on Rails',
                'language': 'ruby',
                'files': ['Gemfile', 'config.ru', 'Rakefile'],
                'patterns': [
                    (r'gem [\'"]rails[\'"]', 'Gemfile'),
                    (r'Rails\.application', 'config.ru'),
                    (r'class \w+Controller < ApplicationController', '**/*.rb'),
                ],
                'structure': ['app/controllers', 'app/models', 'config/routes.rb']
            },
        }
    
    def detect(self) -> TechStack:
        """
        Detect the technology stack of the project.
        
        Returns:
            TechStack: Detected technology information
        """
        log_info("Detecting technology stack...", self.verbose)
        
        scores = {}
        indicators = {}
        
        # Score each framework
        for framework_id, rules in self.detection_rules.items():
            score, found_indicators = self._score_framework(framework_id, rules)
            scores[framework_id] = score
            indicators[framework_id] = found_indicators
            
            if self.verbose and score > 0:
                log_info(f"  {rules['name']}: {score:.2f} confidence", self.verbose)
        
        # Get best match
        if not scores or max(scores.values()) == 0:
            log_info("  ⚠️  Could not detect framework, defaulting to Java Spring", self.verbose)
            return TechStack(
                framework_id='java_spring',
                name='Java Spring Boot',
                language='java',
                confidence=0.0,
                indicators=['default']
            )
        
        best_framework = max(scores, key=lambda k: scores[k])
        best_score = scores[best_framework]
        rules = self.detection_rules[best_framework]
        
        # Extract version if possible
        version = self._extract_version(best_framework)
        
        tech_stack = TechStack(
            framework_id=best_framework,
            name=rules['name'],
            language=rules['language'],
            version=version,
            confidence=best_score,
            indicators=indicators[best_framework]
        )
        
        log_info(f"✓ Detected: {tech_stack.name} (confidence: {best_score:.0%})", self.verbose)
        if version:
            log_info(f"  Version: {version}", self.verbose)
        
        return tech_stack
    
    def _score_framework(self, framework_id: str, rules: Dict) -> tuple:
        """
        Score how well a framework matches the project.
        
        Returns:
            Tuple of (score, indicators_found)
        """
        score = 0.0
        indicators = []
        max_score = 0.0
        
        # Check for required files (0.3 weight)
        max_score += 0.3
        file_matches = 0
        for file_pattern in rules.get('files', []):
            if self._find_files(file_pattern):
                file_matches += 1
                indicators.append(f"file:{file_pattern}")
        
        if file_matches > 0:
            score += 0.3 * (file_matches / len(rules['files']))
        
        # Check for pattern matches (0.5 weight)
        patterns = rules.get('patterns', [])
        if patterns:
            max_score += 0.5
            pattern_matches = 0
            for pattern, file_glob in patterns:
                if self._find_pattern(pattern, file_glob):
                    pattern_matches += 1
                    indicators.append(f"pattern:{pattern[:30]}")
            
            if pattern_matches > 0:
                score += 0.5 * (pattern_matches / len(patterns))
        
        # Check directory structure (0.2 weight)
        structure = rules.get('structure', [])
        if structure:
            max_score += 0.2
            structure_matches = 0
            for dir_pattern in structure:
                if self._find_files(dir_pattern):
                    structure_matches += 1
                    indicators.append(f"structure:{dir_pattern}")
            
            if structure_matches > 0:
                score += 0.2 * (structure_matches / len(structure))
        
        # Check for exclude patterns (negative scoring)
        exclude_patterns = rules.get('exclude_patterns', [])
        for pattern, file_glob in exclude_patterns:
            if self._find_pattern(pattern, file_glob):
                score = 0.0
                indicators = [f"excluded:{pattern[:30]}"]
                break
        
        # Normalize score
        if max_score > 0:
            score = score / max_score
        
        return score, indicators
    
    def _find_files(self, pattern: str) -> List[Path]:
        """Find files matching pattern in repository."""
        try:
            matches = list(self.repo_root.glob(pattern))
            # Also try rglob for recursive patterns
            if not matches and '**' not in pattern:
                matches = list(self.repo_root.rglob(pattern))
            return matches
        except Exception as e:
            log_info(f"  Error searching for {pattern}: {e}", self.verbose)
            return []
    
    def _find_pattern(self, pattern: str, file_glob: str) -> bool:
        """Search for pattern in files matching glob."""
        try:
            files = self._find_files(file_glob)
            regex = re.compile(pattern, re.IGNORECASE)
            
            for file_path in files[:10]:  # Limit to first 10 files for performance
                if not file_path.is_file():
                    continue
                
                try:
                    content = file_path.read_text(encoding='utf-8', errors='ignore')
                    if regex.search(content):
                        return True
                except Exception:
                    continue
            
            return False
        except Exception as e:
            log_info(f"  Error searching pattern {pattern}: {e}", self.verbose)
            return False
    
    def _extract_version(self, framework_id: str) -> Optional[str]:
        """Extract framework version from project files."""
        try:
            if framework_id == 'java_spring':
                return self._extract_java_version()
            elif framework_id.startswith('nodejs_'):
                return self._extract_nodejs_version()
            elif framework_id.startswith('python_'):
                return self._extract_python_version(framework_id)
            elif framework_id == 'dotnet':
                return self._extract_dotnet_version()
            elif framework_id == 'ruby_rails':
                return self._extract_ruby_version()
        except Exception as e:
            log_info(f"  Could not extract version: {e}", self.verbose)
        
        return None
    
    def _extract_java_version(self) -> Optional[str]:
        """Extract Spring Boot version from pom.xml or build.gradle."""
        # Try pom.xml
        pom_files = list(self.repo_root.glob('pom.xml'))
        if pom_files:
            content = pom_files[0].read_text()
            match = re.search(r'<spring-boot\.version>([^<]+)</spring-boot\.version>', content)
            if match:
                return match.group(1)
            match = re.search(r'<version>([^<]+)</version>.*spring-boot', content, re.DOTALL)
            if match:
                return match.group(1)
        
        # Try build.gradle
        gradle_files = list(self.repo_root.glob('build.gradle*'))
        if gradle_files:
            content = gradle_files[0].read_text()
            match = re.search(r'org\.springframework\.boot[\'"]?\s*version\s*[\'"]([^\'\"]+)', content)
            if match:
                return match.group(1)
        
        return None
    
    def _extract_nodejs_version(self) -> Optional[str]:
        """Extract Node.js framework version from package.json."""
        package_json = self.repo_root / 'package.json'
        if package_json.exists():
            try:
                data = json.loads(package_json.read_text())
                deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
                
                # Look for express or @nestjs/core
                for pkg in ['express', '@nestjs/core']:
                    if pkg in deps:
                        version = deps[pkg].lstrip('^~>=')
                        return version
            except Exception:
                pass
        
        return None
    
    def _extract_python_version(self, framework_id: str) -> Optional[str]:
        """Extract Python framework version from requirements.txt."""
        requirements = self.repo_root / 'requirements.txt'
        if requirements.exists():
            content = requirements.read_text()
            
            framework_name = framework_id.replace('python_', '')
            match = re.search(rf'{framework_name}==([0-9.]+)', content, re.IGNORECASE)
            if match:
                return match.group(1)
        
        return None
    
    def _extract_dotnet_version(self) -> Optional[str]:
        """Extract .NET version from .csproj file."""
        csproj_files = list(self.repo_root.rglob('*.csproj'))
        if csproj_files:
            content = csproj_files[0].read_text()
            match = re.search(r'<TargetFramework>([^<]+)</TargetFramework>', content)
            if match:
                return match.group(1)
        
        return None
    
    def _extract_ruby_version(self) -> Optional[str]:
        """Extract Rails version from Gemfile."""
        gemfile = self.repo_root / 'Gemfile'
        if gemfile.exists():
            content = gemfile.read_text()
            match = re.search(r'gem [\'"]rails[\'"],\s*[\'"]([^\'\"]+)[\'"]', content)
            if match:
                return match.group(1)
        
        return None
