"""Review-plan and apply-review command implementations.""" from __future__ import annotations import json import sys from pathlib import Path from typing import Optional import click from vlm.cli_helpers import command_error, default_artifact_path, resolve_legacy_default_input_path, review_plan_tui_streams_ok from vlm.context import CLIContext from vlm.io import load_analysis_json, load_identities_json from vlm.plan_render import ( duplicate_groups_from_plan, preferred_plan_summary, render_review_footer, render_review_preview, render_review_verdict, ) from vlm.plan_review import GROUP_BY_CHOICES, prepare_review_rows, save_review_csv from vlm.plan_structure_preview import write_structure_preview from vlm.planner import apply_review_to_plan, load_plan, save_plan def review_plan_cmd( ctx: CLIContext, input: Path, output: Path, season_threshold: int, episode_threshold: int, preview_limit: int, show_all: bool, tui: bool, identities: Optional[Path], analysis: Optional[Path], group_by: str, sample_safe: int, structure_preview: Optional[Path], ) -> None: """Review a plan and export high-risk operations for manual confirmation.""" logger = ctx.logger try: input = resolve_legacy_default_input_path(input, "input", "plan.json", "--input") if season_threshold < 1 or episode_threshold < 1: click.echo("Error: thresholds must be >= 1", err=True) sys.exit(1) if preview_limit < 1: click.echo("Error: --preview-limit must be >= 1", err=True) sys.exit(1) if sample_safe < 0: click.echo("Error: --sample-safe must be >= 0", err=True) sys.exit(1) group_by = group_by.lower() if group_by not in GROUP_BY_CHOICES: click.echo(f"Error: invalid --group-by {group_by}", err=True) sys.exit(1) if tui: if not review_plan_tui_streams_ok(): click.echo("Error: --tui requires an interactive terminal (TTY)", err=True) sys.exit(1) click.echo(f"Loading plan: {input}") execution_plan = load_plan(input) identities_data = None identities_path = identities if identities_path is None: default_id = default_artifact_path("identities.json") if default_id.is_file(): identities_path = default_id if identities_path is not None and identities_path.is_file(): identities_data = load_identities_json(identities_path) click.echo(f"Loaded identities: {identities_path}") analysis_data = None if analysis is not None and analysis.is_file(): analysis_data = load_analysis_json(analysis) click.echo(f"Loaded analysis: {analysis}") rows, counters = prepare_review_rows( execution_plan, season_threshold=season_threshold, episode_threshold=episode_threshold, library_root=ctx.config.library_root, identities_data=identities_data, analysis_data=analysis_data, sample_safe=sample_safe, group_by=group_by, ) if structure_preview is not None: write_structure_preview( execution_plan, ctx.config.library_root, structure_preview, ) click.echo(f"Wrote structure preview: {structure_preview}") if tui: from vlm.plan_review import build_duplicate_path_maps from vlm.review_tui import ReviewTUIContext, run_plan_review_tui _, path_to_quality = build_duplicate_path_maps(analysis_data) tui_ctx = ReviewTUIContext( rows=rows, counters=counters, library_root=ctx.config.library_root, output_csv=output, plan_input=input, summary_text=preferred_plan_summary(execution_plan), path_to_quality=path_to_quality, ) rc = run_plan_review_tui(tui_ctx) if rc != 0: click.echo("Plan review aborted (no CSV written).", err=True) sys.exit(rc) click.echo(f"Saved manual review CSV to: {output}") logger.info( "Plan review TUI completed: total=%s high_risk=%s output=%s", counters["total_operations"], counters["high_risk_operations"], output, ) return save_review_csv(rows, output) click.echo() click.echo("Plan overview:") click.echo(preferred_plan_summary(execution_plan)) click.echo() click.echo("Plan review summary:") click.echo(f" Total operations: {counters['total_operations']}") click.echo(f" High-risk operations: {counters['high_risk_operations']}") click.echo(f" manual_review: {counters['manual_review']}") click.echo(f" sample_source: {counters['sample_source']}") click.echo(f" high_season: {counters['high_season']}") click.echo(f" high_episode: {counters['high_episode']}") click.echo(f" conflicts: {counters['conflicts']}") click.echo() click.echo(render_review_verdict(counters)) click.echo() click.echo("High-risk operations preview:") if rows: preview_lines, hidden_count = render_review_preview( rows, preview_limit=preview_limit, show_all=show_all, library_root=ctx.config.library_root, ) for line in preview_lines: click.echo(line) if hidden_count > 0: click.echo( f" ... and {hidden_count} more high-risk operations " "(use --show-all to display all)" ) else: click.echo(" (none)") click.echo() click.echo(f"Saved manual review CSV to: {output}") dup_groups = duplicate_groups_from_plan(execution_plan) click.echo() for line in render_review_footer( counters=counters, output_csv=output, plan_input=input, duplicate_groups=dup_groups, ): click.echo(line) logger.info( "Plan review completed: total=%s high_risk=%s output=%s", counters["total_operations"], counters["high_risk_operations"], output, ) except FileNotFoundError: command_error(ctx, f"Error: Plan file not found: {input}", f"Plan file not found: {input}") except json.JSONDecodeError as e: command_error( ctx, f"Error: Failed to parse plan JSON: {e}", f"Plan review JSON parsing failed: {e}", exc_info=True, ) except Exception as e: command_error( ctx, f"Error during plan review: {e}", f"Plan review failed: {e}", exc_info=True, ) def apply_review_cmd( ctx: CLIContext, plan: Path, csv: Path, output: Optional[Path], ) -> None: """Apply modifications from a manual review CSV back to the plan JSON.""" logger = ctx.logger output_path = output or plan try: click.echo(f"Loading plan: {plan}") execution_plan = load_plan(plan) click.echo(f"Applying review from: {csv}") updated_plan = apply_review_to_plan(execution_plan, csv) save_plan(updated_plan, output_path) click.echo(f"Successfully updated plan saved to: {output_path}") modified = 0 for i, op in enumerate(updated_plan.operations): if op.operation_type != execution_plan.operations[i].operation_type: modified += 1 click.echo(f"Total operations modified: {modified}") logger.info("Applied review from %s to %s, modified %s ops", csv, output_path, modified) except Exception as e: command_error( ctx, f"Error applying review: {e}", f"Apply review failed: {e}", exc_info=True, )