# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

import json
import os
import shutil

from azure.cli.command_modules.botservice.converged_app import ConvergedApp
from azure.cli.command_modules.botservice.bot_json_formatter import BotJsonFormatter
from azure.cli.command_modules.botservice.bot_publish_prep import BotPublishPrep
from azure.cli.command_modules.botservice.bot_template_deployer import BotTemplateDeployer
from azure.cli.command_modules.botservice.kudu_client import KuduClient
from azure.cli.command_modules.botservice.web_app_operations import WebAppOperations
from azure.mgmt.botservice.models import (
    Bot,
    BotProperties,
    ConnectionSetting,
    ConnectionSettingProperties,
    ConnectionSettingParameter,
    Sku)

from knack.util import CLIError
from knack.log import get_logger

logger = get_logger(__name__)


def create(cmd, client, resource_group_name, resource_name, kind, description=None, display_name=None,
           endpoint=None, msa_app_id=None, password=None, tags=None, storageAccountName=None,
           location='Central US', sku_name='F0', appInsightsLocation='South Central US',
           language='Csharp', version='v3'):
    """Create a WebApp, Function, or Channels Registration Bot on Azure.

    This method is directly called via "bot create"

    :param cmd:
    :param client:
    :param resource_group_name:
    :param resource_name:
    :param kind:
    :param description:
    :param display_name:
    :param endpoint:
    :param msa_app_id:
    :param password:
    :param tags:
    :param storageAccountName:
    :param location:
    :param sku_name:
    :param appInsightsLocation:
    :param language:
    :param version:
    :return:
    """

    # If display name was not provided, just use the resource name
    display_name = display_name or resource_name

    # Kind parameter validation
    kind = kind.lower()

    registration_kind = 'registration'
    bot_kind = 'bot'
    webapp_kind = 'webapp'
    function_kind = 'function'

    # Mapping: registration is deprecated, we now use 'bot' kind for registration bots
    if kind == registration_kind:
        kind = bot_kind

    if kind not in (bot_kind, webapp_kind, function_kind):
        raise CLIError('Invalid Bot Parameter : kind. Valid kinds are \'registration\' for registration bots, '
                       '\'webapp\' for webapp bots and \'function\' for function bots. Run \'az bot create -h\' '
                       'for more information.')

    # If a Microsoft application id was not provided, provision one for the user
    if not msa_app_id:

        logger.info('Microsoft application id not passed as a parameter. Provisioning a new Microsoft application.')

        msa_app_id, password = ConvergedApp.provision(resource_name)
        logger.info('Microsoft application provisioning successful. Application Id: %s.', msa_app_id)

    logger.info('Creating Azure Bot Service.')

    # Registration bots: simply call ARM and create the bot
    if kind == bot_kind:

        logger.info('Detected kind %s, validating parameters for the specified kind.', kind)

        # Registration bot specific validation
        if not endpoint:
            raise CLIError('Endpoint is required for creating a registration bot.')
        if not msa_app_id:
            raise CLIError('Microsoft application id is required for creating a registration bot.')

        parameters = Bot(
            location='global',
            sku=Sku(name=sku_name),
            kind=kind,
            tags=tags,
            properties=BotProperties(
                display_name=display_name,
                description=description,
                endpoint=endpoint,
                msa_app_id=msa_app_id
            )
        )
        logger.info('Bot parameters client side validation successful.')
        logger.info('Creating bot.')

        return client.bots.create(
            resource_group_name=resource_group_name,
            resource_name=resource_name,
            parameters=parameters
        )
    # Web app and function bots require deploying custom ARM templates, we do that in a separate method
    else:
        logger.info('Detected kind %s, validating parameters for the specified kind.', kind)

        return BotTemplateDeployer.create_app(cmd, logger, client, resource_group_name, resource_name, description,
                                              kind, msa_app_id, password, storageAccountName, location, sku_name,
                                              appInsightsLocation, language, version)


def get_bot(cmd, client, resource_group_name, resource_name, bot_json=None):
    raw_bot_properties = client.bots.get(
        resource_group_name=resource_group_name,
        resource_name=resource_name
    )
    if bot_json:
        return BotJsonFormatter.create_bot_json(cmd, client, resource_group_name, resource_name,
                                                raw_bot_properties=raw_bot_properties)

    return raw_bot_properties


def create_connection(client, resource_group_name, resource_name, connection_name, client_id,
                      client_secret, scopes, service_provider_name, parameters=None):
    """Create a custom OAuth service provider.

    This method is directly called via "bot authsetting create"

    :param client:
    :param resource_group_name:
    :param resource_name:
    :param connection_name:
    :param client_id:
    :param client_secret:
    :param scopes:
    :param service_provider_name:
    :param parameters:
    :return:
    """
    service_provider = get_service_providers(client, name=service_provider_name)
    if not service_provider:
        raise CLIError('Invalid Service Provider Name passed. Use "az bot authsetting list-providers" '
                       'command to see all available providers')
    connection_parameters = []
    if parameters:
        for parameter in parameters:
            pair = parameter.split('=', 1)
            if len(pair) == 1:
                raise CLIError('usage error: --parameters STRING=STRING STRING=STRING')
            connection_parameters.append(ConnectionSettingParameter(key=pair[0], value=pair[1]))
    setting = ConnectionSetting(
        location='global',
        properties=ConnectionSettingProperties(
            client_id=client_id,
            client_secret=client_secret,
            scopes=scopes,
            service_provider_id=service_provider.properties.id,
            parameters=connection_parameters
        )
    )
    return client.bot_connection.create(resource_group_name, resource_name, connection_name, setting)


def download_app(cmd, client, resource_group_name, resource_name, file_save_path=None):  # pylint: disable=too-many-statements, too-many-locals
    """Download the bot's source code.

    This method is directly called via "bot download"

    :param cmd:
    :param client:
    :param resource_group_name:
    :param resource_name:
    :param file_save_path:
    :return:
    """
    logger.info('Retrieving bot information from Azure.')

    # Get the bot and ensure it's not a registration only bot
    bot = client.bots.get(
        resource_group_name=resource_group_name,
        resource_name=resource_name
    )
    logger.info('Bot information retrieved successfully from Azure.')

    if bot.kind == 'bot':
        raise CLIError('Source download is not supported for registration only bots')

    if not file_save_path:
        file_save_path = os.getcwd()
        logger.info('Parameter --file-save-path not provided, defaulting to current working directory, %s. '
                    'For more information, run \'az bot download -h\'', file_save_path)

    if not os.path.isdir(file_save_path):
        raise CLIError('Path name not valid')

    # TODO: Verify that the behavior for download and publish is the same in regards to where the files are downloaded
    # TODO: to and uploaded from.
    folder_path = os.path.join(file_save_path, resource_name)

    logger.info('Bot source will be downloaded to %s.', folder_path)

    if os.path.exists(folder_path):
        raise CLIError('The path {0} already exists. '
                       'Please delete this folder or specify an alternate path'.format(folder_path))

    logger.info('Attempting to preemptively create directory %s', folder_path)
    os.mkdir(folder_path)

    logger.info('Creating Kudu client to download bot source.')
    kudu_client = KuduClient(cmd, resource_group_name, resource_name, bot)

    logger.info('Downloading bot source. This operation may take seconds or minutes depending on the size of '
                'your bot source and the download speed of your internet connection.')
    kudu_client.download_bot_zip(file_save_path, folder_path)

    logger.info('Bot source download successful. Preparing bot project.')

    # TODO: Examine cases where PostDeployScripts, deploy.cmd, etc. do not exist.
    if (os.path.exists(os.path.join(folder_path, 'PostDeployScripts', 'deploy.cmd.template')) and
            os.path.exists(os.path.join(folder_path, 'deploy.cmd'))):

        logger.info('Post deployment scripts and deploy.cmd found in source under folder %s. Copying deploy.cmd.')

        shutil.copyfile(os.path.join(folder_path, 'deploy.cmd'),
                        os.path.join(folder_path, 'PostDeployScripts', 'deploy.cmd.template'))

    # If the bot source contains a .bot file
    # TODO: If there is only one bot file, that is the bot file.
    # TODO: If there are more than one bot file, the user must disambiguate before continuing.
    # TODO: Show error and suggest passsing --bot-file-name

    bot_file_path = os.path.join(folder_path, '{0}.bot'.format(resource_name))
    if os.path.exists(bot_file_path):

        logger.info('Detected bot file %s.', bot_file_path)

        app_settings = WebAppOperations.get_app_settings(
            cmd=cmd,
            resource_group_name=resource_group_name,
            name=kudu_client.bot_site_name
        )
        bot_secret = [item['value'] for item in app_settings if item['name'] == 'botFileSecret']

        # Write a .env file
        bot_env = {
            'botFileSecret': bot_secret[0],
            'botFilePath': '{0}.bot'.format(resource_name),
            'NODE_ENV': 'development'
        }
        # If javascript, write .env file content to .env file
        if os.path.exists(os.path.join(folder_path, 'package.json')):

            logger.info('Detected language as javascript. Package.json present at %s. Creating .env file in that '
                        'folder.', folder_path)

            with open(os.path.join(folder_path, '.env'), 'w') as f:
                for key, value in bot_env.items():
                    f.write('{0}={1}\n'.format(key, value))
        # If C#, write .env file content to appsettings.json
        else:

            app_settings_path = os.path.join(folder_path, 'appsettings.json')

            logger.info('Detected language as CSharp. Loading app settings from %s.', app_settings_path)

            existing = None
            if not os.path.exists(app_settings_path):

                logger.info('App settings not found at %s, defaulting app settings to {}.', app_settings_path)
                existing = '{}'
            else:
                with open(app_settings_path, 'r') as f:
                    existing = json.load(f)
            with open(os.path.join(app_settings_path), 'w+') as f:
                for key, value in bot_env.items():
                    existing[key] = value
                f.write(json.dumps(existing))

        # TODO: Optimize this logic and document. If there is not bot secret, add bot_env download path. Why?
        # TODO: Consider just returning downloadPath as a string rather than this object. There seem to be no
        # TODO: usages of the other properties such as botFileSecret
        if not bot_secret:

            logger.info('Bot secret not found. Setting download path to %s', folder_path)

            bot_env['downloadPath'] = folder_path
            return bot_env

    logger.info('Bot download completed successfully.')

    return {'downloadPath': folder_path}


def get_service_providers(client, name=None):
    """Gets supported OAuth Service providers.

    This method is directly called via "bot authsetting list-providers"

    :param client:
    :param name:
    :return:
    """

    service_provider_response = client.bot_connection.list_service_providers()
    name = name and name.lower()
    if name:
        try:
            return next((item for item in service_provider_response.value if
                         item.properties.service_provider_name.lower() == name.lower()))
        except StopIteration:
            raise CLIError('A service provider with the name {0} was not found'.format(name))
    return service_provider_response


def prepare_publish(cmd, client, resource_group_name, resource_name, sln_name, proj_name, code_dir=None, version='v3'):  # pylint:disable=too-many-statements
    """Adds PostDeployScripts folder with necessary scripts to deploy v3 bot to Azure.

    This method is directly called via "bot prepare-publish"

    :param cmd:
    :param client:
    :param resource_group_name:
    :param resource_name:
    :param sln_name:
    :param proj_name:
    :param code_dir:
    :param version:
    :return:
    """

    # The prepare-publish process for v3 bots and v4 bots differ, so if the user specifies a v4 version, end the command
    # and inform user of az bot publish.
    if version == 'v4':
        raise CLIError('\'az bot prepare-publish\' is only for v3 bots. Please use \'az bot publish\' to prepare and '
                       'publish a v4 bot.')

    bot = client.bots.get(
        resource_group_name=resource_group_name,
        resource_name=resource_name
    )
    if bot.kind == 'bot':
        raise CLIError('Prepare Publish is not supported for registration only bots.')

    if not code_dir:
        code_dir = os.getcwd()
        logger.warning('Parameter --code-dir not provided, defaulting to current working directory, %s. For more '
                       'information, run \'az bot prepare-publish -h\'', code_dir)

    if not os.path.isdir(code_dir):
        raise CLIError('Please supply a valid directory path containing your source code. '
                       'Path {0} does not exist.'.format(code_dir))

    os.chdir(code_dir)

    # Ensure that the directory does not contain appropriate post deploy scripts folder
    if 'PostDeployScripts' in os.listdir(code_dir):
        raise CLIError('Post deploy azure scripts are already in Place.')

    # Download bot source
    download_path = download_app(cmd, client, resource_group_name, resource_name)

    shutil.copytree(os.path.join(download_path['downloadPath'], 'PostDeployScripts'), 'PostDeployScripts')

    # If javascript, we need these files there for Azure WebApps to start
    if os.path.exists(os.path.join('PostDeployScripts', 'publish.js.template')):

        logger.info('Detected language javascript.')

        shutil.copy(os.path.join(download_path['downloadPath'], 'iisnode.yml'), 'iisnode.yml')
        shutil.copy(os.path.join(download_path['downloadPath'], 'publish.js'), 'publish.js')
        shutil.copy(os.path.join(download_path['downloadPath'], 'web.config'), 'web.config')

    # If C#, we need other set of files for the WebApp to start including build.cmd
    else:
        logger.info('Detected language CSharp.')
        solution_path = None
        csproj_path = None
        old_namev4 = 'AspNetCore-EchoBot-With-State'
        old_namev3 = 'Microsoft.Bot.Sample.SimpleEchoBot'
        shutil.copy(os.path.join(download_path['downloadPath'], 'build.cmd'), 'build.cmd')
        shutil.copy(os.path.join(download_path['downloadPath'], '.deployment'), '.deployment')

        # "deploy.cmd.template" does not exist for v4 bots. If the next section of code fails due to deploy.cmd.template
        # not being found, it is most likely due to trying to call prepare-publish on a v4 bot.
        # Inform the user of the potential problem and raise the error to exit the process.
        try:
            shutil.copyfile(os.path.join(download_path['downloadPath'], 'PostDeployScripts', 'deploy.cmd.template'),
                            'deploy.cmd')
        except FileNotFoundError as error:
            logger.error('"deploy.cmd.template" not found. This may be due to calling \'az bot prepare-publish\' on a '
                         'v4 bot. To prepare and publish a v4 bot, please instead use \'az bot publish\'.')
            raise CLIError(error)

        # Find solution and project name
        for root, _, files in os.walk(os.curdir):
            if solution_path and csproj_path:
                break
            for fileName in files:
                if solution_path and csproj_path:
                    break
                if fileName == sln_name:
                    solution_path = os.path.relpath(os.path.join(root, fileName))
                if fileName == proj_name:
                    csproj_path = os.path.relpath(os.path.join(root, fileName))

        # Read deploy script contents
        with open('deploy.cmd') as f:
            content = f.read()

        logger.info('Visual studio solution detected: %s.', solution_path)
        logger.info('Visual studio project detected: %s.', csproj_path)

        # Using the deploy.cmd as a template, adapt it to use our solution and csproj
        with open('deploy.cmd', 'w') as f:
            content = content.replace(old_namev3 + '.sln', solution_path)
            content = content.replace(old_namev3 + '.csproj', csproj_path)
            content = content.replace(old_namev4 + '.sln', solution_path)
            content = content.replace(old_namev4 + '.csproj', csproj_path)
            f.write(content)

    shutil.rmtree(download_path['downloadPath'])

    logger.info('Bot prepare publish completed successfully.')


def publish_app(cmd, client, resource_group_name, resource_name, code_dir=None, proj_name=None, version='v3'):
    """Publish local bot code to Azure.

    This method is directly called via "bot publish"

    :param cmd:
    :param client:
    :param resource_group_name:
    :param resource_name:
    :param code_dir:
    :param proj_name:
    :param version:
    :return:
    """
    # Get the bot information and ensure it's not only a registration bot.
    bot = client.bots.get(
        resource_group_name=resource_group_name,
        resource_name=resource_name
    )
    if bot.kind == 'bot':
        raise CLIError('Bot kind is \'bot\', meaning it is a registration bot. '
                       'Source publish is not supported for registration only bots.')

    # If the user does not pass in a path to the local bot project, get the current working directory.
    if not code_dir:
        code_dir = os.getcwd()

        logger.info('Parameter --code-dir not provided, defaulting to current working directory, %s. '
                    'For more information, run \'az bot publish -h\'', code_dir)

    if not os.path.isdir(code_dir):
        raise CLIError('The path %s is not a valid directory. '
                       'Please supply a valid directory path containing your source code.' % code_dir)

    # Ensure that the directory contains appropriate post deploy scripts folder
    if 'PostDeployScripts' not in os.listdir(code_dir):
        if version == 'v4':

            logger.info('Detected SDK version v4. Running prepare publish in code directory %s and for project file %s'  # pylint:disable=logging-not-lazy
                        % (code_dir, proj_name))

            # Automatically run prepare-publish in case of v4.
            BotPublishPrep.prepare_publish_v4(logger, code_dir, proj_name)
        else:
            logger.info('Detected SDK version v3. PostDeploymentScripts folder not found in directory provided: %s',
                        code_dir)

            raise CLIError('Publish directory provided is uses Bot Builder SDK V3, and as a legacy bot needs to be '
                           'prepared for deployment. Please run prepare-publish. For more information, run \'az bot '
                           'prepare-publish -h\'.')

    logger.info('Creating upload zip file.')
    zip_filepath = BotPublishPrep.create_upload_zip(logger, code_dir, include_node_modules=False)
    logger.info('Zip file path created, at %s.', zip_filepath)

    kudu_client = KuduClient(cmd, resource_group_name, resource_name, bot)
    output = kudu_client.publish(zip_filepath)

    logger.info('Bot source published. Preparing bot application to run the new source.')
    os.remove('upload.zip')

    if os.path.exists(os.path.join('.', 'package.json')):
        logger.info('Detected language javascript. Installing node dependencies in remote bot.')
        kudu_client.install_node_dependencies()

    logger.info('Bot publish completed successfully.')

    return output
