from ast import arg
import json
import os
import platform
import subprocess
import tempfile
import traceback
import webbrowser
import shutil
import click
import requests
import yaml
import uuid
from datetime import datetime, UTC
from sahaj.helpers import (
    check_email_valid,
    check_domain,
    get_version_string,
    get_config_dir,
    get_config_path,
    password_strength,
    read_config,
    write_config,
    run_native,
    run_repo_script,
    write_quickstart_marker,
    read_quickstart_marker,
    get_compose_env,
    project_from_path,
    docker_list_lines,
    make_sh_executable,
)

DOCKER_COMPOSE_URL = (
    "https://raw.githubusercontent.com/concur-live/docker-yml/main/docker-compose.yml"
)
DOCKER_COMPOSE_QUICKSTART_URL = "https://raw.githubusercontent.com/concur-live/docker-yml/main/docker-compose.quickstart.yml"
REPO_URL = "https://github.com/gawravmehta/sahaj-os.git"
DEFAULT_DIRNAME = "sahaj-opensource"


# -----------------------
# Click CLI
# -----------------------
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
def cli():
    """SAHAJ CLI — Open Source Software Installer"""
    pass


# -----------------------
# simple commands
# -----------------------
@cli.command()
def version():
    """Display current version of SAHAJ"""
    click.echo(f"SAHAJ version {get_version_string()} (Last updated: Oct 2025)")


# @cli.command()
# def update():
#     """Check for updates"""
#     click.echo("Checking for updates...")
#     click.echo("No updates available right now. You are using the latest version.")


@cli.command()
def license():
    """Show open-source license details"""
    license_path = os.path.join(os.path.dirname(__file__), "../../LICENSE.md")
    try:
        with open(os.path.abspath(license_path), "r", encoding="utf-8") as f:
            click.echo(f.read())
    except FileNotFoundError:
        click.echo("License file not found.")


@cli.command()
def about():
    """Show information about SAHAJ"""
    click.echo(
        """SAHAJ is an open-source project to simplify installation and management
of community-driven software. It provides an intuitive command-line interface
for deployment, configuration, and discovery of open-source tools.
"""
    )


@cli.command()
def docs():
    """Open documentation in a browser"""
    url = "https://docs.sahaj.live"
    click.echo(f"Opening documentation: {url}")
    webbrowser.open(url)


@cli.command()
def list():
    """List all available open-source modules supported by SAHAJ"""
    modules = [
        "Organization Management – Configure organizational hierarchy, departments, users, and access controls for compliance operations.",
        "Cookie Consent – Automatically scan websites and manage cookie consent to ensure DPDPA compliance.",
        "Data Principal Management – Maintain records and lifecycle of both legacy and new data principals for compliance tracking.",
        "Data Element – Identify, ingest, and classify PII data elements across systems for compliance monitoring.",
        "Purpose Management – Define, manage, and publish data processing purposes aligned with DPDPA consent requirements.",
        "Notice Orchestration – Create and manage Data Principal Notices for transparent data collection and consent communication.",
        "Collection Point – Configure and manage all data collection sources including web, mobile, and offline channels.",
        "Consent Governance – Monitor, search, and administer collected consents aligned with business and legal objectives.",
        "Consent Validation – Validate consent records, scope, and artifacts to ensure lawful and compliant data processing.",
        "Legacy Notice – Collect and regularize consents from existing data principals as mandated under Section 5(2).",
        "Grievance – Track, manage, and resolve data principal grievances in line with BRD-CMS compliance standards.",
        "Data Principal Rights – Handle and fulfill Data Principal access and rights requests efficiently.",
        "Breach Management – Log, assess, and report data breaches, ensuring timely notification to authorities and data principals.",
        "Assets/SKU – Maintain and manage organizational assets, SKUs, and their linkage to consent and cookie compliance activities.",
        "Customer Portal – Deploy a self-service Data Principal portal for managing consents and exercising privacy rights.",
    ]
    click.echo(f"{'No.':<4} {'Module':<25} Description")
    click.echo("-" * 100)
    for i, module in enumerate(modules, start=1):
        if "–" in module:
            name, desc = module.split("–", 1)
        else:
            name, desc = module, ""
        click.echo(f"{i:<4} {name.strip():<25} {desc.strip()}")


# @cli.command()
# @click.option("-y", "--yes", is_flag=True, help="Skip confirmation prompt.")
# @click.option(
#     "--into",
#     type=click.Path(file_okay=False, dir_okay=True, path_type=str),
#     default=None,
#     help="Target repo directory (defaults to ./sahaj-opensource and quickstart marker).",
# )
# def cleanup(yes, into):
#     """
#     Cleanup SAHAJ-related Docker resources for the current/selected project(s):
#     stops & removes containers, removes project-built images, volumes, and networks.
#     """
#     # 1) Collect candidate project names
#     projects = set()

#     # explicit target
#     if into:
#         projects.add(project_from_path(into))

#     # default repo under cwd
#     cwd = os.getcwd()
#     default_repo = os.path.join(cwd, DEFAULT_DIRNAME)
#     if os.path.isdir(default_repo):
#         projects.add(project_from_path(default_repo))

#     # quickstart marker (if any): compose path saved earlier
#     try:
#         q_compose = read_quickstart_marker()
#         if q_compose and os.path.exists(q_compose):
#             projects.add(project_from_path(os.path.dirname(q_compose)))
#     except Exception:
#         pass

#     if not projects:
#         click.echo(
#             "No SAHAJ-managed project detected (no ./sahaj-opensource or quickstart marker). Nothing to clean."
#         )
#         return

#     click.echo("SAHAJ cleanup will target the following project(s):")
#     for p in sorted(projects):
#         click.echo(f"  - {p}")

#     if not yes:
#         click.confirm(
#             "⚠️ This will stop/remove containers, images, volumes, and networks for the projects above. Continue?",
#             abort=True,
#         )

#     # summary counters
#     total = {
#         "containers_stopped": 0,
#         "containers_removed": 0,
#         "images_removed": 0,
#         "volumes_removed": 0,
#         "networks_removed": 0,
#     }

#     for proj in projects:
#         click.echo(f"\n➡️ Cleaning project: {proj}")

#         # --- containers: find by compose project label
#         ps_cmd = [
#             "docker",
#             "ps",
#             "-a",
#             "--filter",
#             f"label=com.docker.compose.project={proj}",
#             "--format",
#             "{{.ID}}",
#         ]
#         container_ids = docker_list_lines(ps_cmd)
#         if container_ids:
#             click.echo(f"Stopping containers: {len(container_ids)}")
#             try:
#                 subprocess.run(["docker", "stop"] + container_ids, check=False)
#                 total["containers_stopped"] += len(container_ids)
#             except Exception as e:
#                 click.echo(f"  ⚠️ Failed to stop containers: {e}")

#             click.echo("Removing containers...")
#             try:
#                 subprocess.run(["docker", "rm", "-f"] + container_ids, check=False)
#                 total["containers_removed"] += len(container_ids)
#             except Exception as e:
#                 click.echo(f"  ⚠️ Failed to remove containers: {e}")
#         else:
#             click.echo("No containers found for this project.")

#         # --- images: remove images that match <project>-* (common compose auto-generated tag)
#         # list all images and filter
#         images_cmd = ["docker", "images", "--format", "{{.Repository}}:{{.Tag}}"]
#         images = docker_list_lines(images_cmd)
#         matched_images = [img for img in images if img.lower().startswith(f"{proj}-")]
#         if matched_images:
#             click.echo(f"Removing images: {len(matched_images)}")
#             for img in matched_images:
#                 try:
#                     subprocess.run(["docker", "rmi", "-f", img], check=False)
#                     total["images_removed"] += 1
#                     click.echo(f"  removed {img}")
#                 except Exception as e:
#                     click.echo(f"  ⚠️ failed to remove {img}: {e}")
#         else:
#             click.echo("No project-built images found (matching '<project>-*').")

#         # --- volumes: find by label or by name prefix
#         vol_cmd = ["docker", "volume", "ls", "--format", "{{.Name}}"]
#         volumes = docker_list_lines(vol_cmd)
#         vol_matches = [
#             v for v in volumes if v.startswith(f"{proj}_") or v.startswith(f"{proj}-")
#         ]
#         # also check volumes with label com.docker.compose.project
#         labeled_vol_cmd = [
#             "docker",
#             "volume",
#             "ls",
#             "--filter",
#             f"label=com.docker.compose.project={proj}",
#             "--format",
#             "{{.Name}}",
#         ]
#         labeled = docker_list_lines(labeled_vol_cmd)
#         for v in sorted(set(vol_matches + labeled)):
#             try:
#                 subprocess.run(["docker", "volume", "rm", "-f", v], check=False)
#                 total["volumes_removed"] += 1
#                 click.echo(f"  removed volume {v}")
#             except Exception as e:
#                 click.echo(f"  ⚠️ failed to remove volume {v}: {e}")
#         if not vol_matches and not labeled:
#             click.echo("No volumes found for this project.")

#         # --- networks: by label or name prefix
#         net_cmd = ["docker", "network", "ls", "--format", "{{.Name}}"]
#         networks = docker_list_lines(net_cmd)
#         net_matches = [
#             n for n in networks if n.startswith(f"{proj}_") or n.startswith(f"{proj}-")
#         ]
#         labeled_net_cmd = [
#             "docker",
#             "network",
#             "ls",
#             "--filter",
#             f"label=com.docker.compose.project={proj}",
#             "--format",
#             "{{.Name}}",
#         ]
#         labeled_nets = docker_list_lines(labeled_net_cmd)
#         for n in sorted(set(net_matches + labeled_nets)):
#             try:
#                 subprocess.run(["docker", "network", "rm", n], check=False)
#                 total["networks_removed"] += 1
#                 click.echo(f"  removed network {n}")
#             except Exception as e:
#                 click.echo(f"  ⚠️ failed to remove network {n}: {e}")
#         if not net_matches and not labeled_nets:
#             click.echo("No networks found for this project.")

#     # final summary
#     click.echo("\n======== SAHAJ cleanup summary ========")
#     click.echo(f"Containers stopped: {total['containers_stopped']}")
#     click.echo(f"Containers removed: {total['containers_removed']}")
#     click.echo(f"Images removed: {total['images_removed']}")
#     click.echo(f"Volumes removed: {total['volumes_removed']}")
#     click.echo(f"Networks removed: {total['networks_removed']}")
#     click.echo("Cleanup finished.")


# -----------------------
# config command
# -----------------------
@cli.command()
@click.option(
    "--overwrite", is_flag=True, help="Overwrite existing config without prompting"
)
def register(overwrite):
    """
    Run config wizard (interactive).
    If --overwrite is provided, the existing config (if any) will be replaced without asking.
    """
    cfg_path = get_config_path()
    cfg_dir = get_config_dir()
    os.makedirs(cfg_dir, exist_ok=True)

    if os.path.exists(cfg_path) and not overwrite:
        # Ask the user if they want to overwrite
        if not click.confirm(f"Config already exists at {cfg_path}. Overwrite?"):
            click.echo("Aborted — existing configuration preserved.")
            return

    # interactive prompts
    click.echo("=== SAHAJ Configuration Setup ===")
    main_domain = click.prompt("Enter your main domain", type=str)
    is_domain_valid = check_domain(main_domain)
    while not is_domain_valid:
        click.echo(
            """The domain you entered is not valid. Please try again. 
            - Valid domain examples: xyz.com, abc.in, example.org
            - No https: or http:
            - No Subdomain [name.xyz.com, sub.abc.in]"""
        )
        main_domain = click.prompt("Enter your main domain", type=str)
        is_domain_valid = check_domain(main_domain)
    superadmin_email = click.prompt("Enter your superadmin email", type=str)
    is_email_valid = check_email_valid(superadmin_email)
    while not is_email_valid:
        click.echo(
            """The email you entered is not valid. Please try again.
            - Format: local_part@domain.part
            - No empty local or domain parts
            - Domain must contain a dot (.)"""
        )
        superadmin_email = click.prompt("Enter your superadmin email", type=str)
        is_email_valid = check_email_valid(superadmin_email)
    temporary_password = click.prompt(
        "Enter your temporary password", type=str, hide_input=True
    )
    retype_password = click.prompt(
        "Retype your temporary password",
        type=str,
        hide_input=True,
    )
    is_password_strong = password_strength(temporary_password)
    while temporary_password != retype_password or not is_password_strong:
        if temporary_password != retype_password:
            click.echo("Passwords do not match. Please try again.")
        else:
            click.echo(
                """The password you entered is not strong enough. Please try again. A strong password is required for OpenSearch.
                - Minimum 8 characters
                - At least one uppercase letter
                - At least one lowercase letter
                - At least one digit
                - At least one special character ➡️ !@^()-_=+[]{}|;:,.<>?
                - Must not contain common passwords like "password", "1234", "qwerty", "test", etc. 
                """
            )
        temporary_password = click.prompt(
            "Enter your temporary password", type=str, hide_input=True
        )
        retype_password = click.prompt(
            "Retype your temporary password", type=str, hide_input=True
        )
        is_password_strong = password_strength(temporary_password)

    config_data = {
        "main_domain": main_domain.strip(),
        "superadmin_email": superadmin_email.strip(),
        "temporary_password": temporary_password.strip(),
    }

    ####STUB - well do api call here and get response, wait 10s for response, else self generate
    response = {
        "df_id": str(uuid.uuid4()),
        "license_key": str(uuid.uuid4()),  ##64 bit hex string
        "created_at": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
    }

    ###FIXME - remove stub when api is ready

    config_data.update(response)

    try:
        write_config(config_data)
        click.echo(f"\nConfiguration saved successfully to: {cfg_path}")
    except OSError as e:
        click.echo(f"Error writing config file {cfg_path}: {e}")


# -----------------------
# init command group with subcommands
# -----------------------
# @cli.group(invoke_without_command=True)
# @click.pass_context
# def init(ctx):
#     """Initialize SAHAJ | run 'sahaj init --help' for subcommands"""
#     if ctx.invoked_subcommand is None:
#         click.echo("Running main init logic (no subcommand provided)")
#         # stub: replace with actual init implementation


@cli.command(name="pull")
@click.option(
    "--into",
    type=click.Path(file_okay=False, dir_okay=True, writable=True, path_type=str),
    default=None,
    help="Directory name (relative) to clone into / pull updates. Defaults to './sahaj-opensource'.",
)
def pull(into):
    """Clone the repo (if missing) or pull latest changes. Copies user config and fixes .sh exec perms."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    # ensure git is available
    git_path = shutil.which("git")
    if git_path is None:
        click.echo("❌ Git not found on PATH. Please install git to use this command.")
        return

    # If target doesn't exist -> clone
    if not os.path.exists(target_path):
        click.echo(f"Cloning {REPO_URL} into {target_path} ...")
        try:
            subprocess.run([git_path, "clone", REPO_URL, target_path], check=True)
            click.echo("✅ Clone complete.")
        except subprocess.CalledProcessError as e:
            click.echo(f"❌ git clone failed: exit {e.returncode}")
            return
        except Exception as e:
            click.echo(f"❌ Unexpected error while cloning: {e}")
            return
    else:
        # folder exists: attempt safe update via git pull
        click.echo(
            f"Target already exists at {target_path}. Attempting to update via git pull..."
        )

        try:
            proc = subprocess.run(
                [git_path, "-C", target_path, "pull", "origin", "main"],
                check=False,
                capture_output=True,
                text=True,
            )
            stdout = (proc.stdout or "").strip()
            stderr = (proc.stderr or "").strip()
            rc = proc.returncode

            if rc == 0:
                click.echo("✅ Successfully pulled latest changes (origin/main).")
            else:
                combined = (stdout + "\n" + stderr).lower()
                # detect local-change/overwrite-by-merge error
                if (
                    "would be overwritten by merge" in combined
                    or "your local changes" in combined
                ):
                    click.echo(
                        "⚠️ git pull failed because you have local changes that would be overwritten."
                    )
                    click.echo(
                        "Files changed locally will be lost if you discard them."
                    )
                    if click.confirm(
                        "Discard local changes and reset to origin/main? This will permanently delete local changes.",
                        default=False,
                    ):
                        # perform forced update
                        fetch = subprocess.run(
                            [git_path, "-C", target_path, "fetch", "origin"],
                            capture_output=True,
                            text=True,
                        )
                        if fetch.returncode != 0:
                            click.echo(
                                f"⚠️ git fetch failed with code {fetch.returncode}. Aborting."
                            )
                            if fetch.stdout:
                                click.echo(fetch.stdout)
                            if fetch.stderr:
                                click.echo(fetch.stderr)
                            return
                        click.echo("Resetting local branch to origin/main (force) ...")
                        reset = subprocess.run(
                            [
                                git_path,
                                "-C",
                                target_path,
                                "reset",
                                "--hard",
                                "origin/main",
                            ],
                            capture_output=True,
                            text=True,
                        )
                        if reset.returncode == 0:
                            click.echo(
                                "✅ Local changes discarded. Repository now matches origin/main."
                            )
                        else:
                            click.echo(
                                f"⚠️ git reset failed with code {reset.returncode}. Please inspect the repo manually."
                            )
                            if reset.stdout:
                                click.echo(reset.stdout)
                            if reset.stderr:
                                click.echo(reset.stderr)
                            return
                    else:
                        click.echo(
                            "Aborted — local changes preserved. Exiting 'sahaj init pull'."
                        )
                        return
                else:
                    # other git error: show output
                    click.echo(
                        f"⚠️ git pull exited with code {rc}. You may need to inspect the repo manually."
                    )
                    if stdout:
                        click.echo("git stdout:")
                        click.echo(stdout)
                    if stderr:
                        click.echo("git stderr:")
                        click.echo(stderr)
                    return
        except FileNotFoundError:
            click.echo(
                "❌ git not found on PATH. Please install git to update the existing folder, or remove the folder to reclone."
            )
            return
        except Exception as e:
            click.echo(f"❌ Unexpected error while updating repo: {e}")
            return
        click.echo("Using existing folder after update step.")

    # --- copy user config into cloned repo root (if present) ---
    try:
        src_cfg = get_config_path()
        if os.path.exists(src_cfg):
            dest_cfg = os.path.join(target_path, "config.json")
            # backup existing repo config if present
            if os.path.exists(dest_cfg):
                ts = datetime.now().strftime("%Y%m%d%H%M%S")
                backup_path = dest_cfg + f".bak.{ts}"
                try:
                    shutil.copy2(dest_cfg, backup_path)
                    click.echo(f"Existing repo config backed up to: {backup_path}")
                except Exception as be:
                    click.echo(f"Warning: failed to back up existing repo config: {be}")
            # copy global config into repo
            try:
                shutil.copy2(src_cfg, dest_cfg)
                click.echo(f"Copied user config to cloned repo: {dest_cfg}")
            except Exception as ce:
                click.echo(f"Warning: failed to copy config into cloned repo: {ce}")
        else:
            click.echo("No user config found to copy into cloned repo.")
    except Exception as e:
        click.echo(f"Warning: config copy step failed: {e}")

    # --- make .sh files executable in repo-root/scripts and repo-root/services recursively ---
    try:
        scripts_dir = os.path.join(target_path, "scripts")
        services_dir = os.path.join(target_path, "services")
        made = make_sh_executable([scripts_dir, services_dir])
        click.echo(
            f"Set executable permission on {made} .sh files under 'scripts' and 'services'."
        )
    except Exception as e:
        click.echo(f"Warning: failed to set executable permissions: {e}")

    click.echo("✅ Pull/setup finished.")


cli.add_command(pull, name="clone")


# -----------------------
# rundev group + subcommands
# -----------------------
@cli.group(name="rundev", invoke_without_command=True)
@click.option(
    "--into",
    type=click.Path(file_okay=False, dir_okay=True, writable=True, path_type=str),
    default=None,
    help="Directory where repo was cloned (defaults to './sahaj-opensource').",
)
@click.pass_context
def rundev(ctx, into):
    """
    rundev group — use subcommands: start, stop, restart, status.
    If called without a subcommand, runs 'start'.
    """
    if ctx.invoked_subcommand is None:
        ctx.invoke(rundev_start, into=into)


@rundev.command("start")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def rundev_start(into):
    """Start dev environment by running init-rundev.sh in the repo root."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo(
            f"Repo folder not found at {target_path}. Run 'sahaj init dev' first to clone."
        )
        return

    # ensure repo has a config.json at root (required)
    repo_cfg_path = os.path.join(target_path, "config.json")
    if not os.path.exists(repo_cfg_path):
        click.echo(
            f"❌ Missing {repo_cfg_path}. Place config.json in repo root before starting rundev."
        )
        return

    try:
        click.echo("🚀 Starting dev environment (init-rundev.sh)...")
        run_repo_script(target_path, "init-rundev", None)
        click.echo("✅ Dev start script completed.")
    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ Dev start script exited with code {e.returncode}")
    except FileNotFoundError:
        click.echo("❌ init-rundev script not found in repo root.")
    except Exception as e:
        click.echo(f"❌ Failed to start dev environment: {e}")


@rundev.command("stop")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
@click.option(
    "--clean-volumes",
    "-a",
    is_flag=True,
    help="Also clean volumes (passes -a to stop-rundev.sh). Recommended for fresh starts.",
)
def rundev_stop(into, clean_volumes):
    """Stop dev environment by running stop-rundev.sh (optionally cleaning volumes)."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo("No repo found — nothing to stop.")
        return

    # Decide whether to pass -a to the underlying script
    script_args = []
    try:
        if not clean_volumes:
            # interactive prompt: recommended to clean volumes after stop (helps fresh starts)
            if click.confirm(
                "Do you also want to clean volumes? This is recommended for a fresh start. (y/n)",
                default=True,
            ):
                script_args = ["-a"]
        else:
            script_args = ["-a"]

        click.echo(
            f"🛑 Stopping dev environment (stop-rundev.sh {' '.join(script_args)})..."
        )
        # pass current environment (scripts handle config.json themselves)
        run_repo_script(target_path, "stop-rundev", None, args=script_args)
        click.echo("✅ Dev stop script completed.")
    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ Dev stop script exited with code {e.returncode}")
    except FileNotFoundError:
        click.echo("❌ stop-rundev script not found in repo root.")
    except Exception as e:
        click.echo(f"❌ Failed to stop dev environment: {e}")


@rundev.command("restart")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def rundev_restart(into):
    """Restart dev environment (stop then start)."""
    ctx = click.get_current_context()
    ctx.invoke(rundev_stop, into=into, clean_volumes=True)
    ctx.invoke(rundev_start, into=into)


@rundev.command("status")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def rundev_status(into):
    """Show dev environment status by running status-rundev.sh."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo("❌ Repo folder not found — cannot show status.")
        return

    try:
        click.echo("📊 Checking dev environment status (status-rundev.sh)...")
        run_repo_script(target_path, "status-rundev", None)
    except FileNotFoundError:
        click.echo("❌ status-rundev.sh not found in repo root.")
    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ Status script exited with code {e.returncode}")
    except Exception as e:
        click.echo(f"❌ Failed to check dev status: {e}")


# -----------------------
# deploy group + subcommands
# -----------------------
@cli.group(name="deploy", invoke_without_command=True)
@click.option(
    "--into",
    type=click.Path(file_okay=False, dir_okay=True, writable=True, path_type=str),
    default=None,
    help="Directory where repo was cloned (defaults to './sahaj-opensource').",
)
@click.pass_context
def deploy(ctx, into):
    """deploy group — subcommands: start, stop, restart, status, full. Default = start."""
    if ctx.invoked_subcommand is None:
        ctx.invoke(deploy_full, into=into)


@deploy.command("full")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def deploy_full(into):
    """Run full pipeline: build-deploy-all.sh (build+deploy combined)."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo(
            f"❌ Repo folder not found at {target_path}. Run 'sahaj init dev' first."
        )
        return

    # require repo config
    repo_cfg_path = os.path.join(target_path, "config.json")
    if not os.path.exists(repo_cfg_path):
        click.echo(
            f"❌ Missing {repo_cfg_path}. Place config.json in repo root before running full deploy."
        )
        return

    try:
        click.echo("🔁 Running full build+deploy: build-deploy-all.sh")
        run_repo_script(target_path, "build-deploy-all", None)
        click.echo("✅ Full build+deploy finished.")
    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ Full build-deploy script exited with code {e.returncode}")
    except FileNotFoundError:
        click.echo("❌ build-deploy-all script not found in repo root.")
    except Exception as e:
        click.echo(f"❌ Failed to run full build-deploy: {e}")


@deploy.command("build")
@click.option(
    "--into",
    type=click.Path(file_okay=False, dir_okay=True, writable=True, path_type=str),
    default=None,
    help="Directory where repo was cloned (defaults to './sahaj-opensource').",
)
def deploy_build(into):
    """
    Run repository build step by invoking the repo's build-images script.
    """
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo(
            f"❌ Repo folder not found at {target_path}. Run 'sahaj init dev' first."
        )
        return

    # ensure repo has a config.json at root (required)
    repo_cfg_path = os.path.join(target_path, "config.json")
    if not os.path.exists(repo_cfg_path):
        click.echo(
            f"❌ Missing {repo_cfg_path}. Place config.json in repo root before running build."
        )
        return

    try:
        click.echo("🏗️ Running repo build: build-images.sh")
        # run_repo_script uses platform-appropriate script (.sh/.bat) from helpers
        run_repo_script(target_path, "build-images", None)
        click.echo("✅ Build script finished successfully.")
    except subprocess.CalledProcessError as e:
        msg = traceback.format_exc()
        click.echo(f"⚠️ Build script exited with code {e.returncode} : {msg}")
    except FileNotFoundError:
        click.echo("❌ build-images script not found in repo root.")
    except Exception as e:
        e = traceback.format_exc()
        click.echo(f"❌ Failed to execute build script: {e}")


@deploy.command("start")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def deploy_start(into):
    """Start deployment by running deploy-all.sh (requires pre-built images and repo config.json)."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo(
            f"❌ Repo folder not found at {target_path}. Run 'sahaj init dev' first."
        )
        return

    # require config present
    repo_cfg_path = os.path.join(target_path, "config.json")
    if not os.path.exists(repo_cfg_path):
        click.echo(
            f"❌ Missing {repo_cfg_path}. Place config.json in repo root before deployment."
        )
        return

    try:
        click.echo("🚀 Running deployment script: deploy-all.sh")
        run_repo_script(target_path, "deploy-all", None)
        click.echo("✅ Deploy script finished.")
    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ Deploy script exited with code {e.returncode}")
    except FileNotFoundError:
        click.echo("❌ deploy-all script not found in repo root.")
    except Exception as e:
        click.echo(f"❌ Failed to run deploy script: {e}")


@deploy.command("stop")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
@click.option(
    "-a",
    "--clean-volume",
    is_flag=True,
    help="Also remove volumes (passes -a to script).",
)
def deploy_stop(into, clean_volume):
    """Stop deployed environment by running stop-deployed.sh."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo("No repo found — nothing to stop.")
        return

    try:
        click.echo("🛑 Running stop-deployed.sh to stop deployment...")

        # Determine CLI args to pass
        script_args = ["-a"] if clean_volume else []

        # run script using your improved helper with args support
        run_repo_script(
            target_path=target_path,
            base_script="stop-deployed",
            run_env=None,  # your stop script doesn't need env
            args=script_args,
        )

        click.echo("✅ Deploy stop script finished.")

    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ Deploy stop script exited with code {e.returncode}")
    except FileNotFoundError:
        click.echo("❌ stop-deployed script not found in repo root/scripts folder.")
    except Exception as e:
        click.echo(f"❌ Failed to stop deployment: {e}")


@deploy.command("reset")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
@click.option(
    "--dry-run",
    is_flag=True,
    help="Dry run: show what would be done without executing.",
)
def deploy_reset(into, dry_run):
    """Reset deployment images by running clean-built-images.sh."""
    cwd = os.getcwd()
    target_name = into if into else DEFAULT_DIRNAME
    target_path = os.path.join(cwd, target_name)

    if not os.path.isdir(target_path):
        click.echo(
            f"❌ Repo folder not found at {target_path}. Run 'sahaj init dev' first."
        )
        return

    if dry_run:
        script_args = []
    else:
        script_args = ["--yes"]
    try:
        click.echo("🧹 Resetting build artifacts (clean-built-images.sh)...")
        run_repo_script(target_path, "clean-built-images", None, args=script_args)
        click.echo("✅ Build artifacts cleaned successfully.")
    except FileNotFoundError:
        click.echo("❌ clean-built-images.sh not found in repo root.")
    except subprocess.CalledProcessError as e:
        click.echo(f"⚠️ clean-built-images.sh exited with code {e.returncode}")
    except Exception as e:
        click.echo(f"❌ Failed to reset build images: {e}")


@deploy.command("restart")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def deploy_restart(into):
    """Restart deploy: stop then start."""
    ctx = click.get_current_context()
    ctx.invoke(deploy_stop, into=into)
    ctx.invoke(deploy_start, into=into)


@deploy.command("status")
@click.option(
    "--into", default=None, help="repo directory (defaults to ./sahaj-opensource)"
)
def deploy_status(into):
    """Status placeholder for deploy (kept as placeholder)."""
    click.echo("deploy status: placeholder — implement later.")


# -----------------------
# quickstart group + subcommands
# -----------------------
@cli.group(name="quickstart", invoke_without_command=True)
@click.pass_context
def quickstart(ctx):
    """Quickstart is not implemented in this version."""
    click.echo("Quickstart is not implemented in this version.")


# @quickstart.command("start")
# def quickstart_start():
#     """Start quickstart: download templated compose and run up -d, remembers compose path."""
#     click.echo("🚀 Starting Quickstart Setup...")

#     config = read_config()
#     if not config:
#         click.echo(
#             "❌ Config file not found or empty. Please run `sahaj config` first."
#         )
#         return

#     nginx_name = config.get("nginx_name", "sahaj-nginx")
#     nginx_port = str(config.get("nginx_port", "8081"))

#     # fetch
#     click.echo("📦 Downloading docker-compose.quickstart.yml...")
#     try:
#         resp = requests.get(DOCKER_COMPOSE_QUICKSTART_URL, timeout=10)
#         resp.raise_for_status()
#     except requests.RequestException as e:
#         click.echo(f"❌ Failed to fetch quickstart compose: {e}")
#         return

#     compose_text = resp.text
#     replacements = {"NGINX-NAME": nginx_name, "NGINX-PORT": nginx_port}
#     for key, val in replacements.items():
#         compose_text = compose_text.replace(f"${{{{{key.upper()}}}}}", val)

#     # save to temp dir and remember path
#     temp_dir = tempfile.mkdtemp(prefix="sahaj-quickstart-")
#     compose_path = os.path.join(temp_dir, "docker-compose.quickstart.yml")
#     with open(compose_path, "w", encoding="utf-8") as f:
#         f.write(compose_text)
#     click.echo(f"✅ Compose file prepared at: {compose_path}")

#     # run up
#     try:
#         run_native(["docker", "compose", "-f", compose_path, "up", "-d"], cwd=temp_dir)
#         click.echo("✅ Quickstart complete! Containers are up and running.")
#     except subprocess.CalledProcessError as e:
#         click.echo(f"⚠️ Quickstart docker compose failed: {e.returncode}")
#     write_quickstart_marker(compose_path)


# @quickstart.command("stop")
# def quickstart_stop():
#     """Stop quickstart using the last remembered compose file."""
#     compose_path = read_quickstart_marker()
#     if not compose_path or not os.path.exists(compose_path):
#         click.echo(
#             "No remembered quickstart compose found. Start quickstart first or re-run start."
#         )
#         return
#     temp_dir = os.path.dirname(compose_path)
#     try:
#         run_native(["docker", "compose", "-f", compose_path, "down"], cwd=temp_dir)
#         click.echo("✅ Quickstart containers stopped.")
#     except subprocess.CalledProcessError as e:
#         click.echo(f"⚠️ docker compose down failed: {e.returncode}")


# @quickstart.command("restart")
# def quickstart_restart():
#     """Restart quickstart (down then up)."""
#     ctx = click.get_current_context()
#     ctx.invoke(quickstart_stop)
#     ctx.invoke(quickstart_start)


# @quickstart.command("status")
# def quickstart_status():
#     """Show status for last quickstart run."""
#     compose_path = read_quickstart_marker()
#     if not compose_path or not os.path.exists(compose_path):
#         click.echo("No remembered quickstart compose found.")
#         return
#     temp_dir = os.path.dirname(compose_path)
#     run_native(["docker", "compose", "-f", compose_path, "ps"], cwd=temp_dir)


# Optional: a real init that fetches compose and runs docker (kept separate)
# @cli.command(name="run-demo")
# @click.option(
#     "--template/--no-template",
#     default=False,
#     help="Replace placeholders in compose with config values",
# )
# def init_run_demo(template):
#     """(Demo) Fetch docker-compose and run containers (careful: this will call docker)."""
#     click.echo("Fetching docker-compose file...")
#     try:
#         response = requests.get(DOCKER_COMPOSE_URL, timeout=15)
#         response.raise_for_status()
#     except requests.RequestException as e:
#         click.echo(f"Error fetching docker-compose file: {e}")
#         return

#     compose_text = response.text

#     if template:
#         cfg = read_config()
#         # example mapping
#         mapping = {
#             "ORG_NAME": cfg.get("organization_name", ""),
#             "ADMIN_EMAIL": cfg.get("admin_email", ""),
#             "REGION": cfg.get("default_region", ""),
#         }
#         for k, v in mapping.items():
#             compose_text = compose_text.replace(f"{{{{{k}}}}}", v)

#     tmpdir = tempfile.mkdtemp(prefix="sahaj_")
#     compose_path = os.path.join(tmpdir, "docker-compose.yml")
#     with open(compose_path, "w", encoding="utf-8") as f:
#         f.write(compose_text)
#     click.echo(f"docker-compose.yml saved to {compose_path}")

#     click.echo("Starting containers with Docker Compose...\n")
#     try:
#         proc = subprocess.Popen(
#             ["docker", "compose", "-f", compose_path, "up", "-d"],
#             stdout=subprocess.PIPE,
#             stderr=subprocess.STDOUT,
#             text=True,
#         )
#         # stream stdout
#         for line in iter(proc.stdout.readline, ""):
#             if line:
#                 click.echo(line.strip())
#         proc.stdout.close()
#         rc = proc.wait()
#         if rc == 0:
#             click.echo("\nContainers started successfully!")
#         else:
#             click.echo("\nDocker Compose finished with errors.")
#     except FileNotFoundError:
#         click.echo("Docker not found. Please ensure Docker is installed and in PATH.")


# -----------------------
# CLI entrypoint
# -----------------------
if __name__ == "__main__":
    cli()
