# AUTOGENERATED! DO NOT EDIT! File to edit: ../../../pts/mod/_cli/multi-sync.pct.py.

# %% top_export
import os
import typer
from typer import Argument, Option
from typing_extensions import Annotated
from types import FunctionType
from typing import Callable, Union, List, Literal
from pathlib import Path
from enum import Enum
import asyncio

import repoyard as proj
from repoyard import const
from repoyard.config import get_config
from repoyard._utils import async_throttler, check_interrupted, enable_soft_interruption, SoftInterruption
from repoyard._utils.sync_helper import SyncSetting, SyncDirection
from repoyard._models import RepoPart
from repoyard._cli.app import app, app_state

@app.command(name='multi-sync')
def cli_multi_sync(
    repo_full_names: list[str]|None = Option(None, "--repo", "-r", help="The full names of the repository, in the form."),
    storage_locations: list[str]|None = Option(None, "--storage-location", "-s", help="The storage locations to sync."),
    max_concurrent_rclone_ops: int|None = Option(None, "--max-concurrent", "-m", help="The maximum number of concurrent rclone operations. If not provided, the default specified in the config will be used."),
    sync_direction: SyncDirection|None = Option(None, "--sync-direction", help="The direction of the sync. If not provided, the appropriate direction will be automatically determined based on the sync status. This mode is only available for the 'CAREFUL' sync setting."),
    sync_setting: SyncSetting = Option(SyncSetting.CAREFUL, "--sync-setting", help="The sync setting to use."),
    sync_choices: list[RepoPart]|None = Option(None, "--sync-choices", "-c", help="The parts of the repository to sync. If not provided, all parts will be synced. By default, all parts are synced."),
    sync_recently_modified_first: bool = Option(False, help="Sync repositories that have been recently modified first."),
    refresh_user_symlinks: bool = Option(True, help="Refresh the user symlinks."),
    show_progress: bool = Option(True, help="Show the progress of the sync."),
    no_print_skipped: bool = Option(True, help="Do not print repositories for which no syncs happened."),
    soft_interruption_enabled: bool = Option(True, help="Enable soft interruption."),
):
    """
    Sync multiple repositories.
    """
    
    # AUTOGENERATED! DO NOT EDIT! File to edit: ../../../pts/mod/_cli/multi-sync.pct.py.
    
    # %% auto 0
    __all__ = ['config', 'repoyard_meta', 'sync_stats', 'finish_monitoring_event', 'FINISHED_REMAIN_TIME', 'sync_task',
               'final_sync_stat_board', 'console', 'get_sync_stat_board']
    
    # %% ../../../pts/mod/_cli/multi-sync.pct.py 4
    from .._models import get_repoyard_meta
    from ..cmds import sync_repo
    from rich.live import Live
    from rich.text import Text
    from rich.console import Console
    from datetime import datetime, timedelta
    import shutil
    
    # %% ../../../pts/mod/_cli/multi-sync.pct.py 11
    if soft_interruption_enabled:
        enable_soft_interruption()
    
    if repo_full_names is not None and storage_locations is not None:
        typer.echo("Cannot provide both `--repo` and `--storage-location`.", err=True)
        raise typer.Exit(code=1)
    
    config = get_config(app_state['config_path'])
    
    if storage_locations is None and repo_full_names is None:
        storage_locations = list(config.storage_locations.keys())
    if storage_locations is not None and any(sl not in config.storage_locations for sl in storage_locations):
        typer.echo(f"Invalid storage location: {storage_locations}")
        raise typer.Exit(code=1)
    
    if max_concurrent_rclone_ops is None:
        max_concurrent_rclone_ops = config.max_concurrent_rclone_ops
    
    repoyard_meta = get_repoyard_meta(config)
    if repo_full_names is None:
        repo_metas = [repo_meta for repo_meta in repoyard_meta.repo_metas if repo_meta.storage_location in storage_locations]
    else:
        if any(repo_full_name not in repoyard_meta.by_full_name for repo_full_name in repo_full_names):
            typer.echo(f"Non-existent repository: {repo_full_names}")
            raise typer.Exit(code=1)
        repo_metas = [repoyard_meta.by_full_name[repo_full_name] for repo_full_name in repo_full_names]
    
    # %% ../../../pts/mod/_cli/multi-sync.pct.py 13
    async def _task(num, repo_meta):
        sync_stats[repo_meta.full_name] = (num, "Syncing...", None, datetime.now(), None)
        try:
            sync_results = await sync_repo(
                config_path=app_state['config_path'],
                repo_full_name=repo_meta.full_name,
                sync_direction=sync_direction,
                sync_setting=sync_setting,
                sync_choices=sync_choices,
                verbose=False,
            )
            sync_stats[repo_meta.full_name] = (num, "Success", None, datetime.now(), sync_results)
        except SoftInterruption:
            sync_stats[repo_meta.full_name] = (num, "Interrupted", None, datetime.now(), None)
        except Exception as e:
            sync_stats[repo_meta.full_name] = (num, "Error", str(e), datetime.now(), None)
    
    # %% ../../../pts/mod/_cli/multi-sync.pct.py 15
    sync_stats = {}
    
    finish_monitoring_event = asyncio.Event()
    
    FINISHED_REMAIN_TIME = 10 # how long to show the finished message
    def get_sync_stat_board(finished: bool):
        console_width = shutil.get_terminal_size((80, 20)).columns
        lines = []
    
        for repo_full_name, (num, sync_stat, e, timestamp, sync_results) in sync_stats.items():
            if not finished and sync_stat not in ["Syncing", "Error"] and timestamp < datetime.now() - timedelta(seconds=FINISHED_REMAIN_TIME):
                continue
    
            status_color = {
                "Syncing": "yellow",
                "Success": "green",
                "Interrupted": "magenta",
                "Error": "red",
            }.get(sync_stat, "")
    
            name_color = {
                "Success": "green",
                "Interrupted": "magenta",
                "Error": "red",
            }.get(sync_stat, "")
    
            left = f"({num+1}/{len(repo_metas)}) [bold {name_color}]{repo_full_name}[/bold {name_color}]"
            right = f"[bold {status_color}]{sync_stat}[/bold {status_color}]"
    
            # Strip markup to compute the real visible lengths
            console = Console()
            left_len = len(Text.from_markup(left).plain)
            right_len = len(Text.from_markup(right).plain)
    
            # compute how many dots are needed
            dots = console_width - left_len - right_len - 1 - 2 # -2 for the space between dots and the left and right text
            if dots < 1:
                dots = 1
    
            line = f"{left} {'.' * dots} {right}"
            syncs_happened = [False if sync_results is None else sync_results[repo_part][1] for repo_part in RepoPart]
            if finished and sync_stat == "Success" and no_print_skipped and all([not synced for synced in syncs_happened]):
                continue
            lines.append(line)
    
            indent = "    "
            if e:
                lines.append(f"{indent}[red]{e}[/red]")
            elif sync_stat == "Success":
                line = []
                for repo_part, synced in zip(RepoPart, syncs_happened):
                    line.append(f"[bold]{repo_part.value}:[/bold] {'[green]Synced[/green]' if synced else '[blue]Skipped[/blue]'}")
                lines.append(indent + f",{indent}".join(line))
            else:
                lines.append(f"{indent}[yellow]Results pending...[/yellow]")
    
        return "\n".join(lines).strip()
    
    async def _progress_monitor_task():
        console = Console()
        with Live(console=console, refresh_per_second=4) as live:
            def _update_live(finished: bool):
                rendered = Text.from_markup(get_sync_stat_board(finished=finished))
                live.update(rendered)
            while not finish_monitoring_event.is_set():
                _update_live(False)
                await asyncio.sleep(0.2)
            live.update(Text.from_markup("Finished. Final results:\n\n"))
    
    # %% ../../../pts/mod/_cli/multi-sync.pct.py 17
    _repo_metas = repo_metas
    if sync_recently_modified_first:
        from repoyard._utils import check_last_time_modified
        def get_last_modified(repo_meta):
            last_modified = check_last_time_modified(repo_meta.get_local_path(config))
            return last_modified.timestamp() if last_modified else 0
        _repo_metas = sorted(_repo_metas, key=get_last_modified, reverse=True)
    
    sync_task = async_throttler(
        [_task(num, repo_meta) for num, repo_meta in enumerate(_repo_metas)],
        max_concurrency=max_concurrent_rclone_ops,
    )
    
    async def _runner():
        if show_progress:
            monitor_task = asyncio.create_task(_progress_monitor_task())
            await sync_task
            finish_monitoring_event.set()
            await monitor_task
        else:
            await sync_task
    
    # %% ../../../pts/mod/_cli/multi-sync.pct.py 19
    from .._utils import is_in_event_loop
    if not is_in_event_loop():
        asyncio.run(_runner())
    
    final_sync_stat_board = get_sync_stat_board(finished=True)
    console = Console()
    console.print(final_sync_stat_board, markup=True)
    
    if refresh_user_symlinks:
        from repoyard.cmds import create_user_symlinks
        create_user_symlinks(config_path=app_state['config_path'])