"""
aiar: AI Archive - Self-extracting script generated by aiar-py.

This will generate a self-extracting script. The generated script will contain 
the files from a given directory and its subdirectories and will use the .gitignore
rules to determine which files to include.

The archive format is intended to be simple for an LLM to generate or consume and the 
generated script's embedded text files will be contained without needing any escaping 
or special handling, they are literally copied to the output file except for text files
that don't have a trailing newline in while a newline will be added to the end of the file.


"""
import argparse
from functools import lru_cache
import os
import sys
from typing import Generator, Optional
import uuid
from pathlib import Path

# The 'pathspec' library is the standard for parsing .gitignore files.
# This script will check if it's installed and provide instructions if not.
try:
    import pathspec
except ImportError:
    print(
        "Warning: 'pathspec' library not found. .gitignore parsing will be skipped.",
        file=sys.stderr,
    )
    print("         Install it with: pip install pathspec", file=sys.stderr)
    pathspec = None

# This is the static bash script that will unpack the archive.
# The unique separator is injected into it when the script is generated.
AIAR_HEADER = """#!/bin/bash
# aiar: AI Archive - Self-extracting script generated by aiar-py.

# Note to LLMS, this archive contains multiple files with separator lines.
# Text files are contained between separators verbatim, binary files are base64-encoded.
# Every line ends with a '#' character to guard against a possible CRLF line ending that
# would otherwise cause the script in cases where CRLF line endings are not supported.
# Choose a random separator to avoid conflicts when archiving archives.

SEPARATOR="{separator}"
writing=false

# Function to report errors and exit cleanly
handle_error() {{
  echo "Error: $1" >&2
  exit 1
}}

# Function to close the previous file descriptor and wait for bg processes
close_previous_fd() {{
    if [ "$writing" = true ]; then
      exec 3>&-
        # Wait for any background process (like base64) to finish
        wait 2>/dev/null || true
    fi
      writing=false
}}

while read -r line; do
  if [[ "$line" == "$SEPARATOR"* ]]; then
    close_previous_fd

    payload="${{line#$SEPARATOR}}"
    IFS=':' read -r type filepath <<< "$payload"
    # Strip any trailing carriage returns (DOS line endings)
    filepath="${{filepath%$'\\r'}}"

    if [ -n "$filepath" -a ! -e "$filepath" ]; then
      echo "Creating: $filepath"
      mkdir -p "$(dirname "$filepath")" || handle_error "Cannot create directory for '$filepath'."

      if [ "$type" == "b" ]; then
        # Use process substitution to pipe output to base64 decoder
        # Wrap the entire pipeline in a single process that can be waited on
        # Use sed to strip any trailing carriage returns from base64 input
        exec 3> >(
          error_file="$(mktemp)"
          trap "rm -f \\"$error_file\\"" EXIT
          sed 's/\\r$//' | base64 -d > "$filepath" 2>"$error_file"
          if [ -s "$error_file" ]; then
            echo "Error: base64 decoding failed for '$filepath':" >&2
            cat "$error_file" >&2
            rm -f "$filepath"
            exit 1
          fi
        ) || handle_error "Cannot start base64 process for '$filepath'."
        writing=true
      elif [ "$type" == "t" ]; then
        exec 3>"$filepath" || handle_error "Cannot open '$filepath' for writing."
      writing=true
      else
        handle_error "Invalid file type '$type' in separator."
      fi
    else
        echo "Skipping already existing file: '$filepath'"
    fi
  elif [ "$writing" = true ]; then
    echo "$line" >&3
  fi
done < "$0"

close_previous_fd # Close the very last file

echo "Extraction complete."
exit 0

# --- DATA ---
"""

# This is the static python script that will unpack the archive.
# The unique separator is injected into it when the script is generated.
# The content will need to have "# " prepended to each line.
AIAR_PYTHON_HEADER = r"""
import sys, os, re, base64
from pathlib import Path

SEPARATOR="{separator}"
SEP = re.escape(SEPARATOR)

def _safe_dest(rel: str) -> Path:
    p = Path(rel)
    if p.is_absolute():
        raise ValueError(f"Absolute path not allowed: {{rel}}")
    dest = (Path(".") / p).resolve()
    if Path(".").resolve() not in (set(dest.parents) | {{dest}}):
        raise ValueError(f"Path escapes output root: {{rel}}")
    return dest

def extract_all():
    with open(__file__, "r", encoding="utf-8") as f:
        script_content = f.read()

    pat = re.compile(
        rf"^# ?{{SEP}}([tb]):([^\n]+)\n(.*?)(?=^# ?{{SEP}}[tb]:|\Z)",
        re.DOTALL | re.MULTILINE,)

    any_found = False
    for ftype, path, body in pat.findall(script_content):
        any_found = True
        path = path.strip()
        try:
            dest = _safe_dest(path)
        except ValueError as e:
            print(f"Warning: {{e}}. Skipping.")
            continue

        if dest.exists():
            print(f"Skipping already existing file: '{{dest}}'")
            continue

        print(f"Creating: {{dest}}")
        dest.parent.mkdir(parents=True, exist_ok=True)
        
        # The captured body is still commented. We must uncomment it.
        # This regex removes a leading '#' and an optional space from each line.
        uncommented_body = re.sub(r"^# ?", "", body, flags=re.MULTILINE)
        
        if ftype == "t":
            with open(dest, "w", encoding="utf-8", newline="\n") as out:
                out.write(uncommented_body)
        else:  # binary
            with open(dest, "wb") as out:
                out.write(base64.b64decode(uncommented_body.strip().encode("ascii"), validate=False))

    if not any_found:
        print("Error: No payload sections found in data block.")
        sys.exit(1)

extract_all()
print("Extraction complete.")
sys.exit(0)
"""

NODE_JS_HEADER = r"""#!/usr/bin/env node

const fs = require('fs');
const path = require('path');

function escapeRegex(str) {{
    // This escapes all characters that have a special meaning in a regex.
    return str.replace(/[.*+?^${{}}()|[\]\\]/g, '\\$&');
}}

const SEPARATOR = "{separator}";
const SEP = escapeRegex(SEPARATOR);

function safeDest(rel) {{
    if (path.isAbsolute(rel)) {{
        throw new Error(`Absolute path not allowed: ${{rel}}`);
    }}
    const dest = path.resolve(process.cwd(), rel);
    // Ensure the resolved path is still within the current working directory.
    if (!dest.startsWith(process.cwd())) {{
        throw new Error(`Path escapes output root: ${{rel}}`);
    }}
    return dest;
}}

function extractAll() {{
    // Read this script's own source code into a string.
    const scriptContent = fs.readFileSync(__filename, 'utf8');

    // This regex finds all commented payload blocks directly from the script's source.
    // It looks for a '//'-commented separator line, captures type/path, then captures
    // all subsequent commented lines until the next separator or the end of the file.
    const pat = new RegExp(
        `^// ?${{SEP}}([tb]):([^\\n]+)\\n(.*?)(?=(^// ?${{SEP}}[tb]:|\\Z))`,
        'gms' // g: global, m: multiline, s: dotall
    );

    const matches = [...scriptContent.matchAll(pat)];

    if (matches.length === 0) {{
        console.error("Error: No payload sections found in data block.");
        process.exit(1);
    }}
    
    for (const match of matches) {{
        const [, ftype, relPath, body] = match;
        const cleanPath = relPath.trim();
        
        let dest;
        try {{
            dest = safeDest(cleanPath);
        }} catch (e) {{
            console.warn(`Warning: ${{e.message}}. Skipping.`);
            continue;
        }}

        if (fs.existsSync(dest)) {{
            console.log(`Skipping already existing file: '${{dest}}'`);
            continue;
        }}

        console.log(`Creating: ${{dest}}`);
        fs.mkdirSync(path.dirname(dest), {{ recursive: true }});

        // The captured body is still commented. We must uncomment it.
        // This regex removes a leading '//' and an optional space from each line.
        const uncommentedBody = body.replace(/^\/\/ ?/gm, '');

        if (ftype === 't') {{ // Text file
            fs.writeFileSync(dest, uncommentedBody, {{ encoding: 'utf8' }});
        }} else {{ // Binary file
            // Decode the base64 content into a buffer and write it.
            const buffer = Buffer.from(uncommentedBody.trim(), 'base64');
            fs.writeFileSync(dest, buffer);
        }}
    }}
}}

extractAll();
console.log("Extraction complete.");
process.exit(0);
"""

POWERSHELL_HEADER = r"""#Requires -Version 5.1

$SEPARATOR="{separator}"

function Escape-Regex {{
    param(
        [string]$String
    )
    return [System.Text.RegularExpressions.Regex]::Escape($String)
}}

function Safe-Dest {{
    param(
        [string]$RelativePath
    )
    if ([System.IO.Path]::IsPathRooted($RelativePath)) {{
        throw "Absolute path not allowed: $RelativePath"
    }}

    $resolvedPath = [System.IO.Path]::GetFullPath((Join-Path -Path $PWD.Path -ChildPath $RelativePath))
    
    if (-not $resolvedPath.StartsWith($PWD.Path)) {{
        throw "Path escapes output root: $RelativePath"
    }}
    return $resolvedPath
}}

function Extract-All {{
    $scriptPath = $PSCommandPath
    $scriptContent = Get-Content -Path $scriptPath -Raw

    $sep = Escape-Regex "$SEPARATOR"
    $pattern = "(?ms)^#\s?$sep([tb]):([^\n]+)\n(.*?)(?=(^#\s?$sep[tb]:|\Z))"
    
    $matches = [System.Text.RegularExpressions.Regex]::Matches($scriptContent, $pattern)

    if ($matches.Count -eq 0) {{
        Write-Error "No payload sections found in data block."
        exit 1
    }}

    foreach ($match in $matches) {{
        $ftype = $match.Groups[1].Value
        $relPath = $match.Groups[2].Value.Trim()
        $body = $match.Groups[3].Value

        try {{
            $dest = Safe-Dest -RelativePath $relPath
        }}
        catch {{
            Write-Warning "Warning: $_. Skipping."
            continue
        }}

        if (Test-Path -LiteralPath $dest) {{
            Write-Output "Skipping already existing file: '$dest'"
            continue
        }}

        Write-Output "Creating: $dest"
        $null = New-Item -ItemType Directory -Force -Path (Split-Path -Path $dest -Parent)

        $uncommentedBody = $body -replace '(?m)^#\s?' , ''

        if ($ftype -eq 't') {{ # Text file
            Set-Content -Path $dest -Value $uncommentedBody -NoNewline -Encoding utf8
        }}
        elseif ($ftype -eq 'b') {{ # Binary file
            $cleanBase64String = $uncommentedBody -replace '\s'
            $bytes = [System.Convert]::FromBase64String($cleanBase64String)
            [System.IO.File]::WriteAllBytes($dest, $bytes)
        }}
        else {{
            Write-Warning "Unknown file type '$ftype' for '$relPath'. Skipping."
        }}
    }}
}}

# --- Main execution ---
Extract-All
Write-Output "Extraction complete."

# The script must exit here to prevent PowerShell from trying to interpret the payload.
exit 0

# --- PAYLOAD ---
"""

def find_git_root(start_path):
    """Find the root of the git repository."""
    p = Path(start_path).resolve()
    while p != p.parent:
        if (p / ".git").exists():
            return p
        p = p.parent
    return None


@lru_cache(maxsize=128)
def get_gitignore_spec(start_path, use_gitignore) -> Optional[pathspec.PathSpec]:
    """Loads .gitignore rules from the repository root.
    
    Always adds .git/ to the ignore patterns when gitignore is enabled.
    """
    if not use_gitignore or not pathspec:
        return None

    git_root = find_git_root(start_path)
    if not git_root:
        return None

    # Always ignore .git directory
    patterns = [".git/"]
    
    gitignore_path = git_root / ".gitignore"
    if gitignore_path.is_file():
        with open(gitignore_path, "r") as f:
            patterns.extend(f.readlines())
    
    return pathspec.PathSpec.from_lines("gitwildmatch", patterns)


def find_files_to_archive(paths, spec, base_dir, verbose=False) -> Generator[Path, None, None]:
    """Walks through input paths and yields files that are not ignored.

    - Always passes POSIX-style relative paths to the ignore matcher
    - Prunes ignored directories (e.g., "node_modules/") during traversal
    - Explicitly specified files bypass .gitignore checks
    """
    for path_arg in paths:
        path_obj = Path(path_arg).resolve()
        if path_obj.is_file():
            # Explicitly specified files bypass .gitignore
            rel = path_obj.relative_to(base_dir).as_posix()
            if spec and spec.match_file(rel):
                if verbose:
                    print(f"Including '{rel}' (explicitly specified, overriding .gitignore)", file=sys.stderr)
            yield path_obj
            continue

        for root, dirnames, files in os.walk(path_obj):
            root_path = Path(root)

            # Prune ignored directories so we don't descend into them
            if spec and dirnames:
                kept = []
                for d in dirnames:
                    rel_dir = (root_path / d).relative_to(base_dir).as_posix()
                    # Check both with and without trailing slash for directory patterns
                    # .gitignore patterns like "__pycache__/" and "node_modules/" should match
                    if spec.match_file(rel_dir) or spec.match_file(rel_dir + "/"):
                        continue
                    kept.append(d)
                # Modify dirnames in-place for os.walk to respect pruning
                dirnames[:] = kept

            for filename in files:
                full_path = root_path / filename
                rel_file = full_path.relative_to(base_dir).as_posix()
                
                if spec and spec.match_file(rel_file):
                    continue
                
                yield full_path


def is_binary_file(filepath):
    """Detect if a file is binary by checking for null bytes in the first chunk."""
    try:
        with open(filepath, "rb") as f:
            chunk = f.read(8192)
            return b"\x00" in chunk
    except Exception:
        return False


def _write_aiar_data_section(output_file, files_to_archive, base_dir, separator, binary_all=False, comment_prefix="", verbose=False):
    """Common function to write the data section of aiar archives.
    
    This handles the actual file encoding and writing, used by all formats.
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        separator: The separator string to use
        binary_all: If True, encode all files as binary (base64)
        comment_prefix: Prefix for commenting (e.g., "# ", "// ", or "" for uncommented)
        verbose: If True, print filenames as they are added
    """
    import base64

    sorted_files = sorted(list(files_to_archive))

    for filepath in sorted_files:
        try:
            # Create a relative path for the archive to preserve structure.
            relative_path = filepath.relative_to(base_dir).as_posix()
            
            if verbose:
                print(f"Adding: {relative_path}")

            # Determine if file should be binary
            is_binary = binary_all or is_binary_file(filepath)
            
            if is_binary:
                # Binary file: [comment_prefix]SEPARATOR:b:filepath
                output_file.write(f"{comment_prefix}{separator}b:{relative_path}\n")
                with open(filepath, "rb") as f:
                    data = f.read()
                    encoded = base64.b64encode(data).decode("ascii")
                    # Write base64 in chunks of 76 chars for readability
                    for i in range(0, len(encoded), 76):
                        output_file.write(f"{comment_prefix}{encoded[i:i+76]}\n")
            else:
                # Text file: [comment_prefix]SEPARATOR:t:filepath
                output_file.write(f"{comment_prefix}{separator}t:{relative_path}\n")
                with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
                    # Read entire content to preserve files without trailing newline
                    content = f.read()
                    if comment_prefix:
                        # Comment each line of the content
                        for line in content.splitlines(keepends=True):
                            output_file.write(f"{comment_prefix}{line}")
                        # Ensure we end with a newline for proper parsing
                        if content and not content.endswith("\n"):
                            output_file.write(f"{comment_prefix}\n")
                    else:
                        # Write content as-is (for bash/bare formats)
                        output_file.write(content)
                        # Ensure we end with a newline for proper parsing
                        if content and not content.endswith("\n"):
                            output_file.write("\n")
        except Exception as e:
            print(
                f"Warning: Could not read file '{filepath}'. Skipping. Error: {e}",
                file=sys.stderr,
            )


def create_aiar_bash(output_file, files_to_archive, base_dir, binary_all=False, verbose=False):
    """Generates a bash self-extracting aiar script.
    
    Binary files are base64-encoded with :b: marker.
    Text files use :t: marker and are written line-by-line.
    Format: SEPARATOR:X:Y:filepath where X is UUID and Y is 'b' or 't'
    
    Note, a limitation of text files is that files without a trailing newline 
    will have a newline added to the end of the file. If this is undesirable,
    you can use the --binary-all option to encode all files as binary.
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        binary_all: If True, encode all files as binary (base64)
        verbose: If True, print filenames as they are added
    """
    # Generate a unique separator to prevent collisions with file content.
    # Format: ++++++++++--------:UUID:
    separator_uuid = str(uuid.uuid4())
    separator = f"++++++++++--------:{separator_uuid}:"

    # Write the header, injecting the unique separator.
    # Normalize all line ending types to \n only, then add " #" to end of each line
    # to make any stray \r characters harmless (they'll be in comments)
    # Skip the first line (shebang) which must not have anything after it
    # Empty lines get just "#" without the space
    header = AIAR_HEADER.format(separator=separator).replace('\r\n', '\n').replace('\r', '\n')
    lines = header.split('\n')
    header_with_comments = lines[0] + '\n' + '\n'.join('#' if line == '' else line + ' #' for line in lines[1:]) + '\n'
    output_file.write(header_with_comments)

    # Write the data section using the common function
    _write_aiar_data_section(output_file, files_to_archive, base_dir, separator, binary_all, verbose=verbose)


def create_aiar_python(output_file, files_to_archive, base_dir, binary_all=False, verbose=False):
    """Generates a Python self-extracting aiar script.
    
    Binary files are base64-encoded with :b: marker.
    Text files use :t: marker and are embedded as commented text.
    Format: # SEPARATOR:X:Y:filepath where X is UUID and Y is 'b' or 't'
    
    The Python script uses regex to extract files from the commented sections.
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        binary_all: If True, encode all files as binary (base64)
        verbose: If True, print filenames as they are added
    """
    # Generate a unique separator to prevent collisions with file content.
    # Format: ++++++++++--------:UUID:
    separator_uuid = str(uuid.uuid4())
    separator = f"++++++++++--------:{separator_uuid}:"

    # Write the header, injecting the unique separator.
    output_file.write(AIAR_PYTHON_HEADER.format(separator=separator))

    # Write the data section with "# " comment prefix
    _write_aiar_data_section(output_file, files_to_archive, base_dir, separator, binary_all, comment_prefix="# ", verbose=verbose)


def create_aiar_nodejs(output_file, files_to_archive, base_dir, binary_all=False, verbose=False):
    """Generates a Node.js self-extracting aiar script.
    
    Binary files are base64-encoded with :b: marker.
    Text files use :t: marker and are embedded as commented text.
    Format: // SEPARATOR:X:Y:filepath where X is UUID and Y is 'b' or 't'
    
    The Node.js script uses regex to extract files from the commented sections.
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        binary_all: If True, encode all files as binary (base64)
        verbose: If True, print filenames as they are added
    """
    # Generate a unique separator to prevent collisions with file content.
    # Format: ++++++++++--------:UUID:
    separator_uuid = str(uuid.uuid4())
    separator = f"++++++++++--------:{separator_uuid}:"

    # Write the header, injecting the unique separator.
    output_file.write(NODE_JS_HEADER.format(separator=separator))

    # Write the data section with "// " comment prefix
    _write_aiar_data_section(output_file, files_to_archive, base_dir, separator, binary_all, comment_prefix="// ", verbose=verbose)


def create_aiar_powershell(output_file, files_to_archive, base_dir, binary_all=False, verbose=False):
    """Generates a PowerShell self-extracting aiar script.
    
    Binary files are base64-encoded with :b: marker.
    Text files use :t: marker and are embedded as commented text.
    Format: # SEPARATOR:X:Y:filepath where X is UUID and Y is 'b' or 't'
    
    The PowerShell script uses regex to extract files from the commented sections.
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        binary_all: If True, encode all files as binary (base64)
        verbose: If True, print filenames as they are added
    """
    # Generate a unique separator to prevent collisions with file content.
    # Format: ++++++++++--------:UUID:
    separator_uuid = str(uuid.uuid4())
    separator = f"++++++++++--------:{separator_uuid}:"

    # Write the header, injecting the unique separator.
    output_file.write(POWERSHELL_HEADER.format(separator=separator))

    # Write the data section with "# " comment prefix
    _write_aiar_data_section(output_file, files_to_archive, base_dir, separator, binary_all, comment_prefix="# ", verbose=verbose)


def create_aiar_bare(output_file, files_to_archive, base_dir, binary_all=False, verbose=False):
    """Generates a bare data file without self-extraction script.
    
    This format only includes the separator definition and the file data,
    without any extraction logic. Useful for embedding in other tools or
    for manual processing.
    
    Binary files are base64-encoded with :b: marker.
    Text files use :t: marker.
    Format: SEPARATOR:X:Y:filepath where X is UUID and Y is 'b' or 't'
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        binary_all: If True, encode all files as binary (base64)
        verbose: If True, print filenames as they are added
    """
    # Generate a unique separator to prevent collisions with file content.
    # Format: ++++++++++--------:UUID:
    separator_uuid = str(uuid.uuid4())
    separator = f"++++++++++--------:{separator_uuid}:"

    # Write just the separator definition
    output_file.write(f"SEPARATOR=\"{separator}\"\n\n")

    # Write the data section using the common function
    _write_aiar_data_section(output_file, files_to_archive, base_dir, separator, binary_all, verbose=verbose)


def create_aiar(output_file, files_to_archive, base_dir, binary_all=False, lang="bash", verbose=False):
    """Dispatches to the appropriate aiar generator based on language.
    
    Args:
        output_file: File object to write the archive to
        files_to_archive: Set/list of Path objects to include
        base_dir: Base directory for relative paths
        binary_all: If True, encode all files as binary (base64)
        lang: Language for self-extractor ("bash", "py", "nodejs", "powershell", or "bare")
        verbose: If True, print filenames as they are added
    """
    if lang == "py":
        create_aiar_python(output_file, files_to_archive, base_dir, binary_all, verbose)
    elif lang == "nodejs":
        create_aiar_nodejs(output_file, files_to_archive, base_dir, binary_all, verbose)
    elif lang == "powershell" or lang == "ps1":
        create_aiar_powershell(output_file, files_to_archive, base_dir, binary_all, verbose)
    elif lang == "bare":
        create_aiar_bare(output_file, files_to_archive, base_dir, binary_all, verbose)
    else:
        create_aiar_bash(output_file, files_to_archive, base_dir, binary_all, verbose)


def _safe_dest(rel: str) -> Path:
    """Validate that a relative path doesn't escape the current directory."""
    p = Path(rel)
    if p.is_absolute():
        raise ValueError(f"Absolute path not allowed: {rel}")
    dest = (Path(".") / p).resolve()
    if Path(".").resolve() not in (set(dest.parents) | {dest}):
        raise ValueError(f"Path escapes output root: {rel}")
    return dest


def extract_aiar(aiar_file, test_mode=False, output_dir=None, include_patterns=None, exclude_patterns=None, verbose=False):
    """Extract files from an aiar archive.
    
    Auto-detects the format (bash, python, or bare) and extracts accordingly.
    
    Args:
        aiar_file: Path to the aiar file to extract
        test_mode: If True, only list files without extracting
        output_dir: Directory to extract to (default: current directory)
        include_patterns: List of glob patterns - only extract matching files
        exclude_patterns: List of glob patterns - skip matching files
        verbose: If True, print filenames as they are extracted
    
    Returns:
        List of extracted/listed file paths
    """
    import base64
    import re
    
    # Resolve the aiar_file path before changing directories
    aiar_file = Path(aiar_file).resolve()
    
    # Store the original working directory and output_dir for verbose output
    original_cwd = Path.cwd()
    output_dir_name = None
    output_dir_resolved = None
    directory_created = False
    
    if output_dir:
        output_dir_name = output_dir  # Keep original name for display
        output_dir_resolved = Path(output_dir).resolve()
        # Don't create directory yet - wait until we know we have files to extract
    else:
        original_cwd = None
    
    def ensure_output_dir():
        """Create and change to output directory if needed (only once, on first file)."""
        nonlocal directory_created
        if output_dir_resolved and not directory_created:
            output_dir_resolved.mkdir(parents=True, exist_ok=True)
            os.chdir(output_dir_resolved)
            directory_created = True
    
    try:
        with open(aiar_file, "r", encoding="utf-8") as f:
            content = f.read()
        
        # Find the separator definition
        # Try different formats: Python/Bash (SEPARATOR="..."), Node.js (const SEPARATOR = "...")
        sep_match = re.search(r'SEPARATOR\s*=\s*"([^"]+)".*', content)
        if not sep_match:
            # Try alternate format (shell variable without quotes)
            sep_match = re.search(r'SEPARATOR=([^\s\n;]+)', content)
        
        if not sep_match:
            raise ValueError("Could not find SEPARATOR definition in aiar file")
        
        separator = sep_match.group(1)
        escaped_sep = re.escape(separator)
        
        # Detect format: check for comment prefixes
        is_python = f"# {separator}" in content and "def extract_all():" in content
        is_nodejs = f"// {separator}" in content
        is_powershell = "$SEPARATOR=" in content or (f"# {separator}" in content and "function Extract-All" in content)
        
        if is_python or is_nodejs or is_powershell:
            # Python, Node.js, or PowerShell format: files are in comments
            if is_nodejs:
                comment_prefix = "// "
            else:
                comment_prefix = "# "
            
            pattern = re.compile(
                rf"^{re.escape(comment_prefix)}?{escaped_sep}([tb]):([^\r\n]+)\r?\n(.*?)(?=^{re.escape(comment_prefix)}?{escaped_sep}[tb]:|\Z)",
                re.DOTALL | re.MULTILINE
            )
            
            extracted_files = []
            found_files = 0
            for ftype, path, body in pattern.findall(content):
                found_files += 1
                path = path.strip()
                
                # Apply include/exclude filters
                if include_patterns and not _matches_patterns(path, include_patterns):
                    continue
                if exclude_patterns and _matches_patterns(path, exclude_patterns):
                    continue
                
                if test_mode:
                    # Show path with output_dir prefix if specified
                    if output_dir_name:
                        display_path = str(Path(output_dir_name) / path)
                    else:
                        display_path = path
                    print(display_path)
                    extracted_files.append(path)
                    continue
                
                # Create output directory now (before checking if file exists)
                ensure_output_dir()
                
                # Resolve dest after potentially changing directory
                try:
                    dest = _safe_dest(path)
                except ValueError as e:
                    print(f"Warning: {e}. Skipping.")
                    continue
                
                if dest.exists():
                    print(f"Skipping already existing file: '{dest}'")
                    continue
                
                if verbose:
                    # Show path with output_dir prefix if specified
                    if output_dir_name:
                        display_path = str(Path(output_dir_name) / path)
                    else:
                        display_path = path
                    print(f"Extracting: {display_path}")
                
                dest.parent.mkdir(parents=True, exist_ok=True)
                
                # Uncomment the body using the detected comment prefix
                uncomment_pattern = r"^" + re.escape(comment_prefix[:-1]) + r" ?"  # Remove trailing space from prefix for pattern
                uncommented_body = re.sub(uncomment_pattern, "", body, flags=re.MULTILINE)
                
                if ftype == "t":
                    with open(dest, "w", encoding="utf-8", newline="\n") as out:
                        out.write(uncommented_body)
                else:  # binary
                    with open(dest, "wb") as out:
                        out.write(base64.b64decode(uncommented_body.strip().encode("ascii"), validate=False))
                
                extracted_files.append(str(dest))
            
            if found_files == 0:
                print("Warning: No files found in archive.")
        
        else:
            # Bash/bare format: files are not commented
            pattern = re.compile(
                rf"^{escaped_sep}([tb]):([^\r\n]+)\r?\n(.*?)(?=^{escaped_sep}[tb]:|\Z)",
                re.DOTALL | re.MULTILINE
            )
            
            extracted_files = []
            found_files = 0
            for ftype, path, body in pattern.findall(content):
                found_files += 1
                path = path.strip()
                
                # Apply include/exclude filters
                if include_patterns and not _matches_patterns(path, include_patterns):
                    continue
                if exclude_patterns and _matches_patterns(path, exclude_patterns):
                    continue
                
                if test_mode:
                    # Show path with output_dir prefix if specified
                    if output_dir_name:
                        display_path = str(Path(output_dir_name) / path)
                    else:
                        display_path = path
                    print(display_path)
                    extracted_files.append(path)
                    continue
                
                # Create output directory now (before checking if file exists)
                ensure_output_dir()
                
                # Resolve dest after potentially changing directory
                try:
                    dest = _safe_dest(path)
                except ValueError as e:
                    print(f"Warning: {e}. Skipping.")
                    continue
                
                if dest.exists():
                    print(f"Skipping already existing file: '{dest}'")
                    continue
                
                if verbose:
                    # Show path with output_dir prefix if specified
                    if output_dir_name:
                        display_path = str(Path(output_dir_name) / path)
                    else:
                        display_path = path
                    print(f"Extracting: {display_path}")
                
                dest.parent.mkdir(parents=True, exist_ok=True)
                
                if ftype == "t":
                    with open(dest, "w", encoding="utf-8", newline="\n") as out:
                        out.write(body)
                else:  # binary
                    with open(dest, "wb") as out:
                        out.write(base64.b64decode(body.strip().encode("ascii"), validate=False))
                
                extracted_files.append(str(dest))
            
            if found_files == 0:
                print("Warning: No files found in archive.")
        
        return extracted_files
    
    finally:
        if original_cwd and directory_created:
            os.chdir(original_cwd)


def _parse_patterns(pattern_args):
    """Parse pattern arguments that may contain comma-separated lists.
    
    Args:
        pattern_args: List of pattern strings (may be None or contain commas)
        
    Returns:
        List of individual patterns
    """
    if not pattern_args:
        return []
    
    patterns = []
    for arg in pattern_args:
        # Split by comma and strip whitespace
        patterns.extend([p.strip() for p in arg.split(',') if p.strip()])
    
    return patterns


def _matches_patterns(filepath, patterns):
    """Check if a filepath matches any of the given glob patterns.
    
    Args:
        filepath: Path object or string to check
        patterns: List of glob patterns
        
    Returns:
        True if matches any pattern, False otherwise
    """
    if not patterns:
        return False
    
    import fnmatch
    filepath_str = str(filepath) if isinstance(filepath, Path) else filepath
    filepath_posix = filepath_str.replace('\\', '/')
    
    for pattern in patterns:
        # Try matching against full path and just filename
        if fnmatch.fnmatch(filepath_posix, pattern) or fnmatch.fnmatch(Path(filepath_posix).name, pattern):
            return True
    
    return False


def _detect_lang_from_extension(filename):
    """Detect the language from the output filename extension.
    
    Args:
        filename: The output filename
        
    Returns:
        Detected language or None if no match
    """
    if not filename:
        return None
    
    ext = Path(filename).suffix.lower()
    
    # Map extensions to languages
    ext_map = {
        '.py': 'py',
        '.js': 'nodejs',
        '.sh': 'bash',
        '.bash': 'bash',
        '.zsh': 'bash',
        '.ps1': 'powershell',
        '.psm1': 'powershell',
        '.aiar': 'bare',
    }
    
    return ext_map.get(ext)


def _main():
    parser = argparse.ArgumentParser(
        description="Generate or extract aiar (AI Archive) files.",
        formatter_class=argparse.RawTextHelpFormatter,
    )
    
    subparsers = parser.add_subparsers(dest="command", help="Command to execute")
    
    # Create command
    create_parser = subparsers.add_parser(
        "create",
        help="Create an aiar archive",
        formatter_class=argparse.RawTextHelpFormatter,
    )
    create_parser.add_argument(
        "paths", nargs="+", help="One or more files or directories to archive."
    )
    create_parser.add_argument(
        "-o",
        "--output",
        help="Output file for the aiar script. Defaults to stdout.",
    )
    create_parser.add_argument(
        "--no-gitignore",
        action="store_true",
        help="Disable using .gitignore files for exclusion.",
    )
    create_parser.add_argument(
        "--binary-all",
        action="store_true",
        help="Treat all files as binary (base64-encode everything).",
    )
    create_parser.add_argument(
        "--lang",
        choices=["bash", "py", "nodejs", "powershell", "ps1", "bare", "aiar"],
        default=None,
        help="Language for self-extracting script: bash, py, nodejs, powershell (or ps1), or bare. Auto-detects from -o extension if not specified (default: bash).",
    )
    create_parser.add_argument(
        "--include",
        action="append",
        help="Include only files matching pattern(s). Can be specified multiple times. Supports comma-separated lists (e.g., --include '*.js,*.ts').",
    )
    create_parser.add_argument(
        "--exclude",
        action="append",
        help="Exclude files matching pattern(s). Can be specified multiple times. Supports comma-separated lists (e.g., --exclude '*.obj,*.tmp').",
    )
    create_parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Print filenames as they are added to the archive.",
    )
    
    # Extract command
    extract_parser = subparsers.add_parser(
        "extract",
        help="Extract an aiar archive",
        formatter_class=argparse.RawTextHelpFormatter,
    )
    extract_parser.add_argument(
        "aiar_file",
        help="The aiar file to extract.",
    )
    extract_parser.add_argument(
        "-t",
        "--test",
        action="store_true",
        help="List files without extracting (like tar -t).",
    )
    extract_parser.add_argument(
        "-C",
        "--directory",
        help="Extract to specified directory.",
    )
    extract_parser.add_argument(
        "--include",
        action="append",
        help="Extract only files matching pattern(s). Can be specified multiple times. Supports comma-separated lists (e.g., --include '*.js,*.ts').",
    )
    extract_parser.add_argument(
        "--exclude",
        action="append",
        help="Skip files matching pattern(s). Can be specified multiple times. Supports comma-separated lists (e.g., --exclude '*.obj,*.tmp').",
    )
    extract_parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="Print filenames as they are extracted.",
    )

    args = parser.parse_args()
    
    # Handle backward compatibility: if no command specified, show help
    if not args.command:
        parser.print_help()
        return
    
    if args.command == "create":
        resolved_paths = [Path(p).resolve() for p in args.paths]
        base_dir = Path(os.path.commonpath(resolved_paths))
        if base_dir.is_file():
            base_dir = base_dir.parent
        # If the common path is one of the input paths (i.e., it was explicitly specified),
        # use its parent to preserve the name in the archive
        elif base_dir in resolved_paths:
            base_dir = base_dir.parent

        # For gitignore purposes, we need to find the appropriate directory that contains
        # the .gitignore file. If we're archiving a subdirectory, we should look for
        # gitignore patterns in that subdirectory's context, not the parent.
        gitignore_base_dir = base_dir
        if len(resolved_paths) == 1 and resolved_paths[0].is_dir():
            # If we're archiving a single directory, use that directory for gitignore
            gitignore_base_dir = resolved_paths[0]

        spec = get_gitignore_spec(gitignore_base_dir, not args.no_gitignore)
        files_to_process = find_files_to_archive(resolved_paths, spec, base_dir, verbose=args.verbose)

        # Use a set to handle potential duplicates if paths overlap
        unique_files = set(files_to_process)

        # Apply include/exclude patterns
        include_patterns = _parse_patterns(args.include)
        exclude_patterns = _parse_patterns(args.exclude)
        
        if include_patterns or exclude_patterns:
            filtered_files = set()
            for f in unique_files:
                rel_path = f.relative_to(base_dir).as_posix()
                
                # If include patterns specified, file must match at least one
                if include_patterns and not _matches_patterns(rel_path, include_patterns):
                    continue
                
                # If exclude patterns specified, file must not match any
                if exclude_patterns and _matches_patterns(rel_path, exclude_patterns):
                    continue
                
                filtered_files.add(f)
            
            unique_files = filtered_files

        if not unique_files:
            print("No files found to archive.", file=sys.stderr)
            return

        # Determine language: use --lang if specified, otherwise detect from extension
        lang = args.lang
        if args.output and not lang:
            detected_lang = _detect_lang_from_extension(args.output)
            if detected_lang:
                lang = detected_lang
        
        # Default to bash if still not determined
        if not lang:
            lang = "bash"
        if lang == "aiar":
            lang = "bare"

        if args.output:
            with open(args.output, "w", encoding="utf-8") as f:
                create_aiar(f, unique_files, base_dir, binary_all=args.binary_all, lang=lang, verbose=args.verbose)
            print(f"aiar script created at '{args.output}'")
            os.chmod(args.output, 0o755) # Make it executable
        else:
            create_aiar(sys.stdout, unique_files, base_dir, binary_all=args.binary_all, lang=lang, verbose=args.verbose)
    
    elif args.command == "extract":
        try:
            include_patterns = _parse_patterns(args.include)
            exclude_patterns = _parse_patterns(args.exclude)
            extract_aiar(
                args.aiar_file,
                test_mode=args.test,
                output_dir=args.directory,
                include_patterns=include_patterns,
                exclude_patterns=exclude_patterns,
                verbose=args.verbose
            )
            if not args.test:
                print("Extraction complete.")
        except Exception as e:
            print(f"Error extracting aiar file: {e}", file=sys.stderr)
            sys.exit(1)


if __name__ == "__main__":
    # testing - comment out when not testing
    # sys.argv = [
    #     sys.argv[0],
    #     "extract",
    #     "-t",
    #     "aiar-test2.sh",
    # ]
    _main()
