"""CLI entry point for rubberduck-index.

Usage:
    rubberduck-index init
    rubberduck-index sync [--force]
    rubberduck-index watch [--daemon]
    rubberduck-index stop
    rubberduck-index status
    rubberduck-index list
"""

import argparse
import logging
import os
import signal
import sys
import time
from pathlib import Path

from . import __version__
from .config import (
    DEFAULT_CONFIG,
    DEFAULT_SERVER,
    find_project_root,
    load_config,
    save_config,
)
from .scanner import scan_project, read_files
from .syncer import Syncer, SyncError


def _is_process_alive(pid: int) -> bool:
    """Check if a process with the given PID exists. Cross-platform."""
    try:
        os.kill(pid, 0)
        return True
    except OSError:
        return False


def _terminate_process(pid: int):
    """Terminate a process by PID. Cross-platform."""
    os.kill(pid, signal.SIGTERM)


def _setup_logging(verbose: bool = False):
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level,
        format="%(message)s",
        handlers=[logging.StreamHandler()],
    )


def _start_daemon_after_init(project_root: Path):
    """Start the daemon watcher after init completes."""
    try:
        from .watcher import ProjectWatcher
        pid_file = project_root / ".rubberduck" / "watcher.pid"
        log_file = project_root / ".rubberduck" / "watcher.log"
        pid = ProjectWatcher.launch_daemon(project_root, pid_file, log_file)
        print(f"Watcher started in background (PID {pid})")
        print(f"  Log:  {log_file}")
        print(f"  Stop: rubberduck-index stop")
    except ImportError:
        print("Note: Install watchdog for auto-sync: pip install watchdog")
    except Exception as e:
        import traceback
        print(f"Warning: Could not start watcher daemon: {e}")
        logging.debug(traceback.format_exc())


def _ensure_gitignore(project_root: Path):
    """Add .rubberduck/ to .gitignore if not already present."""
    gitignore = project_root / ".gitignore"
    marker = ".rubberduck/"
    if gitignore.exists():
        content = gitignore.read_text()
        if marker in content:
            return
        if not content.endswith("\n"):
            content += "\n"
        content += f"\n# RubberDuck indexer config (contains auth token)\n{marker}\n"
        gitignore.write_text(content)
        print(f"  Added {marker} to .gitignore")
    else:
        if (project_root / ".git").exists():
            gitignore.write_text(
                f"# RubberDuck indexer config (contains auth token)\n{marker}\n"
            )
            print(f"  Created .gitignore with {marker}")


def _human_size(size) -> str:
    """Format bytes into human readable size."""
    size = float(size)
    for unit in ("B", "KB", "MB", "GB"):
        if size < 1024:
            if unit == "B":
                return f"{int(size)} B"
            return f"{size:.1f} {unit}"
        size /= 1024
    return f"{size:.1f} TB"


def _progress_bar(current: int, total: int, width: int = 30, suffix: str = ""):
    """Print a progress bar on the same line."""
    pct = current / total if total else 1.0
    filled = int(width * pct)
    bar = "█" * filled + "░" * (width - filled)
    pct_str = f"{pct * 100:.0f}%"
    line = f"\r  [{bar}] {pct_str} {current}/{total} files {suffix}"
    sys.stdout.write(line)
    sys.stdout.flush()
    if current >= total:
        sys.stdout.write("\n")
        sys.stdout.flush()


def _upload_with_progress(syncer, project, files_bytes, deleted=None):
    """Upload files with a progress bar. Returns the result dict."""
    total = len(files_bytes)
    total_size = sum(len(v) for v in files_bytes.values())

    print(f"  Uploading {total} file(s) ({_human_size(total_size)})...")

    t0 = time.time()

    def on_progress(done, total_files):
        _progress_bar(done, total_files)

    result = syncer.upload_files(project, files_bytes, deleted, on_progress=on_progress)
    dt = time.time() - t0

    synced = result.get("synced", 0)
    graphs = result.get("graphs_rebuilt", [])
    chunks = result.get("chunks", 1)

    parts = [f"{synced} file(s) synced"]
    if graphs:
        parts.append(f"{len(graphs)} analysis model(s) built")
    if chunks > 1:
        parts.append(f"{chunks} chunks")
    parts.append(f"{dt:.1f}s")

    print(f"  Done — {', '.join(parts)}")
    return result


# ── init ──────────────────────────────────────────────────────────────

def cmd_init(args):
    """Initialize a project for indexing."""
    project_root = Path(args.directory or os.getcwd()).resolve()

    if not project_root.is_dir():
        print(f"Error: '{project_root}' is not a directory.")
        sys.exit(1)

    server = getattr(args, "server", "") or DEFAULT_SERVER

    if not args.project:
        args.project = project_root.name

    # Resolve token: --token arg > RUBBERDUCK_TOKEN env var > interactive prompt
    token = getattr(args, "token", "") or os.environ.get("RUBBERDUCK_TOKEN", "")
    if not token:
        import getpass
        print("Authentication required.")
        print("  Get your token from your RubberDuck admin.")
        print()
        token = getpass.getpass("Enter your token: ").strip()
        if not token:
            print("Error: Token is required. The server rejects unauthenticated requests.")
            sys.exit(1)
        print()

    # Warn if using HTTP (token sent in plaintext)
    if server.startswith("http://") and "localhost" not in server and "127.0.0.1" not in server:
        print("Warning: Using unencrypted HTTP. Your auth token will be sent in plaintext.")
        print("  Consider using HTTPS if the server supports it.")
        print()

    # Create config
    config = dict(DEFAULT_CONFIG)
    config["server"] = server
    config["project"] = args.project
    config["token"] = token
    if args.include:
        config["include"] = args.include

    config_path = save_config(project_root, config)

    # Auto-add .rubberduck/ to .gitignore to protect the token
    _ensure_gitignore(project_root)

    print(f"Created {config_path}")
    print(f"  Server:  {config['server']}")
    print(f"  Project: {config['project']}")
    print(f"  Token:   {'configured' if token else 'none (set via --token or RUBBERDUCK_TOKEN)'}")
    print()

    # Do initial sync
    print("Scanning project...")
    max_fs = config.get("max_file_size", DEFAULT_CONFIG["max_file_size"])
    hashes = scan_project(
        project_root,
        include_patterns=config.get("include"),
        max_file_size=max_fs,
    )

    if not hashes:
        print("No matching files found. Check your include patterns.")
        return

    total_size = 0
    for p in hashes:
        fp = project_root / p
        if fp.exists():
            total_size += fp.stat().st_size

    print(f"  Found {len(hashes)} file(s) ({_human_size(total_size)})")
    print()

    # Upload everything (first sync)
    syncer = Syncer(config["server"], token=config.get("token", ""))

    try:
        # Step 1: Compare manifest
        print("Checking server...", end="", flush=True)
        manifest_resp = syncer.compare_manifest(config["project"], hashes)
        need_upload = manifest_resp.get("need_upload", list(hashes.keys()))
        print(f" {len(need_upload)} file(s) to upload")

        if not need_upload:
            print("  All files already indexed!")
        else:
            # Step 2: Read and upload files with progress
            files_bytes = read_files(project_root, need_upload)
            _upload_with_progress(syncer, config["project"], files_bytes)

            print()
            print(f"Project \"{config['project']}\" indexed successfully.")

        # Auto-start watcher daemon (unless --no-watch)
        no_watch = getattr(args, "no_watch", False)
        if not no_watch:
            print()
            _start_daemon_after_init(project_root)

    except SyncError as e:
        print(f"\nError: {e}")
        print("Files are saved locally. Run 'rubberduck-index sync' to retry.")
        sys.exit(1)


# ── sync ──────────────────────────────────────────────────────────────

def cmd_sync(args):
    """Sync changed files to the server."""
    project_root = find_project_root()
    if project_root is None:
        print("Error: No .rubberduck/config.json found.")
        print("Run 'rubberduck-index init' first.")
        sys.exit(1)

    config = load_config(project_root)
    if not config.get("server"):
        print("Error: No server configured. Run 'rubberduck-index init' first.")
        sys.exit(1)

    token = config.get("token", "") or os.environ.get("RUBBERDUCK_TOKEN", "")

    if not token:
        print("Warning: No auth token configured. Server will reject requests.")
        print("  Set token in .rubberduck/config.json or RUBBERDUCK_TOKEN env var.")
        print()

    syncer = Syncer(config["server"], token=token)
    project = config["project"]
    max_fs = config.get("max_file_size", DEFAULT_CONFIG["max_file_size"])

    # Step 1: Scan local files
    print("Scanning...", end="", flush=True)
    hashes = scan_project(
        project_root,
        include_patterns=config.get("include"),
        max_file_size=max_fs,
    )
    print(f" {len(hashes)} file(s)")

    if not hashes:
        print("No matching files found.")
        return

    try:
        if args.force:
            need_upload = list(hashes.keys())
            deleted = []
            print(f"Force sync: uploading all {len(need_upload)} file(s)")
        else:
            print("Comparing with server...", end="", flush=True)
            manifest_resp = syncer.compare_manifest(project, hashes)
            need_upload = manifest_resp.get("need_upload", [])
            already = manifest_resp.get("already_synced", [])
            deleted = manifest_resp.get("deleted_on_client", [])
            print(
                f" {len(need_upload)} changed, "
                f"{len(already)} synced, "
                f"{len(deleted)} deleted"
            )

        if not need_upload and not deleted:
            print("Already up to date.")
            return

        # Step 2: Read and upload changed files
        if need_upload:
            files_bytes = read_files(project_root, need_upload)
            _upload_with_progress(syncer, project, files_bytes, deleted or None)
        elif deleted:
            result = syncer.upload_files(project, {}, deleted)
            print(f"  Deleted {len(deleted)} file(s) from server")

    except SyncError as e:
        print(f"\nError: {e}")
        sys.exit(1)


# ── watch ─────────────────────────────────────────────────────────────

def cmd_watch(args):
    """Watch for file changes and auto-sync."""
    from .watcher import ProjectWatcher, HAS_WATCHDOG
    if not HAS_WATCHDOG:
        print("Error: watchdog is required for watch mode.")
        print("Install with: pip install watchdog")
        sys.exit(1)

    project_root = find_project_root()
    if project_root is None:
        print("Error: No .rubberduck/config.json found.")
        print("Run 'rubberduck-index init' first.")
        sys.exit(1)

    config = load_config(project_root)
    if not config.get("server"):
        print("Error: No server configured. Run 'rubberduck-index init' first.")
        sys.exit(1)

    # Check if already running
    pid_file = project_root / ".rubberduck" / "watcher.pid"
    if pid_file.exists():
        try:
            old_pid = int(pid_file.read_text().strip())
        except (ValueError, OSError):
            pid_file.unlink(missing_ok=True)
            old_pid = None
        if old_pid is not None:
            if _is_process_alive(old_pid):
                print(f"Watcher already running (PID {old_pid}).")
                print("Run 'rubberduck-index stop' to stop it first.")
                sys.exit(1)
            else:
                pid_file.unlink()

    # Initial sync first
    print("Running initial sync...")
    sync_args = argparse.Namespace(force=False, verbose=False)
    cmd_sync(sync_args)
    print()

    # Start watcher
    token = config.get("token", "") or os.environ.get("RUBBERDUCK_TOKEN", "")
    max_fs = config.get("max_file_size", DEFAULT_CONFIG["max_file_size"])
    watcher = ProjectWatcher(
        project_root=project_root,
        server_url=config["server"],
        project_name=config["project"],
        debounce_ms=config.get("watch_debounce_ms", 500),
        token=token,
        include_patterns=config.get("include", ["**/*.py"]),
        max_file_size=max_fs,
    )

    daemon = getattr(args, "daemon", False)
    if daemon:
        from .watcher import ProjectWatcher as PW
        log_file = project_root / ".rubberduck" / "watcher.log"
        pid = PW.launch_daemon(project_root, pid_file, log_file)
        print(f"Watcher started in background (PID {pid})")
        print(f"  Log:  {log_file}")
        print(f"  Stop: rubberduck-index stop")
    else:
        watcher.start()


def cmd_stop(args):
    """Stop the background watcher daemon."""
    project_root = find_project_root()
    if project_root is None:
        print("Error: No .rubberduck/config.json found.")
        sys.exit(1)

    pid_file = project_root / ".rubberduck" / "watcher.pid"
    if not pid_file.exists():
        print("No watcher running (no PID file found).")
        return

    try:
        pid = int(pid_file.read_text().strip())
    except (ValueError, OSError):
        print("Corrupted PID file. Removing it.")
        pid_file.unlink(missing_ok=True)
        return
    try:
        _terminate_process(pid)
        print(f"Watcher stopped (PID {pid}).")
    except OSError:
        print(f"Watcher process {pid} not found (already stopped).")
    finally:
        try:
            pid_file.unlink()
        except OSError:
            pass


# ── status ────────────────────────────────────────────────────────────

def cmd_status(args):
    """Show index status for the current project."""
    project_root = find_project_root()
    if project_root is None:
        print("Error: No .rubberduck/config.json found.")
        print("Run 'rubberduck-index init' first.")
        sys.exit(1)

    config = load_config(project_root)
    project = config["project"]
    server = config["server"]
    max_fs = config.get("max_file_size", DEFAULT_CONFIG["max_file_size"])

    # Local scan
    hashes = scan_project(
        project_root,
        include_patterns=config.get("include"),
        max_file_size=max_fs,
    )

    print(f"Project:  {project}")
    print(f"Server:   {server}")
    print(f"Local:    {len(hashes)} file(s)")

    # Server status
    try:
        token = config.get("token", "") or os.environ.get("RUBBERDUCK_TOKEN", "")
        syncer = Syncer(server, token=token)
        status = syncer.get_status(project)
        if "error" in status:
            print(f"Server:   Not indexed yet")
        else:
            print(f"Indexed:  {status.get('total_files', '?')} file(s)")
            print(f"Python:   {status.get('python_files', '?')} file(s)")
            print(f"Analyses: {len(status.get('cached_analyses', status.get('cached_graphs', [])))}")
            print(f"Size:     {status.get('total_size_human', '?')}")
    except SyncError as e:
        print(f"Server:   Unreachable ({e})")


# ── list ──────────────────────────────────────────────────────────────

def cmd_list(args):
    """List all indexed projects on the server."""
    server = getattr(args, "server", None)
    token = ""
    if not server:
        project_root = find_project_root()
        if project_root:
            config = load_config(project_root)
            server = config.get("server")
            token = config.get("token", "")

    token = token or os.environ.get("RUBBERDUCK_TOKEN", "")

    if not server:
        print("Error: No server URL. Use --server or run from an initialized project.")
        sys.exit(1)

    try:
        syncer = Syncer(server, token=token)
        result = syncer.list_projects()
        projects = result.get("projects", [])

        if not projects:
            print("No indexed projects on server.")
            return

        print(f"Indexed projects on {server}:")
        print()
        for p in projects:
            name = p.get("project", "?")
            files = p.get("files", 0)
            py = p.get("python_files", 0)
            size = p.get("total_size_human", "?")
            analyses = p.get("cached_analyses", p.get("cached_graphs", 0))
            print(f"  {name:30s} {files:4d} files  {py:4d} .py  {analyses:3d} analyses  {size}")

    except SyncError as e:
        print(f"Error: {e}")
        sys.exit(1)


# ── remove ────────────────────────────────────────────────────────────

def cmd_remove(args):
    """Remove an indexed project from the server."""
    project_root = find_project_root()
    config = {}
    if project_root:
        config = load_config(project_root)

    server = getattr(args, "server", None) or config.get("server")
    project = args.project or config.get("project")
    token = config.get("token", "") or os.environ.get("RUBBERDUCK_TOKEN", "")

    if not server:
        print("Error: No server URL.")
        sys.exit(1)
    if not project:
        print("Error: No project name. Use --project or run from an initialized project.")
        sys.exit(1)

    try:
        syncer = Syncer(server, token=token)
        result = syncer.remove_project(project)
        if "error" in result:
            print(f"Error: {result['error']}")
        else:
            print(f"Removed project '{project}' from server.")
    except SyncError as e:
        print(f"Error: {e}")
        sys.exit(1)


# ── CLI parser ────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(
        prog="rubberduck-index",
        description="RubberDuck Index — sync local code to the RubberDuck MCP server",
    )
    parser.add_argument(
        "--version", action="version", version=f"%(prog)s {__version__}"
    )
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")

    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # init
    p_init = subparsers.add_parser("init", help="Initialize project for indexing")
    p_init.add_argument(
        "--server", default="",
        help=f"MCP server URL (default: {DEFAULT_SERVER})",
    )
    p_init.add_argument(
        "--project", default="",
        help="Project name (default: directory name)",
    )
    p_init.add_argument(
        "--directory", default="",
        help="Project directory (default: current directory)",
    )
    p_init.add_argument(
        "--include", nargs="*", default=None,
        help="File patterns to include (default: **/*.py)",
    )
    p_init.add_argument(
        "--token", default="",
        help="Bearer token (or set RUBBERDUCK_TOKEN env var; prompted if not set)",
    )
    p_init.add_argument(
        "--no-watch", action="store_true",
        help="Don't start background watcher after init",
    )

    # sync
    p_sync = subparsers.add_parser("sync", help="Sync changed files to server")
    p_sync.add_argument(
        "--force", action="store_true",
        help="Force full re-upload (ignore hash cache)",
    )

    # watch
    p_watch = subparsers.add_parser("watch", help="Watch for changes and auto-sync")
    p_watch.add_argument(
        "--daemon", "-d", action="store_true",
        help="Run in background (daemon mode). Use 'rubberduck-index stop' to stop.",
    )

    # stop
    subparsers.add_parser("stop", help="Stop the background watcher daemon")

    # status
    p_status = subparsers.add_parser("status", help="Show index status")

    # list
    p_list = subparsers.add_parser("list", help="List indexed projects on server")
    p_list.add_argument("--server", default="", help="MCP server URL")

    # remove
    p_remove = subparsers.add_parser("remove", help="Remove project from server")
    p_remove.add_argument("--server", default="", help="MCP server URL")
    p_remove.add_argument("--project", default="", help="Project name")

    args = parser.parse_args()
    _setup_logging(args.verbose)

    if args.command is None:
        parser.print_help()
        sys.exit(0)

    commands = {
        "init": cmd_init,
        "sync": cmd_sync,
        "watch": cmd_watch,
        "stop": cmd_stop,
        "status": cmd_status,
        "list": cmd_list,
        "remove": cmd_remove,
    }

    cmd_func = commands.get(args.command)
    if cmd_func:
        cmd_func(args)
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()
