"""
Command-line interface for gp4c.

Provides utilities for installing the C library system-wide and other operations.
"""

import os
import re
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
from rich.panel import Panel

app = typer.Typer(
    name="gp4c",
    help="High-performance GP sampler with integral and derivative support",
    add_completion=False,
)
console = Console()


def get_project_root() -> Path:
    """Get the project root directory."""
    # Start from this file and go up to find project root
    current = Path(__file__).resolve().parent.parent
    return current


def _update_class_setup_py() -> None:
    """Update CLASS setup.py to include gp4c library for Python wrapper."""
    setup_path = Path.cwd() / "setup.py"

    # Check if setup.py exists
    if not setup_path.exists():
        console.print(
            "\n[yellow]Warning:[/yellow] No setup.py found in current directory",
        )
        console.print("[dim]Skipping setup.py integration (classy will still work via Makefile)[/dim]")
        return

    # Read setup.py content
    try:
        with open(setup_path, "r") as f:
            content = f.read()
    except Exception as e:
        console.print(f"\n[red]Error reading setup.py:[/red] {e}")
        return

    # Check if it's a CLASS setup.py
    if "classy" not in content.lower() and "class" not in content.lower():
        console.print(
            "\n[yellow]Warning:[/yellow] This doesn't appear to be a CLASS setup.py",
        )
        if not typer.confirm("Continue anyway?"):
            return

    # Check if already integrated
    if "gp4c" in content:
        console.print("\n[yellow]Note:[/yellow] gp4c already appears to be integrated in setup.py")
        return

    # Create backup
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = setup_path.with_suffix(f".backup_{timestamp}")
    shutil.copy2(setup_path, backup_path)
    console.print(f"[dim]Created backup:[/dim] {backup_path.name}")

    # Find where to insert gp4c library detection (after liblist initialization)
    lines = content.split("\n")

    # Look for the liblist line to insert gp4c detection after
    liblist_insert_idx = None
    for i, line in enumerate(lines):
        if "liblist" in line and "+=" in line and ("mvec" in line or '"m"' in line):
            liblist_insert_idx = i + 1
            break

    if liblist_insert_idx is None:
        # Try alternate pattern
        for i, line in enumerate(lines):
            if line.strip().startswith("liblist") and "=" in line:
                # Find the end of liblist initialization
                for j in range(i, min(i + 10, len(lines))):
                    if "mvec" in lines[j] or ('"m"' in lines[j] and "+=" in lines[j]):
                        liblist_insert_idx = j + 1
                        break
                if liblist_insert_idx:
                    break

    if liblist_insert_idx is None:
        console.print("[red]Error:[/red] Could not find liblist in setup.py")
        return

    # Find Extension definition to update include_dirs and library_dirs
    extension_idx = None
    for i, line in enumerate(lines):
        if "Extension(" in line and "classy" in line:
            extension_idx = i
            break

    if extension_idx is None:
        console.print("[red]Error:[/red] Could not find Extension definition in setup.py")
        return

    # Prepare gp4c library detection code
    gp4c_detection = '''
# Add gp4c library for GP-based primordial spectrum
try:
    GP4C_LIBS = sbp.check_output(['pkg-config', '--libs-only-l', 'gp4c'], timeout=5).decode().strip()
    GP4C_LIBDIRS = sbp.check_output(['pkg-config', '--libs-only-L', 'gp4c'], timeout=5).decode().strip()
    GP4C_INCLUDES = sbp.check_output(['pkg-config', '--cflags-only-I', 'gp4c'], timeout=5).decode().strip()
    # Parse libraries (remove -l prefix)
    for lib in GP4C_LIBS.split():
        if lib.startswith('-l'):
            liblist.append(lib[2:])
    # Parse library directories (remove -L prefix)
    gp4c_lib_dirs = [d[2:] for d in GP4C_LIBDIRS.split() if d.startswith('-L')]
    # Parse include directories (remove -I prefix)
    gp4c_include_dirs = [d[2:] for d in GP4C_INCLUDES.split() if d.startswith('-I')]
except (sbp.CalledProcessError, FileNotFoundError, sbp.TimeoutExpired):
    print("Warning: gp4c not found via pkg-config, GP spectrum may not work", file=sys.stderr)
    gp4c_lib_dirs = []
    gp4c_include_dirs = []
'''

    # Insert the detection code
    lines.insert(liblist_insert_idx, gp4c_detection)

    # Now update the Extension definition to use gp4c_include_dirs and gp4c_lib_dirs
    # We need to find and modify the include_dirs and library_dirs lines
    updated_content = "\n".join(lines)

    # Update include_dirs in Extension() call (match the specific pattern with leading whitespace)
    # This pattern specifically matches the Extension argument, not the gp4c variable assignment
    updated_content = re.sub(
        r'(\s+include_dirs\s*=\s*\[[^\]]+\])',
        r'\1 + gp4c_include_dirs',
        updated_content,
        count=1
    )

    # Update library_dirs in Extension() call
    updated_content = re.sub(
        r'(\s+library_dirs\s*=\s*\[[^\]]+\])',
        r'\1 + gp4c_lib_dirs',
        updated_content,
        count=1
    )

    # Write updated setup.py
    try:
        with open(setup_path, "w") as f:
            f.write(updated_content)
        console.print(
            "[bold green]✓ setup.py updated successfully![/bold green]"
        )
    except Exception as e:
        console.print(f"\n[red]Error writing setup.py:[/red] {e}")
        console.print(f"[dim]Restoring from backup...[/dim]")
        shutil.copy2(backup_path, setup_path)


def _update_class_makefile() -> None:
    """Update CLASS Makefile to include gp4c integration."""
    makefile_path = Path.cwd() / "Makefile"

    # Check if Makefile exists
    if not makefile_path.exists():
        console.print(
            "\n[yellow]Warning:[/yellow] No Makefile found in current directory",
        )
        console.print("[dim]Skipping CLASS integration[/dim]")
        return

    # Read Makefile content
    try:
        with open(makefile_path, "r") as f:
            content = f.read()
    except Exception as e:
        console.print(f"\n[red]Error reading Makefile:[/red] {e}")
        return

    # Check if it's a CLASS Makefile
    if "CLASS" not in content and "class.o" not in content:
        console.print(
            "\n[yellow]Warning:[/yellow] This doesn't appear to be a CLASS Makefile",
        )
        if not typer.confirm("Continue anyway?"):
            return

    # Check if already integrated
    if "gp4c" in content or "GP4C" in content:
        console.print("\n[yellow]Note:[/yellow] gp4c already appears to be integrated")
        if not typer.confirm("Update anyway?"):
            return

    # Create backup
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = makefile_path.with_suffix(f".backup_{timestamp}")
    shutil.copy2(makefile_path, backup_path)
    console.print(f"\n[dim]Created backup:[/dim] {backup_path.name}")

    # Find where to insert gp4c flags
    lines = content.split("\n")
    insert_idx = None

    # Look for the line with "LDFLAG = " to insert after it
    for i, line in enumerate(lines):
        if line.startswith("LDFLAG") and "=" in line:
            insert_idx = i + 1
            break

    if insert_idx is None:
        console.print("[red]Error:[/red] Could not find LDFLAG in Makefile")
        return

    # Prepare gp4c integration lines
    gp4c_lines = [
        "",
        "# gp4c GP sampler integration (added by gp4c install --class)",
        "GP4C_CFLAGS = $(shell pkg-config --cflags gp4c)",
        "GP4C_LIBS = $(shell pkg-config --libs gp4c)",
        "INCLUDES += $(GP4C_CFLAGS)",
        "LDFLAG += $(GP4C_LIBS)",
    ]

    # Insert the lines
    lines[insert_idx:insert_idx] = gp4c_lines

    # Write updated Makefile
    try:
        with open(makefile_path, "w") as f:
            f.write("\n".join(lines))
        console.print(
            "\n[bold green]✓ CLASS Makefile updated successfully![/bold green]"
        )
        console.print("\n[dim]Next steps:[/dim]")
        console.print("  1. Run: [cyan]make clean && make class[/cyan]")
        console.print("  2. For Python wrapper: [cyan]make classy[/cyan]")
    except Exception as e:
        console.print(f"\n[red]Error writing Makefile:[/red] {e}")
        console.print(f"[dim]Restoring from backup...[/dim]")
        shutil.copy2(backup_path, makefile_path)


def _codesign_if_macos(path: Path) -> None:
    """Ad-hoc codesign a binary on macOS. No-op on Linux."""
    import platform
    if platform.system() != "Darwin":
        return
    result = subprocess.run(
        ["codesign", "--force", "--sign", "-", str(path)],
        capture_output=True, text=True,
    )
    if result.returncode == 0:
        console.print(f"  [green]✓[/green] Signed: {path.name}")
    else:
        console.print(f"  [yellow]⚠[/yellow] Could not sign {path.name}: {result.stderr.strip()}")


def _rebuild_and_sign_classy() -> None:
    """Rebuild _classy.so via pip install -e . in CWD, then codesign it on macOS."""
    console.print("\n[bold]Rebuilding CLASS Python wrapper...[/bold]")
    console.print(f"[dim]Using: {sys.executable}[/dim]")

    result = subprocess.run(
        [sys.executable, "-m", "pip", "install", "-e", ".", "--no-build-isolation"],
        cwd=Path.cwd(),
        check=False,
    )
    if result.returncode != 0:
        console.print(
            "[yellow]⚠ CLASS rebuild failed — rebuild manually with:[/yellow]\n"
            f"  [cyan]pip install -e . --no-build-isolation[/cyan]"
        )
        return

    console.print("[green]✓ CLASS Python wrapper rebuilt[/green]")

    # Find _classy*.so in the active site-packages and sign it
    import site
    signed = False
    for sp in site.getsitepackages():
        for so in Path(sp).glob("classy/_classy*.so"):
            _codesign_if_macos(so)
            signed = True
    if not signed:
        console.print("[dim]Could not locate _classy.so — if on macOS, sign it manually:[/dim]")
        console.print("  [cyan]codesign --force --sign - <path/to/_classy*.so>[/cyan]")


def _update_shell_config() -> bool:
    """Update shell config file with gp4c environment variables.

    Returns True if config was updated, False otherwise.
    """
    import platform

    home = Path.home()

    # Detect shell and config file
    shell = os.environ.get("SHELL", "")
    if "zsh" in shell:
        config_file = home / ".zshrc"
        shell_name = "zsh"
    else:
        config_file = home / ".bashrc"
        shell_name = "bash"

    # Check if config file exists
    if not config_file.exists():
        # Try to create it
        try:
            config_file.touch()
            console.print(f"[dim]Created {config_file}[/dim]")
        except Exception as e:
            console.print(f"[yellow]Warning:[/yellow] Could not create {config_file}: {e}")
            return False

    # Read current content
    try:
        content = config_file.read_text()
    except Exception as e:
        console.print(f"[yellow]Warning:[/yellow] Could not read {config_file}: {e}")
        return False

    # Check if already configured
    if "gp4c" in content and "PKG_CONFIG_PATH" in content:
        console.print(f"[dim]Shell config already contains gp4c settings[/dim]")
        return True

    # Prepare the exports
    is_macos = platform.system() == "Darwin"

    exports = '''
# gp4c library environment (added by gp4c install)
export PKG_CONFIG_PATH="$HOME/.local/lib/pkgconfig:$PKG_CONFIG_PATH"
'''
    # Note: DYLD_LIBRARY_PATH is NOT added on macOS as it causes hangs and conflicts with SIP
    # The install script uses -install_name to embed the library path correctly
    if not is_macos:
        exports += 'export LD_LIBRARY_PATH="$HOME/.local/lib:$LD_LIBRARY_PATH"\n'

    # Create backup
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = config_file.with_suffix(f".backup_gp4c_{timestamp}")
    try:
        shutil.copy2(config_file, backup_path)
        console.print(f"[dim]Created backup: {backup_path.name}[/dim]")
    except Exception as e:
        console.print(f"[yellow]Warning:[/yellow] Could not create backup: {e}")

    # Append exports
    try:
        with open(config_file, "a") as f:
            f.write(exports)
        console.print(f"[bold green]✓ Updated {config_file.name}[/bold green]")
        console.print(f"[dim]Run [cyan]source ~/{config_file.name}[/cyan] or restart your terminal[/dim]")
        return True
    except Exception as e:
        console.print(f"[red]Error:[/red] Could not update {config_file}: {e}")
        return False


@app.command()
def install(
    user: bool = typer.Option(
        False,
        "--user",
        help="Install to user directory (~/.local) instead of system-wide (/usr/local)",
    ),
    force: bool = typer.Option(
        False,
        "--force",
        "-f",
        help="Force installation even if already installed",
    ),
    class_integration: bool = typer.Option(
        False,
        "--class",
        help="Automatically update CLASS Makefile and setup.py in current directory",
    ),
    setup_shell: bool = typer.Option(
        False,
        "--setup-shell",
        help="Automatically add environment variables to ~/.bashrc or ~/.zshrc",
    ),
):
    """
    Install the gp4c C library system-wide for use in C/C++ projects.

    This makes the library available to Boltzmann solvers and other C/C++ codes.

    Examples:
        gp4c install              # Install system-wide (requires sudo)
        gp4c install --user       # Install to ~/.local (no sudo needed)
        gp4c install --user --setup-shell  # Install and configure shell
        gp4c install --user --class  # Install and update CLASS Makefile/setup.py
        gp4c install --user --setup-shell --class  # Full installation for CLASS
    """
    # Check if pkg-config is installed
    try:
        subprocess.run(
            ["pkg-config", "--version"],
            capture_output=True,
            check=True,
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        console.print(
            "[bold red]Error:[/bold red] pkg-config is not installed\n",
        )
        console.print(
            "[yellow]pkg-config is required to install the C library.[/yellow]\n"
        )
        console.print("[dim]Install it with:[/dim]")
        console.print("  [cyan]brew install pkg-config[/cyan]  # macOS")
        console.print("  [cyan]sudo apt install pkg-config[/cyan]  # Ubuntu/Debian")
        console.print("  [cyan]sudo yum install pkg-config[/cyan]  # Fedora/RHEL\n")
        raise typer.Exit(code=1)

    project_root = get_project_root()
    install_script = project_root / "install_library.sh"

    # Check if script exists
    if not install_script.exists():
        console.print(
            f"[red]Error:[/red] Installation script not found at {install_script}",
            style="bold red",
        )
        console.print(
            "\n[yellow]Tip:[/yellow] Make sure you're running this from the gp4c package directory.",
        )
        raise typer.Exit(code=1)

    # Make script executable
    install_script.chmod(0o755)

    # Build command
    cmd = [str(install_script)]
    if user:
        cmd.append("--user")
    if force:
        cmd.append("--force")

    # Show installation info
    install_type = "user directory (~/.local)" if user else "system-wide (/usr/local)"
    needs_sudo = "No" if user else "Yes (will prompt)"

    console.print(
        Panel.fit(
            f"[bold cyan]Installing gp4c C Library[/bold cyan]\n\n"
            f"[dim]Location:[/dim] {install_type}\n"
            f"[dim]Requires sudo:[/dim] {needs_sudo}\n"
            f"[dim]Force reinstall:[/dim] {'Yes' if force else 'No'}\n"
            f"[dim]Setup shell:[/dim] {'Yes' if setup_shell else 'No'}\n"
            f"[dim]CLASS integration:[/dim] {'Yes' if class_integration else 'No'}",
            border_style="cyan",
        )
    )

    # Run installation
    try:
        console.print("\n[dim]Running installation script...[/dim]\n")

        result = subprocess.run(
            cmd,
            cwd=project_root,
            check=False,
            text=True,
        )

        if result.returncode == 0:
            console.print(
                "\n[bold green]✓ Installation successful![/bold green]\n",
            )

            import platform
            lib_ext = "dylib" if platform.system() == "Darwin" else "so"
            prefix = Path.home() / ".local" if user else Path("/usr/local")
            _codesign_if_macos(prefix / "lib" / f"libgp4c.{lib_ext}")

            if user:
                if setup_shell:
                    console.print("\n[bold]Configuring shell environment...[/bold]")
                    _update_shell_config()
                else:
                    console.print(
                        Panel(
                            "[yellow]Add these to your ~/.bashrc or ~/.zshrc:[/yellow]\n\n"
                            '[dim]export PKG_CONFIG_PATH="$HOME/.local/lib/pkgconfig:$PKG_CONFIG_PATH"\n'
                            'export LD_LIBRARY_PATH="$HOME/.local/lib:$LD_LIBRARY_PATH"  # Linux only[/dim]\n\n'
                            '[dim]Note: DYLD_LIBRARY_PATH not needed on macOS (causes hangs)[/dim]\n\n'
                            '[dim]Or run: [cyan]gp4c install --user --setup-shell[/cyan][/dim]',
                            title="Environment Setup Required",
                            border_style="yellow",
                        )
                    )

            console.print("\n[dim]Test the installation:[/dim]")
            console.print("  [cyan]pkg-config --modversion gp4c[/cyan]")
            console.print("  [cyan]pkg-config --cflags --libs gp4c[/cyan]")

            # Update CLASS Makefile and setup.py if requested
            if class_integration:
                _update_class_makefile()   # idempotent — skips if already patched
                _update_class_setup_py()   # idempotent — skips if already patched
                _rebuild_and_sign_classy() # always rebuild + sign

        else:
            console.print(
                f"\n[bold red]✗ Installation failed with exit code {result.returncode}[/bold red]\n",
            )
            raise typer.Exit(code=result.returncode)

    except KeyboardInterrupt:
        console.print("\n[yellow]Installation cancelled by user.[/yellow]")
        raise typer.Exit(code=130)
    except Exception as e:
        console.print(f"\n[bold red]Error:[/bold red] {e}", style="bold red")
        raise typer.Exit(code=1)


@app.command()
def uninstall(
    user: bool = typer.Option(
        False,
        "--user",
        help="Uninstall from user directory (~/.local) instead of system (/usr/local)",
    ),
):
    """
    Uninstall the gp4c C library.

    Removes the installed library, headers, and pkg-config files.

    Examples:
        gp4c uninstall        # Uninstall from system (requires sudo)
        gp4c uninstall --user # Uninstall from ~/.local
    """
    import platform

    prefix = Path.home() / ".local" if user else Path("/usr/local")

    lib_ext = "dylib" if platform.system() == "Darwin" else "so"
    files_to_remove = [
        prefix / "lib" / f"libgp4c.{lib_ext}",
        prefix / "include" / "gp_sampler_core.h",
        prefix / "lib" / "pkgconfig" / "gp4c.pc",
    ]

    needs_sudo = not user

    console.print(
        Panel.fit(
            f"[bold red]Uninstalling gp4c C Library[/bold red]\n\n"
            f"[dim]Location:[/dim] {prefix}\n"
            f"[dim]Requires sudo:[/dim] {'Yes' if needs_sudo else 'No'}",
            border_style="red",
        )
    )

    # Check which files exist
    existing_files = [f for f in files_to_remove if f.exists()]

    if not existing_files:
        console.print("\n[yellow]No gp4c installation found.[/yellow]")
        raise typer.Exit(code=0)

    console.print(f"\n[dim]Files to remove:[/dim]")
    for f in existing_files:
        console.print(f"  [red]✗[/red] {f}")

    # Confirm
    if not typer.confirm("\nProceed with uninstallation?"):
        console.print("[yellow]Uninstallation cancelled.[/yellow]")
        raise typer.Exit(code=0)

    # Remove files
    try:
        for f in existing_files:
            if needs_sudo:
                subprocess.run(["sudo", "rm", "-f", str(f)], check=True)
            else:
                f.unlink(missing_ok=True)

        # Update ldconfig on Linux if system install
        if not user and platform.system() == "Linux":
            subprocess.run(["sudo", "ldconfig"], check=False)

        console.print("\n[bold green]✓ Uninstallation successful![/bold green]")

    except subprocess.CalledProcessError as e:
        console.print(f"\n[bold red]Error:[/bold red] {e}", style="bold red")
        raise typer.Exit(code=1)
    except Exception as e:
        console.print(f"\n[bold red]Error:[/bold red] {e}", style="bold red")
        raise typer.Exit(code=1)


@app.command()
def info():
    """
    Display information about the gp4c installation and usage.

    Shows version, installation status, and usage examples.
    """
    from importlib.metadata import version as get_version

    try:
        pkg_version = get_version("gp4c")
    except Exception:
        pkg_version = "unknown"

    console.print(
        Panel.fit(
            f"[bold cyan]gp4c - GP Sampler[/bold cyan]\n\n"
            f"[dim]Version:[/dim] {pkg_version}\n"
            f"[dim]Python package:[/dim] Installed\n",
            border_style="cyan",
        )
    )

    # Check C library installation
    console.print("\n[bold]C Library Status:[/bold]")

    try:
        result = subprocess.run(
            ["pkg-config", "--modversion", "gp4c"],
            capture_output=True,
            text=True,
            check=True,
        )
        c_version = result.stdout.strip()
        console.print(f"  [green]✓[/green] System library: v{c_version}")

        # Get installation path
        result = subprocess.run(
            ["pkg-config", "--variable=libdir", "gp4c"],
            capture_output=True,
            text=True,
            check=True,
        )
        lib_path = result.stdout.strip()
        console.print(f"  [dim]Location:[/dim] {lib_path}")

    except (subprocess.CalledProcessError, FileNotFoundError):
        console.print("  [yellow]✗[/yellow] C library not installed")
        console.print("  [dim]Run:[/dim] [cyan]gp4c install[/cyan] to install")

    console.print("\n[bold]Usage Examples:[/bold]")
    console.print("  [dim]Python:[/dim]")
    console.print("    [cyan]from gp4c import sample_prior[/cyan]")
    console.print("    [cyan]spec = SamplingSpec(x_f=x, x_g=x)[/cyan]")
    console.print("    [cyan]result = sample_prior(spec, n_samples=10)[/cyan]")
    console.print("\n  [dim]C/C++:[/dim]")
    console.print("    [cyan]gcc solver.c $(pkg-config --cflags --libs gp4c)[/cyan]")

    console.print("\n[dim]Documentation:[/dim] See examples/C_INTEGRATION.md")


def main():
    """Entry point for the CLI."""
    app()


if __name__ == "__main__":
    main()
