#!/usr/bin/env python3
"""
Build script for Text Texture Generator addon.
Creates ZIP packages for both full and free versions.
"""

import os
import sys
import json
import shutil
import zipfile
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
import tempfile
import argparse
import fnmatch
import ast
import tokenize
import io
import re

def minify_python_code(source_code: str) -> str:
    """
    Minify Python source code by removing comments and docstrings.
    
    This function:
    1. Removes all # comments (inline and standalone)
    2. Preserves bl_info dictionary (Blender requirement)
    3. Removes other function/class docstrings
    4. Optimizes whitespace (max 1 blank line between definitions)
    5. Maintains valid Python syntax
    
    Args:
        source_code: Python source code to minify
        
    Returns:
        Minified Python code as a string
    """
    # Parse the code to find docstring line ranges to remove
    try:
        tree = ast.parse(source_code)
    except SyntaxError:
        return source_code
    
    # Collect line numbers of docstrings to remove
    docstring_lines_to_remove = set()
    
    def process_body(body, parent_is_module=False):
        """Process a body of statements to find docstrings."""
        if not body:
            return
        
        first_stmt = body[0]
        if isinstance(first_stmt, ast.Expr):
            value = first_stmt.value
            if isinstance(value, (ast.Str, ast.Constant)):
                # Check if this is a docstring (string literal as first statement)
                if isinstance(value, ast.Constant) and not isinstance(value.value, str):
                    return
                
                # Always remove docstrings (module, class, and function)
                # The bl_info DICT itself will be preserved by not being marked
                # Mark these lines for removal
                for lineno in range(first_stmt.lineno, first_stmt.end_lineno + 1):
                    docstring_lines_to_remove.add(lineno)
    
    # Process module-level docstrings
    process_body(tree.body, parent_is_module=True)
    
    # Process function and class docstrings
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            process_body(node.body, parent_is_module=False)
    
    # First pass: Remove comments and docstring lines using tokenize
    # This safely handles # symbols inside strings
    result_lines = []
    try:
        tokens = tokenize.generate_tokens(io.StringIO(source_code).readline)
        last_lineno = -1
        last_col = 0
        
        for tok in tokens:
            token_type = tok[0]
            token_string = tok[1]
            start_line, start_col = tok[2]
            end_line, end_col = tok[3]
            
            # Skip comments
            if token_type == tokenize.COMMENT:
                continue
            
            # Skip tokens that are part of docstrings we want to remove
            if start_line in docstring_lines_to_remove:
                continue
            
            # Handle newlines and track lines
            if start_line > last_lineno:
                last_col = 0
            
            # Add spacing if needed
            if start_col > last_col:
                result_lines.append(" " * (start_col - last_col))
            
            # Add the token
            result_lines.append(token_string)
            
            # Update position tracking
            last_col = end_col
            last_lineno = end_line
            
    except tokenize.TokenError:
        # If tokenization fails, return original
        return source_code
    
    # Join tokens back into source
    result = "".join(result_lines)
    
    # Second pass: Optimize whitespace (max 1 blank line between definitions)
    lines = result.split('\n')
    optimized_lines = []
    blank_count = 0
    
    for line in lines:
        if line.strip() == '':
            blank_count += 1
            # Allow max 1 consecutive blank line
            if blank_count <= 1:
                optimized_lines.append(line)
        else:
            blank_count = 0
            optimized_lines.append(line)
    
    # Remove trailing blank lines
    while optimized_lines and optimized_lines[-1].strip() == '':
        optimized_lines.pop()
    
    return '\n'.join(optimized_lines)


def get_project_root() -> Path:
    """Get the project root directory."""
    return Path(__file__).parent.parent

def load_config() -> Dict:
    """Load build configuration."""
    config_path = get_project_root() / "build" / "config.json"
    with open(config_path, 'r') as f:
        return json.load(f)

def load_exclusions_config() -> Dict:
    """Load file exclusions configuration."""
    config_path = get_project_root() / "build" / "file_exclusions.json"
    with open(config_path, 'r') as f:
        return json.load(f)

def clean_directory(path: Path):
    """Clean a directory by removing all contents."""
    if path.exists():
        shutil.rmtree(path)
    path.mkdir(parents=True, exist_ok=True)

def should_exclude_file(file_path: Path, source_dir: Path, exclude_patterns: List[str], exclude_files: List[str]) -> bool:
    """Check if a file should be excluded based on patterns and specific files."""
    file_str = str(file_path)
    name = file_path.name
    
    # Get relative path from source directory for exclusion checking
    try:
        rel_path = file_path.relative_to(source_dir)
        rel_path_str = str(rel_path)
    except ValueError:
        rel_path_str = str(file_path)
    
    # Check specific file exclusions
    for exclude_file in exclude_files:
        # Handle directory exclusions (ending with /)
        if exclude_file.endswith('/'):
            exclude_dir = exclude_file.rstrip('/')
            if rel_path_str.startswith(exclude_dir + '/') or rel_path_str == exclude_dir:
                return True
        # Handle exact file matches
        elif rel_path_str == exclude_file:
            return True
    
    # Check general patterns (for cache files, etc.)
    for pattern in exclude_patterns:
        if pattern in file_str or pattern in name:
            return True
        
        # Handle wildcards
        if pattern.startswith('*.'):
            extension = pattern[1:]
            if file_str.endswith(extension):
                return True
        
        # Handle glob patterns
        if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(rel_path_str, pattern):
            return True
    
    return False

def _format_version_tuple(version_tuple: tuple) -> str:
    """Format version tuple for insertion into bl_info."""
    return "(" + ", ".join(str(part) for part in version_tuple) + ")"


def _edition_version_tuple(version: str, version_type: str) -> tuple:
    """Return edition-specific version tuple without altering the provided version."""
    try:
        major_str, minor_str, patch_str = version.split(".", 2)
        major, minor, patch = int(major_str), int(minor_str), int(patch_str)
    except ValueError:
        # Fallback if version string unexpected
        return (1, 0, 0)

    return (major, minor, patch)


def _apply_edition_overrides(rel_path: Path, source_code: str, version_type: str, version: str) -> str:
    """Inject edition-specific metadata into copied source files."""
    if rel_path.as_posix() == "__init__.py":
        edition_name = "Text Texture Generator Pro" if version_type == "full" else "Text Texture Generator (Free)"
        target_version_tuple = _edition_version_tuple(version, version_type)

        source_code = re.sub(
            r'"name":\s*"Text Texture Generator[^"]*"',
            f'"name": "{edition_name}"',
            source_code,
            count=1
        )

        source_code = re.sub(
            r'"version":\s*\([^\)]+\)',
            f'"version": {_format_version_tuple(target_version_tuple)}',
            source_code,
            count=1
        )
    return source_code


def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None, minify: bool = True, version_type: str = "free", version: str = "1.0.0"):
    """Copy source files to destination, excluding specified patterns and files."""
    if exclude_files is None:
        exclude_files = []
    
    print(f"Copying files from {source_dir} to {dest_dir}")
    print(f"Excluding files: {exclude_files}")
    print(f"Excluding patterns: {exclude_patterns}")
    
    copied_count = 0
    excluded_count = 0
    
    for item in source_dir.rglob('*'):
        if item.is_file():
            if should_exclude_file(item, source_dir, exclude_patterns, exclude_files):
                rel_path = item.relative_to(source_dir)
                print(f"  Excluded: {rel_path}")
                excluded_count += 1
            else:
                # Calculate relative path
                rel_path = item.relative_to(source_dir)
                dest_file = dest_dir / rel_path
                
                # Create parent directories
                dest_file.parent.mkdir(parents=True, exist_ok=True)
                
                # Copy file with optional minification for Python files
                if minify and item.suffix == '.py':
                    # Read, minify, and write Python files
                    try:
                        source_code = item.read_text(encoding='utf-8')
                        source_code = _apply_edition_overrides(rel_path, source_code, version_type, version)
                        minified_code = minify_python_code(source_code)
                        dest_file.write_text(minified_code, encoding='utf-8')
                        print(f"  Copied (minified): {rel_path}")
                    except Exception as e:
                        # If minification fails, fall back to regular copy
                        print(f"  Warning: Could not minify {rel_path}, copying as-is: {e}")
                        source_code = item.read_text(encoding='utf-8')
                        source_code = _apply_edition_overrides(rel_path, source_code, version_type, version)
                        dest_file.write_text(source_code, encoding='utf-8')
                        print(f"  Copied: {rel_path}")
                else:
                    # Copy non-Python files or when minification is disabled
                    if item.suffix == '.py':
                        source_code = item.read_text(encoding='utf-8')
                        source_code = _apply_edition_overrides(rel_path, source_code, version_type, version)
                        dest_file.write_text(source_code, encoding='utf-8')
                    else:
                        shutil.copy2(item, dest_file)
                    print(f"  Copied: {rel_path}")
                copied_count += 1
    
    print(f"Files copied: {copied_count}, excluded: {excluded_count}")

def create_zip_package(source_dir: Path, output_file: Path, addon_name: str):
    """Create a ZIP package from source directory."""
    print(f"Creating ZIP package: {output_file}")
    
    with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zip_file:
        for file_path in source_dir.rglob('*'):
            if file_path.is_file():
                # Calculate archive path (include addon name as root folder)
                rel_path = file_path.relative_to(source_dir)
                archive_path = Path(addon_name) / rel_path
                
                zip_file.write(file_path, archive_path)
                print(f"  Added to ZIP: {archive_path}")
    
    print(f"ZIP package created: {output_file}")

def get_version_from_git() -> Optional[str]:
    """Get version from git tags."""
    project_root = get_project_root()
    try:
        config = load_config()
        version_pattern = config['versions']['git_tag_pattern']
    except Exception as e:
        print(f"Warning: Could not load version pattern: {e}")
        version_pattern = r"^v?([0-9]+\.[0-9]+\.[0-9]+)$"

    version_regex = re.compile(version_pattern)

    try:
        result = subprocess.run(
            ['git', 'tag', '-l', '--sort=-version:refname'],
            capture_output=True,
            text=True,
            cwd=project_root,
            check=True
        )
    except subprocess.CalledProcessError as e:
        print(f"Warning: Could not get version from git: {e}")
        return None
    except FileNotFoundError:
        print("Warning: Git executable not found.")
        return None
    except Exception as e:
        print(f"Warning: Unexpected error while reading git tags: {e}")
        return None

    for tag in result.stdout.splitlines():
        tag = tag.strip()
        if not tag:
            continue
        match = version_regex.match(tag)
        if match:
            # Prefer captured group when pattern defines one
            if match.lastindex:
                return match.group(1)
            return tag.lstrip('v')
    return None

def build_addon(version_type: str, version: Optional[str] = None, output_dir: Optional[Path] = None, minify: bool = True):
    """Build addon for specified version type."""
    # Use current working directory as project root (supports testing with temp dirs)
    project_root = Path.cwd()
    
    # Load configs from project root
    config_path = project_root / "build" / "config.json"
    with open(config_path, 'r') as f:
        config = json.load(f)
    
    exclusions_path = project_root / "build" / "file_exclusions.json"
    with open(exclusions_path, 'r') as f:
        exclusions_config = json.load(f)
    
    # Determine version
    if version is None:
        version = get_version_from_git() or "1.0.0"
    
    # Set up paths
    source_dir = project_root / config['build']['source_dir']
    if output_dir is None:
        output_dir = project_root / config['build']['dist_dir']
    temp_dir = project_root / config['build']['temp_dir']
    
    # Clean and create directories
    clean_directory(temp_dir)
    output_dir.mkdir(parents=True, exist_ok=True)
    
    # Sync version files for the specific version type (simplified - no feature flags)
    print(f"Synchronizing version files for {version_type} version {version}")
    sync_result = subprocess.run([
        'python3', 'build/sync_version.py',
        '--version', version,
        '--type', version_type
    ], cwd=project_root)
    
    if sync_result.returncode != 0:
        raise RuntimeError(f"Version synchronization failed")
    
    # Get version-specific exclusions
    version_config = exclusions_config['version_configs'][version_type]
    exclude_files = version_config.get('exclude_files', [])
    exclude_patterns = config['build']['exclude_patterns'] + version_config.get('exclude_patterns', [])
    
    print(f"\n📋 Build configuration for {version_type} version:")
    print(f"Max texture size: {version_config.get('max_texture_size', 'unlimited')}")
    print(f"Max concurrent operations: {version_config.get('max_concurrent_operations', 'unlimited')}")
    if exclude_files:
        print(f"Files to exclude: {exclude_files}")
    if version_config.get('exclude_patterns'):
        print(f"Additional patterns to exclude: {version_config.get('exclude_patterns')}")
    
    # Copy source files to temp directory with version-specific exclusions
    build_temp_dir = temp_dir / f"{config['project']['id']}_{version_type}"
    copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files, minify, version_type=version_type, version=version)
    
    # Create output filename
    version_suffix = f"_{version_type}" if version_type == "free" else ""
    output_filename = f"{config['project']['id']}_v{version}{version_suffix}.zip"
    output_file = output_dir / output_filename
    
    # Create ZIP package
    create_zip_package(build_temp_dir, output_file, config['project']['id'])
    
    # Clean up temp directory
    shutil.rmtree(temp_dir)
    
    print(f"\n✅ Build complete!")
    print(f"Package: {output_file}")
    print(f"Version: {version} ({version_type})")
    
    return output_file

def build_all_versions(version: Optional[str] = None, minify: bool = True):
    """Build both full and free versions."""
    print("Building all versions...")
    
    built_packages = []
    
    # Build full version
    print("\n" + "="*50)
    print("BUILDING FULL VERSION")
    print("="*50)
    resolved_version = version or get_version_from_git() or "1.0.0"

    full_package = build_addon("full", resolved_version, minify=minify)
    built_packages.append(("full", full_package))
    
    # Build free version
    print("\n" + "="*50)
    print("BUILDING FREE VERSION")
    print("="*50)
    free_package = build_addon("free", resolved_version, minify=minify)
    built_packages.append(("free", free_package))
    
    print("\n" + "="*50)
    print("BUILD SUMMARY")
    print("="*50)
    for version_type, package_path in built_packages:
        file_size = package_path.stat().st_size / 1024  # KB
        print(f"{version_type.upper():>4}: {package_path.name} ({file_size:.1f} KB)")
    
    # Restore development files to the full-version state so local testing keeps premium features visible
    project_root = get_project_root()
    restore_sync = subprocess.run(
        [
            'python3', 'build/sync_version.py',
            '--version', resolved_version,
            '--type', 'full'
        ],
        cwd=project_root
    )
    if restore_sync.returncode != 0:
        raise RuntimeError("Failed to resync version metadata after build")

    return built_packages

def main():
    """Main entry point."""
    parser = argparse.ArgumentParser(description="Build Text Texture Generator addon")
    parser.add_argument("--version", help="Version to build (defaults to git tag)")
    parser.add_argument("--type", choices=["free", "full", "all"], default="all",
                       help="Version type to build")
    parser.add_argument("--output", type=Path, help="Output directory")
    parser.add_argument("--minify", dest="minify", action="store_true", default=True,
                       help="Minify Python code (default: enabled)")
    parser.add_argument("--no-minify", dest="minify", action="store_false",
                       help="Disable Python code minification")
    
    args = parser.parse_args()
    
    try:
        if args.type == "all":
            build_all_versions(args.version, args.minify)
        else:
            build_addon(args.type, args.version, args.output, args.minify)
            
    except Exception as e:
        print(f"❌ Build failed: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()
