#!/usr/bin/env python3
# Copyright 2021 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import shutil
from typing import Iterable, Optional

from impl.common import run_commands, argh, console, strip_ansi_escape_sequences
from impl import testvm
from impl.testvm import Arch, VmState

USAGE = """Manages VMs for testing crosvm.

Can run an x86_64 and an aarch64 vm via `./tools/x86vm` and `./tools/aarch64vm`.
Both are a wrapper around `./tools/testvm --arch=x86_64/aarch64`.

The easiest way to use the VM is:

  $ ./tools/aarch64vm ssh

Which will initialize and boot the VM, then wait for SSH to be available and
opens an SSH session. The VM will stay alive between calls.

Available commands are:
    - up: Start the VM if it is not already running.
    - stop: Gracefully stop the VM
    - kill: Send SIGKILL to the VM
    - clean: Stop the VM and delete all images
    - logs: Print logs of the VM console

All of these can be called on `./tools/x86vm` or `./tools/aarch64vm`, but also on
`tools/testvm` to apply to both VMs.
"""


def cli_shorthand(arch: Arch):
    if arch == "x86_64":
        return "tools/x86vm"
    elif arch == "aarch64":
        return "tools/aarch64vm"
    else:
        raise Exception(f"Unknown architecture: {arch}")


def arch_or_all(arch: Optional[Arch]):
    return (arch,) if arch else testvm.ARCH_OPTIONS


ARCHS = testvm.ARCH_OPTIONS


@argh.arg("--arch-list", "--arch", nargs="*", type=str, default=ARCHS, choices=ARCHS)
def up(arch_list: Iterable[Arch] = [], reset: bool = False, wait: bool = False, timeout: int = 120):
    "Start the VM if it's not already running."
    for arch in arch_list:
        testvm.up(arch, reset, wait, timeout)


@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS)
def run(arch: Arch = "x86_64", reset: bool = False):
    "Run the VM in foreground for debugging purposes."
    if testvm.is_running(arch):
        raise Exception("VM is already running")
    testvm.build_if_needed(arch, reset)
    testvm.run_qemu(
        arch,
        testvm.rootfs_img_path(arch),
        background=False,
    )


@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS)
def shell(arch: Arch = "x86_64", timeout: int = 120):
    "Starts an interactive shell via SSH, will ensure the VM is running."
    testvm.up(arch, wait=True, timeout=timeout)
    testvm.ssh_exec(arch)


@argh.arg("--arch-list", "--arch", nargs="*", type=str, default=ARCHS, choices=ARCHS)
def stop(arch_list: Iterable[Arch] = []):
    "Gracefully stops the running VM."
    for arch in arch_list:
        if not testvm.is_running(arch):
            print(f"{arch} VM is not running")
            break
        console.print(f"Stopping {arch} VM")
        testvm.ssh_exec(arch, "sudo poweroff")


@argh.arg("--arch-list", "--arch", nargs="*", type=str, default=ARCHS, choices=ARCHS)
def kill(arch_list: Iterable[Arch] = []):
    "Kills the running VM with SIGKILL."
    for arch in arch_list:
        if not testvm.is_running(arch):
            console.print(f"{arch} VM is not running")
            break
        console.print(f"Killing {arch} VM process")
        testvm.kill_vm(arch)
        print()


@argh.arg("--arch-list", "--arch", nargs="*", type=str, default=ARCHS, choices=ARCHS)
def clean(arch_list: Iterable[Arch] = []):
    "Stops the VM or VMs and deletes all data."
    for arch in arch_list:
        if testvm.is_running(arch):
            kill(arch)
        if testvm.data_dir(arch).exists():
            console.print("Cleaning data directory", testvm.data_dir(arch))
            shutil.rmtree(testvm.data_dir(arch))
        print()


def vm_status(arch: Arch):
    def cli_tip(*args: str):
        return f"[green][bold]{cli_shorthand(arch)} {' '.join(args)}[/bold][/green]"

    vm = f"{arch} VM"
    port = f"[blue]{testvm.SSH_PORTS[arch]}[/blue]"

    state = testvm.state(arch)
    if state == VmState.REACHABLE:
        console.print(f"{vm} is [green]reachable[/green] on port {port}")
        console.print(f"Start a shell with {cli_tip('shell')}")
    elif state == VmState.STOPPED:
        console.print(f"{vm} is [red]stopped[/red]")
        console.print(f"Start the VM with {cli_tip('up')}")
    else:
        console.print(f"{vm} is running but [red]not reachable[/red] on port {port}")
        console.print(f"Recent logs:")
        logs(arch, 10, style="light_slate_grey")
        console.print(f"See all logs with {cli_tip('logs')}")


@argh.arg("--arch-list", "--arch", nargs="*", type=str, default=ARCHS, choices=ARCHS)
def status(arch_list: Iterable[Arch] = []):
    for arch in arch_list:
        vm_status(arch)
        print()


@argh.arg("--arch", required=True, choices=testvm.ARCH_OPTIONS)
def logs(arch: Arch = "x86_64", n: int = 0, style: Optional[str] = None):
    log_lines = testvm.log_path(arch).read_text().splitlines()
    if n > 0 and len(log_lines) > n:
        log_lines = log_lines[-n:]
    for line in log_lines:
        if style:
            console.print(
                strip_ansi_escape_sequences(line), style=style, markup=False, highlight=False
            )
        else:
            print(line)


if __name__ == "__main__":
    run_commands(up, run, shell, stop, kill, clean, status, logs, usage=USAGE, default_fn=status)
