# Copyright 2022 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Implements commands for serving a TUF repository."""

import argparse
import contextlib
import json
import logging
from typing import Iterator, Optional

import monitors

from common import run_ffx_command, REPO_ALIAS

_REPO_NAME = 'chromium-test-package-server'


def _stop_serving(repo_name: str, target: Optional[str]) -> None:
    """Stop serving a repository."""

    # Attempt to clean up.
    with monitors.time_consumption('repository', 'deregister'):
        run_ffx_command(
            cmd=['target', 'repository', 'deregister', '-r', repo_name],
            target_id=target,
            check=False,
        )

    with monitors.time_consumption('repository', 'stop'):
        run_ffx_command(
            cmd=['repository', 'server', 'stop', repo_name], check=False
        )


def _start_serving(
    repo_dir: str, repo_name: str, target: Optional[str]
) -> None:
    """Start serving a repository to a target device.

    Args:
        repo_dir: directory the repository is served from.
        repo_name: repository name.
        target: Fuchsia device the repository is served to.
    """

    cmd = [
        'repository',
        'server',
        'start',
        '--background',
        '--address',
        '[::]:0',
        '--repository',
        repo_name,
        '--repo-path',
        repo_dir,
        '--no-device',
    ]

    with monitors.time_consumption('repository', 'start'):
        start_cmd = run_ffx_command(cmd=cmd, check=False)

    logging.warning(
        'ffx repository server start returns %d: %s %s',
        start_cmd.returncode,
        start_cmd.stderr,
        start_cmd.stdout,
    )

    _assert_server_running(repo_name)

    cmd = [
        'target',
        'repository',
        'register',
        '-r',
        repo_name,
        '--alias',
        REPO_ALIAS,
    ]
    with monitors.time_consumption('repository', 'register'):
        run_ffx_command(cmd=cmd, target_id=target)


def _assert_server_running(repo_name: str) -> None:
    """Raises RuntimeError if the repository server is not running."""

    with monitors.time_consumption('repository', 'list'):
        list_cmd = run_ffx_command(
            cmd=[
                '--machine',
                'json',
                'repository',
                'server',
                'list',
                '--name',
                repo_name,
            ],
            check=False,
            capture_output=True,
        )
    try:
        response = json.loads(list_cmd.stdout.strip())
        if 'ok' in response and response['ok']['data']:
            if response['ok']['data'][0]['name'] != repo_name:
                raise RuntimeError(
                    'Repository server %s is not running. Output: %s stderr: %s'
                    % (repo_name, list_cmd.stdout, list_cmd.stderr)
                )
            return
    except json.decoder.JSONDecodeError as error:
        # Log the json parsing error, but don't raise an exception since it
        # does not have the full context of the error.
        logging.error(
            'Unexpected json string: %s, exception: %s, stderr: %s',
            list_cmd.stdout,
            error,
            list_cmd.stderr,
        )
    raise RuntimeError(
        'Repository server %s is not running. Output: %s stderr: %s'
        % (repo_name, list_cmd.stdout, list_cmd.stderr)
    )


def register_serve_args(arg_parser: argparse.ArgumentParser) -> None:
    """Register common arguments for repository serving."""

    serve_args = arg_parser.add_argument_group(
        'serve', 'repo serving arguments'
    )
    serve_args.add_argument(
        '--serve-repo',
        dest='repo',
        help='Directory the repository is served from.',
    )
    serve_args.add_argument(
        '--repo-name', default=_REPO_NAME, help='Name of the repository.'
    )


@contextlib.contextmanager
def serve_repository(args: argparse.Namespace) -> Iterator[None]:
    """Context manager for serving a repository."""
    _start_serving(args.repo, args.repo_name, args.target_id)
    try:
        yield None
    finally:
        _stop_serving(args.repo_name, args.target_id)
