#!/usr/bin/env python3
"""
Pre-flight Scanner — Design System Detection

Analyzes a codebase for existing design signals before generating new design output.
Prevents stomping on established palettes, font stacks, and component systems.

Usage:
    python3 preflight-scan.py [directory]
    python3 preflight-scan.py . --format json
"""

import json
import os
import re
import sys
from pathlib import Path
from dataclasses import dataclass, asdict
from typing import Optional


@dataclass
class PreflightFindings:
    """Results of pre-flight scan."""
    
    # Design system presence
    design_md: bool = False
    design_md_path: Optional[str] = None
    
    # Font stack
    fonts: list = None
    font_sources: dict = None
    
    # Color palette
    palette_format: Optional[str] = None  # oklch, hex, hsl, rgb, tailwind
    colors: dict = None
    palette_sources: list = None
    
    # Motion/interaction
    motion_library: Optional[str] = None  # framer-motion, gsap, motion, lenis, lottie
    motion_stance: str = "unknown"  # on, off, unknown
    
    # Spacing
    spacing_scale: list = None
    spacing_format: Optional[str] = None  # tailwind, css-var, fixed
    
    # Framework
    framework: Optional[str] = None  # next, astro, vue, svelte, remix, react, vanilla
    framework_version: Optional[str] = None
    
    # Component library
    component_lib: Optional[str] = None  # shadcn, mui, chakra, radix
    tailwind_config: bool = False
    tailwind_version: Optional[str] = None
    
    # Project memory
    hallmark_log: bool = False
    design_log: bool = False
    
    def __post_init__(self):
        if self.fonts is None:
            self.fonts = []
        if self.font_sources is None:
            self.font_sources = {}
        if self.colors is None:
            self.colors = {}
        if self.palette_sources is None:
            self.palette_sources = []
        if self.spacing_scale is None:
            self.spacing_scale = []
    
    def to_dict(self) -> dict:
        return asdict(self)
    
    def format_markdown(self) -> str:
        """Format findings as readable markdown."""
        lines = ["## Pre-flight Findings\n"]
        
        if not any([self.design_md, self.fonts, self.colors, self.component_lib, self.framework]):
            return "## Pre-flight Findings\n\n*No design signals detected — proceeding with full design system.*\n"
        
        if self.framework:
            lines.append(f"- **Framework:** {self.framework}" + 
                        (f" {self.framework_version}" if self.framework_version else ""))
        
        if self.design_md:
            lines.append(f"- **Design System:** `design.md` found at `{self.design_md_path}` — *this overrides everything*")
        
        if self.fonts:
            font_list = ", ".join(f"`{f}`" for f in self.fonts[:5])
            if len(self.fonts) > 5:
                font_list += f" (+{len(self.fonts) - 5} more)"
            lines.append(f"- **Fonts:** {font_list}")
            for src, fonts in self.font_sources.items():
                lines.append(f"  - {src}: {', '.join(fonts)}")
        
        if self.palette_format:
            color_count = len(self.colors)
            lines.append(f"- **Palette:** {self.palette_format.upper()} ({color_count} colors)")
            for src in self.palette_sources:
                lines.append(f"  - Source: `{src}`")
        
        if self.motion_stance != "unknown":
            stance = "Motion ON" if self.motion_stance == "on" else "Motion OFF"
            library = f" ({self.motion_library})" if self.motion_library else ""
            lines.append(f"- **Motion stance:** {stance}{library}")
        
        if self.spacing_scale:
            scale_str = " · ".join(str(s) for s in self.spacing_scale[:6])
            if len(self.spacing_scale) > 6:
                scale_str += f" ... (+{len(self.spacing_scale) - 6} more)"
            lines.append(f"- **Spacing:** {self.spacing_format or 'custom'} ({scale_str})")
        
        if self.component_lib:
            lines.append(f"- **Component library:** {self.component_lib}")
        
        if self.tailwind_config:
            v = f" v{self.tailwind_version}" if self.tailwind_version else ""
            lines.append(f"- **Tailwind CSS:** detected{v}")
        
        # Summary
        lines.append("\n**Design will preserve:** " + ", ".join(self.preserved_items()))
        lines.append("\n**Design will introduce:** macrostructure, component system, slop-test gates.")
        
        return "\n".join(lines)
    
    def preserved_items(self) -> list:
        """List items that will be preserved from existing system."""
        items = []
        if self.design_md:
            items.append("design.md system")
        if self.fonts:
            items.append("font stack")
        if self.colors:
            items.append("palette")
        if self.motion_stance != "unknown":
            items.append("motion stance")
        if self.spacing_scale:
            items.append("spacing scale")
        if self.component_lib:
            items.append("component library")
        if not items:
            items.append("nothing (fresh project)")
        return items


class PreflightScanner:
    """Scans codebase for design system signals."""
    
    FONT_PATTERNS = [
        # next/font
        (r'next/font', r'next/font/(\w+)', 'next/font'),
        # @fontsource
        (r'@fontsource/(\w+)', r'@fontsource/(\w+)', 'fontsource'),
        # Google Fonts link
        (r'href=["\'].*?fonts\.googleapis\.com.*?family=([^"\']+)', None, 'google-fonts'),
        # CSS @import
        (r'@import\s+url\(["\'].*?fonts\.googleapis\.com.*?family=([^"\']+)', None, 'google-fonts'),
        # font-family declarations
        (r'font-family:\s*["\']([^"\']+)["\']', None, 'css-declaration'),
        # Tailwind fontFamily
        (r'fontFamily:\s*\{([^}]+)\}', None, 'tailwind'),
    ]
    
    COLOR_PATTERNS = [
        # OKLCH
        (r'oklch\(\s*[\d.]+%?\s+[\d.]+\s+[\d.]+', 'oklch'),
        # HSL
        (r'hsl\(\s*[\d.]+\s+[\d.]+%?\s+[\d.]+%?', 'hsl'),
        # Hex
        (r'#[0-9a-fA-F]{3,8}', 'hex'),
        # CSS custom properties
        (r'--[\w-]+:\s*(#[0-9a-fA-F]{3,8}|oklch|hsl|rgb)', 'css-var'),
        # Tailwind colors
        (r'colors:\s*\{', 'tailwind'),
    ]
    
    MOTION_LIBRARIES = {
        'framer-motion': 'framer-motion',
        'motion': 'motion',
        'gsap': 'gsap',
        'lenis': 'lenis',
        'lottie-react': 'lottie',
        '@react-spring/web': 'react-spring',
    }
    
    FRAMEWORKS = {
        'package.json': {
            '"next"': 'next',
            '"astro"': 'astro',
            '"vue"': 'vue',
            '"svelte"': 'svelte',
            '"@sveltejs/kit"': 'sveltekit',
            '"@remix-run/': 'remix',
            '"react"': 'react',
        },
        'next.config': 'next',
        'astro.config': 'astro',
        'svelte.config': 'svelte',
        'vite.config': 'vite',
    }
    
    COMPONENT_LIBS = {
        'shadcn': ['components/ui', 'components.json'],
        '@mui/material': 'mui',
        '@chakra-ui': 'chakra',
        '@radix-ui': 'radix',
    }
    
    def __init__(self, root_dir: str = "."):
        self.root = Path(root_dir).resolve()
        self.findings = PreflightFindings()
    
    def scan(self) -> PreflightFindings:
        """Run all scan methods."""
        self._scan_design_md()
        self._scan_package_json()
        self._scan_css_files()
        self._scan_config_files()
        self._scan_project_memory()
        return self.findings
    
    def _scan_design_md(self):
        """Check for design.md at project root."""
        candidates = ['design.md', 'DESIGN.md', '.design.md', 'docs/design.md']
        for name in candidates:
            path = self.root / name
            if path.exists():
                self.findings.design_md = True
                self.findings.design_md_path = str(path.relative_to(self.root))
                break
    
    def _scan_package_json(self):
        """Scan package.json for dependencies."""
        pkg_path = self.root / 'package.json'
        if not pkg_path.exists():
            return
        
        try:
            with open(pkg_path) as f:
                pkg = json.load(f)
        except json.JSONDecodeError:
            return
        
        deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
        
        # Framework detection
        for marker, framework in self.FRAMEWORKS.get('package.json', {}).items():
            if marker.strip('"\'') in deps:
                self.findings.framework = framework
                self.findings.framework_version = deps[marker.strip('"\' ')]
                break
        
        # Motion library detection
        for lib, name in self.MOTION_LIBRARIES.items():
            if lib in deps:
                self.findings.motion_library = name
                self.findings.motion_stance = "on"
                break
        
        # Component library detection
        for marker, lib in self.COMPONENT_LIBS.items():
            if marker in deps:
                self.findings.component_lib = lib if isinstance(lib, str) else marker
                break
        
        # Tailwind detection
        if 'tailwindcss' in deps:
            self.findings.tailwind_config = True
            self.findings.tailwind_version = deps.get('tailwindcss', '').lstrip('^~>')
    
    def _scan_css_files(self):
        """Scan CSS files for colors and fonts."""
        css_files = list(self.root.rglob('*.css')) + list(self.root.rglob('*.scss'))
        css_files = [f for f in css_files if 'node_modules' not in str(f)]
        
        fonts_found = set()
        colors_found = {}
        spacing_found = []
        palette_format = None
        spacing_format = None
        
        for css_file in css_files:
            try:
                content = css_file.read_text(errors='ignore')
                rel_path = str(css_file.relative_to(self.root))
                
                # Font scanning
                for pattern, match_pattern, source in self.FONT_PATTERNS:
                    if re.search(pattern, content, re.IGNORECASE):
                        if source == 'google-fonts':
                            # Extract font names from Google Fonts URL
                            matches = re.findall(r'family=([^\'"&]+)', pattern)
                            for match in matches:
                                font = match.replace('+', ' ')
                                fonts_found.add(font)
                                self.findings.font_sources.setdefault(rel_path, []).append(font)
                        elif source == 'tailwind':
                            # Extract Tailwind fontFamily keys
                            match = re.search(pattern, content)
                            if match:
                                fonts = re.findall(r'["\']?(\w+)["\']?\s*:', match.group(1))
                                for font in fonts:
                                    fonts_found.add(font)
                                    self.findings.font_sources.setdefault(rel_path, []).append(f"tailwind:{font}")
                        else:
                            match = re.search(match_pattern, content) if match_pattern else None
                            if match:
                                font = match.group(1) if match.groups() else pattern
                                fonts_found.add(font)
                                self.findings.font_sources.setdefault(rel_path, []).append(font)
                
                # Color scanning
                for pattern, color_format in self.COLOR_PATTERNS:
                    if re.search(pattern, content, re.IGNORECASE):
                        if palette_format is None:
                            palette_format = color_format
                        # Count occurrences
                        matches = re.findall(pattern, content, re.IGNORECASE)
                        colors_found[rel_path] = len(matches)
                        self.findings.palette_sources.append(rel_path)
                        break
                
                # Spacing scanning
                space_pattern = r'--space-(\w+):\s*(\d+)px'
                for match in re.finditer(space_pattern, content):
                    name, value = match.groups()
                    spacing_found.append((name, int(value)))
                
                if spacing_found and spacing_format is None:
                    spacing_format = 'css-var'
                
            except (PermissionError, OSError):
                continue
        
        self.findings.fonts = sorted(set(f.replace('tailwind:', '') for f in fonts_found))
        self.findings.colors = colors_found
        self.findings.palette_format = palette_format
        self.findings.spacing_scale = [v for _, v in sorted(set(spacing_found), key=lambda x: x[1])]
        self.findings.spacing_format = spacing_format
    
    def _scan_config_files(self):
        """Scan config files for Tailwind, design tokens, etc."""
        # Tailwind config
        for config_name in ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.cjs']:
            config_path = self.root / config_name
            if config_path.exists():
                try:
                    content = config_path.read_text()
                    self.findings.tailwind_config = True
                    
                    # Extract version hint
                    if 'tailwindcss' in content.lower():
                        self.findings.tailwind_version = 'custom'
                    
                    # Extract fontFamily
                    ff_match = re.search(r'fontFamily:\s*\{([^}]+)\}', content)
                    if ff_match:
                        fonts = re.findall(r'["\']?(\w+)["\']?\s*:', ff_match.group(1))
                        for font in fonts:
                            self.findings.fonts.append(f"tailwind:{font}")
                    
                    # Extract colors
                    colors_match = re.search(r'colors:\s*\{([^}]+)\}', content)
                    if colors_match:
                        self.findings.palette_format = 'tailwind'
                        self.findings.palette_sources.append(str(config_path.relative_to(self.root)))
                    
                    # Extract spacing
                    spacing_match = re.search(r'spacing:\s*\{([^}]+)\}', content)
                    if spacing_match:
                        spacing_format = 'tailwind'
                        spaces = re.findall(r'["\']?(\w+)["\']?\s*:\s*[\d.]+', spacing_match.group(1))
                        self.findings.spacing_scale = spaces
                        self.findings.spacing_format = spacing_format
                        
                except (PermissionError, OSError):
                    continue
        
        # Design tokens files
        for token_name in ['tokens.json', 'design-tokens.json', 'tokens.yml', 'design-system.json']:
            token_path = self.root / token_name
            if token_path.exists():
                self.findings.palette_format = 'dtcg'
                self.findings.palette_sources.append(str(token_path.relative_to(self.root)))
    
    def _scan_project_memory(self):
        """Check for Hallmark/design log files."""
        hallmark_log = self.root / '.hallmark' / 'log.json'
        design_log = self.root / '.design' / 'log.json'
        
        self.findings.hallmark_log = hallmark_log.exists()
        self.findings.design_log = design_log.exists()


def main():
    import argparse
    
    parser = argparse.ArgumentParser(description='Pre-flight design system scanner')
    parser.add_argument('directory', nargs='?', default='.', help='Directory to scan')
    parser.add_argument('--format', choices=['markdown', 'json'], default='markdown',
                       help='Output format')
    parser.add_argument('--output', help='Output file path')
    args = parser.parse_args()
    
    scanner = PreflightScanner(args.directory)
    findings = scanner.scan()
    
    if args.format == 'json':
        output = json.dumps(findings.to_dict(), indent=2)
    else:
        output = findings.format_markdown()
    
    if args.output:
        Path(args.output).write_text(output)
        print(f"Written to {args.output}")
    else:
        print(output)


if __name__ == '__main__':
    main()
