"""
Project Scanner

Scans file system to discover project structure, frameworks, and style files.
"""

import json
import re
from pathlib import Path
from typing import List, Dict, Any, Optional, Set, Tuple
from dataclasses import dataclass, field

from .base import (
    Framework,
    StylingApproach,
    StyleFile,
    ProjectAnalysis,
)


# Directories to skip during scanning
SKIP_DIRS = {
    'node_modules',
    '.git',
    '.next',
    '.nuxt',
    'dist',
    'build',
    'out',
    '.cache',
    'coverage',
    '__pycache__',
    '.venv',
    'venv',
    '.turbo',
    '.vercel',
}

# File extensions to scan
SCAN_EXTENSIONS = {
    # JavaScript/TypeScript
    '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
    # Styles
    '.css', '.scss', '.sass', '.less', '.styl',
    # Config
    '.json',
}


@dataclass
class ScanResult:
    """Result of file system scan."""
    files: List[Path] = field(default_factory=list)
    style_files: List[Path] = field(default_factory=list)
    component_files: List[Path] = field(default_factory=list)
    config_files: Dict[str, Path] = field(default_factory=dict)
    total_lines: int = 0


class ProjectScanner:
    """
    Scans a project directory to identify:
    - Framework (React, Next, Vue, etc.)
    - Styling approach (CSS modules, styled-components, Tailwind, etc.)
    - Component files
    - Style files

    Results are cached in memory for the session.
    """

    # Class-level cache: path -> (timestamp, analysis)
    _cache: Dict[str, Tuple[float, ProjectAnalysis]] = {}
    _cache_ttl: float = 60.0  # Cache for 60 seconds

    def __init__(self, root_path: str, use_cache: bool = True):
        self.root = Path(root_path).resolve()
        self.use_cache = use_cache
        if not self.root.exists():
            raise FileNotFoundError(f"Project path not found: {root_path}")

    async def scan(self) -> ProjectAnalysis:
        """
        Perform full project scan.

        Returns:
            ProjectAnalysis with detected framework, styles, and files
        """
        # Check cache if enabled
        if self.use_cache:
            import time
            cache_key = str(self.root)
            if cache_key in self._cache:
                timestamp, cached_analysis = self._cache[cache_key]
                if time.time() - timestamp < self._cache_ttl:
                    return cached_analysis

        # Scan file system
        scan_result = self._scan_files()

        # Detect framework
        framework, version = self._detect_framework(scan_result.config_files)

        # Detect styling approaches
        styling = self._detect_styling(scan_result)

        # Collect style files
        style_files = self._analyze_style_files(scan_result.style_files)

        # Build analysis result
        analysis = ProjectAnalysis(
            project_path=str(self.root),
            framework=framework,
            framework_version=version,
            style_files=style_files,
            style_file_count=len(style_files),
            stats={
                "total_files_scanned": len(scan_result.files),
                "total_lines": scan_result.total_lines,
                "component_files": len(scan_result.component_files),
                "style_files": len(scan_result.style_files),
            }
        )

        # Determine primary styling approach
        if styling:
            analysis.styling_approaches = styling
            # Primary is the one with most occurrences
            analysis.primary_styling = max(
                styling, key=lambda x: x.count
            ).type if styling else None

        # Cache result if enabled
        if self.use_cache:
            import time
            cache_key = str(self.root)
            self._cache[cache_key] = (time.time(), analysis)

        return analysis

    def _scan_files(self) -> ScanResult:
        """Scan directory for relevant files."""
        result = ScanResult()

        for path in self.root.rglob("*"):
            # Skip directories in skip list
            if any(skip in path.parts for skip in SKIP_DIRS):
                continue

            if not path.is_file():
                continue

            suffix = path.suffix.lower()
            if suffix not in SCAN_EXTENSIONS:
                continue

            result.files.append(path)

            # Categorize files
            if suffix in {'.css', '.scss', '.sass', '.less', '.styl'}:
                result.style_files.append(path)
            elif suffix in {'.jsx', '.tsx'}:
                result.component_files.append(path)
            elif suffix in {'.js', '.ts'}:
                # Check if it's a component or config
                name = path.name.lower()
                if any(cfg in name for cfg in ['config', 'rc', '.config']):
                    result.config_files[name] = path
                elif self._looks_like_component(path):
                    result.component_files.append(path)

            # Count lines (approximate for large files)
            try:
                content = path.read_text(encoding='utf-8', errors='ignore')
                result.total_lines += content.count('\n') + 1
            except Exception:
                pass

        # Look for specific config files
        config_names = [
            'package.json',
            'tsconfig.json',
            'tailwind.config.js',
            'tailwind.config.ts',
            'next.config.js',
            'next.config.mjs',
            'vite.config.js',
            'vite.config.ts',
            'nuxt.config.js',
            'nuxt.config.ts',
            '.eslintrc.json',
            '.eslintrc.js',
        ]

        for name in config_names:
            config_path = self.root / name
            if config_path.exists():
                result.config_files[name] = config_path

        return result

    def _looks_like_component(self, path: Path) -> bool:
        """Check if a JS/TS file looks like a React component."""
        name = path.stem
        # PascalCase is a strong indicator
        if name[0].isupper() and not name.isupper():
            return True
        # Common component patterns
        if any(x in name.lower() for x in ['component', 'page', 'view', 'screen']):
            return True
        return False

    def _detect_framework(
        self,
        config_files: Dict[str, Path]
    ) -> Tuple[Framework, str]:
        """Detect the UI framework and version."""
        # Check package.json for dependencies
        pkg_json = config_files.get('package.json')
        if not pkg_json:
            return Framework.UNKNOWN, ""

        try:
            pkg = json.loads(pkg_json.read_text())
            deps = {
                **pkg.get('dependencies', {}),
                **pkg.get('devDependencies', {}),
            }

            # Check for Next.js first (it includes React)
            if 'next' in deps:
                return Framework.NEXT, deps.get('next', '').lstrip('^~')

            # Check for Nuxt (Vue-based)
            if 'nuxt' in deps:
                return Framework.NUXT, deps.get('nuxt', '').lstrip('^~')

            # Check for other frameworks
            if 'react' in deps:
                return Framework.REACT, deps.get('react', '').lstrip('^~')

            if 'vue' in deps:
                return Framework.VUE, deps.get('vue', '').lstrip('^~')

            if '@angular/core' in deps:
                return Framework.ANGULAR, deps.get('@angular/core', '').lstrip('^~')

            if 'svelte' in deps:
                return Framework.SVELTE, deps.get('svelte', '').lstrip('^~')

            if 'solid-js' in deps:
                return Framework.SOLID, deps.get('solid-js', '').lstrip('^~')

        except (json.JSONDecodeError, KeyError):
            pass

        return Framework.UNKNOWN, ""

    def _detect_styling(self, scan_result: ScanResult) -> List:
        """Detect styling approaches used in the project."""
        from .base import StylePattern, Location

        patterns: Dict[StylingApproach, StylePattern] = {}

        # Check config files for styling indicators
        pkg_json = scan_result.config_files.get('package.json')
        if pkg_json:
            try:
                pkg = json.loads(pkg_json.read_text())
                deps = {
                    **pkg.get('dependencies', {}),
                    **pkg.get('devDependencies', {}),
                }

                # Tailwind
                if 'tailwindcss' in deps:
                    patterns[StylingApproach.TAILWIND] = StylePattern(
                        type=StylingApproach.TAILWIND,
                        count=1,
                        examples=["tailwindcss in dependencies"]
                    )

                # styled-components
                if 'styled-components' in deps:
                    patterns[StylingApproach.STYLED_COMPONENTS] = StylePattern(
                        type=StylingApproach.STYLED_COMPONENTS,
                        count=1,
                        examples=["styled-components in dependencies"]
                    )

                # Emotion
                if '@emotion/react' in deps or '@emotion/styled' in deps:
                    patterns[StylingApproach.EMOTION] = StylePattern(
                        type=StylingApproach.EMOTION,
                        count=1,
                        examples=["@emotion in dependencies"]
                    )

                # SASS/SCSS
                if 'sass' in deps or 'node-sass' in deps:
                    patterns[StylingApproach.SASS_SCSS] = StylePattern(
                        type=StylingApproach.SASS_SCSS,
                        count=1,
                        examples=["sass in dependencies"]
                    )

            except (json.JSONDecodeError, KeyError):
                pass

        # Check tailwind config
        if 'tailwind.config.js' in scan_result.config_files or \
           'tailwind.config.ts' in scan_result.config_files:
            if StylingApproach.TAILWIND not in patterns:
                patterns[StylingApproach.TAILWIND] = StylePattern(
                    type=StylingApproach.TAILWIND,
                    count=1,
                    examples=["tailwind.config found"]
                )

        # Scan component files for styling patterns
        for comp_file in scan_result.component_files[:100]:  # Limit for performance
            try:
                content = comp_file.read_text(encoding='utf-8', errors='ignore')
                self._detect_patterns_in_file(
                    content, str(comp_file), patterns
                )
            except Exception:
                pass

        # Check style files
        for style_file in scan_result.style_files:
            suffix = style_file.suffix.lower()

            if suffix == '.css':
                # Check for CSS modules
                if '.module.css' in style_file.name.lower():
                    approach = StylingApproach.CSS_MODULES
                else:
                    approach = StylingApproach.VANILLA_CSS

                if approach not in patterns:
                    patterns[approach] = StylePattern(type=approach)
                patterns[approach].count += 1
                patterns[approach].locations.append(
                    Location(str(style_file), 1)
                )

            elif suffix in {'.scss', '.sass'}:
                if StylingApproach.SASS_SCSS not in patterns:
                    patterns[StylingApproach.SASS_SCSS] = StylePattern(
                        type=StylingApproach.SASS_SCSS
                    )
                patterns[StylingApproach.SASS_SCSS].count += 1

        return list(patterns.values())

    def _detect_patterns_in_file(
        self,
        content: str,
        file_path: str,
        patterns: Dict[StylingApproach, Any]
    ) -> None:
        """Detect styling patterns in a single file."""
        from .base import StylePattern, Location

        # CSS Modules import
        css_module_pattern = re.compile(
            r"import\s+\w+\s+from\s+['\"].*\.module\.(css|scss|sass)['\"]"
        )
        for match in css_module_pattern.finditer(content):
            if StylingApproach.CSS_MODULES not in patterns:
                patterns[StylingApproach.CSS_MODULES] = StylePattern(
                    type=StylingApproach.CSS_MODULES
                )
            patterns[StylingApproach.CSS_MODULES].count += 1
            line_num = content[:match.start()].count('\n') + 1
            patterns[StylingApproach.CSS_MODULES].locations.append(
                Location(file_path, line_num)
            )

        # styled-components
        styled_pattern = re.compile(
            r"(styled\.|styled\()|(from\s+['\"]styled-components['\"])"
        )
        for match in styled_pattern.finditer(content):
            if StylingApproach.STYLED_COMPONENTS not in patterns:
                patterns[StylingApproach.STYLED_COMPONENTS] = StylePattern(
                    type=StylingApproach.STYLED_COMPONENTS
                )
            patterns[StylingApproach.STYLED_COMPONENTS].count += 1

        # Emotion
        emotion_pattern = re.compile(
            r"(css`|@emotion|from\s+['\"]@emotion)"
        )
        for match in emotion_pattern.finditer(content):
            if StylingApproach.EMOTION not in patterns:
                patterns[StylingApproach.EMOTION] = StylePattern(
                    type=StylingApproach.EMOTION
                )
            patterns[StylingApproach.EMOTION].count += 1

        # Inline styles
        inline_pattern = re.compile(
            r'style\s*=\s*\{\s*\{[^}]+\}\s*\}'
        )
        for match in inline_pattern.finditer(content):
            if StylingApproach.INLINE_STYLES not in patterns:
                patterns[StylingApproach.INLINE_STYLES] = StylePattern(
                    type=StylingApproach.INLINE_STYLES
                )
            patterns[StylingApproach.INLINE_STYLES].count += 1
            line_num = content[:match.start()].count('\n') + 1
            patterns[StylingApproach.INLINE_STYLES].locations.append(
                Location(file_path, line_num)
            )
            patterns[StylingApproach.INLINE_STYLES].examples.append(
                match.group(0)[:100]
            )

        # Tailwind classes
        tailwind_pattern = re.compile(
            r'className\s*=\s*["\'][^"\']*(?:flex|grid|p-|m-|bg-|text-|border-)[^"\']*["\']'
        )
        for match in tailwind_pattern.finditer(content):
            if StylingApproach.TAILWIND not in patterns:
                patterns[StylingApproach.TAILWIND] = StylePattern(
                    type=StylingApproach.TAILWIND
                )
            patterns[StylingApproach.TAILWIND].count += 1

    def _analyze_style_files(self, style_paths: List[Path]) -> List[StyleFile]:
        """Analyze style files for metadata."""
        style_files = []

        for path in style_paths:
            try:
                content = path.read_text(encoding='utf-8', errors='ignore')

                # Determine type
                suffix = path.suffix.lower()
                if '.module.' in path.name.lower():
                    file_type = 'css-module'
                elif suffix == '.scss':
                    file_type = 'scss'
                elif suffix == '.sass':
                    file_type = 'sass'
                elif suffix == '.less':
                    file_type = 'less'
                else:
                    file_type = 'css'

                # Count variables
                var_count = 0
                if file_type == 'css' or file_type == 'css-module':
                    var_count = len(re.findall(r'--[\w-]+\s*:', content))
                elif file_type in {'scss', 'sass'}:
                    var_count = len(re.findall(r'\$[\w-]+\s*:', content))

                # Count selectors (approximate)
                selector_count = len(re.findall(r'[.#][\w-]+\s*\{', content))

                # Find imports
                imports = re.findall(r'@import\s+["\']([^"\']+)["\']', content)

                style_files.append(StyleFile(
                    path=str(path.relative_to(self.root)),
                    type=file_type,
                    size_bytes=path.stat().st_size,
                    line_count=content.count('\n') + 1,
                    variable_count=var_count,
                    selector_count=selector_count,
                    imports=imports,
                ))

            except Exception:
                continue

        return style_files

    def get_file_tree(self, max_depth: int = 3) -> Dict[str, Any]:
        """Get project file tree structure."""
        def build_tree(path: Path, depth: int) -> Dict[str, Any]:
            if depth > max_depth:
                return {"...": "truncated"}

            result = {}
            try:
                for item in sorted(path.iterdir()):
                    if item.name in SKIP_DIRS:
                        continue

                    if item.is_dir():
                        result[item.name + "/"] = build_tree(item, depth + 1)
                    elif item.suffix in SCAN_EXTENSIONS:
                        result[item.name] = item.stat().st_size

            except PermissionError:
                pass

            return result

        return build_tree(self.root, 0)
