#!/usr/bin/env python3
"""
Cross-Platform Export Utilities for Agent-Skill-Creator

Packages Claude Code skills for Desktop/Web/API use with versioning and validation.
"""

import os
import sys
import zipfile
import subprocess
from datetime import datetime
from typing import Dict, List, Tuple

# Directories and files to exclude from exports
EXCLUDE_DIRS = {
    '.git', '__pycache__', 'node_modules', '.claude-plugin',
    'venv', 'env', '.venv', '.pytest_cache', '.mypy_cache',
    'dist', 'build', '*.egg-info'
}

EXCLUDE_FILES = {
    '.DS_Store', '.gitignore', 'Thumbs.db', '*.pyc', '*.pyo',
    '.env', 'credentials.json', '*.log', '.python-version'
}

# API package size limit (8MB per Claude API requirements)
MAX_API_SIZE_MB = 8
MAX_API_SIZE_BYTES = MAX_API_SIZE_MB * 1024 * 1024

# SKILL.md validation limits
MAX_NAME_LENGTH = 64
MAX_DESCRIPTION_LENGTH = 1024


def get_skill_version(skill_path: str, override_version: str = None) -> str:
    """
    Determine skill version from git tags, SKILL.md, or use default.

    Args:
        skill_path: Path to skill directory
        override_version: User-specified version (takes precedence)

    Returns:
        Version string in format "vX.Y.Z"
    """
    if override_version:
        return override_version if override_version.startswith('v') else f'v{override_version}'

    # Try git tags first
    try:
        os.chdir(skill_path)
        result = subprocess.run(
            ['git', 'describe', '--tags', '--abbrev=0'],
            capture_output=True,
            text=True,
            timeout=5
        )
        if result.returncode == 0:
            version = result.stdout.strip()
            return version if version.startswith('v') else f'v{version}'
    except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
        pass

    # Try SKILL.md frontmatter
    skill_md_path = os.path.join(skill_path, 'SKILL.md')
    if os.path.exists(skill_md_path):
        try:
            with open(skill_md_path, 'r', encoding='utf-8') as f:
                content = f.read()
                # Look for version: in frontmatter
                if content.startswith('---'):
                    frontmatter_end = content.find('---', 3)
                    if frontmatter_end > 0:
                        frontmatter = content[3:frontmatter_end]
                        for line in frontmatter.split('\n'):
                            if line.strip().startswith('version:'):
                                version = line.split(':', 1)[1].strip()
                                return version if version.startswith('v') else f'v{version}'
        except Exception:
            pass

    # Default version
    return 'v1.0.0'


def validate_skill_structure(skill_path: str) -> Tuple[bool, List[str]]:
    """
    Validate that skill has required structure for export.

    Args:
        skill_path: Path to skill directory

    Returns:
        Tuple of (is_valid, list_of_issues)
    """
    issues = []

    # Check if path exists and is directory
    if not os.path.exists(skill_path):
        issues.append(f"Path does not exist: {skill_path}")
        return False, issues

    if not os.path.isdir(skill_path):
        issues.append(f"Path is not a directory: {skill_path}")
        return False, issues

    # Check for SKILL.md
    skill_md_path = os.path.join(skill_path, 'SKILL.md')
    if not os.path.exists(skill_md_path):
        issues.append("SKILL.md not found (required)")
        return False, issues

    # Validate SKILL.md frontmatter
    try:
        with open(skill_md_path, 'r', encoding='utf-8') as f:
            content = f.read()

            if not content.startswith('---'):
                issues.append("SKILL.md missing frontmatter (must start with ---)")
            else:
                # Extract frontmatter
                frontmatter_end = content.find('---', 3)
                if frontmatter_end == -1:
                    issues.append("SKILL.md frontmatter not closed (missing second ---)")
                else:
                    frontmatter = content[3:frontmatter_end]

                    # Check for required fields
                    has_name = False
                    has_description = False
                    name_length = 0
                    desc_length = 0

                    for line in frontmatter.split('\n'):
                        line = line.strip()
                        if line.startswith('name:'):
                            has_name = True
                            name = line.split(':', 1)[1].strip()
                            name_length = len(name)
                            if name_length > MAX_NAME_LENGTH:
                                issues.append(f"name too long: {name_length} chars (max {MAX_NAME_LENGTH})")
                        elif line.startswith('description:'):
                            has_description = True
                            desc = line.split(':', 1)[1].strip()
                            desc_length = len(desc)
                            if desc_length > MAX_DESCRIPTION_LENGTH:
                                issues.append(f"description too long: {desc_length} chars (max {MAX_DESCRIPTION_LENGTH})")

                    if not has_name:
                        issues.append("SKILL.md missing 'name:' field in frontmatter")
                    if not has_description:
                        issues.append("SKILL.md missing 'description:' field in frontmatter")

    except Exception as e:
        issues.append(f"Error reading SKILL.md: {str(e)}")

    return len(issues) == 0, issues


def should_include_file(file_path: str, filename: str) -> bool:
    """
    Determine if a file should be included in export.

    Args:
        file_path: Full path to file
        filename: Just the filename

    Returns:
        True if file should be included
    """
    # Check excluded filenames
    if filename in EXCLUDE_FILES:
        return False

    # Check excluded patterns
    for pattern in EXCLUDE_FILES:
        if '*' in pattern:
            extension = pattern.replace('*', '')
            if filename.endswith(extension):
                return False

    # Check for sensitive files
    if filename in {'.env', 'credentials.json', 'secrets.json', 'api_keys.json'}:
        return False

    return True


def get_directory_size(path: str) -> int:
    """
    Calculate total size of directory in bytes.

    Args:
        path: Directory path

    Returns:
        Total size in bytes
    """
    total = 0
    for root, dirs, files in os.walk(path):
        # Filter excluded directories
        dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]

        for file in files:
            if should_include_file(os.path.join(root, file), file):
                try:
                    total += os.path.getsize(os.path.join(root, file))
                except OSError:
                    pass
    return total


def create_export_package(
    skill_path: str,
    output_dir: str,
    variant: str = 'desktop',
    version: str = 'v1.0.0',
    skill_name: str = None
) -> Dict:
    """
    Create optimized export package for specified variant.

    Args:
        skill_path: Path to skill directory
        output_dir: Where to save the .zip file
        variant: 'desktop' or 'api'
        version: Version string (e.g., 'v1.0.0')
        skill_name: Override skill name (default: directory name)

    Returns:
        Dict with 'success', 'zip_path', 'size_mb', 'files_included', 'message'
    """
    if skill_name is None:
        skill_name = os.path.basename(os.path.abspath(skill_path))

    # Create output filename
    zip_filename = f"{skill_name}-{variant}-{version}.zip"
    zip_path = os.path.join(output_dir, zip_filename)

    files_included = []
    total_size = 0

    try:
        with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED, compresslevel=9) as zipf:
            for root, dirs, files in os.walk(skill_path):
                # Filter excluded directories
                dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]

                # For API variant, exclude .claude-plugin
                if variant == 'api' and '.claude-plugin' in dirs:
                    dirs.remove('.claude-plugin')

                for file in files:
                    if not should_include_file(os.path.join(root, file), file):
                        continue

                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, skill_path)

                    # For API variant, apply additional filtering
                    if variant == 'api':
                        # Skip large documentation files
                        if file.endswith('.md') and file not in {'SKILL.md', 'README.md'}:
                            continue
                        # Skip example files
                        if 'examples' in arcname.lower():
                            continue

                    try:
                        zipf.write(file_path, arcname)
                        files_included.append(arcname)
                        total_size += os.path.getsize(file_path)
                    except Exception as e:
                        print(f"Warning: Could not add {arcname}: {e}", file=sys.stderr)

        # Check final size
        final_size = os.path.getsize(zip_path)
        size_mb = final_size / (1024 * 1024)

        # Warn if API package is too large
        if variant == 'api' and final_size > MAX_API_SIZE_BYTES:
            return {
                'success': False,
                'zip_path': zip_path,
                'size_mb': size_mb,
                'files_included': files_included,
                'message': f"API package too large: {size_mb:.2f} MB (max {MAX_API_SIZE_MB} MB)"
            }

        return {
            'success': True,
            'zip_path': zip_path,
            'size_mb': size_mb,
            'files_included': files_included,
            'message': f"Package created successfully: {len(files_included)} files, {size_mb:.2f} MB"
        }

    except Exception as e:
        return {
            'success': False,
            'zip_path': None,
            'size_mb': 0,
            'files_included': [],
            'message': f"Error creating package: {str(e)}"
        }


def generate_installation_guide(
    skill_name: str,
    version: str,
    desktop_package: Dict = None,
    api_package: Dict = None,
    output_dir: str = None
) -> str:
    """
    Generate platform-specific installation guide.

    Args:
        skill_name: Name of the skill
        version: Version string
        desktop_package: Desktop package info dict (optional)
        api_package: API package info dict (optional)
        output_dir: Where to save the guide

    Returns:
        Path to generated installation guide
    """
    guide_filename = f"{skill_name}-{version}_INSTALL.md"
    guide_path = os.path.join(output_dir, guide_filename)

    # Build guide content
    content = f"""# {skill_name} - Installation Guide

**Version:** {version}
**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

---

## 📦 Export Packages

"""

    if desktop_package and desktop_package['success']:
        content += f"""### Desktop/Web Package

**File:** `{os.path.basename(desktop_package['zip_path'])}`
**Size:** {desktop_package['size_mb']:.2f} MB
**Files:** {len(desktop_package['files_included'])} files included

✅ Optimized for Claude Desktop and claude.ai manual upload

"""

    if api_package and api_package['success']:
        content += f"""### API Package

**File:** `{os.path.basename(api_package['zip_path'])}`
**Size:** {api_package['size_mb']:.2f} MB
**Files:** {len(api_package['files_included'])} files included

✅ Optimized for programmatic Claude API integration

"""

    content += f"""---

## Cross-Platform Installation

This skill works on all platforms supporting the Agent Skills Open Standard.

### Using install.sh (Recommended)

If the skill includes an `install.sh` script:

```bash
# Auto-detect platform and install
./install.sh

# Install to specific platform
./install.sh --platform claude-code
./install.sh --platform copilot
./install.sh --platform cursor

# Project-level install
./install.sh --project

# Preview without installing
./install.sh --dry-run
```

### Manual Installation by Platform

#### Claude Code
```bash
# User-level
cp -r {skill_name}/ ~/.claude/skills/{skill_name}/

# Project-level
cp -r {skill_name}/ .claude/skills/{skill_name}/
```

#### GitHub Copilot
```bash
cp -r {skill_name}/ .github/skills/{skill_name}/
```

#### Cursor
```bash
cp -r {skill_name}/ .cursor/rules/{skill_name}/
```

#### Windsurf
```bash
cp -r {skill_name}/ .windsurf/skills/{skill_name}/
```

#### Cline
```bash
cp -r {skill_name}/ .clinerules/{skill_name}/
```

#### OpenAI Codex CLI
```bash
cp -r {skill_name}/ .codex/skills/{skill_name}/
```

#### Gemini CLI
```bash
cp -r {skill_name}/ .gemini/skills/{skill_name}/
```

### Claude Desktop / claude.ai (Web)

1. Locate the Desktop package: `{skill_name}-desktop-{{version}}.zip`
2. Open Claude Desktop or claude.ai
3. Go to **Settings > Skills > Upload skill**
4. Select the .zip file and confirm

### Claude API

1. Locate the API package: `{skill_name}-api-{{version}}.zip`
2. Upload programmatically:
```python
import anthropic
client = anthropic.Anthropic()
with open('{skill_name}-api-{{version}}.zip', 'rb') as f:
    skill = client.skills.create(file=f, name="{skill_name}")
```

---

## Platform Comparison

| Platform | Install Method | Updates | marketplace.json |
|----------|---------------|---------|-----------------|
| **Claude Code** | install.sh / copy | git pull | Optional |
| **GitHub Copilot** | install.sh / copy | git pull | Not used |
| **Cursor** | install.sh / copy | git pull | Not used |
| **Windsurf** | install.sh / copy | git pull | Not used |
| **Cline** | install.sh / copy | git pull | Not used |
| **Codex CLI** | install.sh / copy | git pull | Not used |
| **Gemini CLI** | install.sh / copy | git pull | Not used |
| **Desktop/Web** | .zip upload | Re-upload | Not used |
| **Claude API** | API upload | New upload | Not used |

---

## Technical Details

### What's Included

"""

    if desktop_package and desktop_package['success']:
        content += """**Desktop Package:**
- SKILL.md (core functionality)
- Complete scripts/ directory
- Full references/ documentation
- All assets/ and templates
- README.md and requirements.txt

"""

    if api_package and api_package['success']:
        content += """**API Package:**
- SKILL.md (required)
- Essential scripts only
- Minimal documentation (execution-focused)
- Size-optimized (< 8MB)

"""

    content += """### What's Excluded (Security)

For both packages:
- `.git/` (version control history)
- `__pycache__/` (compiled Python)
- `.env` files (environment variables)
- `credentials.json` (API keys/secrets)
- `.DS_Store` (system metadata)

For API package additionally:
- `.claude-plugin/` (Claude Code specific)
- Large documentation files
- Example files (size optimization)

---

## 🔧 Troubleshooting

### Upload fails with "File too large"

**Desktop/Web:**
- Maximum size varies by platform
- Try the API package instead (smaller)
- Contact support if needed

**API:**
- Maximum: 8MB
- The API package is already optimized
- May need to reduce documentation or scripts

### Skill doesn't activate

**Check:**
1. SKILL.md has valid frontmatter
2. `name:` field is present and ≤ 64 characters
3. `description:` field is present and ≤ 1024 characters
4. Description clearly explains when to use the skill

### API errors

**Common issues:**
- Missing beta headers (required!)
- Skill ID incorrect (check `skill.id` after upload)
- Network/pip install attempted (not allowed in API environment)

---

## 📚 Additional Resources

- **Export Guide:** See `references/export-guide.md` in the main repository
- **Cross-Platform Guide:** See `references/cross-platform-guide.md`
- **Main Documentation:** See the main README.md

---

## ✅ Verification Checklist

After installation, verify:

- [ ] Skill appears in Skills list
- [ ] Skill activates with relevant queries
- [ ] Scripts execute correctly
- [ ] Documentation is accessible
- [ ] No error messages on activation

---

**Need help?** Refer to the platform-specific documentation or the main repository guides.

**Generated by:** agent-skill-creator v4.0 cross-platform export system
"""

    # Write guide to file
    with open(guide_path, 'w', encoding='utf-8') as f:
        f.write(content)

    return guide_path


def export_skill(
    skill_path: str,
    variants: List[str] = ['desktop', 'api'],
    platform: str = None,
    version_override: str = None,
    output_dir: str = None
) -> Dict:
    """
    Main export function - validates, packages, and generates guides.

    Args:
        skill_path: Path to skill directory
        variants: List of variants to create ('desktop', 'api', or both)
        platform: Target platform for platform-specific output (optional)
        version_override: User-specified version (optional)
        output_dir: Where to save exports (default: exports/ in parent dir)

    Returns:
        Dict with export results
    """
    # Normalize path
    skill_path = os.path.abspath(skill_path)
    skill_name = os.path.basename(skill_path)

    # Determine output directory
    if output_dir is None:
        # Use exports/ in parent directory
        parent_dir = os.path.dirname(skill_path)
        output_dir = os.path.join(parent_dir, 'exports')

    # Create output directory if needed
    os.makedirs(output_dir, exist_ok=True)

    # Validate skill structure
    print("🔍 Validating skill structure...")
    valid, issues = validate_skill_structure(skill_path)
    if not valid:
        return {
            'success': False,
            'message': 'Skill validation failed',
            'issues': issues
        }
    print("✅ Skill structure valid")

    # Run spec validation if validate.py is available
    validate_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'validate.py')
    if os.path.exists(validate_script):
        print("🔍 Running spec validation...")
        try:
            result = subprocess.run(
                [sys.executable, validate_script, skill_path, '--json'],
                capture_output=True, text=True, timeout=30
            )
            if result.returncode != 0:
                import json as _json
                try:
                    val_result = _json.loads(result.stdout)
                    if val_result.get('errors'):
                        print(f"⚠️  Spec validation warnings: {len(val_result['errors'])} errors")
                        for err in val_result['errors']:
                            print(f"   - {err}")
                except (ValueError, KeyError):
                    pass
            else:
                print("✅ Spec validation passed")
        except (subprocess.TimeoutExpired, Exception):
            print("⚠️  Spec validation skipped (script error)")

    # Run security scan if security_scan.py is available
    security_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'security_scan.py')
    if os.path.exists(security_script):
        print("🔍 Running security scan...")
        try:
            result = subprocess.run(
                [sys.executable, security_script, skill_path, '--json'],
                capture_output=True, text=True, timeout=30
            )
            if result.returncode != 0:
                import json as _json
                try:
                    sec_result = _json.loads(result.stdout)
                    if sec_result.get('issues'):
                        print(f"⚠️  Security issues found: {len(sec_result['issues'])}")
                        for issue in sec_result['issues'][:5]:
                            print(f"   - [{issue.get('severity', 'unknown')}] {issue.get('description', '')}")
                except (ValueError, KeyError):
                    pass
            else:
                print("✅ Security scan passed")
        except (subprocess.TimeoutExpired, Exception):
            print("⚠️  Security scan skipped (script error)")

    # Determine version
    version = get_skill_version(skill_path, version_override)
    print(f"📌 Version: {version}")

    # Create packages
    results = {
        'success': True,
        'version': version,
        'packages': {}
    }

    if 'desktop' in variants:
        print("\n🔨 Creating Desktop/Web package...")
        desktop_result = create_export_package(
            skill_path, output_dir, 'desktop', version, skill_name
        )
        results['packages']['desktop'] = desktop_result
        if desktop_result['success']:
            print(f"✅ Desktop package: {os.path.basename(desktop_result['zip_path'])} ({desktop_result['size_mb']:.2f} MB)")
        else:
            print(f"❌ Desktop package failed: {desktop_result['message']}")
            results['success'] = False

    if 'api' in variants:
        print("\n🔨 Creating API package...")
        api_result = create_export_package(
            skill_path, output_dir, 'api', version, skill_name
        )
        results['packages']['api'] = api_result
        if api_result['success']:
            print(f"✅ API package: {os.path.basename(api_result['zip_path'])} ({api_result['size_mb']:.2f} MB)")
        else:
            print(f"❌ API package failed: {api_result['message']}")
            results['success'] = False

    # Generate installation guide
    if results['success']:
        print("\n📄 Generating installation guide...")
        guide_path = generate_installation_guide(
            skill_name,
            version,
            desktop_package=results['packages'].get('desktop'),
            api_package=results['packages'].get('api'),
            output_dir=output_dir
        )
        results['guide_path'] = guide_path
        print(f"✅ Installation guide: {os.path.basename(guide_path)}")

    return results


def main():
    """CLI interface for export_utils.py"""
    if len(sys.argv) < 2:
        print("""
Usage: python export_utils.py <skill-path> [options]

Arguments:
  skill-path              Path to skill directory

Options:
  --variant VARIANT       Export variant: desktop, api, or both (default: both)
  --version VERSION       Override version (default: auto-detect)
  --output-dir DIR        Output directory (default: exports/)

Examples:
  python export_utils.py ./my-skill
  python export_utils.py ./my-skill --variant desktop
  python export_utils.py ./my-skill --version 2.0.1
  python export_utils.py ./my-skill --variant api --output-dir ./dist
""")
        sys.exit(1)

    skill_path = sys.argv[1]

    # Parse options
    variants = ['desktop', 'api']  # default: both
    version_override = None
    output_dir = None

    i = 2
    while i < len(sys.argv):
        if sys.argv[i] == '--variant':
            variant_arg = sys.argv[i + 1]
            if variant_arg == 'both':
                variants = ['desktop', 'api']
            else:
                variants = [variant_arg]
            i += 2
        elif sys.argv[i] == '--version':
            version_override = sys.argv[i + 1]
            i += 2
        elif sys.argv[i] == '--output-dir':
            output_dir = sys.argv[i + 1]
            i += 2
        else:
            print(f"Unknown option: {sys.argv[i]}")
            sys.exit(1)

    # Run export
    print(f"\n🚀 Exporting skill: {os.path.basename(skill_path)}\n")
    results = export_skill(skill_path, variants, version_override, output_dir)

    # Print summary
    print(f"\n{'='*60}")
    if results['success']:
        print("✅ Export completed successfully!")
        print("\n📦 Packages created:")
        for variant, package in results['packages'].items():
            if package['success']:
                print(f"   - {variant.capitalize()}: {os.path.basename(package['zip_path'])}")
        if 'guide_path' in results:
            print(f"\n📄 Installation guide: {os.path.basename(results['guide_path'])}")
        print(f"\n🎯 All files saved to: {output_dir or 'exports/'}")
    else:
        print("❌ Export failed!")
        if 'issues' in results:
            print("\nIssues found:")
            for issue in results['issues']:
                print(f"   - {issue}")
    print(f"{'='*60}\n")

    sys.exit(0 if results['success'] else 1)


if __name__ == '__main__':
    main()
