import typer
import json
import hashlib
from pathlib import Path
import shutil
import difflib
import smtplib
from email.message import EmailMessage
import uuid
import requests
import os
from datetime import datetime

app = typer.Typer(help=""" 
Welcome to Nebula Cli
Developed by:
- Udit N. Khare
- Apoorv Verma
- Swara Verma
""")

NB_DIR = ".nb"
CONFIG_PATH = Path("Nebula/.nb/config.json")
HASH_PATH = Path("Nebula/.nb/hashes.json")
NB_DIR = Path("Nebula/.nb")
CONFIG_PATH = NB_DIR / "config.json"
HASH_PATH = NB_DIR / "hashes.json"
SNAPSHOT_DIR = NB_DIR / "snapshots"
UPDATE_DIR = NB_DIR / "updates"
STAGED_PATH = NB_DIR / "staged.json"
LOCK_PATH = NB_DIR / "locks.json"
SESSION_PATH = Path.home() / ".nebula_session.json"
SERVER_ROOT = Path.home() / "NebulaServer" / "projects"
SERVER_URL = "https://web-production-558b9.up.railway.app"
# SERVER_URL = "http://127.0.0.1:8000/"



def get_hash(path: Path):
    hasher = hashlib.md5()

    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hasher.update(chunk)

    return hasher.hexdigest()


def get_all_files():

    workspace = Path("workspace")

    if not workspace.exists():
        return []

    return [p for p in workspace.rglob("*") if p.is_file()]


def generate_hash_map():
    files = get_all_files()

    data = {}

    for f in files:
        data[str(f)] = get_hash(f)

    return data

def build_changes(old_hashes, current_hashes):

    changes = {
        "added": {},
        "modified": {},
        "deleted": []
    }

    for path, new_hash in current_hashes.items():

        if path not in old_hashes:
            # NEW FILE
            with open(path) as f:
                changes["added"][path] = {
                    "content": f.read()
                }

        elif old_hashes[path] != new_hash:
            # MODIFIED FILE
            with open(path) as f:
                changes["modified"][path] = {
                    "content": f.read()
                }

    for path in old_hashes:
        if path not in current_hashes:
            changes["deleted"].append(path)

    return changes

def load_update(update_id):
    path = UPDATE_DIR / f"{update_id}.json"

    if not path.exists():
        return None

    with open(path) as f:
        return json.load(f)
    
def get_update_chain(update_id):

    chain = []

    current = load_update(update_id)

    while current:
        chain.append(current)
        prev = current.get("previous")

        if not prev:
            break

        current = load_update(prev)

    return list(reversed(chain))

def build_state(update_id):

    state = {}

    chain = get_update_chain(update_id)

    for update in chain:

        changes = update.get("changes", {})

        for path, data in changes.get("added", {}).items():
            state[path] = data["content"]

        for path, data in changes.get("modified", {}).items():
            state[path] = data["content"]

        for path in changes.get("deleted", []):
            state.pop(path, None)

    return state


@app.command("startproject")
def startproject(name: str):

    project_path = Path(name)
    project_code = "NEB-" + uuid.uuid4().hex[:6].upper()

    # -------------------------
    # Check existing folder
    # -------------------------
    if project_path.exists():
        typer.echo("Project folder already exists.")
        return

    # -------------------------
    # 🔐 REQUIRE LOGIN
    # -------------------------
    token = get_token()

    if not token:
        typer.secho("❌ Not logged in. Run: nebula login", fg=typer.colors.RED)
        return

    # -------------------------
    # Create project on server FIRST
    # -------------------------
    try:
        headers = {
            "Authorization": f"Bearer {token}"
        }

        response = requests.post(
            f"{SERVER_URL}/project/create",
            json={   # ✅ use JSON, not params
                "project_code": project_code,
                "project_name": name
            },
            headers=headers,
            timeout=10
        )

        # -------------------------
        # HTTP ERROR
        # -------------------------
        if response.status_code != 200:
            typer.secho(f"❌ Server error ({response.status_code})", fg=typer.colors.RED)
            typer.echo(response.text)
            return

        data = response.json()

        # -------------------------
        # BUSINESS LOGIC ERRORS
        # -------------------------
        if data.get("error") == "project_name_exists":
            typer.secho("❌ You already have a project with this name.", fg=typer.colors.RED)
            return

        if data.get("error"):
            typer.secho("❌ Server error while creating project.", fg=typer.colors.RED)
            typer.echo(data)
            return

        # -------------------------
        # SUCCESS CASES
        # -------------------------
        if data.get("status") == "created":
            typer.secho("✅ Project created on server.", fg=typer.colors.GREEN)

        elif data.get("status") == "exists":
            typer.secho("⚠️ Project already exists on server.", fg=typer.colors.YELLOW)

        else:
            typer.secho("❌ Unexpected server response.", fg=typer.colors.RED)
            typer.echo(data)
            return

    except requests.exceptions.Timeout:
        typer.secho("❌ Server timeout. Try again.", fg=typer.colors.RED)
        return

    except requests.exceptions.ConnectionError:
        typer.secho("❌ Cannot connect to Nebula server.", fg=typer.colors.RED)
        return

    except Exception as e:
        typer.secho("❌ Unexpected error.", fg=typer.colors.RED)
        typer.echo(str(e))
        return

    # -------------------------
    # Create local structure (AFTER server success)
    # -------------------------
    project_path.mkdir()
    (project_path / "workspace").mkdir()

    nb = project_path / "Nebula" / ".nb"
    nb.mkdir(parents=True)

    (nb / "snapshots").mkdir()
    (nb / "updates").mkdir()

    # -------------------------
    # Get current logged user
    # -------------------------
    with open(SESSION_PATH) as f:
        session = json.load(f)

    username = session.get("username")

    # -------------------------
    # Save config
    # -------------------------
    config = {
        "project_name": name,
        "project_code": project_code,
        "server_url": SERVER_URL,
        "owner": username,
        "current_update": None,
        "updates": [],
        "snapshot": 0,
        "collaborators": [],
        "token": token 
    }

    with open(nb / "config.json", "w") as f:
        json.dump(config, f, indent=4)

    # -------------------------
    # Initialize files
    # -------------------------
    with open(nb / "hashes.json", "w") as f:
        json.dump({}, f)

    with open(nb / "staged.json", "w") as f:
        json.dump({}, f)

    with open(nb / "locks.json", "w") as f:
        json.dump({}, f)

    # -------------------------
    # SUCCESS
    # -------------------------
    typer.secho("🚀 Nebula project created successfully!", fg=typer.colors.GREEN)
    typer.echo(f"Project Name: {name}")
    typer.echo(f"Project Code: {project_code}")
    typer.echo(f"Owner: {username}")
    typer.echo(f"Server: {SERVER_URL}")

@app.command("startupdate")
def startupdate(name: str):
    stage_changes()

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    updates = config.get("updates", [])

    if name in updates:
        typer.echo("Update already exists.")
        return

    previous = updates[-1] if updates else None

    updates.append(name)

    config["updates"] = updates
    config["current_update"] = name

    with open(CONFIG_PATH, "w") as f:
        json.dump(config, f, indent=4)

    UPDATE_DIR.mkdir(parents=True, exist_ok=True)

    update_file = UPDATE_DIR / f"{name}.json"

    data = {
        "id": name,
        "previous": previous,
        "type": "normal",
        "timestamp": datetime.utcnow().isoformat(),
        "changes": {
            "added": {},
            "modified": {},
            "deleted": []
        },
        "status": "in-progress"
    }

    with open(update_file, "w") as f:
        json.dump(data, f, indent=4)

    typer.secho(f"🚀 Update '{name}' created.", fg=typer.colors.GREEN)

def stage_changes():
    """
    NEW SYSTEM:
    This function no longer modifies updates or tracks files.
    It only checks if there are pending changes (optional helper).
    """

    if not CONFIG_PATH.exists():
        return

    # Load hashes
    if HASH_PATH.exists():
        with open(HASH_PATH) as f:
            old_hashes = json.load(f)
    else:
        old_hashes = {}

    current_hashes = generate_hash_map()

    changes = build_changes(old_hashes, current_hashes)

    has_changes = (
        changes["added"] or
        changes["modified"] or
        changes["deleted"]
    )

    # Just return info (do NOT modify anything)
    return has_changes

@app.command("status")
def status():
    stage_changes()
    """
    nb status
    """

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    with open(HASH_PATH) as f:
        old_hashes = json.load(f)

    typer.echo(f"\nProject: {config['project_name']}")
    typer.echo(f"Current Update: {config['current_update']}")
    typer.echo(f"Snapshot: {config['snapshot']}")

    current_hashes = generate_hash_map()

    added = []
    modified = []
    deleted = []

    for path, h in current_hashes.items():

        if path not in old_hashes:
            added.append(path)

        elif old_hashes[path] != h:
            modified.append(path)

    for path in old_hashes:

        if path not in current_hashes:
            deleted.append(path)

    typer.echo("\nChanges:")

    for f in added:
        typer.secho(f" (+) {f}", fg=typer.colors.GREEN)

    for f in modified:
        typer.secho(f" (m) {f}", fg=typer.colors.YELLOW)

    for f in deleted:
        typer.secho(f" (d) {f}", fg=typer.colors.RED)
    current = config.get("current_update")

    if current:

        update_file = UPDATE_DIR / f"{current}.json"

        if update_file.exists():

            with open(update_file) as f:
                update_data = json.load(f)

            for f in added + modified:
                if f not in update_data["files"]:
                    update_data["files"].append(f)

            with open(update_file, "w") as f:
                json.dump(update_data, f, indent=4)


@app.command("workon")
def workon(name: str):
    stage_changes()

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    updates = config.get("updates", [])

    if name not in updates:
        typer.echo("Update does not exist.")
        return

    config["current_update"] = name

    with open(CONFIG_PATH, "w") as f:
        json.dump(config, f, indent=4)

    typer.secho(f"🔧 Now working on '{name}'", fg=typer.colors.GREEN)

@app.command("cu")
def current_update():
    stage_changes()
    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    typer.echo(f"Current update: {config['current_update']}")

def auto_record_changes():
    """
    Detect changed files and attach them ONLY to the current update.
    Prevent files from being attached to multiple updates.
    """

    if not CONFIG_PATH.exists():
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    current = config.get("current_update")

    if not current:
        return

    update_file = UPDATE_DIR / f"{current}.json"

    if not update_file.exists():
        return

    # collect files already owned by other updates
    taken_files = set()

    for f in UPDATE_DIR.glob("*.json"):
        with open(f) as u:
            data = json.load(u)
            for file in data.get("files", []):
                taken_files.add(file)

    with open(update_file) as f:
        update_data = json.load(f)

    current_hashes = generate_hash_map()

    for path in current_hashes:

        # skip files already attached to another update
        if path in taken_files:
            continue

        if path not in update_data["files"]:
            update_data["files"].append(path)

    with open(update_file, "w") as f:
        json.dump(update_data, f, indent=4)

@app.command("updates")
def updates():

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    server_url = config["server_url"]
    project_code = config["project_code"]

    local_updates = []

    if UPDATE_DIR.exists():
        for file in UPDATE_DIR.glob("*.json"):
            with open(file) as f:
                data = json.load(f)

            if data.get("status") != "published":
                local_updates.append(data.get("id"))

    try:
        r = requests.get(f"{server_url}/updates/{project_code}")
        server_updates = r.json()
    except:
        server_updates = []

    typer.secho("\n🛠 Unpublished Updates (Local):", fg=typer.colors.YELLOW)
    if local_updates:
        for u in local_updates:
            typer.echo(f"  - {u}")
    else:
        typer.echo("  (none)")

    typer.secho("\n☁️ Published Updates (Server):", fg=typer.colors.GREEN)
    if server_updates:
        for u in server_updates:
            typer.echo(f"  - {u.get('id')}")
    else:
        typer.echo("  (none)")

@app.command("publish")
def publish(update: str = typer.Argument(None)):
    """
    Publish updates.

    nb publish           -> publish current update
    nb publish update1   -> publish specific update
    nb publish all       -> publish all updates
    """

    stage_changes()

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    server_url = config["server_url"]
    project_code = config["project_code"]

    updates = config.get("updates", [])

    # ---------------------------
    # Determine target updates
    # ---------------------------

    if update == "all":
        target_updates = updates

    elif update:
        if update not in updates:
            typer.echo(f"Update '{update}' does not exist.")
            return
        target_updates = [update]

    else:
        current = config.get("current_update")
        if not current:
            typer.echo("No active update. Use 'nb workon <update>'")
            return
        target_updates = [current]


    # ---------------------------
    # Load previous hashes
    # ---------------------------

    if HASH_PATH.exists():
        with open(HASH_PATH) as f:
            old_hashes = json.load(f)
    else:
        old_hashes = {}


    # ---------------------------
    # Build publish queue (NEW LOGIC)
    # ---------------------------

    publish_queue = []

    current_hashes = generate_hash_map()
    changes = build_changes(old_hashes, current_hashes)

    # 🚨 IMPORTANT: check if there is ANY change
    has_changes = (
        changes["added"] or
        changes["modified"] or
        changes["deleted"]
    )

    if not has_changes:
        typer.echo("No changes to publish.")
        return


    for u in target_updates:

        update_file = UPDATE_DIR / f"{u}.json"

        if not update_file.exists():
            typer.echo(f"Update metadata missing for {u}")
            continue

        with open(update_file) as f:
            update_data = json.load(f)

        # ✅ attach changes (NEW SYSTEM)
        update_data["changes"] = changes

        publish_queue.append((u, update_file, update_data))


    if not publish_queue:
        typer.echo("Nothing to publish.")
        return
    # ---------------------------
# Fetch server hashes
# ---------------------------

    try:
        r = requests.get(f"{server_url}/project/hashes/{project_code}")

        if r.status_code != 200:
            typer.echo("Failed to fetch server hashes.")
            raise typer.Exit()

        server_hashes = r.json()

    except Exception:
        typer.echo("Cannot connect to Nebula server.")
        typer.echo("Make sure server.py is running.")
        raise typer.Exit()

   # ---------------------------
# Conflict detection
# ---------------------------

    conflict_files = []

    for _, _, update_data in publish_queue:

        changes = update_data.get("changes", {})

        files = (
            list(changes.get("added", {}).keys()) +
            list(changes.get("modified", {}).keys())
        )

        for file in files:

            src = Path(file)

            if not src.exists():
                continue

            relative = str(src.relative_to("workspace"))

            local_hash = get_hash(src)
            server_hash = server_hashes.get(relative)

            base_hash = old_hashes.get(str(src))

            if server_hash and base_hash and server_hash != base_hash:
                if local_hash != server_hash:
                    conflict_files.append(relative)

    # ---------------------------
    # Snapshot creation
    # ---------------------------

    snapshot_id = config["snapshot"] + 1
    snapshot_path = SNAPSHOT_DIR / f"snapshot_{snapshot_id}.zip"

    SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)

    import zipfile

    with zipfile.ZipFile(snapshot_path, "w") as zipf:
        for file in get_all_files():
            zipf.write(file, file.relative_to("workspace"))

        # ---------------------------
# ---------------------------
# Upload files to server
# ---------------------------
    server_hash_updates = {}

    # 🔐 ALWAYS use global session token
    token = get_token()

    if not token:
        typer.secho("❌ Not authenticated. Run 'nb login'", fg=typer.colors.RED)
        raise typer.Exit()

    headers = {
        "Authorization": f"Bearer {token}"
    }

    for u, update_file, update_data in publish_queue:

        changes = update_data.get("changes", {})

        # collect files to upload (only added + modified)
        files_to_upload = (
            list(changes.get("added", {}).keys()) +
            list(changes.get("modified", {}).keys())
        )

        if not files_to_upload:
            typer.echo(f"⚠️ No files to upload for update '{u}'")
            continue

        typer.echo(f"\n🚀 Uploading files for update '{u}'...\n")

        for file in files_to_upload:

            src = Path(file)

            if not src.exists():
                typer.secho(f"⚠️ Skipping missing file: {file}", fg=typer.colors.YELLOW)
                continue

            try:
                relative = src.relative_to("workspace")

            except Exception:
                typer.secho(f"❌ Invalid file path: {file}", fg=typer.colors.RED)
                continue

            try:
                with open(src, "rb") as f:

                    r = requests.post(
                        f"{server_url}/project/upload",
                        headers=headers,
                        data={
                            "project_code": project_code,
                            "relative_path": str(relative)
                        },
                        files={"file": f},
                        timeout=15
                    )

                # -------------------------
                # AUTH ERROR
                # -------------------------
                if r.status_code == 401:
                    typer.secho(
                        "❌ Unauthorized. Please login again (nb login).",
                        fg=typer.colors.RED
                    )
                    raise typer.Exit()

                # -------------------------
                # OTHER ERRORS
                # -------------------------
                if r.status_code != 200:
                    typer.secho(f"❌ Failed to upload {relative}", fg=typer.colors.RED)
                    typer.echo(r.text)
                    raise typer.Exit()   # 🔥 STOP (important)

                response = r.json()

                # ✅ update server hash tracking
                server_hash_updates[str(src)] = response.get("hash")

                typer.secho(f"⬆ Uploaded: {relative}", fg=typer.colors.GREEN)

            except requests.exceptions.Timeout:
                typer.secho(f"❌ Timeout uploading {relative}", fg=typer.colors.RED)
                raise typer.Exit()

            except requests.exceptions.ConnectionError:
                typer.secho("❌ Cannot connect to server.", fg=typer.colors.RED)
                raise typer.Exit()

            except Exception as e:
                typer.secho(f"❌ Server upload failed for {relative}", fg=typer.colors.RED)
                typer.echo(str(e))
                raise typer.Exit()
        # ---------------------------
    # SEND UPDATE METADATA TO SERVER (FIXED)
    # ---------------------------

    r = requests.post(
        f"{server_url}/update/publish",
        json={
            "project_code": project_code,
            "update": update_data
        },
        headers=headers   # 🔥 THIS FIXES YOUR 401
    )

    if r.status_code == 401:
        typer.secho("❌ Unauthorized while publishing update", fg=typer.colors.RED)
        raise typer.Exit()

    if r.status_code != 200:
        typer.secho(f"❌ Failed to register update '{u}' on server", fg=typer.colors.RED)
        typer.echo(r.text)
        raise typer.Exit()

    typer.secho(f"📦 Update '{u}' published.", fg=typer.colors.GREEN)


    # ---------------------------
    # Sync local hashes with server
    # ---------------------------

    if HASH_PATH.exists():

        with open(HASH_PATH) as f:
            local_hashes = json.load(f)

    else:
        local_hashes = {}

    local_hashes = current_hashes

    with open(HASH_PATH, "w") as f:
        json.dump(local_hashes, f, indent=4)


    # ---------------------------
    # Update config snapshot
    # ---------------------------

    config["snapshot"] = snapshot_id

    if update != "all":
        config["current_update"] = None

    with open(CONFIG_PATH, "w") as f:
        json.dump(config, f, indent=4)

    typer.echo(f"Snapshot created: snapshot_{snapshot_id}")

@app.command("graph")
def graph():
    """
    Show update timeline.
    """

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    if not UPDATE_DIR.exists():
        typer.echo("No updates found.")
        return

    typer.echo("\nNebula Update Graph\n")

    updates = []

    for file in UPDATE_DIR.glob("*.json"):

        with open(file) as f:
            data = json.load(f)

        updates.append(data)

    # sort by snapshot if available
    updates.sort(key=lambda x: x.get("snapshot", 999999))

    for u in updates:

        name = u.get("name")
        snapshot = u.get("snapshot")
        status = u.get("status")

        if snapshot:
            typer.secho(f"snapshot_{snapshot}  ── {name}", fg=typer.colors.CYAN)

        else:
            typer.secho(f"pending      ── {name}", fg=typer.colors.YELLOW)
@app.command("diff")
def diff(update: str):
    """
    Show file differences for an update.
    """

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    update_file = UPDATE_DIR / f"{update}.json"

    if not update_file.exists():
        typer.echo(f"Update '{update}' does not exist.")
        return

    with open(update_file) as f:
        data = json.load(f)

    files = data.get("files", [])

    if not files:
        typer.echo("No files recorded for this update.")
        return

    typer.echo(f"\nDiff for {update}\n")

    for file in files:

        workspace_path = Path(file)

        if not workspace_path.exists():
            typer.echo(f"Missing file: {file}")
            continue

        relative = workspace_path.relative_to("workspace")
        main_path = Path("main") / relative

        typer.secho(f"\nFile: {relative}", fg=typer.colors.CYAN)

        # New file
        if not main_path.exists():
            typer.secho(f"New file: {workspace_path}", fg=typer.colors.GREEN)
            continue

        with open(main_path) as f1:
            main_lines = f1.readlines()

        with open(workspace_path) as f2:
            workspace_lines = f2.readlines()

        diff_lines = difflib.unified_diff(
            main_lines,
            workspace_lines,
            fromfile=str(main_path),
            tofile=str(workspace_path),
        )

        for line in diff_lines:

            if line.startswith("+") and not line.startswith("+++"):
                typer.secho(line.rstrip(), fg=typer.colors.GREEN)

            elif line.startswith("-") and not line.startswith("---"):
                typer.secho(line.rstrip(), fg=typer.colors.RED)

            else:
                typer.echo(line.rstrip())


@app.command("updateinfo")
def updateinfo(update: str):
    stage_changes()
    """
    Show detailed information about an update.

    Example:
    nb updateinfo update1
    """

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    update_file = UPDATE_DIR / f"{update}.json"

    if not update_file.exists():
        typer.echo(f"Update '{update}' does not exist.")
        return

    with open(update_file) as f:
        data = json.load(f)

    name = data.get("name")
    status = data.get("status", "unknown")
    snapshot = data.get("snapshot")
    files = data.get("files", [])

    typer.secho(f"\nUpdate: {name}", fg=typer.colors.CYAN)
    typer.echo(f"Status: {status}")

    if snapshot:
        typer.echo(f"Snapshot: snapshot_{snapshot}")
    else:
        typer.echo("Snapshot: pending")

    typer.echo(f"\nFiles ({len(files)}):\n")

    if not files:
        typer.echo(" No files recorded.")
        return

    for fpath in files:
        typer.echo(f" - {fpath}")

@app.command("deleteupdate")
def deleteupdate(update: str):
    """
    Delete an update from the project.

    Example:
    nb deleteupdate update2
    """

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    update_file = UPDATE_DIR / f"{update}.json"

    if not update_file.exists():
        typer.echo(f"Update '{update}' does not exist.")
        return

    # Load config
    with open(CONFIG_PATH) as f:
        config = json.load(f)

    updates = config.get("updates", [])

    if update not in updates:
        typer.echo(f"Update '{update}' not found in config.")
        return

    # Load update metadata
    with open(update_file) as f:
        update_data = json.load(f)

    files = update_data.get("files", [])

    # Remove staged ownership
    if STAGED_PATH.exists():

        with open(STAGED_PATH) as f:
            staged = json.load(f)

        for fpath in files:
            if fpath in staged:
                del staged[fpath]

        with open(STAGED_PATH, "w") as f:
            json.dump(staged, f, indent=4)

    # Remove update metadata file
    update_file.unlink()

    # Remove update from config
    updates.remove(update)
    config["updates"] = updates

    # Reset active update if needed
    if config.get("current_update") == update:
        config["current_update"] = None

    with open(CONFIG_PATH, "w") as f:
        json.dump(config, f, indent=4)

    typer.secho(f"🗑 Update '{update}' deleted.", fg=typer.colors.RED)

import smtplib
from email.message import EmailMessage


def send_invite_email(email, username, password, project_code, project_name):

    msg = EmailMessage()
    msg["Subject"] = f"Invitation to collaborate on {project_name}"
    msg["From"] = "Nebula <yourgmail@gmail.com>"
    msg["To"] = email

    # Plain text fallback
    msg.set_content(f"""
You have been invited to collaborate on a Nebula project.

Project: {project_name}

Project Code: {project_code}
Username: {username}
Password: {password}

To join the project run:

nb joinproject

Then enter your credentials.

Nebula CLI
""")

    # HTML invitation
    html_content = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{
    font-family: Arial, sans-serif;
    background-color: #f6f8fb;
    margin: 0;
    padding: 0;
}}

.container {{
    max-width: 600px;
    margin: 40px auto;
    background: white;
    border-radius: 10px;
    box-shadow: 0 4px 14px rgba(0,0,0,0.08);
    overflow: hidden;
}}

.header {{
    background: #0f172a;
    color: white;
    padding: 24px;
    text-align: center;
    font-size: 22px;
    font-weight: bold;
}}

.content {{
    padding: 30px;
    color: #333;
    line-height: 1.6;
}}

.credentials {{
    background: #f1f5f9;
    padding: 16px;
    border-radius: 6px;
    margin: 20px 0;
    font-family: monospace;
}}

.button {{
    display: inline-block;
    padding: 12px 20px;
    margin-top: 15px;
    background: #2563eb;
    color: white;
    text-decoration: none;
    border-radius: 6px;
}}

.footer {{
    text-align: center;
    font-size: 12px;
    color: #888;
    padding: 20px;
}}
</style>
</head>

<body>

<div class="container">

<div class="header">
🍳 Nebula Collaboration Invite
</div>

<div class="content">

<p>Hello <b>{username}</b>,</p>

<p>You have been invited to collaborate on the project:</p>

<p><b>{project_name}</b></p>

<p>Use the credentials below to join:</p>

<div class="credentials">
Project Code: <b>{project_code}</b><br>
Username: <b>{username}</b><br>
Password: <b>{password}</b>
</div>

<p>Install Nebula CLI and run:</p>

<div class="credentials">
nb joinproject
</div>

<p>Enter the project code and your credentials to access the project.</p>

<a class="button" href="https://pypi.org/">Install Nebula</a>

</div>

<div class="footer">
Nebula • Collaborative Development Platform
</div>

</div>

</body>
</html>
"""

    msg.add_alternative(html_content, subtype="html")

    with smtplib.SMTP("smtp.gmail.com", 587) as smtp:
        smtp.starttls()
        smtp.login("uditkhare214@gmail.com", "spds nxuh mkdh bdlj")
        smtp.send_message(msg)

@app.command("addcollab")
def addcollab():

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    username = typer.prompt("Username")
    email = typer.prompt("Email (optional)", default="")

    # -----------------------------
    # Load config
    # -----------------------------
    with open(CONFIG_PATH) as f:
        config = json.load(f)

    project_code = config.get("project_code")

    if not project_code:
        typer.secho("❌ Invalid project config.", fg=typer.colors.RED)
        return

    # -----------------------------
    # AUTH
    # -----------------------------
    token = get_token()

    if not token:
        typer.secho("❌ Not logged in. Run 'nb login'", fg=typer.colors.RED)
        return

    headers = {
        "Authorization": f"Bearer {token}"
    }

    # -----------------------------
    # CALL SERVER (INVITE SYSTEM)
    # -----------------------------
    try:
        r = requests.post(
            f"{SERVER_URL}/project/add-collaborator",
            json={
                "project_code": project_code,
                "email": email   # ✅ FIX: use email, not username
            },
            headers=headers,
            timeout=10
        )

        # -------------------------
        # DEBUG (remove later)
        # -------------------------
        print("STATUS:", r.status_code)
        print("RESPONSE:", r.text)

        # -------------------------
        # AUTH ERROR
        # -------------------------
        if r.status_code == 401:
            typer.secho("❌ Unauthorized. Please login again.", fg=typer.colors.RED)
            return

        # -------------------------
        # SERVER ERROR
        # -------------------------
        if r.status_code != 200:
            typer.secho(f"❌ Server error ({r.status_code})", fg=typer.colors.RED)
            typer.echo(r.text)
            return

        data = r.json()

        # -------------------------
        # BUSINESS LOGIC HANDLING
        # -------------------------
        if data.get("error") == "missing_fields":
            typer.secho("❌ Missing project code or email.", fg=typer.colors.RED)
            return

        if data.get("error") == "project_not_found":
            typer.secho("❌ Project not found.", fg=typer.colors.RED)
            return

        if data.get("error") == "not_allowed":
            typer.secho("❌ Only project owner can send invites.", fg=typer.colors.RED)
            return

        # -------------------------
        # SUCCESS CASES
        # -------------------------
        if data.get("status") == "already_invited":
            typer.secho("⚠️ Invite already pending for this email.", fg=typer.colors.YELLOW)
            return

        if data.get("status") == "invited":
            typer.secho(f"📧 Invite sent to {email}", fg=typer.colors.GREEN)

            # optional: show invite code in debug/dev mode
            if data.get("invite_code"):
                typer.echo(f"(Invite Code: {data['invite_code']})")

            return

        # -------------------------
        # UNKNOWN RESPONSE
        # -------------------------
        typer.secho("❌ Unexpected server response.", fg=typer.colors.RED)
        typer.echo(data)

    except requests.exceptions.Timeout:
        typer.secho("❌ Server timeout. Try again.", fg=typer.colors.RED)

    except requests.exceptions.ConnectionError:
        typer.secho("❌ Cannot connect to server", fg=typer.colors.RED)

    except Exception as e:
        typer.secho("❌ Unexpected error", fg=typer.colors.RED)
        typer.echo(str(e))

@app.command("joinproject")
def joinproject():

    project_code = typer.prompt("Project Code")
    invite_code = typer.prompt("Invite Code")

    email = typer.prompt("Email")
    password = typer.prompt("Password", hide_input=True)

    server_url = SERVER_URL

    # -------------------------
    # STEP 1: LOGIN (EMAIL BASED)
    # -------------------------
    try:
        r = requests.post(
            f"{server_url}/auth/login",
            json={
                "email": email,
                "password": password
            },
            timeout=10
        )

        if r.status_code != 200:
            typer.secho("❌ Login failed.", fg=typer.colors.RED)
            typer.echo(r.text)
            return

        data = r.json()
        token = data.get("access_token")

        if not token:
            typer.secho("❌ No token received.", fg=typer.colors.RED)
            return

    except Exception as e:
        typer.secho("❌ Cannot connect to server.", fg=typer.colors.RED)
        typer.echo(str(e))
        return

    # -------------------------
    # STEP 2: JOIN PROJECT USING INVITE
    # -------------------------
    headers = {
        "Authorization": f"Bearer {token}"
    }

    try:
        r = requests.post(
            f"{server_url}/project/join",
            json={
                "project_code": project_code,
                "invite_code": invite_code
            },
            headers=headers,
            timeout=10
        )

        if r.status_code != 200:
            typer.secho("❌ Failed to join project.", fg=typer.colors.RED)
            typer.echo(r.text)
            return

        data = r.json()

        if data.get("error") == "invalid_invite":
            typer.secho("❌ Invalid invite code.", fg=typer.colors.RED)
            return

        if data.get("error") == "email_mismatch":
            typer.secho("❌ This invite is not for your email.", fg=typer.colors.RED)
            return

        if data.get("status") == "joined":
            typer.secho("✅ Joined project successfully!", fg=typer.colors.GREEN)

    except requests.exceptions.Timeout:
        typer.secho("❌ Server timeout.", fg=typer.colors.RED)

    except requests.exceptions.ConnectionError:
        typer.secho("❌ Cannot connect to server.", fg=typer.colors.RED)

    except Exception as e:
        typer.secho("❌ Unexpected error.", fg=typer.colors.RED)
        typer.echo(str(e))

    # ✅ FIX: define project_name BEFORE using it
    project_name = project_code

    project_path = Path(project_name)

    if project_path.exists():
        typer.echo("Project already exists locally.")
        return

    # -----------------------------
    # Create project structure
    # -----------------------------
    project_path.mkdir()
    (project_path / "workspace").mkdir()

    nb = project_path / "Nebula" / ".nb"
    nb.mkdir(parents=True)

    (nb / "snapshots").mkdir()
    (nb / "updates").mkdir()

    # -----------------------------
    # ✅ FINAL CONFIG (WITH TOKEN)
    # -----------------------------
    config = {
        "project_name": project_name,
        "project_code": project_code,
        "server_url": server_url,
        "current_update": None,
        "updates": [],
        "snapshot": 0,
        "token": token   # ✅ IMPORTANT FIX
    }

    with open(nb / "config.json", "w") as f:
        json.dump(config, f, indent=4)

    with open(nb / "hashes.json", "w") as f:
        json.dump({}, f)

    with open(nb / "staged.json", "w") as f:
        json.dump({}, f)

    with open(nb / "locks.json", "w") as f:
        json.dump({}, f)

    typer.secho(f"\nWelcome {email}!", fg=typer.colors.GREEN)
    typer.echo("Downloading project from server...")

    os.chdir(project_path)

    pull()

    typer.secho("Project joined successfully.", fg=typer.colors.GREEN)

@app.command("projectcode")
def projectcode():

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    typer.echo(f"Project Code: {config.get('project_code')}")

@app.command("install")
def pull(update: str = typer.Argument(None)):

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    server_url = config["server_url"]
    project_code = config["project_code"]

    workspace = Path("workspace")
    workspace.mkdir(exist_ok=True)

    # =========================================================
    # 🔽 FULL PROJECT PULL (latest state)
    # =========================================================
    if update is None:

        typer.echo("Pulling latest project from server...")

        try:
            r = requests.get(f"{server_url}/updates/{project_code}", timeout=10)

            if r.status_code != 200:
                typer.secho("❌ Failed to fetch updates.", fg=typer.colors.RED)
                return

            updates = r.json()

        except Exception:
            typer.secho("❌ Cannot connect to server.", fg=typer.colors.RED)
            return

        if not updates:
            typer.echo("No updates found on server.")
            return

        # -------------------------
        # BUILD FULL STATE
        # -------------------------
        state = {}

        for u in updates:
            changes = u.get("changes", {})

            # added
            for file, data in changes.get("added", {}).items():
                state[file] = data["content"]

            # modified
            for file, data in changes.get("modified", {}).items():
                state[file] = data["content"]

            # deleted
            for file in changes.get("deleted", []):
                state.pop(file, None)

        # -------------------------
        # APPLY STATE
        # -------------------------
        # clear workspace
        for item in workspace.iterdir():
            if item.is_dir():
                shutil.rmtree(item)
            else:
                item.unlink()

        # write files
        for file, content in state.items():
            path = workspace / Path(file).relative_to("workspace")
            path.parent.mkdir(parents=True, exist_ok=True)

            with open(path, "w", encoding="utf-8") as f:
                f.write(content)

        typer.secho("✅ Workspace fully synced with server.", fg=typer.colors.GREEN)

    # =========================================================
    # 🔽 PULL SPECIFIC UPDATE
    # =========================================================
    else:

        typer.echo(f"Pulling update '{update}'...")

        try:
            r = requests.get(f"{server_url}/updates/{project_code}", timeout=10)

            if r.status_code != 200:
                typer.secho("❌ Failed to fetch updates.", fg=typer.colors.RED)
                return

            updates = r.json()

        except Exception:
            typer.secho("❌ Cannot connect to server.", fg=typer.colors.RED)
            return

        target = None

        for u in updates:
            if u.get("id") == update:
                target = u
                break

        if not target:
            typer.echo("Update not found on server.")
            return

        changes = target.get("changes", {})

        has_changes = (
            changes.get("added") or
            changes.get("modified") or
            changes.get("deleted")
        )

        if not has_changes:
            typer.echo("Update has no changes.")
            return

        # -------------------------
        # APPLY ADDED FILES
        # -------------------------
        for file, data in changes.get("added", {}).items():
            path = workspace / Path(file).relative_to("workspace")
            path.parent.mkdir(parents=True, exist_ok=True)

            with open(path, "w", encoding="utf-8") as f:
                f.write(data["content"])

        # -------------------------
        # APPLY MODIFIED FILES
        # -------------------------
        for file, data in changes.get("modified", {}).items():
            path = workspace / Path(file).relative_to("workspace")

            if not path.exists():
                typer.secho(f"⚠️ Missing file for modify: {file}", fg=typer.colors.YELLOW)

            with open(path, "w", encoding="utf-8") as f:
                f.write(data["content"])

        # -------------------------
        # APPLY DELETED FILES
        # -------------------------
        for file in changes.get("deleted", []):
            path = workspace / Path(file).relative_to("workspace")

            if path.exists():
                path.unlink()

        typer.secho(f"✅ Update '{update}' applied.", fg=typer.colors.GREEN)

    # =========================================================
    # 🔄 SYNC HASHES
    # =========================================================
    try:
        r = requests.get(f"{server_url}/project/hashes/{project_code}", timeout=5)

        if r.status_code == 200:
            with open(HASH_PATH, "w") as f:
                json.dump(r.json(), f, indent=4)

    except Exception:
        typer.secho("⚠️ Could not sync hashes.", fg=typer.colors.YELLOW)

def login_and_get_token(server_url, project_code, username, password):

    r = requests.post(
        f"{server_url}/auth/login",
        json={
            "username": username,
            "password": password,
            "project_code": project_code
        }
    )

    if r.status_code != 200:
        return None

    data = r.json()

    return data.get("access_token")

def get_token():
    if not SESSION_PATH.exists():
        typer.secho("❌ Not logged in. Run 'nebula login'", fg=typer.colors.RED)
        raise typer.Exit()

    with open(SESSION_PATH) as f:
        data = json.load(f)

    return data.get("token")

@app.command("login")
def login_global():

    email = typer.prompt("Email")
    password = typer.prompt("Password", hide_input=True)



    try:
        r = requests.post(
    f"{SERVER_URL}/auth/login",
    json={
        "email": email,
        "password": password
    }
)

        if r.status_code != 200:
            typer.secho("❌ Login failed", fg=typer.colors.RED)
            typer.echo(r.text)
            return

        data = r.json()
        token = data.get("access_token")

        if not token:
            typer.secho("❌ No token received", fg=typer.colors.RED)
            return

        # SAVE SESSION
        with open(SESSION_PATH, "w") as f:
            json.dump({
                "email": email ,
                "token": token
            }, f)

        typer.secho("✅ Logged in successfully", fg=typer.colors.GREEN)

    except Exception as e:
        typer.secho("❌ Cannot connect to server", fg=typer.colors.RED)
        typer.echo(str(e))


def compute_reverse_changes(state_current, state_target, mode="full"):

    changes = {
        "added": {},
        "modified": {},
        "deleted": []
    }

    current_files = set(state_current.keys())
    target_files = set(state_target.keys())

    # -------------------------
    # FULL MODE (current behavior)
    # -------------------------
    if mode == "full":

        # delete extra files
        for file in current_files - target_files:
            changes["deleted"].append(file)

    # -------------------------
    # SOFT MODE (new behavior)
    # -------------------------
    elif mode == "soft":
        # DO NOT delete extra files
        pass

    # -------------------------
    # restore missing files
    # -------------------------
    for file in target_files - current_files:
        changes["added"][file] = {
            "content": state_target[file]
        }

    # -------------------------
    # modify existing files
    # -------------------------
    for file in current_files & target_files:

        if state_current[file] != state_target[file]:
            changes["modified"][file] = {
                "content": state_target[file]
            }

    return changes

def apply_state_to_workspace(state):

    workspace = Path("workspace")

    # clear workspace
    for file in workspace.rglob("*"):
        if file.is_file():
            file.unlink()

    # write new files
    for path, content in state.items():
        file_path = workspace / Path(path).relative_to("workspace")
        file_path.parent.mkdir(parents=True, exist_ok=True)

        with open(file_path, "w") as f:
            f.write(content)
@app.command("revert")
def revert(
    target_update: str,
    mode: str = typer.Option("full", help="Revert mode: full or soft"),
    dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes only"),
    file: str = typer.Option(None, "--file", help="Revert only a specific file")
):
    """
    Revert project to a previous update.

    Modes:
    - full → exact state (delete extra files)
    - soft → only revert common files (keep new files)
    """

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    if mode not in ["full", "soft"]:
        typer.echo("Invalid mode. Use 'full' or 'soft'")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    updates = config.get("updates", [])

    if target_update not in updates:
        typer.echo(f"Update '{target_update}' not found.")
        return

    if not updates:
        typer.echo("No updates found.")
        return

    head_update = updates[-1]

    if target_update == head_update:
        typer.echo("Already at this update.")
        return

    typer.echo(f"Reverting to {target_update} (mode={mode})...")

    # ---------------------------
    # Build states
    # ---------------------------
    state_current = build_state(head_update)
    state_target = build_state(target_update)

    if file:
        if file not in state_target and file not in state_current:
            typer.echo(f"File '{file}' not found in either state.")
            return

        state_current = {
            k: v for k, v in state_current.items() if k == file
        }

        state_target = {
            k: v for k, v in state_target.items() if k == file
        }

    # ---------------------------
# Compute reverse changes
# ---------------------------

    reverse_changes = {
        "added": {},
        "modified": {},
        "deleted": []
    }

    current_files = set(state_current.keys())
    target_files = set(state_target.keys())

    # FULL MODE → delete extra files
    if mode == "full":
        for f in current_files - target_files:
            reverse_changes["deleted"].append(f)

    # Restore missing files
    for f in target_files - current_files:
        reverse_changes["added"][f] = {
            "content": state_target[f]
        }

    # Modify changed files
    for f in current_files & target_files:
        if state_current[f] != state_target[f]:
            reverse_changes["modified"][f] = {
                "content": state_target[f]
            }

    # ---------------------------
    # NOW Dry Run (after compute)
    # ---------------------------
    if dry_run:

        typer.echo("\n🔍 Dry Run Preview:\n")

        added = reverse_changes["added"]
        modified = reverse_changes["modified"]
        deleted = reverse_changes["deleted"]

        if added:
            typer.echo("➕ Files to be added/restored:")
            for f in added:
                typer.echo(f"   + {f}")

        if modified:
            typer.echo("\n✏️ Files to be modified:")
            for f in modified:
                typer.echo(f"   ~ {f}")

        if deleted:
            typer.echo("\n🗑 Files to be deleted:")
            for f in deleted:
                typer.echo(f"   - {f}")

        if not (added or modified or deleted):
            typer.echo("No changes.")

        return
    # ---------------------------
    # Create new update
    # ---------------------------
    import uuid
    new_update_id = f"revert_{uuid.uuid4().hex[:6]}"

    new_update = {
        "id": new_update_id,
        "previous": head_update,
        "type": "revert",
        "reverted_from": target_update,
        "mode": mode,
        "timestamp": datetime.utcnow().isoformat(),
        "changes": reverse_changes,
        "status": "completed"
    }

    # ---------------------------
    # Save update
    # ---------------------------
    update_file = UPDATE_DIR / f"{new_update_id}.json"

    with open(update_file, "w") as f:
        json.dump(new_update, f, indent=4)

    # ---------------------------
    # Update config
    # ---------------------------
    updates.append(new_update_id)
    config["updates"] = updates
    config["current_update"] = None

    with open(CONFIG_PATH, "w") as f:
        json.dump(config, f, indent=4)

    # ---------------------------
    # Apply new state
    # ---------------------------
    new_state = build_state(new_update_id)
    apply_state_to_workspace(new_state)

    # ---------------------------
    # Update hashes
    # ---------------------------
    new_hashes = generate_hash_map()

    with open(HASH_PATH, "w") as f:
        json.dump(new_hashes, f, indent=4)

    msg = f"✅ Reverted to {target_update} using {new_update_id} (mode={mode})"

    if file:
        msg += f" [file={file}]"

    typer.secho(msg, fg=typer.colors.GREEN)

@app.command("signup")
def signup():
    email = typer.prompt("Email")
    username = typer.prompt("Username")
    password = typer.prompt("Password", hide_input=True)

    try:
        r = requests.post(
            f"{SERVER_URL}/auth/signup",
            json={
                "email": email,
                "username": username,
                "password": password
            }
        )

        data = r.json()

        if data.get("status") == "created":
            typer.secho("✅ Account created successfully", fg=typer.colors.GREEN)
        else:
            typer.secho(f"❌ {data}", fg=typer.colors.RED)

    except Exception as e:
        typer.secho("❌ Cannot connect to server", fg=typer.colors.RED)
        typer.echo(str(e))

@app.command("projects")
def list_projects():

    token = get_token()

    if not token:
        typer.secho("❌ Not logged in.", fg=typer.colors.RED)
        return

    try:
        r = requests.get(
            f"{SERVER_URL}/projects",
            headers={"Authorization": f"Bearer {token}"}
        )

        if r.status_code != 200:
            typer.secho("❌ Failed to fetch projects", fg=typer.colors.RED)
            return

        data = r.json()

    except Exception as e:
        typer.secho("❌ Cannot connect to server", fg=typer.colors.RED)
        typer.echo(str(e))
        return

    owned = data.get("owned", [])
    joined = data.get("joined", [])

    typer.secho("\n📦 Your Projects:", fg=typer.colors.CYAN)
    if owned:
        for p in owned:
            typer.echo(f"  - {p['name']} ({p['code']})")
    else:
        typer.echo("  (none)")

    typer.secho("\n🤝 Joined Projects:", fg=typer.colors.GREEN)
    if joined:
        for p in joined:
            typer.echo(f"  - {p}")
    else:
        typer.echo("  (none)")

@app.command("workonproject")
def workon_project(project_name: str):

    project_path = Path(project_name)

    # -------------------------
    # If already exists locally
    # -------------------------
    if project_path.exists():
        os.chdir(project_path)
        typer.secho(f"📂 Switched to project '{project_name}'", fg=typer.colors.GREEN)
        return

    # -------------------------
    # Otherwise → clone (join)
    # -------------------------
    typer.echo("Project not found locally. Downloading...")

    token = get_token()

    if not token:
        typer.secho("❌ Not logged in.", fg=typer.colors.RED)
        return

    # create folder
    project_path.mkdir()
    (project_path / "workspace").mkdir()

    nb = project_path / "Nebula" / ".nb"
    nb.mkdir(parents=True)

    (nb / "snapshots").mkdir()
    (nb / "updates").mkdir()

    config = {
        "project_name": project_name,
        "project_code": project_name,
        "server_url": SERVER_URL,
        "current_update": None,
        "updates": [],
        "snapshot": 0,
        "token": token
    }

    with open(nb / "config.json", "w") as f:
        json.dump(config, f, indent=4)

    with open(nb / "hashes.json", "w") as f:
        json.dump({}, f)

    with open(nb / "staged.json", "w") as f:
        json.dump({}, f)

    with open(nb / "locks.json", "w") as f:
        json.dump({}, f)

    os.chdir(project_path)

    pull()

    typer.secho(f"🚀 Now working on '{project_name}'", fg=typer.colors.GREEN)

@app.command("deleteproject")
def delete_project(project_code: str):

    password = typer.prompt("Enter your password to confirm", hide_input=True)

    token = get_token()

    try:
        r = requests.delete(
            f"{SERVER_URL}/project/delete",
            json={
                "project_code": project_code,
                "password": password
            },
            headers={"Authorization": f"Bearer {token}"}
        )

        if r.status_code != 200:
            typer.secho("❌ Failed to delete project", fg=typer.colors.RED)
            typer.echo(r.text)
            return

        typer.secho(f"🗑 Project '{project_code}' deleted successfully", fg=typer.colors.GREEN)

    except Exception as e:
        typer.secho("❌ Server error", fg=typer.colors.RED)
        typer.echo(str(e))


@app.command("deleteaccount")
def delete_account():

    typer.secho("⚠️ This will delete your entire account and all projects!", fg=typer.colors.RED)

    confirm = typer.prompt("Type 'DELETE' to confirm")

    if confirm != "DELETE":
        typer.echo("Cancelled.")
        return

    password = typer.prompt("Enter your password", hide_input=True)

    token = get_token()

    try:
        r = requests.delete(
            f"{SERVER_URL}/auth/delete",
            json={
                "password": password
            },
            headers={"Authorization": f"Bearer {token}"}
        )

        if r.status_code != 200:
            typer.secho("❌ Failed to delete account", fg=typer.colors.RED)
            typer.echo(r.text)
            return

        # remove local session
        if SESSION_PATH.exists():
            SESSION_PATH.unlink()

        typer.secho("💀 Account deleted permanently", fg=typer.colors.GREEN)

    except Exception as e:
        typer.secho("❌ Server error", fg=typer.colors.RED)
        typer.echo(str(e))


@app.command("collaborators")
def collaborators():

    if not CONFIG_PATH.exists():
        typer.echo("Not a Nebula project.")
        return

    with open(CONFIG_PATH) as f:
        config = json.load(f)

    token = get_token()

    r = requests.get(
        f"{SERVER_URL}/project/collaborators/{config['project_code']}",
        headers={"Authorization": f"Bearer {token}"}
    )

    if r.status_code != 200:
        typer.secho("❌ Failed to fetch collaborators", fg=typer.colors.RED)
        return

    data = r.json()

    typer.secho("\n👥 Accepted:", fg=typer.colors.GREEN)
    for u in data.get("accepted", []):
        typer.echo(f"  - {u}")

    typer.secho("\n⏳ Pending:", fg=typer.colors.YELLOW)
    for e in data.get("pending", []):
        typer.echo(f"  - {e}")

    typer.secho("\n❌ Rejected:", fg=typer.colors.RED)
    for e in data.get("rejected", []):
        typer.echo(f"  - {e}")

if __name__ == "__main__":
    app()