"""CLI entry points: standalone dashboard, MCP mode, auth, init, exclude/include, sync."""

from __future__ import annotations

import asyncio
import signal
import sys
from pathlib import Path

import click


# ---------------------------------------------------------------------------
# Main group
# ---------------------------------------------------------------------------


@click.group(invoke_without_command=True)
@click.option("--mcp", is_flag=True, help="Run as MCP server (stdio)")
@click.option("--drop-current-cache", is_flag=True,
              help="Delete and rebuild the cache database")
@click.pass_context
def main(ctx: click.Context, mcp: bool, drop_current_cache: bool) -> None:
    """project-hub: Multi-repo workspace manager."""
    ctx.ensure_object(dict)
    ctx.obj["drop_cache"] = drop_current_cache
    if ctx.invoked_subcommand:
        return
    if mcp:
        run_mcp()
    else:
        run_standalone(drop_cache=drop_current_cache)


# ---------------------------------------------------------------------------
# Standalone mode
# ---------------------------------------------------------------------------


def run_standalone(drop_cache: bool = False) -> None:
    """Launch the standalone web dashboard with a background refresh loop."""
    import logging
    logging.basicConfig(stream=sys.stderr, level=logging.WARNING, format="%(message)s")
    logging.getLogger("project_hub.core.cache").setLevel(logging.INFO)

    from .git.ops import check_git_version
    from .core.workspace import detect_state, WorkspaceState

    check_git_version()
    state = detect_state(Path.cwd())

    if state == WorkspaceState.DISABLED_OPT_OUT:
        click.echo(
            "project-hub is disabled here. Delete .no-project-hub to re-enable.",
            err=True,
        )
        sys.exit(0)

    if state == WorkspaceState.DISABLED_SUB_REPO:
        click.echo(
            "Inside a workspace sub-repo — project-hub runs from the parent directory.",
            err=True,
        )
        sys.exit(0)

    if state == WorkspaceState.DORMANT:
        if click.confirm("No workspace found. Initialize here?", default=True):
            _do_init(Path.cwd())
        else:
            sys.exit(0)
        return

    asyncio.run(_run_standalone_async(drop_cache=drop_cache))


async def _run_standalone_async(drop_cache: bool = False) -> None:
    """Async core of standalone mode: start workspace, serve web UI, refresh loop."""
    from .core.cache import CacheMigrationRequired
    from .core.workspace import Workspace
    from .web.app import create_app, start_webui

    ws = Workspace(Path.cwd())
    try:
        await ws.start(drop_cache=drop_cache)
    except CacheMigrationRequired:
        click.echo(
            "Breaking change: cache database format has changed.\n"
            "Run with --drop-current-cache to rebuild it.\n\n"
            "This will delete the cache database. Forge data (MRs, pipelines, vulns)\n"
            "is refreshed automatically. Dashboard settings (pinned repos, card order)\n"
            "will be lost.\n\n"
            "  uv run project-hub --drop-current-cache",
            err=True,
        )
        sys.exit(1)

    app = create_app(**ws.web_app_kwargs())
    server, port = await start_webui(app, ws.config.http_port)
    click.echo(f"Dashboard running at http://localhost:{port}", err=True)
    import webbrowser
    await asyncio.to_thread(webbrowser.open, f"http://localhost:{port}")

    loop = asyncio.get_event_loop()
    stop_event = asyncio.Event()
    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, stop_event.set)

    refresh_task = asyncio.create_task(ws.refresh_loop())
    await stop_event.wait()
    refresh_task.cancel()
    try:
        await refresh_task
    except asyncio.CancelledError:
        pass
    finally:
        server.should_exit = True
        await ws.stop()


# ---------------------------------------------------------------------------
# MCP mode
# ---------------------------------------------------------------------------


def run_mcp() -> None:
    """Start the MCP server on stdio transport."""
    import logging

    logging.basicConfig(stream=sys.stderr, level=logging.WARNING)

    from .mcp.server import mcp as mcp_server

    mcp_server.run()  # stdio transport, blocks


# ---------------------------------------------------------------------------
# Init logic (shared by `init` command and standalone DORMANT path)
# ---------------------------------------------------------------------------

_KNOWN_HOSTS: dict[str, str] = {
    "github.com": "github",
    "gitlab.com": "gitlab",
}


def _detect_forge_hosts(repos) -> dict[str, str]:
    """Classify forge hosts from discovered repos, prompting for unknown ones."""
    hosts: dict[str, str] = {}
    unknown_hosts: set[str] = set()
    for r in repos:
        if not r.forge_host:
            continue
        if r.forge_host in _KNOWN_HOSTS:
            hosts[r.forge_host] = _KNOWN_HOSTS[r.forge_host]
        elif "github" in r.forge_host:
            hosts[r.forge_host] = "github"
        elif "gitlab" in r.forge_host:
            hosts[r.forge_host] = "gitlab"
        else:
            unknown_hosts.add(r.forge_host)
    for host in sorted(unknown_hosts):
        forge = click.prompt(
            f"Unknown host '{host}' — forge type?",
            type=click.Choice(["gitlab", "github", "skip"]),
            default="gitlab",
        )
        if forge != "skip":
            hosts[host] = forge
    return hosts


def _do_init(cwd: Path) -> None:
    """Bootstrap a .project-hub workspace: scan repos, detect forges, write config."""
    hub = cwd / ".project-hub"

    if hub.is_dir():
        click.echo("project-hub is already initialized here.", err=True)
        sys.exit(1)

    from .core.config import Config
    from .core.discovery import discover_repos
    import yaml

    repos = discover_repos(cwd, Config())
    if not repos:
        click.echo("No git repos found in this directory.", err=True)
        sys.exit(1)

    hosts = _detect_forge_hosts(repos)

    config_data: dict = {}
    if hosts:
        config_data["forges"] = {h: {"type": t} for h, t in sorted(hosts.items())}

    hub.mkdir()
    config_yaml = (
        yaml.dump(config_data, default_flow_style=False) if config_data else ""
    )
    (hub / "config.yaml").write_text(config_yaml)
    (hub / "workspace.md").write_text("")

    repo_names = sorted(r.name for r in repos)
    click.echo(f"Initialized project-hub in {cwd}")
    click.echo(f"Discovered {len(repos)} repos: {', '.join(repo_names)}")
    if hosts:
        click.echo(f"Forges: {', '.join(sorted(hosts))}")
    else:
        click.echo(
            "No forge hosts detected — edit .project-hub/config.yaml to add them."
        )


# ---------------------------------------------------------------------------
# init command
# ---------------------------------------------------------------------------


@main.command()
def init() -> None:
    """Initialize a .project-hub workspace in the current directory."""
    _do_init(Path.cwd())


@main.command("sync-now")
@click.pass_context
def sync_now(ctx: click.Context) -> None:
    """Force a workspace synchronization from the CLI."""
    asyncio.run(_do_sync_now())


async def _do_sync_now() -> None:
    """Run a single refresh cycle from the CLI and exit."""
    from .core.workspace import Workspace, WorkspaceState, detect_state

    state = detect_state(Path.cwd())
    if state != WorkspaceState.ACTIVE:
        click.echo(
            "No active workspace found. Run from a directory with .project-hub/",
            err=True,
        )
        sys.exit(1)

    ws = Workspace(Path.cwd())
    await ws.start()
    click.echo("Synchronizing workspace...")
    await ws.refresh_once()
    await ws.stop()
    click.echo("Sync complete.")


# ---------------------------------------------------------------------------
# auth group
# ---------------------------------------------------------------------------


@main.group()
def auth() -> None:
    """Manage authentication."""


@auth.command("login")
@click.argument("instance")
def auth_login(instance: str) -> None:
    """Authenticate with a forge instance."""
    from .forge.auth import store_token

    forge_type = "github" if "github" in instance else "gitlab"
    token = click.prompt(f"Personal Access Token for {instance}", hide_input=True)

    try:
        if forge_type == "github":
            import github
            gh = github.Github(auth=github.Auth.Token(token))
            user = gh.get_user().login
        else:
            import gitlab
            gl = gitlab.Gitlab(f"https://{instance}", private_token=token)
            gl.auth()
            user = gl.user.username
    except Exception as e:
        click.echo(f"Authentication failed: {e}", err=True)
        sys.exit(1)

    store_token(instance, token)
    click.echo(f"Authenticated as @{user} on {instance}")


@main.command()
@click.argument("repo")
def exclude(repo: str) -> None:
    """Exclude a repo from the workspace."""
    from .core.config import load_config, save_config

    config_path = Path.cwd() / ".project-hub" / "config.yaml"
    if not config_path.exists():
        click.echo("No .project-hub/config.yaml found. Are you in a workspace?", err=True)
        sys.exit(1)

    config = load_config(config_path)

    if repo in config.exclude:
        click.echo(f"{repo} is already excluded.")
        return

    config.exclude.append(repo)
    save_config(config_path, config)
    click.echo(f"Excluded {repo}.")


@main.command("include")
@click.argument("repo")
def include_repo(repo: str) -> None:
    """Re-include a previously excluded repo."""
    from .core.config import load_config, save_config

    config_path = Path.cwd() / ".project-hub" / "config.yaml"
    if not config_path.exists():
        click.echo("No .project-hub/config.yaml found. Are you in a workspace?", err=True)
        sys.exit(1)

    config = load_config(config_path)

    if repo not in config.exclude:
        click.echo(f"{repo} is not in the exclude list.")
        return

    config.exclude.remove(repo)
    save_config(config_path, config)
    click.echo(f"Re-included {repo}.")


@auth.command("status")
def auth_status_cmd() -> None:
    """Show authentication status."""
    from .forge.auth import auth_status, migration_warning

    statuses = auth_status()
    if not statuses:
        click.echo("No stored credentials.")
        return
    for s in statuses:
        marker = "authenticated" if s.authenticated else "not authenticated"
        click.echo(f"  {s.instance}: {marker} ({s.backend})")
    warning = migration_warning()
    if warning:
        click.echo(f"\n  Warning: {warning}", err=True)


@auth.command("migrate")
def auth_migrate_cmd() -> None:
    """Migrate plaintext tokens from auth.json to OS keyring."""
    from .forge.auth import migrate_to_keyring, _keyring_available

    if not _keyring_available():
        click.echo("No OS keyring backend available.", err=True)
        sys.exit(1)

    migrated, failed = migrate_to_keyring()
    if not migrated and not failed:
        click.echo("Nothing to migrate — all tokens already in keyring.")
        return
    for inst in migrated:
        click.echo(f"  {inst}: migrated to keyring")
    for inst in failed:
        click.echo(f"  {inst}: migration failed", err=True)
    if migrated:
        click.echo(f"\n{len(migrated)} token(s) migrated. Plaintext removed from auth.json.")
