# Copyright 2020 The StackStorm Authors.
# Copyright 2019 Extreme Networks, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import

import getpass
import json
import logging
import os

import requests
import six
from six.moves.configparser import ConfigParser
from six.moves import http_client

from st2client.base import BaseCLIApp
from st2client import config_parser
from st2client.models.auth import ApiKey
from st2client.commands import resource
from st2client.commands.noop import NoopCommand
from st2client.exceptions.operations import OperationFailureException
from st2client.formatters import table


LOG = logging.getLogger(__name__)


class TokenCreateCommand(resource.ResourceCommand):

    display_attributes = ["user", "token", "expiry"]

    def __init__(self, resource, *args, **kwargs):

        kwargs["has_token_opt"] = False

        super(TokenCreateCommand, self).__init__(
            resource,
            kwargs.pop("name", "create"),
            "Authenticate user and acquire access token.",
            *args,
            **kwargs,
        )

        self.parser.add_argument("username", help="Name of the user to authenticate.")

        self.parser.add_argument(
            "-p",
            "--password",
            dest="password",
            help="Password for the user. If password is not provided, "
            "it will be prompted for.",
        )
        self.parser.add_argument(
            "-l",
            "--ttl",
            type=int,
            dest="ttl",
            default=None,
            help="The life span of the token in seconds. "
            "Max TTL configured by the admin supersedes this.",
        )
        self.parser.add_argument(
            "-t",
            "--only-token",
            action="store_true",
            dest="only_token",
            default=False,
            help="On successful authentication, print only token to the " "console.",
        )

    def run(self, args, **kwargs):
        if not args.password:
            args.password = getpass.getpass()
        instance = self.resource(ttl=args.ttl) if args.ttl else self.resource()
        return self.manager.create(
            instance, auth=(args.username, args.password), **kwargs
        )

    def run_and_print(self, args, **kwargs):
        instance = self.run(args, **kwargs)

        if args.only_token:
            print(instance.token)
        else:
            self.print_output(
                instance,
                table.PropertyValueTable,
                attributes=self.display_attributes,
                json=args.json,
                yaml=args.yaml,
            )


class LoginCommand(resource.ResourceCommand):
    display_attributes = ["user", "token", "expiry"]

    def __init__(self, resource, *args, **kwargs):

        kwargs["has_token_opt"] = False

        super(LoginCommand, self).__init__(
            resource,
            kwargs.pop("name", "create"),
            "Authenticate user, acquire access token, and update CLI config directory",
            *args,
            **kwargs,
        )

        self.parser.add_argument("username", help="Name of the user to authenticate.")

        self.parser.add_argument(
            "-p",
            "--password",
            dest="password",
            help="Password for the user. If password is not provided, "
            "it will be prompted for.",
        )
        self.parser.add_argument(
            "-l",
            "--ttl",
            type=int,
            dest="ttl",
            default=None,
            help="The life span of the token in seconds. "
            "Max TTL configured by the admin supersedes this.",
        )
        self.parser.add_argument(
            "-w",
            "--write-password",
            action="store_true",
            default=False,
            dest="write_password",
            help="Write the password in plain text to the config file "
            "(default is to omit it)",
        )

    def run(self, args, **kwargs):

        if not args.password:
            args.password = getpass.getpass()
        instance = self.resource(ttl=args.ttl) if args.ttl else self.resource()

        cli = BaseCLIApp()

        # Determine path to config file
        try:
            config_file = cli._get_config_file_path(args)
        except ValueError:
            # config file not found in args or in env, defaulting
            config_file = config_parser.ST2_CONFIG_PATH

        # Retrieve token
        manager = self.manager.create(
            instance, auth=(args.username, args.password), **kwargs
        )
        cli._cache_auth_token(token_obj=manager)

        # Update existing configuration with new credentials
        config = ConfigParser()
        config.read(config_file)

        # Modify config (and optionally populate with password)
        if not config.has_section("credentials"):
            config.add_section("credentials")

        config.set("credentials", "username", args.username)
        if args.write_password:
            config.set("credentials", "password", args.password)
        else:
            # Remove any existing password from config
            config.remove_option("credentials", "password")

        config_existed = os.path.exists(config_file)
        with open(config_file, "w") as cfg_file_out:
            config.write(cfg_file_out)
        # If we created the config file, correct the permissions
        if not config_existed:
            os.chmod(config_file, 0o660)

        return manager

    def run_and_print(self, args, **kwargs):
        try:
            self.run(args, **kwargs)
        except Exception as e:
            if self.app.client.debug:
                raise

            raise Exception(
                "Failed to log in as %s: %s" % (args.username, six.text_type(e))
            )

        print("Logged in as %s" % (args.username))

        if not args.write_password:
            # Note: Client can't depend and import from common so we need to hard-code this
            # default value
            token_expire_hours = 24

            print("")
            print(
                "Note: You didn't use --write-password option so the password hasn't been "
                "stored in the client config and you will need to login again in %s hours when "
                "the auth token expires." % (token_expire_hours)
            )
            print(
                'As an alternative, you can run st2 login command with the "--write-password" '
                "flag, but keep it mind this will cause it to store the password in plain-text "
                "in the client config file (~/.st2/config)."
            )


class WhoamiCommand(resource.ResourceCommand):
    display_attributes = ["user", "token", "expiry"]

    def __init__(self, resource, *args, **kwargs):

        kwargs["has_token_opt"] = False

        super(WhoamiCommand, self).__init__(
            resource,
            kwargs.pop("name", "create"),
            "Display the currently authenticated user",
            *args,
            **kwargs,
        )

    def run(self, args, **kwargs):
        user_info = self.app.client.get_user_info(**kwargs)
        return user_info

    def run_and_print(self, args, **kwargs):
        try:
            user_info = self.run(args, **kwargs)
        except Exception as e:
            response = getattr(e, "response", None)
            status_code = getattr(response, "status_code", None)
            is_unathorized_error = status_code == http_client.UNAUTHORIZED

            if response and is_unathorized_error:
                print("Not authenticated")
            else:
                print("Unable to retrieve currently logged-in user")

            if self.app.client.debug:
                raise

            return

        print('Currently logged in as "%s".' % (user_info["username"]))
        print("")
        print("Authentication method: %s" % (user_info["authentication"]["method"]))

        if user_info["authentication"]["method"] == "authentication token":
            print(
                "Authentication token expire time: %s"
                % (user_info["authentication"]["token_expire"])
            )

        print("")
        print("RBAC:")
        print(" - Enabled: %s" % (user_info["rbac"]["enabled"]))
        print(" - Roles: %s" % (", ".join(user_info["rbac"]["roles"])))


class ApiKeyBranch(resource.ResourceBranch):
    def __init__(self, description, app, subparsers, parent_parser=None):
        super(ApiKeyBranch, self).__init__(
            ApiKey,
            description,
            app,
            subparsers,
            parent_parser=parent_parser,
            commands={
                "list": ApiKeyListCommand,
                "get": ApiKeyGetCommand,
                "create": ApiKeyCreateCommand,
                "update": NoopCommand,
                "delete": ApiKeyDeleteCommand,
            },
        )

        self.commands["enable"] = ApiKeyEnableCommand(
            self.resource, self.app, self.subparsers
        )
        self.commands["disable"] = ApiKeyDisableCommand(
            self.resource, self.app, self.subparsers
        )
        self.commands["load"] = ApiKeyLoadCommand(
            self.resource, self.app, self.subparsers
        )


class ApiKeyListCommand(resource.ResourceListCommand):
    detail_display_attributes = ["all"]
    display_attributes = ["id", "user", "metadata"]

    def __init__(self, resource, *args, **kwargs):
        super(ApiKeyListCommand, self).__init__(resource, *args, **kwargs)

        self.parser.add_argument(
            "-u",
            "--user",
            type=str,
            help="Only return ApiKeys belonging to the provided user",
        )
        self.parser.add_argument(
            "-d", "--detail", action="store_true", help="Full list of attributes."
        )
        self.parser.add_argument(
            "--show-secrets", action="store_true", help="Full list of attributes."
        )

    @resource.add_auth_token_to_kwargs_from_cli
    def run(self, args, **kwargs):
        filters = {}
        filters["user"] = args.user
        filters.update(**kwargs)
        # show_secrets is not a filter but a query param. There is some special
        # handling for filters in the get method which reuqires this odd hack.
        if args.show_secrets:
            params = filters.get("params", {})
            params["show_secrets"] = True
            filters["params"] = params
        return self.manager.get_all(**filters)

    def run_and_print(self, args, **kwargs):
        instances = self.run(args, **kwargs)
        attr = self.detail_display_attributes if args.detail else args.attr
        self.print_output(
            instances,
            table.MultiColumnTable,
            attributes=attr,
            widths=args.width,
            json=args.json,
            yaml=args.yaml,
        )


class ApiKeyGetCommand(resource.ResourceGetCommand):
    display_attributes = ["all"]
    attribute_display_order = ["id", "user", "metadata"]

    pk_argument_name = "key_or_id"  # name of the attribute which stores resource PK


class ApiKeyCreateCommand(resource.ResourceCommand):
    def __init__(self, resource, *args, **kwargs):
        super(ApiKeyCreateCommand, self).__init__(
            resource,
            "create",
            "Create a new %s." % resource.get_display_name().lower(),
            *args,
            **kwargs,
        )

        self.parser.add_argument(
            "-u",
            "--user",
            type=str,
            help="User for which to create API Keys.",
            default="",
        )
        self.parser.add_argument(
            "-m",
            "--metadata",
            type=json.loads,
            help="Optional metadata to associate with the API Keys.",
            default={},
        )
        self.parser.add_argument(
            "-k",
            "--only-key",
            action="store_true",
            dest="only_key",
            default=False,
            help="Only print API Key to the console on creation.",
        )

    @resource.add_auth_token_to_kwargs_from_cli
    def run(self, args, **kwargs):
        data = {}
        if args.user:
            data["user"] = args.user
        if args.metadata:
            data["metadata"] = args.metadata
        instance = self.resource.deserialize(data)
        return self.manager.create(instance, **kwargs)

    def run_and_print(self, args, **kwargs):
        try:
            instance = self.run(args, **kwargs)
            if not instance:
                raise Exception("Server did not create instance.")
        except Exception as e:
            message = six.text_type(e)
            print("ERROR: %s" % (message))
            raise OperationFailureException(message)
        if args.only_key:
            print(instance.key)
        else:
            self.print_output(
                instance,
                table.PropertyValueTable,
                attributes=["all"],
                json=args.json,
                yaml=args.yaml,
            )


class ApiKeyLoadCommand(resource.ResourceCommand):
    def __init__(self, resource, *args, **kwargs):
        super(ApiKeyLoadCommand, self).__init__(
            resource,
            "load",
            "Load %s from a file." % resource.get_display_name().lower(),
            *args,
            **kwargs,
        )

        self.parser.add_argument(
            "file",
            help=(
                "JSON/YAML file containing the %s(s) to load."
                % resource.get_display_name().lower()
            ),
            default="",
        )

        self.parser.add_argument(
            "-w",
            "--width",
            nargs="+",
            type=int,
            default=None,
            help=("Set the width of columns in output."),
        )

    @resource.add_auth_token_to_kwargs_from_cli
    def run(self, args, **kwargs):
        resources = resource.load_meta_file(args.file)
        if not resources:
            print(
                "No %s found in %s."
                % (self.resource.get_display_name().lower(), args.file)
            )
            return None
        if not isinstance(resources, list):
            resources = [resources]
        instances = []
        for res in resources:
            # pick only the meaningful properties.
            data = {
                "user": res["user"],  # required
                "key_hash": res["key_hash"],  # required
                "metadata": res.get("metadata", {}),
                "enabled": res.get("enabled", False),
            }

            if "id" in res:
                data["id"] = res["id"]

            instance = self.resource.deserialize(data)

            try:
                result = self.manager.update(instance, **kwargs)
            except requests.exceptions.HTTPError as e:
                if e.response.status_code == http_client.NOT_FOUND:
                    instance = self.resource.deserialize(data)
                    # Key doesn't exist yet, create it instead
                    result = self.manager.create(instance, **kwargs)
                else:
                    raise e

            instances.append(result)
        return instances

    def run_and_print(self, args, **kwargs):
        instances = self.run(args, **kwargs)
        if instances:
            self.print_output(
                instances,
                table.MultiColumnTable,
                attributes=ApiKeyListCommand.display_attributes,
                widths=args.width,
                json=args.json,
                yaml=args.yaml,
            )


class ApiKeyDeleteCommand(resource.ResourceDeleteCommand):
    pk_argument_name = "key_or_id"  # name of the attribute which stores resource PK


class ApiKeyEnableCommand(resource.ResourceEnableCommand):
    pk_argument_name = "key_or_id"  # name of the attribute which stores resource PK


class ApiKeyDisableCommand(resource.ResourceDisableCommand):
    pk_argument_name = "key_or_id"  # name of the attribute which stores resource PK
