#!/usr/bin/env python3
"""
React Component Generator

Generates React/Next.js component files with TypeScript, Tailwind CSS,
and optional test files following best practices.

Usage:
    python component_generator.py Button --dir src/components/ui
    python component_generator.py ProductCard --type client --with-test
    python component_generator.py UserProfile --type server --with-story
"""

import argparse
import os
import re
import sys
from pathlib import Path
from datetime import datetime


# Component templates
TEMPLATES = {
    "client": '''\'use client\';

import {{ useState }} from 'react';
import {{ cn }} from '@/lib/utils';

interface {name}Props {{
  className?: string;
  children?: React.ReactNode;
}}

export function {name}({{ className, children }}: {name}Props) {{
  return (
    <div className={{cn('', className)}}>
      {{children}}
    </div>
  );
}}
''',

    "server": '''import {{ cn }} from '@/lib/utils';

interface {name}Props {{
  className?: string;
  children?: React.ReactNode;
}}

export async function {name}({{ className, children }}: {name}Props) {{
  return (
    <div className={{cn('', className)}}>
      {{children}}
    </div>
  );
}}
''',

    "hook": '''import {{ useState, useEffect, useCallback }} from 'react';

interface Use{name}Options {{
  // Add options here
}}

interface Use{name}Return {{
  // Add return type here
  isLoading: boolean;
  error: Error | null;
}}

export function use{name}(options: Use{name}Options = {{}}): Use{name}Return {{
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {{
    // Effect logic here
  }}, []);

  return {{
    isLoading,
    error,
  }};
}}
''',

    "test": '''import {{ render, screen }} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {{ {name} }} from './{name}';

describe('{name}', () => {{
  it('renders correctly', () => {{
    render(<{name}>Test content</{name}>);
    expect(screen.getByText('Test content')).toBeInTheDocument();
  }});

  it('applies custom className', () => {{
    render(<{name} className="custom-class">Content</{name}>);
    expect(screen.getByText('Content').parentElement).toHaveClass('custom-class');
  }});

  // Add more tests here
}});
''',

    "story": '''import type {{ Meta, StoryObj }} from '@storybook/react';
import {{ {name} }} from './{name}';

const meta: Meta<typeof {name}> = {{
  title: 'Components/{name}',
  component: {name},
  tags: ['autodocs'],
  argTypes: {{
    className: {{
      control: 'text',
      description: 'Additional CSS classes',
    }},
  }},
}};

export default meta;
type Story = StoryObj<typeof {name}>;

export const Default: Story = {{
  args: {{
    children: 'Default content',
  }},
}};

export const WithCustomClass: Story = {{
  args: {{
    className: 'bg-blue-100 p-4',
    children: 'Styled content',
  }},
}};
''',

    "index": '''export {{ {name} }} from './{name}';
export type {{ {name}Props }} from './{name}';
''',
}

_COMPONENT_NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$")


def _safe_component_name(name: str) -> str:
    name = name.strip()
    if not _COMPONENT_NAME_RE.fullmatch(name):
        raise ValueError("Component name must start with a letter and contain only letters, numbers, hyphens, or underscores")
    return name


def _safe_component_dir(output_dir: Path, pascal_name: str, flat: bool) -> Path:
    output_root = output_dir.resolve()
    component_dir = output_root if flat else (output_root / pascal_name).resolve()
    component_dir.relative_to(output_root)
    return component_dir


def _safe_output_dir(raw_dir: str) -> Path:
    raw = str(raw_dir).strip()
    if "\x00" in raw:
        raise ValueError("Output directory contains an invalid null byte")
    parts = Path(raw).parts
    if any(part == ".." for part in parts):
        raise ValueError("Output directory must not contain '..' segments")
    return Path(raw).expanduser().resolve()


def _safe_component_file(component_dir: Path, filename: str) -> Path:
    if "/" in filename or "\\" in filename or "\x00" in filename or ".." in filename:
        raise ValueError(f"Unsafe generated filename: {filename}")
    target = (component_dir / filename).resolve()
    target.relative_to(component_dir.resolve())
    return target


def to_pascal_case(name: str) -> str:
    """Convert string to PascalCase."""
    name = _safe_component_name(name)
    # Handle kebab-case and snake_case
    words = name.replace('-', '_').split('_')
    return ''.join(word.capitalize() for word in words)


def to_kebab_case(name: str) -> str:
    """Convert PascalCase to kebab-case."""
    result = []
    for i, char in enumerate(name):
        if char.isupper() and i > 0:
            result.append('-')
        result.append(char.lower())
    return ''.join(result)


def generate_component(
    name: str,
    output_dir: Path,
    component_type: str = "client",
    with_test: bool = False,
    with_story: bool = False,
    with_index: bool = True,
    flat: bool = False,
) -> dict:
    """Generate component files."""
    pascal_name = to_pascal_case(name)
    kebab_name = to_kebab_case(pascal_name)

    # Determine output path
    component_dir = _safe_component_dir(output_dir, pascal_name, flat)

    files_created = []

    # Create directory
    component_dir.mkdir(parents=True, exist_ok=True)

    # Generate main component file
    if component_type == "hook":
        main_file = _safe_component_file(component_dir, f"use{pascal_name}.ts")
        template = TEMPLATES["hook"]
    else:
        main_file = _safe_component_file(component_dir, f"{pascal_name}.tsx")
        template = TEMPLATES[component_type]

    content = template.format(name=pascal_name)
    main_file.write_text(content)
    files_created.append(str(main_file))

    # Generate test file
    if with_test and component_type != "hook":
        test_file = _safe_component_file(component_dir, f"{pascal_name}.test.tsx")
        test_content = TEMPLATES["test"].format(name=pascal_name)
        test_file.write_text(test_content)
        files_created.append(str(test_file))

    # Generate story file
    if with_story and component_type != "hook":
        story_file = _safe_component_file(component_dir, f"{pascal_name}.stories.tsx")
        story_content = TEMPLATES["story"].format(name=pascal_name)
        story_file.write_text(story_content)
        files_created.append(str(story_file))

    # Generate index file
    if with_index and not flat:
        index_file = _safe_component_file(component_dir, "index.ts")
        index_content = TEMPLATES["index"].format(name=pascal_name)
        index_file.write_text(index_content)
        files_created.append(str(index_file))

    return {
        "name": pascal_name,
        "type": component_type,
        "directory": str(component_dir),
        "files": files_created,
    }


def print_result(result: dict, verbose: bool = False) -> None:
    """Print generation result."""
    print(f"\n{'='*50}")
    print(f"Component Generated: {result['name']}")
    print(f"{'='*50}")
    print(f"Type: {result['type']}")
    print(f"Directory: {result['directory']}")
    print(f"\nFiles created:")
    for file in result['files']:
        print(f"  - {file}")
    print(f"{'='*50}\n")

    # Print usage hint
    if result['type'] != 'hook':
        print("Usage:")
        print(f"  import {{ {result['name']} }} from '@/components/{result['name']}';")
        print(f"\n  <{result['name']}>Content</{result['name']}>")
    else:
        print("Usage:")
        print(f"  import {{ use{result['name']} }} from '@/hooks/use{result['name']}';")
        print(f"\n  const {{ isLoading, error }} = use{result['name']}();")


def self_test() -> None:
    assert to_pascal_case("product-card") == "ProductCard"
    for bad_name in ("../Card", "Bad.Name", "", "1Card"):
        try:
            to_pascal_case(bad_name)
        except ValueError:
            pass
        else:
            raise AssertionError(f"accepted unsafe component name: {bad_name!r}")


def main():
    parser = argparse.ArgumentParser(
        description="Generate React/Next.js components with TypeScript and Tailwind CSS"
    )
    parser.add_argument(
        "name",
        nargs="?",
        help="Component name (PascalCase or kebab-case)"
    )
    parser.add_argument(
        "--self-test",
        action="store_true",
        help="Run safety self-checks"
    )
    parser.add_argument(
        "--dir", "-d",
        default="src/components",
        help="Output directory (default: src/components)"
    )
    parser.add_argument(
        "--type", "-t",
        choices=["client", "server", "hook"],
        default="client",
        help="Component type (default: client)"
    )
    parser.add_argument(
        "--with-test",
        action="store_true",
        help="Generate test file"
    )
    parser.add_argument(
        "--with-story",
        action="store_true",
        help="Generate Storybook story file"
    )
    parser.add_argument(
        "--no-index",
        action="store_true",
        help="Skip generating index.ts file"
    )
    parser.add_argument(
        "--flat",
        action="store_true",
        help="Create files directly in output dir without subdirectory"
    )
    parser.add_argument(
        "--dry-run",
        action="store_true",
        help="Show what would be generated without creating files"
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Enable verbose output"
    )

    args = parser.parse_args()

    if args.self_test:
        self_test()
        return

    if not args.name:
        parser.error("name is required unless --self-test is used")

    output_dir = _safe_output_dir(args.dir)
    pascal_name = to_pascal_case(args.name)

    if args.dry_run:
        print(f"\nDry run - would generate:")
        print(f"  Component: {pascal_name}")
        print(f"  Type: {args.type}")
        print(f"  Directory: {output_dir / pascal_name if not args.flat else output_dir}")
        print(f"  Test: {'Yes' if args.with_test else 'No'}")
        print(f"  Story: {'Yes' if args.with_story else 'No'}")
        return

    try:
        result = generate_component(
            name=args.name,
            output_dir=output_dir,
            component_type=args.type,
            with_test=args.with_test,
            with_story=args.with_story,
            with_index=not args.no_index,
            flat=args.flat,
        )
        print_result(result, args.verbose)

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()
