"""Command-line interface for glreview."""

from __future__ import annotations

import sys
from pathlib import Path

import click

from . import __version__, cli_claude, cli_reports, state_machine
from .config import load_config
from .discovery import discover_modules
from .errors import AlreadyInStateError, InvalidStateTransitionError
from .git import (
    count_lines,
    get_current_commit,
    get_gitlab_host,
    get_gitlab_project,
    has_changes_since,
)
from .gitlab import (
    add_issue_comment,
    close_issue,
    get_compare_url,
    get_issue,
    get_project_members,
    gitlab_available,
)
from .registry import ModuleStatus, Registry, load_registry, save_registry
from .review_service import ReviewService
from .review_state import ReviewStateChecker


@click.group()
@click.version_option(version=__version__)
@click.pass_context
def main(ctx: click.Context) -> None:
    """glreview - GitLab code review tracking for scientific software."""
    ctx.ensure_object(dict)
    ctx.obj["config"] = load_config()


# Register commands from submodules
cli_claude.register_commands(main)
cli_reports.register_commands(main)


@main.command()
@click.pass_context
def init(ctx: click.Context) -> None:
    """Initialize glreview in a project.

    Requires [tool.glreview] configuration in pyproject.toml with at least
    a 'sources' pattern defined.

    This command is idempotent: if a registry already exists, it behaves
    like 'glreview sync' - adding new modules and removing deleted ones
    while preserving review status for existing modules.
    """
    config = ctx.obj["config"]

    # Check that sources are configured
    if not config.sources:
        click.secho("Error: No source patterns configured.", fg="red")
        click.echo()
        click.echo("Add to pyproject.toml:")
        click.echo()
        click.echo("  [tool.glreview]")
        click.echo('  sources = ["src/**/*.py"]  # adjust to match your project')
        click.echo('  exclude = ["**/_version.py", "**/test_*.py"]')
        click.echo()
        click.echo("Then run 'glreview init' again.")
        sys.exit(1)

    # Idempotent: if registry exists, sync instead of failing
    if config.registry_path.exists():
        click.echo(f"Registry exists at {config.registry}, syncing...")
        ctx.invoke(cli_reports.sync)
        return

    # Discover modules
    modules = discover_modules(config.root, config.sources, config.exclude)

    if not modules:
        click.echo(f"No modules found matching patterns: {config.sources}")
        click.echo("Check your source patterns in pyproject.toml.")
        return

    # Create registry
    registry = Registry()

    for path in modules:
        lines = count_lines(path, cwd=config.root)

        registry.set(
            ModuleStatus(
                path=path,
                status="needs_review",
                lines=lines,
            )
        )

    # Save
    commit = get_current_commit(cwd=config.root)
    save_registry(registry, config.registry_path, commit=commit)

    click.echo(f"Initialized glreview with {len(modules)} modules")
    click.echo(f"Registry: {config.registry}")
    click.echo()
    click.echo("Next steps:")
    click.echo("  1. Commit the .glreview/ directory")
    click.echo("  2. Run 'glreview status' to see review coverage")
    click.echo("  3. Run 'glreview ci-config' for GitLab CI setup")


@main.command()
@click.argument("path", required=False)
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.pass_context
def status(ctx: click.Context, path: str | None, as_json: bool) -> None:
    """Check review status."""
    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)

    if not registry.modules:
        click.echo("No modules in registry. Run 'glreview init' first.")
        return

    if path:
        _show_module_status(config, registry, path, as_json)
    else:
        _show_summary_status(config, registry, as_json)


def _show_module_status(config, registry, path: str, as_json: bool) -> None:
    """Show status for a single module."""
    module = registry.get(path)
    if not module:
        click.echo(f"Error: {path} not in registry.")
        sys.exit(1)

    if as_json:
        import json

        click.echo(json.dumps(module.to_dict(), indent=2))
        return

    click.echo(f"Module: {path}")
    click.echo(f"  Status: {module.status}")
    click.echo(f"  Lines: {module.lines}")

    if module.status == "reviewed":
        click.echo(f"  Last reviewed: {module.last_reviewed_date}")
        click.echo(f"  Commit: {module.last_reviewed_commit}")
        click.echo(f"  Reviewers: {', '.join(module.reviewers)}")

        state_checker = ReviewStateChecker(config)
        is_stale, error = state_checker.is_module_stale(module)
        if error:
            click.secho(f"  ⚠ Could not check for changes: {error}", fg="yellow")
        elif is_stale:
            click.secho("  ⚠ Changes detected since last review!", fg="yellow")
        else:
            click.secho("  ✓ No changes since last review", fg="green")

    elif module.status == "in_progress":
        click.echo(f"  Issue: {module.review_issue or 'unknown'}")
        click.echo(f"  Assigned: {module.assigned_reviewer or 'unassigned'}")


def _show_summary_status(config, registry, as_json: bool) -> None:
    """Show summary status for all modules."""
    from .review_service import ReviewService

    state_checker = ReviewStateChecker(config)
    state = state_checker.get_registry_state(registry)

    if as_json:
        import json

        service = ReviewService(registry, config)
        click.echo(json.dumps(service.get_summary(), indent=2))
        return

    total = state.total
    reviewed = state.reviewed_count
    pct = (reviewed / total * 100) if total > 0 else 0
    line_pct = (
        (state.reviewed_lines / state.total_lines * 100) if state.total_lines > 0 else 0
    )

    # Header with progress bar
    bar_width = 20
    filled = int(bar_width * pct / 100)
    bar = "█" * filled + "░" * (bar_width - filled)

    click.echo()
    click.echo(f"  Review Progress: [{bar}] {pct:.0f}%")
    click.echo()
    click.echo(f"  Modules: {reviewed}/{total} reviewed")
    lines_str = f"{state.reviewed_lines:,}/{state.total_lines:,}"
    click.echo(f"  Lines:   {lines_str} ({line_pct:.0f}%)")
    click.echo()

    # In Progress section
    if state.in_progress:
        click.secho(f"◐ In Progress ({len(state.in_progress)})", fg="cyan", bold=True)
        for info in sorted(state.in_progress, key=lambda i: i.module.path):
            mod = info.module
            assignee = mod.assigned_reviewer or "unassigned"
            issue = mod.review_issue or "none"
            click.echo(f"    {mod.path}")
            click.echo(f"      Assignee: {assignee}")
            click.echo(f"      Issue: {issue}")
            if mod.review_started_commit:
                click.echo(f"      Started at: {mod.review_started_commit}")
        click.echo()

    # Needs Review section
    if state.needs_review:
        click.secho(
            f"○ Needs Review ({len(state.needs_review)})", fg="yellow", bold=True
        )
        for info in sorted(state.needs_review, key=lambda i: i.module.path):
            mod = info.module
            click.echo(f"    {mod.path} ({mod.lines} lines)")
        click.echo()

    # Reviewed but Changed section
    if state.reviewed_stale:
        click.secho(
            f"⚠ Reviewed but Changed ({len(state.reviewed_stale)})",
            fg="red",
            bold=True,
        )
        for info in sorted(state.reviewed_stale, key=lambda i: i.module.path):
            mod = info.module
            click.echo(f"    {mod.path}")
            date = mod.last_reviewed_date
            commit = mod.last_reviewed_commit
            click.echo(f"      Last reviewed: {date} ({commit})")
        click.echo()

    # Reviewed and current section
    if state.reviewed_current:
        click.secho(
            f"✓ Reviewed ({len(state.reviewed_current)})", fg="green", bold=True
        )
        count = len(state.reviewed_current)
        click.echo(f"    All {count} reviewed modules are up to date.")
        click.echo()


@main.command("list")
@click.option("--status", "filter_status", help="Filter by status")
@click.pass_context
def list_modules(
    ctx: click.Context,
    filter_status: str | None,
) -> None:
    """List modules in the registry."""
    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)

    modules = list(registry.filter(status=filter_status))

    if not modules:
        click.echo("No modules match the filters.")
        return

    for mod in sorted(modules, key=lambda m: m.path):
        status_icon = {
            "reviewed": "✓",
            "in_progress": "◐",
            "needs_review": "○",
            "needs_update": "!",
        }.get(mod.status, "?")

        click.echo(f"{status_icon} {mod.path}")


@main.command()
@click.argument("path")
@click.option("--assignee", "-a", help="Assign reviewer (@username)")
@click.option("--issue", "-i", help="Manual issue number (if glab unavailable)")
@click.pass_context
def start(
    ctx: click.Context, path: str, assignee: str | None, issue: str | None
) -> None:
    """Start a review for a module."""
    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)
    service = ReviewService(registry, config)

    # Check module exists
    module = registry.get(path)
    if not module:
        click.echo(f"Error: {path} not in registry.")
        click.echo()
        click.echo("Available modules:")
        for m in sorted(registry.modules.keys())[:10]:
            click.echo(f"  {m}")
        if len(registry.modules) > 10:
            click.echo(f"  ... and {len(registry.modules) - 10} more")
        sys.exit(1)

    # Check if already in progress (before GitLab check)
    if module.status == "in_progress":
        click.echo(f"Review already in progress for {path}")
        if module.review_issue:
            click.echo(f"  Issue: {module.review_issue}")
        sys.exit(1)

    # Check if already reviewed with no changes (before GitLab check)
    if module.status == "reviewed":
        state_checker = ReviewStateChecker(config)
        is_stale, _ = state_checker.is_module_stale(module)
        if not is_stale:
            click.secho(f"✓ {path} is already reviewed at current commit", fg="green")
            click.echo("  No changes since last review.")
            sys.exit(0)

    # Check GitLab availability for issue creation
    host = get_gitlab_host(cwd=config.root)
    if not gitlab_available(host) and not issue:
        module_name = Path(path).stem
        labels = config.issue_labels
        click.secho("Warning: GitLab authentication not configured.", fg="yellow")
        click.echo()
        click.echo("Set GITLAB_PRIVATE_TOKEN environment variable, or")
        click.echo("create the issue manually with:")
        click.echo(f"  Title: [REVIEW] {module_name}")
        click.echo(f"  Labels: {', '.join(labels)}")
        click.echo()
        click.echo("Then run:")
        click.echo(f"  glreview start {path} --issue <number>")
        sys.exit(1)

    click.echo(f"Creating review issue for {path}...")

    # Use service to start review (creates issue if needed)
    result = service.start_review(
        path,
        assignee=assignee,
        issue_url=issue,
        create_issue=(issue is None),
    )

    if not result.success:
        # Handle specific failure cases
        if "already in progress" in result.message:
            click.echo(f"Review already in progress for {path}")
            if result.issue_url:
                click.echo(f"  Issue: {result.issue_url}")
            sys.exit(1)
        elif "already reviewed" in result.message:
            click.secho(f"✓ {path} is already reviewed at current commit", fg="green")
            click.echo("  No changes since last review.")
            sys.exit(0)
        else:
            click.secho(f"Error: {result.message}", fg="red")
            sys.exit(1)

    # Save registry
    save_registry(registry, config.registry_path, commit=result.commit)

    click.echo()
    click.secho(f"✓ Review started for {path}", fg="green")
    if result.issue_url:
        click.echo(f"  Issue: {result.issue_url}")
    if assignee:
        click.echo(f"  Assignee: {assignee}")
    click.echo()
    click.echo("When review is complete, run:")
    click.echo(f"  glreview signoff {path}")


@main.command()
@click.argument("path")
@click.option("--reviewer", "-r", help="Reviewer (@username)")
@click.option("--skip-verify", is_flag=True, help="Skip issue verification")
@click.option("--force", "-f", is_flag=True, help="Force sign-off")
@click.option(
    "--acknowledge", is_flag=True, help="Acknowledge changes made during review"
)
@click.pass_context
def signoff(
    ctx: click.Context,
    path: str,
    reviewer: str | None,
    skip_verify: bool,
    force: bool,
    acknowledge: bool,
) -> None:
    """Sign off on a completed review."""
    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)

    module = registry.get(path)
    if not module:
        click.echo(f"Error: {path} not in registry.")
        sys.exit(1)

    if module.status == "reviewed" and not force:
        click.secho(f"✓ {path} is already reviewed.", fg="green")
        click.echo("  Use --force to re-sign-off.")
        sys.exit(0)

    if module.status != "in_progress" and not force:
        click.echo(f"Error: {path} is not in review (status: {module.status})")
        click.echo("Start a review first with:")
        click.echo(f"  glreview start {path}")
        sys.exit(1)

    # Check if file changed since review started
    if module.review_started_commit and not acknowledge:
        if has_changes_since(path, module.review_started_commit, cwd=config.root):
            project = get_gitlab_project(cwd=config.root)
            host = get_gitlab_host(cwd=config.root) or "gitlab.com"
            current = get_current_commit(cwd=config.root)
            diff_url = get_compare_url(
                project, host, module.review_started_commit, current
            )

            click.secho("⚠ File changed during review!", fg="yellow")
            click.echo()
            click.echo(f"  Review started at: {module.review_started_commit}")
            click.echo(f"  Current commit:    {current}")
            click.echo(f"  View changes: {diff_url}")
            click.echo()
            click.echo("Options:")
            click.echo("  --acknowledge  Sign off confirming you reviewed the changes")
            click.echo("  --force        Sign off without checks")
            sys.exit(1)

    # Extract issue number using centralized logic
    issue_number = ReviewStateChecker.extract_issue_number(module.review_issue)

    # Verify issue is closed
    project = get_gitlab_project(cwd=config.root)
    host = get_gitlab_host(cwd=config.root) or "gitlab.com"

    if issue_number and not skip_verify:
        if not gitlab_available(host):
            click.secho(
                "Warning: GitLab not available (no token?). Cannot verify issue.",
                fg="yellow",
            )
            click.echo("Set GITLAB_PRIVATE_TOKEN or use --skip-verify")
        else:
            click.echo(f"Checking issue #{issue_number}...")
            issue = get_issue(issue_number, project_path=project, host=host)

            if issue:
                if issue.state != "closed":
                    msg = f"Error: Issue #{issue_number} is not closed "
                    msg += f"(state: {issue.state})"
                    click.secho(msg, fg="red")
                    click.echo()
                    click.echo("Close the issue first, or use --skip-verify to bypass.")
                    sys.exit(1)

                # Extract reviewer from issue
                if not reviewer and issue.assignees:
                    reviewer = f"@{issue.assignees[0]}"

                click.secho(f"  State: {issue.state} ✓", fg="green")
            else:
                click.secho(
                    f"Warning: Could not fetch issue #{issue_number}", fg="yellow"
                )

    if not reviewer:
        click.secho("Error: Could not determine reviewer.", fg="red")
        click.echo("Specify with --reviewer @username")
        sys.exit(1)

    # Update registry using state machine
    current_commit = get_current_commit(cwd=config.root)

    try:
        state_machine.signoff(module, current_commit, reviewer, force=force)
    except (AlreadyInStateError, InvalidStateTransitionError) as e:
        click.secho(f"Error: {e}", fg="red")
        sys.exit(1)

    save_registry(registry, config.registry_path, commit=current_commit)

    click.echo()
    click.secho(f"✓ {path}", fg="green")
    click.echo("  Status: reviewed")
    click.echo(f"  Commit: {current_commit}")
    click.echo(f"  Reviewers: {', '.join(module.reviewers)}")
    if module.review_issue:
        click.echo(f"  Issue: {module.review_issue}")

    click.echo()
    click.echo("Don't forget to commit and push the registry:")
    click.echo(f"  git add {config.registry}")
    click.echo(f'  git commit -m "signoff: {path}"')
    click.echo("  git push")


@main.command()
@click.argument("path")
@click.option("--leave-open", is_flag=True, help="Leave GitLab issue open")
@click.option("--restart", is_flag=True, help="Restart review (update start commit)")
@click.option("--reason", "-m", help="Reason for cancellation")
@click.pass_context
def cancel(
    ctx: click.Context,
    path: str,
    leave_open: bool,
    restart: bool,
    reason: str | None,
) -> None:
    """Cancel or restart a review in progress."""
    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)

    module = registry.get(path)
    if not module:
        click.echo(f"Error: {path} not in registry.")
        sys.exit(1)

    if module.status != "in_progress":
        click.echo(f"Error: {path} is not in progress (status: {module.status})")
        sys.exit(1)

    # Extract issue number using centralized logic
    issue_url = module.review_issue
    issue_number = ReviewStateChecker.extract_issue_number(issue_url)

    project = get_gitlab_project(cwd=config.root)
    host = get_gitlab_host(cwd=config.root)
    current_commit = get_current_commit(cwd=config.root)

    if restart:
        # Restart: update start commit, add comment to issue
        state_machine.restart_review(module, current_commit)

        if issue_number and gitlab_available(host):
            comment = f"**Review restarted** at commit `{current_commit}`"
            if reason:
                comment += f"\n\nReason: {reason}"
            if add_issue_comment(issue_number, project, comment, host=host):
                click.echo(f"Added restart comment to issue #{issue_number}")
            else:
                click.secho("Warning: Could not add comment to issue.", fg="yellow")
        elif issue_number:
            click.secho(
                f"Warning: Could not update issue #{issue_number} (no API access).",
                fg="yellow",
            )
            click.echo("Issue may have stale information.")

        save_registry(registry, config.registry_path, commit=current_commit)

        click.echo()
        click.secho(f"✓ Review restarted for {path}", fg="green")
        click.echo(f"  New start commit: {current_commit}")
        if issue_url:
            click.echo(f"  Issue: {issue_url}")
        return

    # Cancel: restore previous state
    was_previously_reviewed = module.last_reviewed_commit is not None

    # Try to close the issue
    issue_closed = False
    if issue_number and not leave_open:
        if gitlab_available(host):
            comment = "**Review canceled**"
            if reason:
                comment += f"\n\nReason: {reason}"
            if was_previously_reviewed:
                prev = module.last_reviewed_commit
                comment += f"\n\nRestoring to previous review at `{prev}`"

            if close_issue(issue_number, project, host=host, comment=comment):
                issue_closed = True
                click.echo(f"Closed issue #{issue_number}")
            else:
                click.secho(
                    f"Warning: Could not close issue #{issue_number}.", fg="yellow"
                )
        else:
            click.secho("Error: Cannot close issue (no GitLab API access).", fg="red")
            click.echo()
            click.echo("Options:")
            click.echo("  --leave-open  Proceed without closing issue")
            click.echo("  Set GITLAB_PRIVATE_TOKEN to enable API access")
            sys.exit(1)

    # Use state machine to handle the transition
    new_status = state_machine.cancel_review(module, force=True)

    # Store reason in notes if provided
    if reason:
        module.notes = f"Canceled: {reason}"

    save_registry(registry, config.registry_path, commit=current_commit)

    click.echo()
    click.secho(f"✓ Review canceled for {path}", fg="green")
    if new_status == "reviewed":
        click.echo("  Status: restored to reviewed")
        click.echo(f"  Previous commit: {module.last_reviewed_commit}")
        click.echo(f"  Previous reviewers: {', '.join(module.reviewers) or 'none'}")
    else:
        click.echo("  Status: needs_review (no previous review)")

    if issue_url and not issue_closed:
        click.echo()
        click.secho(
            f"  Note: Issue {issue_url} left open - close manually", fg="yellow"
        )


@main.command()
@click.argument("path")
@click.option(
    "--since-start", is_flag=True, help="Show diff since review started (in progress)"
)
@click.option("--web", is_flag=True, help="Open diff in browser instead of terminal")
@click.pass_context
def diff(ctx: click.Context, path: str, since_start: bool, web: bool) -> None:
    """Show changes to a module since last review or since review started.

    By default, shows the diff since the last signed-off review.
    Use --since-start to see changes made during the current review.
    """
    import subprocess
    import webbrowser

    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)

    module = registry.get(path)
    if not module:
        click.echo(f"Error: {path} not in registry.")
        sys.exit(1)

    project = get_gitlab_project(cwd=config.root)
    host = get_gitlab_host(cwd=config.root) or "gitlab.com"
    current_commit = get_current_commit(cwd=config.root)

    if since_start:
        # Diff since review started
        if module.status != "in_progress":
            click.secho(
                f"Error: {path} is not in progress (status: {module.status})",
                fg="red",
            )
            click.echo("--since-start only works for modules currently in review.")
            sys.exit(1)

        if not module.review_started_commit:
            click.secho("Error: No review start commit recorded.", fg="red")
            sys.exit(1)

        from_commit = module.review_started_commit
        label = "since review started"
    else:
        # Diff since last review (default)
        if not module.last_reviewed_commit:
            click.secho(f"Error: {path} has never been reviewed.", fg="red")
            click.echo("No previous review to diff against.")
            sys.exit(1)

        from_commit = module.last_reviewed_commit
        label = "since last review"

    # Check if there are any changes
    if not has_changes_since(path, from_commit, cwd=config.root):
        click.secho(f"No changes {label}.", fg="green")
        click.echo(f"  Commit: {from_commit[:7]}")
        sys.exit(0)

    if web:
        # Open in browser
        diff_url = get_compare_url(project, host, from_commit, current_commit)
        if diff_url:
            click.echo(f"Opening diff {label}...")
            click.echo(f"  {diff_url}")
            webbrowser.open(diff_url)
        else:
            click.secho("Error: Could not generate GitLab URL.", fg="red")
            click.echo("Is this a GitLab repository?")
            sys.exit(1)
    else:
        # Show in terminal
        click.echo(f"Diff for {path} {label}:")
        click.echo(f"  From: {from_commit[:7]}")
        click.echo(f"  To:   {current_commit[:7]} (HEAD)")
        click.echo()

        result = subprocess.run(
            ["git", "diff", from_commit, "HEAD", "--", path],
            cwd=config.root,
            capture_output=True,
            text=True,
        )

        if result.returncode != 0:
            click.secho(f"Error running git diff: {result.stderr}", fg="red")
            sys.exit(1)

        if result.stdout.strip():
            click.echo(result.stdout)
        else:
            click.secho("No diff output.", fg="yellow")


@main.command()
@click.option("--team", "-t", help="Filter by team name")
@click.pass_context
def reviewers(ctx: click.Context, team: str | None) -> None:
    """List available reviewers."""
    config = ctx.obj["config"]

    # Check for team filter
    if team:
        if team not in config.teams:
            click.echo(f"Unknown team: {team}")
            click.echo(f"Available teams: {', '.join(config.teams.keys()) or 'none'}")
            sys.exit(1)

        click.echo(f"Team '{team}':")
        for member in config.teams[team]:
            click.echo(f"  {member}")
        return

    # Show configured teams
    if config.teams:
        click.echo("Configured teams:")
        for team_name, members in config.teams.items():
            click.echo(f"  {team_name}: {', '.join(members)}")
        click.echo()

    # Query GitLab
    project = get_gitlab_project(cwd=config.root)
    host = get_gitlab_host(cwd=config.root)

    if not gitlab_available(host):
        click.secho("GitLab authentication not configured.", fg="yellow")
        click.echo()
        click.echo("Set one of these environment variables:")
        click.echo("  GITLAB_PRIVATE_TOKEN - Personal access token")
        click.echo("  GITLAB_TOKEN         - Alias for above")
        click.echo("  CI_JOB_TOKEN         - For CI pipelines")
        click.echo()
        click.echo("Optionally set GITLAB_URL for self-hosted instances.")
        return

    click.echo("Fetching project members...")
    members = get_project_members(project_path=project, host=host)

    if not members:
        click.echo("Could not fetch project members.")
        click.echo("Check your token has 'read_api' scope.")
        return

    # Group by access level
    by_level: dict[str, list] = {}
    for member in sorted(members, key=lambda m: (-m.access_level, m.username)):
        level_name = member.access_level_name
        if level_name not in by_level:
            by_level[level_name] = []
        by_level[level_name].append(member)

    click.echo()
    click.echo("Project members:")
    for level_name in ["Owner", "Maintainer", "Developer", "Reporter", "Guest"]:
        if level_name not in by_level:
            continue
        click.echo(f"\n  {level_name}s:")
        for member in by_level[level_name]:
            click.echo(f"    @{member.username:<20} {member.name}")

    click.echo()
    click.echo("Usage: glreview start <file> --assignee @username")


@main.command("batch-start")
@click.option("--claude-review", is_flag=True, help="Run claude-review --post for each")
@click.option("--model", default="opus", help="Claude model (with --claude-review)")
@click.option("--dry-run", is_flag=True, help="Show what would be done")
@click.pass_context
def batch_start(
    ctx: click.Context,
    claude_review: bool,
    model: str,
    dry_run: bool,
) -> None:
    """Start reviews for all modules needing review.

    Iterates over all modules with status 'needs_review' and starts
    a review for each (creates GitLab issue, updates registry).

    With --claude-review, also runs AI-assisted review and posts
    findings to the issue.
    """
    config = ctx.obj["config"]
    registry = load_registry(config.registry_path)
    service = ReviewService(registry, config)

    # Find modules needing review
    needs_review = service.get_modules_needing_review()

    if not needs_review:
        click.secho("No modules need review.", fg="green")
        return

    click.echo(f"Found {len(needs_review)} module(s) needing review")
    if dry_run:
        click.secho("(dry-run mode)", fg="yellow")
    click.echo()

    # Check GitLab availability
    host = get_gitlab_host(cwd=config.root)
    if not dry_run and not gitlab_available(host):
        click.secho("Error: GitLab authentication not configured.", fg="red")
        click.echo("Set GITLAB_PRIVATE_TOKEN environment variable.")
        sys.exit(1)

    # Check Claude availability if needed
    if claude_review and not dry_run:
        from .claude import claude_available

        if not claude_available():
            click.secho("Error: claude CLI not found.", fg="red")
            click.echo("Install Claude Code or remove --claude-review flag.")
            sys.exit(1)

    if dry_run:
        # Dry run - just show what would happen
        for i, (path, _module) in enumerate(needs_review, 1):
            click.echo(f"[{i}/{len(needs_review)}] {path}")
            click.echo("  Would start review")
            if claude_review:
                click.echo("  Would run claude-review --post")

        click.echo()
        click.echo("=" * 40)
        click.secho("Summary (dry-run):", bold=True)
        click.echo(f"  Would start: {len(needs_review)}")
        return

    # Progress callback for service
    progress_count = {"current": 0}

    def on_progress(path: str, step: str, result) -> None:
        if step == "start":
            progress_count["current"] += 1
            n = progress_count["current"]
            click.echo(f"[{n}/{len(needs_review)}] {path}")
            if result.success:
                click.secho(f"  ✓ Started: {result.issue_url}", fg="green")
            else:
                click.secho(f"  ✗ {result.message}", fg="red")
        elif step == "review":
            if result.success and result.posted:
                click.secho("  ✓ Review posted", fg="green")
            else:
                click.secho("  ✗ Review failed", fg="yellow")

    # Run batch operation
    result = service.batch_start_reviews(
        claude_review=claude_review,
        model=model,
        on_progress=on_progress,
    )

    # Save registry
    if result.started > 0:
        current_commit = get_current_commit(cwd=config.root)
        save_registry(registry, config.registry_path, commit=current_commit)

    # Summary
    click.echo()
    click.echo("=" * 40)
    click.secho("Summary:", bold=True)
    click.echo(f"  Started: {result.started}")
    if claude_review:
        click.echo(f"  Reviewed: {result.reviewed}")
    if result.errors:
        click.secho(f"  Errors: {result.errors}", fg="red")


if __name__ == "__main__":
    main()
