# Copyright 2022 The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Debusine command line interface."""

import argparse
import signal
import sys

import yaml
from yaml import YAMLError

from debusine.client import exceptions
from debusine.client.config import ConfigHandler
from debusine.client.debusine import Debusine
from debusine.client.exceptions import DebusineError
from debusine.client.models import WorkRequest


class Cli:
    """
    Entry point for the command line debusine client.

    Usage:
        main = Cli(sys.argv[1:]) # [1:] to exclude the script name
        main.execute()
    """

    def __init__(self, argv, stdout=sys.stdout, stderr=sys.stderr):
        """Initialize object."""
        self._argv = argv
        self._stdout = stdout
        self._stderr = stderr

    @staticmethod
    def _exit(signum, frame):  # noqa: U100
        raise SystemExit(0)

    def _parse_args(self):
        """Parse argv and store results in self.args."""
        parser = argparse.ArgumentParser(
            prog='debusine',
            description='Interacts with a debusine server.',
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        )

        parser.add_argument(
            '--server',
            help='Set server to be used (use configuration file default '
            'it not specified)',
        )

        parser.add_argument(
            '--config-file',
            default=ConfigHandler.DEFAULT_CONFIG_FILE_PATH,
            help='Config file path',
        )

        subparsers = parser.add_subparsers(
            help='Sub command', dest='sub-command', required=True
        )
        work_request_status = subparsers.add_parser(
            'work-request-status',
            help='Print the status of a work request',
        )
        work_request_status.add_argument(
            'work_request_id',
            type=int,
            help='Work request id to show the status',
        )

        create_work_request = subparsers.add_parser(
            'create-work-request',
            help='Create a work request and schedule the execution. '
            'Work request is read from stdin in YAML format',
        )
        create_work_request.add_argument(
            'task_name', type=str, help='Task name for the work request'
        )
        self.args = parser.parse_args(self._argv)

    def _build_debusine_object(self):
        """Return the debusine object matching the command line parameters."""
        configuration = ConfigHandler(
            server_name=self.args.server,
            config_file_path=self.args.config_file,
        )

        server_configuration = configuration.server_configuration()
        return Debusine(
            api_url=server_configuration['url'],
            api_token=server_configuration['token'],
        )

    def execute(self):
        """Execute the command requested by the user."""
        signal.signal(signal.SIGINT, self._exit)
        signal.signal(signal.SIGTERM, self._exit)

        self._parse_args()
        debusine = self._build_debusine_object()

        sub_command = getattr(self.args, 'sub-command')

        if sub_command == 'work-request-status':
            self._print_work_request_status(debusine, self.args.work_request_id)
        elif sub_command == 'create-work-request':
            data = sys.stdin.read()
            self._create_work_request(debusine, self.args.task_name, data)
        else:  # pragma: no cover
            pass  # Can never be reached

    def _print_yaml(self, dictionary):
        """Print dictionary to stdout as yaml."""
        output = yaml.safe_dump(dictionary, sort_keys=False)
        self._stdout.write(output)

    def _fail(self, error, *, summary=None):
        print(error, file=self._stderr)
        if summary is not None:
            print(summary, file=self._stderr)
        raise SystemExit(3)

    def _print_work_request_status(self, debusine, work_request_id):
        """Print the task status for work_request_id."""
        result = self._api_call_or_fail(
            debusine.work_request_status, work_request_id
        )
        self._print_yaml(result.dict())

    def _create_work_request(self, debusine, task_name, task_data):
        work_request = WorkRequest()

        work_request.task_name = task_name

        try:
            work_request.task_data = yaml.safe_load(task_data)
        except YAMLError as err:
            self._fail(
                f"Error parsing YAML: {err}",
                summary="Work request not created. Fix the task data YAML.",
            )
        try:
            work_request_created = self._api_call_or_fail(
                debusine.work_request_create, work_request
            )
        except DebusineError as err:
            output = {"result": "failure", "error": err.asdict()}
        else:
            output = {
                'result': 'success',
                'message': f'Work request registered on {debusine.api_url} '
                f'with id {work_request_created.id}.',
                'work_request_id': work_request_created.id,
            }
        self._print_yaml(output)

    def _api_call_or_fail(self, method, *args, **kwargs):
        """
        Call method with args and kwargs.

        :raises: exceptions.NotFoundError: server returned 404.
        :raises: UnexpectedResponseError: e.g. invalid JSON.
        :raises: ClientConnectionError: e.g. cannot connect to the server.
        :raises: DebusineError (via method() call) when debusine server
          reports an error.
        """
        try:
            result = method(*args, **kwargs)
        except exceptions.NotFoundError as exc:
            self._fail(exc)
        except exceptions.UnexpectedResponseError as exc:
            self._fail(exc)
        except exceptions.ClientForbiddenError as server_error:
            self._fail(f'Server rejected connection: {server_error}')
        except exceptions.ClientConnectionError as client_error:
            self._fail(f'Error connecting to debusine: {client_error}')

        return result
