#!python

import argparse
import subprocess
import shlex
import yaml
import sys
import time
import os
import threading
from typing import List, Literal


class TermColors:
    HEADER = '\n\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


def cprint(msg, style=TermColors.OKBLUE):
    if style == True:
        style = TermColors.OKGREEN
    elif style == False:
        style = TermColors.FAIL
    print(style + str(msg) + TermColors.ENDC)


# This should be the same for every smurf-srv
if 'SMURF_CONFIG_DIR' in os.environ:
    cfg_dir = os.environ['SMURF_CONFIG_DIR']
elif 'OCS_CONFIG_DIR' in os.environ:
    cfg_dir = os.environ['OCS_CONFIG_DIR']
    style = TermColors.WARNING
    cprint("SMURF_CONFIG_DIR not found in environ...", style=TermColors.WARNING)
    cprint( f"Using OCS_CONFIG_DIR instead: {cfg_dir}...", style=TermColors.WARNING )
else:
    raise ValueError(
        "SMURF_CONFIG_DIR or OCS_CONFIG_DIR must be set in the environment"
    )

cwd=cfg_dir
sys_config_file = os.path.join(cfg_dir, 'sys_config.yml')

with open(sys_config_file, 'r') as stream:
    sys_config = yaml.safe_load(stream)

use_hostmanager: bool = sys_config.get('use_hostmanager', False)
docker_compose_cmd: str = sys_config.get('docker_compose_command', 'docker compose')

def get_pysmurf_controller_docker_service(slot: int) -> str:
    """
    Returns the pysmurf-controller docker service name to use for a given slot.
    Defaults to "ocs-pysmurf-s<slot>" if not specified in the sys_config.
    """
    slot_cfg = sys_config['slots'][f'SLOT[{slot}]']
    return slot_cfg.get("pysmurf_controller_docker_service", f"ocs-pysmurf-s{slot}")


def get_slot_ip(slot):
    if 'switch_ip' not in sys_config:
        return f"10.0.{sys_config['crate_id']}.{slot + 100}"
    ip_list = sys_config['switch_ip'].split('.')
    res =  '.'.join(ip_list[:-1] + [str(slot + 100)])
    print(res)
    return res

########################################################################
# Various utility functions
########################################################################
def get_docker_services():
    """
    Returns a list of docker services that are available from the docker-compose
    file in the ocs config directory.
    """
    cmd = 'docker compose config --services'
    res = subprocess.run(shlex.split(cmd), cwd=cwd, stdout=subprocess.PIPE)
    return res.stdout.decode().split()

def util_run(cmd, args=[], name=None, rm=True, **run_kwargs):
    """
    Runs a command using subproces.run within the sodetlib util docker.

    Args
    ------
    cmd : string
        Command to run
    args : List[string]
        List of arguments to pass to the command
    name : string, optional
        Name of the docker container. If none is specified this will not be
        set and docker-compose will choose the default.
    rm : bool
        If True, will remove the container when the command has finished.
    run_kwargs : Additional keyword arguments
        Any additional kwargs specified will be passed directly to the
        subprocess.run function. See the subprocess docs for allowed kwargs:
        https://docs.python.org/3/library/subprocess.html#subprocess.run
    """
    cmd  = f'{docker_compose_cmd} run --entrypoint={cmd} '
    if name is not None:
        cmd += f'--name={name} '
    if rm:
        cmd += '--rm '
    cmd += f'smurf-util {" ".join(args)}'
    return subprocess.run(shlex.split(cmd), cwd=cwd, **run_kwargs)


def check_epics_connection(epics_server, retry=False):
    """
        Checks if we can connect to a specific epics server. 

        Args:
            epics_server (string): 
                epics server to connect to
            retry (bool):
                If true, will continuously check until a connection has been 
                established.
    """
    if retry:
        print(f"Waiting for epics connection to {epics_server}", end='', flush=True)
        while True:
            x = util_run(
                'caget', args=[f'{epics_server}:AMCc:enable'],
                stdout=subprocess.PIPE, stderr=subprocess.STDOUT
            )
            if "True" in x.stdout.decode():
                break
            print('.', end='', flush=True)

        print("\nConnected!")
        return True
    else:
        x = util_run(
            'caget', args=[f'{epics_server}:AMCc:enable'],
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT
        )
        return "True" in x.stdout.decode()


def get_running_dockers(get_all=True):
    """
        Gets all currently running dockers. 

        Returns: 
            A list of tuples of the (cid, image, name)
    """
    cmd = 'docker ps '
    if get_all:
        cmd += '-a '
    x = subprocess.run(cmd.split(), stdout=subprocess.PIPE)
    containers=[]
    for line in x.stdout.decode().split('\n')[1:]:
        if not line:
            continue

        cid, image, *_, cname = line.split()
        containers.append((cid, image, cname))

    return containers


def kill_bad_dockers(slots, kill_monitor=False, names=[], images=[]):
    """
        Kills relevant dockers for a given set of slots.

        Args:
            slots (list of ints):
                list of slots which we should kill related dockers
            kill_monitor (bool):
                If true, will also kill the pysmurf_monitor
            names (list of strings):
                List of additional docker names we should kill
            images (list of strings):
    """

    print(f"Killing bad dockers for slots {slots}")
    bad_names = names

    for slot in slots:
        # conflicting names created by jackhammer
        bad_names.append(f'smurf-streamer-s{slot}')
        bad_names.append(f'pysmurf-client-s{slot}')

        # conflicting names created by shawnhammer
        bad_names.append(f"pysmurf_s{slot}")
        bad_names.append(f"smurf_server_s{slot}")
        bad_names.append(f"pysmurf-ipython-slot{slot}")

        if not use_hostmanager:
            bad_names.append(f'ocs-pysmurf-s{slot}')
            bad_names.append(get_pysmurf_controller_docker_service(slot))

    if kill_monitor:
        bad_names.append('ocs-pysmurf-monitor')

    bad_images = images

    containers = get_running_dockers()

    for cid, full_image, name in containers:
        # This will usually get the real image name. 
        # We want 'pysmurf-server' from 'tidair/pysmurf-server:R0.0.0'
        image = full_image.split('/')[-1].split(':')[0]

        if (name in bad_names) or (image in bad_images):
            print(f"Stopping docker {name} ({image})")
            subprocess.run(f'docker stop {cid}'.split())
            subprocess.run(f'docker rm {cid}'.split())

def dump_docker_logs(slots, dump_rogue_tree=False):
    """
        Dumps all docker logs and the rogue state for the specified
        slots to text files.

        Args:
            slots (list of ints):
                list of slots which we should dump the rogue state of.
    """
    dump_dir = os.path.join(
        '/data/logs',
        str(time.time())[:5],
        str(int(time.time()))
    )

    if not os.path.exists(dump_dir):
        os.makedirs(dump_dir)

    cprint(f"Dumping docker logs to {dump_dir}", style=TermColors.HEADER)
    docker_state_file = os.path.join(dump_dir, 'docker_state.log')
    with open(docker_state_file, 'w') as f:
        print(f"Saving 'docker ps' to {docker_state_file}")
        subprocess.run('docker ps -a'.split(), stdout=f, stderr=f)

    for cid, image, name in get_running_dockers(get_all=True):
        log_file = os.path.join(dump_dir, f'{name}.log')
        with open(log_file, 'w') as f:
            print(f"Saving {name} logs to {log_file}")
            subprocess.run(f'docker logs {cid}'.split(), stdout=f, stderr=f)

    if dump_rogue_tree:
        dump_script = '/sodetlib/scripts/dump_rogue_state.py'
        for slot in slots:
            if check_epics_connection(f'smurf_server_s{slot}', retry=False):
                out_file = os.path.join(dump_dir, f'rogue_state_s{slot}.yml')
                cprint(f"Dumping s{slot} state to {out_file}", style=TermColors.HEADER)
                util_run('python3', args=[dump_script, str(slot), out_file],
                         name=f'rogue_dump_s{slot}')
            else:
                print(f"Could not connect to epics for slot {slot}")

def run_on_shelf_manager(cmd_str):
    """ Runs a command on the shelf manager. Takes in the command as a string"""
    cmd = ['ssh', f'root@{sys_config["shelf_manager"]}', f'{cmd_str}']
    print(cmd)
    subprocess.run(cmd)

def enter_pysmurf(slot, agg=False):
    """
        Enters into a pysmurf ipython session.

        Args: 
            slot (int): slot to enter into
            agg (bool): if true, use the matplotlib agg backend.
    """
    print(f"Entering pysmurf on slot {slot}", flush=True)
    name = f'pysmurf-ipython-slot{slot}'
    container_names = [c[2] for c in get_running_dockers()]
    active_container_names = [c[2] for c in get_running_dockers(get_all=False)]

    if name in active_container_names:
        print(f"Container {name} is already running... Attaching to it now")
        cmd = shlex.split(f'docker attach {name}')
        subprocess.run(cmd, cwd=cwd)
    else:
        if name in container_names:
            print(f"Removing stopped docker {name}")
            subprocess.run(f'docker rm {name}'.split())

        util_run(
            'python3',
            args=f"/sodetlib/scripts/start_pysmurf_ipython.py -N {slot}".split(),
            name=name, rm=False
        )

def write_docker_env():
    docker_env = sys_config.get('docker_env')
    with open(os.path.join(cfg_dir, ".env"), "w") as env_file:
        for k,v in docker_env.items():
            env_file.write(f'{k}={v}\n')

def start_services(services, write_env=False):
    """
        Starts docker-compose services

        Args:
            services (list or str): docker-compose services to restart
            write_env(bool):
                If true, will write sys_config['docker-env'] to the
                $SMURF_CONFIG_DIR/.env file. This is so the most recent
                environment variables can be used by standard docker-compose
                commands.
    """
    docker_env = sys_config.get('docker_env')

    # Writes docker_env to $SMURF_CONFIG_DIR/.env so docker-compose still works
    if write_env and docker_env is not None:
        write_docker_env()

    if isinstance(services, str):
        services = [services]

    cmd = f'{docker_compose_cmd} up -d'.split()
    cmd.extend(services)

    subprocess.run(cmd, cwd=cwd)

def controller_cmd(
    slots: List[int],
    action: Literal['up', 'down', 'logs']
) -> None:
    """
    Brings pysmurf-controllers up or down for specified slots.
    """
    if use_hostmanager:
        cprint(
            "sys_config['use_hostmanager'] is set to True, leaving controller dockers alone.",
            style=TermColors.WARNING
        )
        return

    print(f"Bringing controllers {action} for slots {slots}...")
    services = []
    for slot in slots:
        services.append(get_pysmurf_controller_docker_service(slot))
    
    if action == 'up':
        cmd = f'{docker_compose_cmd} up -d'.split()
    elif action == 'down':
        cmd = f'{docker_compose_cmd} stop'.split()
    elif action == 'logs':
        cmd = 'docker logs -f'.split()
    else:
        raise ValueError(f"action {action} not recognized. Must be up or down.")

    cmd.extend(services)
    subprocess.run(cmd, cwd=cwd)


def controller_cmd_func(args) -> None:
    """
    Entrypoint for the `jackhammer controller` command
    """
    available_slots = sys_config['slot_order']
    if args.slots is None or not args.slots:
        slots = available_slots
    else:
        for slot in args.slots:
            if slot not in available_slots:
                raise ValueError(
                    f"Slot {slot} is not listed in the available slots: "
                    f"{available_slots}."
                )
        slots = args.slots
    controller_cmd(slots, args.action)


########################################################################
# jackhammer subcommand entrypoints
########################################################################

# Entrypoint for jackhammer pysmurf
def pysmurf_func(args):
    available_slots = sys_config['slot_order']

    if args.slot is not None:
        if args.slot not in available_slots:
            print(f"Slot {args.slot} is not listed in the available smurf_slots in {sys_config_file}")
            raise ValueError
        slot = args.slot
    else:
        slot = available_slots[0]

    enter_pysmurf(slot, agg=args.agg)


# Entrypoint for jackhammer hammer
def hammer_func(args):
    # here we go....
    if not args.slots:
        slots = sys_config['slot_order']
    else:
        slots = args.slots
        for s in slots:
            if s not in sys_config['slot_order']:
                raise ValueError(
                    f"Slot {s} is not valid for this system! Can only use "
                    f"slots in: {sys_config['slot_order']}")

    reboot = not (args.no_reboot)
    all_slots = len(slots) == len(sys_config['slot_order'])

    reboot_str = "hard" if reboot else "soft"
    cmd = input(f"You are {reboot_str}-resetting slots {slots}. "
                "Are you sure (y/n)? ")
    if cmd.lower() not in ["y", "yes"]:
        return

    # dump docker logs for debugging.
    if not args.no_dump:
        dump_docker_logs(slots)

    cprint(f"Hammering for slots {slots}", True)

    cprint("Killing conflicting dockers", style=TermColors.HEADER)
    kill_bad_dockers(slots, kill_monitor=False)

    if all_slots:
        cprint("Restarting smurf-util", style=TermColors.HEADER)
        # Restarts smurf-util to clean up any running processes
        subprocess.run('docker stop smurf-util'.split(), cwd=cwd)
        subprocess.run('docker rm smurf-util'.split(), cwd=cwd)
        start_services('smurf-util', write_env=True)

        # Sets fan levels on crate
        cprint("Setting fan levels", style=TermColors.HEADER)
        setup_fans()

    if reboot:
        cprint(f"Rebooting slots: {slots}", style=TermColors.HEADER)
        deactivate_commands = []
        activate_commands = []
        for slot in slots:
            deactivate_commands.append(f'clia deactivate board {slot}')
            activate_commands.append(f'clia activate board {slot}')

        print(f"Deactivating carriers: {slots}")
        run_on_shelf_manager('; '.join(deactivate_commands))

        print("Waiting 5 seconds before re-activating carriers")
        time.sleep(5)

        print(f"Activating carriers: {slots}")
        run_on_shelf_manager('; '.join(activate_commands))

        print("Waiting for carriers to come back online (this takes a bit)")
        for slot in slots:
            # ip = f'10.0.{sys_config["crate_id"]}.{slot + 100}'
            ip = get_slot_ip(slot)
            subprocess.run(['ping_carrier', ip])
    else:
        print("Skipping reboot process")

    #Brings up all smurf-streamer dockers
    cprint('Bringing up smurf dockers', style=TermColors.HEADER)
    if all_slots:
        services = ['smurf-jupyter', 'smurf-util']
        if not use_hostmanager:
            services.append('ocs-pysmurf-monitor')

        start_sync_dockers()
    else:
        services = []

    for slot in slots:
        services.append(f'smurf-streamer-s{slot}')

    start_services(services)
    start_sync_dockers()
    controller_cmd(slots, 'up')

    # Waits for streamer-dockers to start
    print("Waiting for server dockers to connect. This might take a few minutes...")
    for slot in slots:
        epics_server = f'smurf_server_s{slot}'
        check_epics_connection(epics_server, retry=True)

    if reboot and not args.skip_setup:
        cprint("Configuring pysmurf", style=TermColors.HEADER)
        setup_smurfs(slots)
        print("Finished configuring pysmurf!")

    # Enters into an ipython notebook for the first specified slot   
    cprint(f"Entering pysmurf slot {slots[0]}", style=TermColors.HEADER)
    enter_pysmurf(slots[0], agg=args.agg)

def setup_smurfs(slots):
    """
    Runs S.setup on one or more slots in parallel

    Args
    -----
    slots : list
        List of slots to run on
    """
    threads = []
    for s in slots:
        print(f"Configuring pysmurf on slot {s}...")
        kw = {
            'args': ('python3',),
            'kwargs': {'args': f'/sodetlib/scripts/setup_pysmurf.py -N {s}'.split()}

        }
        th = threading.Thread(target=util_run, **kw )
        th.start()
        threads.append(th)

    for th in threads:
        th.join()

def start_sync_dockers():
    """
    Begins suprsync docker services (by choosing all docker services that have
    "sync" in their name).
    """
    if use_hostmanager:
        cprint(
            "sys_config['use_hostmanager'] is set to True, leaving sync dockers alone.",
            style=TermColors.WARNING
        )
        return

    services = get_docker_services()
    start_services([s for s in services if 'sync' in s])

def start_sync_func(args):
    """Entrypoint for ``jackhammer start-sync``"""
    start_sync_dockers()


# Entrypoint for jackhammer setup
def setup_func(args):
    if not args.slots:
        slots = sys_config['slot_order']
    else:
        slots = args.slots
    setup_smurfs(slots)


# Entrypoint for jackhammer logs
def log_func(args):
    cmd = shlex.split(f'{docker_compose_cmd} logs -f')
    cmd.extend(args.log_args)
    subprocess.run(cmd, cwd=cwd)


# Entrypoint for jackhamer util
def util_func(args):
    util_run('bash', rm=(not args.detached))


def gui_func(args):
    if args.port is not None:
        server_port = args.port
    else:
        available_slots = sys_config['slot_order']
        if args.slot is not None:
            if args.slot not in available_slots:
                print(f"Slot {args.slot} is not listed in the available "
                      f"smurf_slots in {sys_config_file}")
                raise ValueError
            slot = args.slot
        else:
            slot = available_slots[0]
        server_port = 9000 + 2*slot

    sodetlib_root = os.environ.get('SODETLIB_ROOT', '/home/cryo/sodetlib')
    script_path = os.path.join(sodetlib_root, 'hammers', 'run_gui.sh')
    subprocess.run(f'sh {script_path} {server_port}'.split())


# Entrypoint for jackhammer dump
def dump_func(args):
    if not args.slots:
        slots = sys_config['slot_order']
    else:
        slots = args.slots
        for s in slots:
            if s not in sys_config['slot_order']:
                raise ValueError(
                    f"Slot {s} is not valid for this system! Can only use "
                    f"slots in: {sys_config['slot_order']}")

    dump_docker_logs(slots, dump_rogue_tree=args.dump_rogue)


def deactivate_func(args):
    cmds = [
        f'clia deactivate board {s}' for s in args.slots
    ]
    run_on_shelf_manager('; '.join(cmds))

def activate_func(args):
    cmds = [
        f'clia activate board {s}' for s in args.slots
    ]
    run_on_shelf_manager('; '.join(cmds))


# Entrypoint for jackhammer write-env
def write_env_func(args):
    write_docker_env()

def setup_fans():
    if 'smurf_fans' not in sys_config:
        # Old way of setting up fans
        cprint(
            "'smurf_fans' key not found in sys config. Setting fan levels using "
            "'max_fan_level', however this allows fan speed to change over "
            "time."
        )

        min_fan_level = sys_config.get('min_fan_level')
        init_fan_level = sys_config.get('init_fan_level')

        if (min_fan_level is None) or (init_fan_level is None):
            # Run with old way, using max_fan_level to set both minfanlevel and
            # init
            min_fan_level = sys_config['max_fan_level']
            init_fan_level = sys_config['max_fan_level']
            cprint("Using max_fan_level sys_config value to set minfanlevel and "
                   "initial level. If you've switched to a Comtel crate, please "
                   "change your sys_config to use the `min_fan_level` and "
                   "`init_fan_level` variables", style=TermColors.WARNING)

        cmd = f'clia minfanlevel {min_fan_level}; '
        cmd += f'clia setfanlevel all {init_fan_level}'
        print(f"Setting crate fans to {init_fan_level}...")
        run_on_shelf_manager(cmd)
        return

    fans = sys_config['smurf_fans']
    cmd = ''
    for addr in fans['addresses']:
        a, b = addr

        cmd += f"clia setfanpolicy {a} {b} {fans['policy']}; "
    cmd += f"clia setfanlevel all {fans['speed']};"
    run_on_shelf_manager(cmd)

# Entrypoint for jackhammer setup-fans
def setup_fans_func(args):
    setup_fans()

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    subparsers = parser.add_subparsers()

    ########### Jackhammer pysmurf parser ############
    pysmurf_parser = subparsers.add_parser('pysmurf',
        help="Drops user into ipython session with pysmurf already initialized"
    )
    pysmurf_parser.set_defaults(func=pysmurf_func)
    pysmurf_parser.add_argument('slot', type=int, nargs="?",
        help="Slot number of pysmurf to connect to. If left blank it will "
             "try to figure it out from the sys_config file."
    )
    pysmurf_parser.add_argument('--config-file', '-c',
        help="Pysmurf config file. Defaults to what is specified in sys config file."
    )
    pysmurf_parser.add_argument('--setup', '-s', action='store_true',
        help="If specified, pysmurf will start with setup=True."
    )
    pysmurf_parser.add_argument('--agg', action='store_true',
        help="If specified, matplotlib will use the agg backend. This can be "
             "helpful if you are connecting over ssh without a display."
    )

    ########### Jackhammer hammer parser ############
    hammer_parser = subparsers.add_parser("hammer")
    hammer_parser.set_defaults(func=hammer_func)
    hammer_parser.add_argument('slots', nargs='*', type=int,
                               help="Specifies the slots to hammer")
    hammer_parser.add_argument('--no-reboot', '--soft', '-n',
                               action='store_true',
                               help="If True, will not reboot slots.")
    hammer_parser.add_argument('--no-dump', action='store_true',
                               help="If True, will not dump logs.")
    hammer_parser.add_argument('--agg', action='store_true')
    hammer_parser.add_argument('--skip-setup', action='store_true',
                               help="Skip pysmurf setup functions. If `--soft` is set, defaults to True.")
    hammer_parser.add_argument('--dump-rogue', action='store_true',
                               help="If true, will attempt to connect to pysmurf smurf and dump the rogue tree")

    setup_parser = subparsers.add_parser("setup")
    setup_parser.set_defaults(func=setup_func)
    setup_parser.add_argument('slots', nargs='*', type=int,
                               help="Specifies the slots to setup")


    ########### Jackhammer logs parser ############
    log_parser = subparsers.add_parser('logs')
    log_parser.add_argument('log_args', nargs="*", type=str,
                            help="args passed to docker compose logs")
    log_parser.set_defaults(func=log_func)

    ########### Jackhammer util parser ############
    util_parser = subparsers.add_parser('util')
    util_parser.add_argument('--detached', '-d', action='store_true')
    util_parser.set_defaults(func=util_func)

    ########### Jackhammer gui parser ###########
    gui_parser = subparsers.add_parser('gui')
    gui_parser.add_argument('slot', nargs='?', type=int)
    gui_parser.add_argument('--port', '-p', type=int, help='gui server port')
    gui_parser.set_defaults(func=gui_func)

    ########### Jackhammer dump parser ###########
    dump_parser = subparsers.add_parser('dump', help='Dumps all docker logs')
    dump_parser.add_argument('slots', nargs='*', type=int,
                             help='Specifies the slots to dump rogue states')
    dump_parser.add_argument('--dump-rogue', action='store_true',
                             help="If true, will attempt to connect to pysmurf smurf and dump the rogue tree")
    dump_parser.set_defaults(func=dump_func)

    ########### Jackhammer deactivate parser ###########
    deactivate_parser = subparsers.add_parser('deactivate', help='Deactivates slots')
    deactivate_parser.add_argument('slots', nargs='+', type=int,
                             help='Specifies the slots to deactivate')
    deactivate_parser.set_defaults(func=deactivate_func)

    ########### Jackhammer activate parser ###########
    activate_parser = subparsers.add_parser('activate', help='Activates slots')
    activate_parser.add_argument('slots', nargs='+', type=int,
                             help='Specifies the slots to activate')
    activate_parser.set_defaults(func=activate_func)

    ########### Jackhammer write-env parser ###########
    write_env_parser = subparsers.add_parser('write-env', help='writes docker-env to .env file')
    write_env_parser.set_defaults(func=write_env_func)

    ########### Jackhammer start-sync parser ###########
    start_sync_parser = subparsers.add_parser('start-sync')
    start_sync_parser.set_defaults(func=start_sync_func)

    ########### Jackhammer setup-fans parser ###########
    start_sync_parser = subparsers.add_parser(
        'setup-fans', 
        help="Sets fan speeds and policy on crate based on sys-config"
    )
    start_sync_parser.set_defaults(func=setup_fans_func)

    ########### Jackhammer setup-fans parser ###########
    controller_cmd_parser = subparsers.add_parser(
        'controller', 
        help="Sets fan speeds and policy on crate based on sys-config"
    )
    controller_cmd_parser.add_argument('action', choices=['up', 'down', 'logs'])
    controller_cmd_parser.add_argument('slots', type=int, nargs='*', default=None)
    controller_cmd_parser.set_defaults(func=controller_cmd_func)


    args = parser.parse_args()
    if hasattr(args, 'func'):
        args.func(args)
    else:
        parser.print_help()
