"""Terminal `pj`"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_cli.ipynb.

# %% auto 0
__all__ = ['hr', 'kill_processes', 'find_free_port', 'setup_dark_theme', 'check_cmd', 'run_cmd', 'get_git_config',
           'check_prereqs', 'init_nbdev', 'sync', 'main']

# %% ../nbs/01_cli.ipynb 2
import os
import socket
import argparse
import subprocess
import sys
from pathlib import Path
import shutil

# %% ../nbs/01_cli.ipynb 3
hr = "_"

# %% ../nbs/01_cli.ipynb 5
def kill_processes(args):
    """Kill all running pj-related processes"""
    print("🛑 Stopping all background processes...")
    
    processes = [
        ("jupyter", "Jupyter Lab servers and kernels"),
        ("nbdev_preview", "nbdev preview servers"),
        ("quarto.js preview", "Quarto.js Deno process"),
        ("quarto preview", "Quarto preview servers"),
    ]
    
    for pattern, name in processes:
        result = subprocess.run(
            ["pkill", "-f", pattern],
            capture_output=True
        )
        if result.returncode == 0:
            print(f"   ✓ Stopped {name}")
        else:
            print(f"   - No {name} running")
    
    print("\n✅ All processes stopped!")

# %% ../nbs/01_cli.ipynb 7
def find_free_port(start=64000):
    """Find first available port starting from given port"""
    for port in range(start, start + 100):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            if s.connect_ex(('localhost', port)) != 0:
                return port
    return start

# %% ../nbs/01_cli.ipynb 9
def setup_dark_theme(project_path, log_file=None, verbose=False):
    """Set up dark mode theme for nbdev docs"""
    nbs_path = project_path / "nbs"
    
    # 1. Update _quarto.yml
    quarto_yml = nbs_path / "_quarto.yml"
    quarto_content = """project:
  type: website

format:
  html:
    theme:
      light: cosmo
      dark: [cosmo, dark.scss]
    css: styles.css
    toc: true
    keep-md: true
  commonmark: default

website:
  twitter-card: true
  open-graph: true
  repo-actions: [issue]
  navbar:
    background: primary
    search: true
  sidebar:
    style: floating

metadata-files: [nbdev.yml, sidebar.yml]
"""
    quarto_yml.write_text(quarto_content)
    
    # 2. Update styles.css
    styles_css = nbs_path / "styles.css"
    styles_content = """.cell {
  margin-bottom: 1rem;
}

.cell > .sourceCode {
  margin-bottom: 0;
}

.cell-output > pre {
  margin-bottom: 0;
}

.cell-output > pre, .cell-output > .sourceCode > pre, .cell-output-stdout > pre {
  margin-left: 0.8rem;
  margin-top: 0;
  background: none;
  border-left: 2px solid lightsalmon;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.cell-output > .sourceCode {
  border: none;
}

.cell-output > .sourceCode {
  background: none;
  margin-top: 0;
}

div.description {
  padding-left: 2px;
  padding-top: 5px;
  font-style: italic;
  font-size: 135%;
  opacity: 70%;
}
"""
    styles_css.write_text(styles_content)
    
    # 3. Create dark.scss
    dark_scss = nbs_path / "dark.scss"
    dark_content = """/*-- scss:defaults --*/

// Base document colors for dark mode
$body-bg: #181818;
$body-color: #ccc;
$link-color: #75AADB;

// Code blocks
$code-block-bg-alpha: -.9;

// Navbar
$navbar-bg: #2a2a2a;

/*-- scss:rules --*/

// Fix cell output text visibility in dark mode
.cell-output,
.cell-output-display {
  color: #ccc;
}

.cell-output pre {
  color: #ccc;
  background-color: #1e1e1e;
}

// Ensure code output is visible
.cell-output > pre code,
.cell-output-stdout pre {
  color: #ccc;
}

// Fix inline code in dark mode
code:not(pre > code) {
  background-color: #2a2a2a !important;
  color: #ab8dff;
}

// Fix blockquote code blocks in dark mode
blockquote pre,
blockquote pre code {
  color: #ccc;
  background-color: #1e1e1e;
}

blockquote {
  border-left-color: #555;
  color: #bbb;
}
"""
    dark_scss.write_text(dark_content)
    
    if log_file:
        with open(log_file, 'a') as f:
            f.write("\n" + hr*60 + "\n")
            f.write("Set up dark theme customization\n")
            f.write(hr*60 + "\n")


# %% ../nbs/01_cli.ipynb 11
def check_cmd(cmd, install_hint):
    """Check if a command exists in PATH"""
    if not shutil.which(cmd):
        print(f"❌ Error: '{cmd}' is not installed")
        print(f"   Install with: {install_hint}")
        return False
    return True

# %% ../nbs/01_cli.ipynb 13
def run_cmd(cmd, cwd=None, check=True, capture_output=False, log_file=None, verbose=False):
    """Run a shell command with optional logging and pretty output"""
    
    # Show command if verbose
    if verbose and not capture_output:
        print(f"   > {' '.join(cmd)}")
    
    # Log command to file
    if log_file:
        with open(log_file, 'a') as f:
            f.write(f"\n{hr*60}\n")
            f.write(f"Command: {' '.join(cmd)}\n")
            f.write(f"{hr*60}\n")
    
    # Handle capture_output mode (for getting return values)
    if capture_output:
        result = subprocess.run(cmd, cwd=cwd, check=check, capture_output=True, text=True)
        if log_file:
            with open(log_file, 'a') as f:
                f.write(result.stdout)
                if result.stderr:
                    f.write(result.stderr)
        return result
    
    # Verbose mode: stream output with box drawing
    if verbose:
        print("   ┌──────────")
        process = subprocess.Popen(
            cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
        )
        for line in process.stdout:
            line = line.rstrip()
            print(f"   │ {line}")
            if log_file:
                with open(log_file, 'a') as f:
                    f.write(line + '\n')
        process.wait()
        print("   └─")
        
        if check and process.returncode != 0:
            raise subprocess.CalledProcessError(process.returncode, cmd)
        
        # Create a mock result object for compatibility
        class Result:
            def __init__(self, returncode):
                self.returncode = returncode
                self.stdout = ""
                self.stderr = ""
        return Result(process.returncode)
    
    # Silent mode: just log to file
    else:
        if log_file:
            with open(log_file, 'a') as f:
                result = subprocess.run(
                    cmd, cwd=cwd, check=check,
                    stdout=f, stderr=subprocess.STDOUT, text=True
                )
        else:
            result = subprocess.run(
                cmd, cwd=cwd, check=check,
                stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, text=True
            )
        return result

# %% ../nbs/01_cli.ipynb 15
def get_git_config(key):
    """Get git config value"""
    try:
        result = subprocess.run(
            ["git", "config", "--get", key],
            capture_output=True,
            text=True,
            check=True
        )
        return result.stdout.strip()
    except:
        return None

# %% ../nbs/01_cli.ipynb 17
def check_prereqs():
    """
    Validate all prerequisites and gather system info.
    Returns dict with user info or exits on failure.
    """
    print(hr * 60)
    print("PHASE 1: CHECKS")
    
    # Check required tools
    checks = [
        ("gh", "sudo apt install gh  # See https://github.com/cli/cli/blob/trunk/docs/install_linux.md"),
        ("uv", "curl -LsSf https://astral.sh/uv/install.sh | sh"),
        ("direnv", "sudo apt install direnv  # Then add: eval \"$(direnv hook bash)\" to ~/.bashrc"),
        ("nbdev_new", "uv tool install nbdev  # or: pipx install nbdev"),
        ("git", "sudo apt install git"),
    ]
    
    all_ok = True
    for cmd, hint in checks:
        if not check_cmd(cmd, hint):
            all_ok = False
    
    if not all_ok:
        sys.exit(1)
    
    # Check GitHub auth
    try:
        result = subprocess.run(
            ["gh", "auth", "status"],
            capture_output=True,
            text=True
        )
        if result.returncode != 0:
            print("❌ Error: GitHub CLI is not authenticated")
            print("   Run: gh auth login")
            sys.exit(1)
    except Exception:
        print("❌ Error: Cannot verify GitHub authentication")
        sys.exit(1)
    
    print("✅ All prerequisites satisfied")
    
    # Gather user info
    git_user = get_git_config("user.name") or "Your Name"
    git_email = get_git_config("user.email") or "you@example.com"
    
    # Get GitHub username
    gh_user_result = subprocess.run(
        ["gh", "api", "user", "--jq", ".login"],
        capture_output=True,
        text=True,
        check=False
    )
    gh_username = gh_user_result.stdout.strip() if gh_user_result.returncode == 0 else "username"
    
    return {
        'git_user': git_user,
        'git_email': git_email,
        'gh_username': gh_username
    }

# %% ../nbs/01_cli.ipynb 20
def init_nbdev(args):
    """Initialize a new nbdev project with full configuration."""
    project_name = args.name
    
    # Phase 1: Checks
    user_info = check_prereqs()
    
    # Set up logging
    project_path = Path.cwd() / project_name

    if args.no_log:
        log_file = None
    else:
        log_file = Path(args.logfile) if args.logfile else Path.cwd() / "init.log"
        # Clear log file if it exists
        if log_file and log_file.exists():
            log_file.unlink()
        # Create empty log file
        if log_file:
            log_file.touch()

    
    # Phase 2: Setup
    print(hr * 60)
    print("PHASE 2: SETUP")
    
    # Build gh repo create command
    gh_cmd = ["gh", "repo", "create"]

    # If org specified, use ORG/PROJECT format
    if args.org:
        gh_cmd.append(f"{args.org}/{project_name}")
    else:
        gh_cmd.append(project_name)

    gh_cmd.append("--public" if args.public else "--private")
    gh_cmd.append("--clone")

    if args.description:
        gh_cmd.extend(["--description", args.description])

    #  1. Create GitHub repo
    print("📦 1. Creating GitHub repository")
    run_cmd(gh_cmd, log_file=log_file, verbose=args.verbose)
    
    #  2. Run nbdev_new
    print("📓 2. Setting up nbdev project")
    nbdev_cmd = [
        "nbdev_new",
        "--repo", project_name,
        "--user", user_info['gh_username'],  # Use org for GitHub Pages URL
        "--author", args.author or user_info['git_user'],
        "--author_email", args.author_email or user_info['git_email'],
        "--description", args.description or f"A new nbdev project: {project_name}",
        "--jupyter_hooks", "True",
    ]
    
    if args.license:
        nbdev_cmd.extend(["--license", args.license])
    if args.min_python:
        nbdev_cmd.extend(["--min_python", args.min_python])
    
    run_cmd(nbdev_cmd, cwd=project_path, log_file=log_file, verbose=args.verbose)
    
    # 2b. Set up dark theme
    print("🎨 2b. Setting up dark theme")
    setup_dark_theme(project_path, log_file=log_file, verbose=args.verbose)

    #  3. Create venv
    print("🐍 3. Creating virtual environment with uv")
    uv_venv_cmd = ["uv", "venv"]
    if args.python:
        uv_venv_cmd.extend(["--python", args.python])
    run_cmd(uv_venv_cmd, cwd=project_path, log_file=log_file, verbose=args.verbose)
    
    #  4. Install package
    print("📥 4. Installing package in editable mode")
    venv_python = project_path / ".venv" / "bin" / "python"
    run_cmd(
        ["uv", "pip", "install", "--python", str(venv_python), "-e", ".[dev]"],
        cwd=project_path, log_file=log_file, verbose=args.verbose
    )
    
    #  5. Install ipykernel
    print("🔧 5. Installing ipykernel")
    run_cmd(
        ["uv", "pip", "install", "--python", str(venv_python), "ipykernel"],
        cwd=project_path, log_file=log_file, verbose=args.verbose
    )
    
    #  6. Register Jupyter kernel
    print("📊 6. Registering Jupyter kernel")
    run_cmd(
        [str(venv_python), "-m", "ipykernel", "install", "--user", f"--name={project_name}"],
        log_file=log_file, verbose=args.verbose
    )
    
    #  7. Set up direnv
    print("🔄 7. Setting up direnv")
    envrc_path = project_path / ".envrc"
    envrc_path.write_text("source .venv/bin/activate\n")
    run_cmd(["direnv", "allow"], cwd=project_path, log_file=log_file, verbose=args.verbose)
    
    #  8. Update .gitignore
    print("✏️ 8. Updating .gitignore")
    gitignore_path = project_path / ".gitignore"
    entries_to_add = [".venv/", "init.log"]
    
    if gitignore_path.exists():
        content = gitignore_path.read_text()
        new_entries = [e for e in entries_to_add if e not in content]
        if new_entries:
            with gitignore_path.open("a") as f:
                f.write("\n" + "\n".join(new_entries) + "\n")
    else:
        gitignore_path.write_text("\n".join(entries_to_add) + "\n")
    
    # Phase 3: Sync
    print(hr * 60)
    print("PHASE 3: SYNC")
    
    #  9. Run nbdev_prepare
    print("🔧 9. Running nbdev_prepare")
    run_cmd(["nbdev_prepare"], cwd=project_path, check=False, log_file=log_file, verbose=args.verbose)
    
    # 10. Commit and push
    print("💾 10. Committing and pushing initial setup")
    run_cmd(["git", "add", "-A"], cwd=project_path, log_file=log_file, verbose=args.verbose)
    run_cmd(["git", "commit", "-m", "setup nbdev"], cwd=project_path, log_file=log_file, verbose=args.verbose)
    run_cmd(["git", "push", "-u", "origin", "HEAD"], cwd=project_path, log_file=log_file, verbose=args.verbose)

    # Move log file to project directory
    if log_file and log_file.exists():
        final_log_path = project_path / "init.log"
        log_file.rename(final_log_path)
        log_file = final_log_path

    # Success!
    print(hr * 60)
    print("✅ Project initialized successfully!")

    if log_file:
        print(f"\n📋 Full log: {log_file}")

    # Display project structure
    print(f"\n📁 Project structure:")
    if shutil.which("tree"):
        subprocess.run(["tree", "-a", "-I", ".git", "-L", "2"], cwd=project_path, check=False)
    else:
        subprocess.run(["ls", "-la"], cwd=project_path, check=False)

    # Handle quiet mode
    if args.quiet:
        args.no_preview = True
        args.no_lab = True

    # Launch nbdev_preview
    if not args.no_preview:
        print("\n🌐 Launching nbdev_preview")
        subprocess.Popen(
            ["nbdev_preview"],
            cwd=project_path,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True
        )

    # Launch Jupyter Lab
    if not args.no_lab:
        port = find_free_port(64000)
        print(f"\n📓 Launching Jupyter Lab on port {port}")
        subprocess.Popen(
            ["jupyter", "lab", f"--port={port}", "--NotebookApp.token=''", "--NotebookApp.password=''"],
            # ["jupyter", "lab", f"--port={port}", "--no-browser", "--NotebookApp.token=''", "--NotebookApp.password=''"],
            cwd=project_path,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True
        )
        print(f"   Open: http://localhost:{port}")

    # Open VSCode
    if args.code:
        print("\n💻 Opening VSCode")
        subprocess.Popen(["code", "."], cwd=project_path)

    # Show background processes info
    if not args.no_preview or not args.no_lab:
        print(f"\n💡 Tip: Background processes running:")
        if not args.no_preview:
            print(f"   - nbdev_preview (kill with: pkill -f nbdev_preview)")
        if not args.no_lab:
            print(f"   - jupyter lab on port {port} (kill with: pkill -f 'jupyter.*{port}')")

    # Final message
    print(f"\n🎉 All done! Entering project directory...\n")

    # Change to project directory and exec into new shell
    os.chdir(project_path)
    os.execvp(os.environ.get("SHELL", "bash"), [os.environ.get("SHELL", "bash")])

# %% ../nbs/01_cli.ipynb 22
def sync(args):
    """Sync project: pull, prepare (export/test/clean), commit, and push"""
    print(hr * 60)
    print("PHASE 3: SYNC")
    
    # 1. Git pull
    print("⬇️  1. Pulling latest changes")
    result = run_cmd(["git", "pull"], check=False, verbose=args.verbose)
    
    if result.returncode != 0:
        status_result = run_cmd(["git", "status", "--porcelain"], capture_output=True, check=False)
        if "UU" in status_result.stdout or "AA" in status_result.stdout:
            print("\n❌ Merge conflict detected!")
            print("   Resolve conflicts manually, then run 'pj sync' again")
            sys.exit(1)
        else:
            print("\n❌ Git pull failed!")
            print("   Fix the issue manually, then try again")
            sys.exit(1)
    
    # 2. nbdev_prepare
    print("🔧 2. Running nbdev_prepare (export, test, clean)")
    prepare_result = run_cmd(["nbdev_prepare"], check=False, capture_output=True)
    
    if prepare_result.returncode != 0:
        print("\n❌ nbdev_prepare failed!")
        print("\nOutput:")
        print(prepare_result.stdout)
        if prepare_result.stderr:
            print("\nErrors:")
            print(prepare_result.stderr)
        print("\n   Fix the errors and try again")
        sys.exit(1)
    
    if args.verbose:
        print(prepare_result.stdout)
    
    # 3. Git commit
    commit_message = args.message or "save"
    print(f"💾 3. Committing changes: '{commit_message}'")
    
    status_result = run_cmd(["git", "status", "--porcelain"], capture_output=True)
    if not status_result.stdout.strip():
        print("   No changes to commit")
    else:
        run_cmd(["git", "add", "-A"], verbose=args.verbose)
        run_cmd(["git", "commit", "-m", commit_message], verbose=args.verbose)
    
    # 4. Git push
    print("📤 4. Pushing to GitHub")
    run_cmd(["git", "push"], verbose=args.verbose)
    
    print()
    print(hr * 60)
    print("✅ Sync complete!")

# %% ../nbs/01_cli.ipynb 24
def main():
    """Main CLI entry point"""
    parser = argparse.ArgumentParser(
        description="pj: the ProJect management toolkit",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  pj init my-project
  pj init my-project -v --desc "My awesome ML library" --private
  pj init my-project --python 3.11 --author "John Doe"
  pj sync
  pj sync -m "Added new feature"

For more info, visit: https://nbdev.fast.ai
        """
    )
    
    subparsers = parser.add_subparsers(dest="command", help="Available commands")
    
    # init subcommand
    init_parser = subparsers.add_parser(
        "init",
        help="Initialize a new nbdev project",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description="""
Initialize a new nbdev project with all the bells and whistles:
- GitHub repository creation
- nbdev project structure with Jupyter hooks
- Virtual environment with uv
- Jupyter kernel registration
- direnv auto-activation
        """,
        epilog="""
Examples:
  pj init my-project
  pj init my-project -v --desc "ML utilities" --private
  pj init my-project --python 3.11 --license mit
        """
    )
    
    # Required arguments
    init_parser.add_argument("name", help="Project name (will be repo and package name)")
    
    # Output options
    output_group = init_parser.add_argument_group("output options")
    output_group.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Show detailed command output"
    )
    output_group.add_argument(
        "--logfile",
        help="Path to log file (default: PROJECT/init.log)"
    )
    output_group.add_argument(
        "--no-log",
        action="store_true",
        help="Disable logging to file"
    )
    
    # GitHub options
    gh_group = init_parser.add_argument_group("GitHub options")
    gh_group.add_argument(
        "--org",
        help="Create repository under this organization (default: personal account)"
    )
    gh_group.add_argument(
        "--public",
        action="store_true",
        help="Create public repository (default: private)"
    )
    gh_group.add_argument(
        "--description", "--desc",
        help="Repository description"
    )
    
    # Python/venv options
    py_group = init_parser.add_argument_group("Python options")
    py_group.add_argument(
        "--python",
        help="Python version (e.g., 3.11, 3.12)"
    )
    
    # nbdev options
    nbdev_group = init_parser.add_argument_group("nbdev options")
    nbdev_group.add_argument(
        "--author",
        help="Author name (default: from git config)"
    )
    nbdev_group.add_argument(
        "--author-email",
        dest="author_email",
        help="Author email (default: from git config)"
    )
    nbdev_group.add_argument(
        "--license",
        choices=["apache2", "mit", "gpl3", "bsd3"],
        help="License type (default: apache2)"
    )
    nbdev_group.add_argument(
        "--min-python",
        dest="min_python",
        help="Minimum Python version (default: 3.9)"
    )
    
    # Post-init options
    post_group = init_parser.add_argument_group("post-init options")
    post_group.add_argument(
        "--no-preview",
        action="store_true",
        help="Skip opening nbdev_preview"
    )
    post_group.add_argument(
        "--no-lab",
        action="store_true",
        help="Skip launching Jupyter Lab"
    )
    post_group.add_argument(
        "-c", "--code",
        action="store_true",
        help="Open VSCode"
    )
    post_group.add_argument(
        "-q", "--quiet",
        action="store_true",
        help="Quiet mode: skip preview and lab (just cd + tree)"
    )

    # sync subcommand
    sync_parser = subparsers.add_parser(
        "sync",
        help="Sync project: pull, prepare, and push to GitHub",
        description="""
Sync your nbdev project in one command:
1. git pull (aborts on merge conflicts)
2. nbdev_prepare (export, test, clean - aborts if tests fail)
3. git commit -am "message"
4. git push
        """,
        epilog="""
Examples:
  pj sync                    # Uses default message "save"
  pj sync -m "Added tests"   # Custom commit message
  pj sync -v                 # Verbose output
        """
    )
    sync_parser.add_argument(
        "--message", "-m",
        help="Commit message (default: 'save')"
    )
    sync_parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Show detailed command output"
    )

    # kill subcommand
    kill_parser = subparsers.add_parser(
        "kill",
        help="Kill all running background processes (jupyter, nbdev_preview, quarto)"
    )
    
    args = parser.parse_args()
    
    if args.command == "init":
        init_nbdev(args)
    elif args.command == "sync":
        sync(args)
    elif args.command == "kill":
        kill_processes(args)
    else:
        parser.print_help()
        sys.exit(1)
