"""
Storybook Scanner

Discovers and analyzes existing Storybook stories in a project.
"""

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


@dataclass
class StoryInfo:
    """Information about a Storybook story."""
    name: str                          # Story name (e.g., "Primary")
    title: str                         # Story title (e.g., "Components/Button")
    component: str                     # Component name
    file_path: str                     # Path to story file
    args: Dict[str, Any] = field(default_factory=dict)  # Default args
    parameters: Dict[str, Any] = field(default_factory=dict)
    decorators: List[str] = field(default_factory=list)
    tags: List[str] = field(default_factory=list)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "name": self.name,
            "title": self.title,
            "component": self.component,
            "file_path": self.file_path,
            "args": self.args,
            "parameters": self.parameters,
            "decorators": self.decorators,
            "tags": self.tags,
        }


@dataclass
class StorybookConfig:
    """Storybook configuration details."""
    version: str = ""
    framework: str = ""                # react, vue, angular, etc.
    builder: str = ""                  # vite, webpack5, etc.
    addons: List[str] = field(default_factory=list)
    stories_patterns: List[str] = field(default_factory=list)
    static_dirs: List[str] = field(default_factory=list)
    config_path: str = ""

    def to_dict(self) -> Dict[str, Any]:
        return {
            "version": self.version,
            "framework": self.framework,
            "builder": self.builder,
            "addons": self.addons,
            "stories_patterns": self.stories_patterns,
            "static_dirs": self.static_dirs,
            "config_path": self.config_path,
        }


class StorybookScanner:
    """
    Scans a project for Storybook configuration and stories.
    """

    # Common story file patterns
    STORY_PATTERNS = [
        '*.stories.tsx',
        '*.stories.ts',
        '*.stories.jsx',
        '*.stories.js',
        '*.stories.mdx',
    ]

    def __init__(self, root_path: str):
        self.root = Path(root_path).resolve()

    async def scan(self) -> Dict[str, Any]:
        """
        Perform full Storybook scan.

        Returns:
            Dict with configuration and story inventory
        """
        config = await self._find_config()
        stories = await self._find_stories()

        # Group stories by component
        by_component: Dict[str, List[StoryInfo]] = {}
        for story in stories:
            if story.component not in by_component:
                by_component[story.component] = []
            by_component[story.component].append(story)

        return {
            "config": config.to_dict() if config else None,
            "stories_count": len(stories),
            "components_with_stories": len(by_component),
            "stories": [s.to_dict() for s in stories],
            "by_component": {
                comp: [s.to_dict() for s in stories_list]
                for comp, stories_list in by_component.items()
            },
        }

    async def _find_config(self) -> Optional[StorybookConfig]:
        """Find and parse Storybook configuration."""
        # Look for .storybook directory
        storybook_dir = self.root / ".storybook"
        if not storybook_dir.exists():
            # Try alternative locations
            for alt in ["storybook", ".storybook"]:
                alt_path = self.root / alt
                if alt_path.exists():
                    storybook_dir = alt_path
                    break
            else:
                return None

        config = StorybookConfig(config_path=str(storybook_dir))

        # Parse main.js/ts
        for main_file in ["main.ts", "main.js", "main.mjs"]:
            main_path = storybook_dir / main_file
            if main_path.exists():
                await self._parse_main_config(main_path, config)
                break

        # Check package.json for Storybook version
        pkg_json = self.root / "package.json"
        if pkg_json.exists():
            try:
                pkg = json.loads(pkg_json.read_text())
                deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}

                # Get Storybook version
                for pkg_name in ["@storybook/react", "@storybook/vue3", "@storybook/angular"]:
                    if pkg_name in deps:
                        config.version = deps[pkg_name].lstrip("^~")
                        config.framework = pkg_name.split("/")[1]
                        break

                # Get builder
                if "@storybook/builder-vite" in deps:
                    config.builder = "vite"
                elif "@storybook/builder-webpack5" in deps:
                    config.builder = "webpack5"

                # Get addons
                config.addons = [
                    pkg for pkg in deps.keys()
                    if pkg.startswith("@storybook/addon-")
                ]

            except (json.JSONDecodeError, KeyError):
                pass

        return config

    async def _parse_main_config(self, main_path: Path, config: StorybookConfig) -> None:
        """Parse main.js/ts for configuration."""
        try:
            content = main_path.read_text(encoding="utf-8")

            # Extract stories patterns
            stories_match = re.search(
                r'stories\s*:\s*\[([^\]]+)\]',
                content,
                re.DOTALL
            )
            if stories_match:
                patterns_str = stories_match.group(1)
                patterns = re.findall(r'["\']([^"\']+)["\']', patterns_str)
                config.stories_patterns = patterns

            # Extract static dirs
            static_match = re.search(
                r'staticDirs\s*:\s*\[([^\]]+)\]',
                content,
                re.DOTALL
            )
            if static_match:
                dirs_str = static_match.group(1)
                dirs = re.findall(r'["\']([^"\']+)["\']', dirs_str)
                config.static_dirs = dirs

            # Extract framework
            framework_match = re.search(
                r'framework\s*:\s*["\'](@storybook/[^"\']+)["\']',
                content
            )
            if framework_match:
                config.framework = framework_match.group(1)

        except Exception:
            pass

    async def _find_stories(self) -> List[StoryInfo]:
        """Find all story files in the project."""
        stories = []
        skip_dirs = {'node_modules', '.git', 'dist', 'build'}

        for pattern in self.STORY_PATTERNS:
            for story_path in self.root.rglob(pattern):
                if any(skip in story_path.parts for skip in skip_dirs):
                    continue

                try:
                    file_stories = await self._parse_story_file(story_path)
                    stories.extend(file_stories)
                except Exception:
                    continue

        return stories

    async def _parse_story_file(self, story_path: Path) -> List[StoryInfo]:
        """Parse a story file to extract story information."""
        content = story_path.read_text(encoding="utf-8", errors="ignore")
        rel_path = str(story_path.relative_to(self.root))
        stories = []

        # Extract meta/default export
        title = ""
        component = ""

        # CSF3 format: const meta = { title: '...', component: ... }
        meta_match = re.search(
            r'(?:const\s+meta|export\s+default)\s*[=:]\s*\{([^}]+)\}',
            content,
            re.DOTALL
        )
        if meta_match:
            meta_content = meta_match.group(1)

            title_match = re.search(r'title\s*:\s*["\']([^"\']+)["\']', meta_content)
            if title_match:
                title = title_match.group(1)

            comp_match = re.search(r'component\s*:\s*(\w+)', meta_content)
            if comp_match:
                component = comp_match.group(1)

        # If no title, derive from file path
        if not title:
            # Convert path to title (e.g., src/components/Button.stories.tsx -> Components/Button)
            parts = story_path.stem.replace('.stories', '').split('/')
            title = '/'.join(p.title() for p in parts[-2:] if p)

        if not component:
            component = story_path.stem.replace('.stories', '')

        # Find exported stories (CSF3 format)
        # export const Primary: Story = { ... }
        story_pattern = re.compile(
            r'export\s+const\s+(\w+)\s*(?::\s*\w+)?\s*=\s*\{([^}]*)\}',
            re.DOTALL
        )

        for match in story_pattern.finditer(content):
            story_name = match.group(1)
            story_content = match.group(2)

            # Skip meta export
            if story_name.lower() in ['meta', 'default']:
                continue

            # Parse args
            args = {}
            args_match = re.search(r'args\s*:\s*\{([^}]*)\}', story_content)
            if args_match:
                args_str = args_match.group(1)
                # Simple key-value extraction
                for kv_match in re.finditer(r'(\w+)\s*:\s*["\']?([^,\n"\']+)["\']?', args_str):
                    args[kv_match.group(1)] = kv_match.group(2).strip()

            stories.append(StoryInfo(
                name=story_name,
                title=title,
                component=component,
                file_path=rel_path,
                args=args,
            ))

        # Also check for older CSF2 format
        # export const Primary = Template.bind({})
        csf2_pattern = re.compile(
            r'export\s+const\s+(\w+)\s*=\s*Template\.bind\(\{\}\)'
        )
        for match in csf2_pattern.finditer(content):
            story_name = match.group(1)
            if not any(s.name == story_name for s in stories):
                stories.append(StoryInfo(
                    name=story_name,
                    title=title,
                    component=component,
                    file_path=rel_path,
                ))

        return stories

    async def get_components_without_stories(
        self,
        component_files: List[str]
    ) -> List[str]:
        """
        Find components that don't have Storybook stories.

        Args:
            component_files: List of component file paths

        Returns:
            List of component paths without stories
        """
        # Get all components with stories
        result = await self.scan()
        components_with_stories = set(result.get("by_component", {}).keys())

        # Find components without stories
        without_stories = []
        for comp_path in component_files:
            # Extract component name from path
            comp_name = Path(comp_path).stem
            if comp_name not in components_with_stories:
                without_stories.append(comp_path)

        return without_stories

    async def get_story_coverage(self) -> Dict[str, Any]:
        """
        Calculate story coverage statistics.

        Returns:
            Coverage statistics including counts and percentages
        """
        result = await self.scan()

        stories_count = result.get("stories_count", 0)
        components_count = result.get("components_with_stories", 0)

        # Count stories per component
        by_component = result.get("by_component", {})
        stories_per_component = {
            comp: len(stories) for comp, stories in by_component.items()
        }

        avg_stories = (
            sum(stories_per_component.values()) / len(stories_per_component)
            if stories_per_component else 0
        )

        return {
            "total_stories": stories_count,
            "components_covered": components_count,
            "average_stories_per_component": round(avg_stories, 1),
            "stories_per_component": stories_per_component,
        }
