#
#    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.

import os
import shutil
import tempfile
from urllib import parse as urlparse

import jsonschema
from oslo_log import log
from oslo_utils import fileutils

from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import image_service
from ironic.common import swift
from ironic.conf import CONF
from ironic.drivers.modules.redfish import utils as redfish_utils

LOG = log.getLogger(__name__)

_UPDATE_FIRMWARE_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "update_firmware clean step schema",
    "type": "array",
    # list of firmware update images
    "items": {
        "type": "object",
        "required": ["url", "checksum"],
        "properties": {
            "url": {
                "description": "URL for firmware file",
                "type": "string",
                "minLength": 1
            },
            "checksum": {
                "description": "SHA1 checksum for firmware file",
                "type": "string",
                "minLength": 1
            },
            "wait": {
                "description": "optional wait time for firmware update",
                "type": "integer",
                "minimum": 1
            },
            "source":
            {
                "description": "optional firmware_source to override global "
                "setting for firmware file",
                "type": "string",
                "enum": ["http", "local", "swift"]
            }
        },
        "additionalProperties": False
    }
}

_FIRMWARE_INTERFACE_UPDATE_SCHEMA = {
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "update_firmware clean step schema",
    "type": "array",
    "minItems": 1,
    # list of firmware update images
    "items": {
        "type": "object",
        "required": ["component", "url"],
        "properties": {
            "component": {
                "description": "name of the firmware component to be updated",
                "type": "string",
                "anyOf": [
                    {"enum": redfish_utils.FIRMWARE_COMPONENTS},
                    {"pattern": "^%s.*" % redfish_utils.NIC_COMPONENT_PREFIX}
                ]
            },
            "url": {
                "description": "URL for firmware file",
                "type": "string",
                "minLength": 1
            },
            "wait": {
                "description": "optional wait time for firmware update",
                "type": "integer",
                "minimum": 1
            }
        },
        "additionalProperties": False
    }
}

_FIRMWARE_SUBDIR = 'firmware'


def validate_update_firmware_args(firmware_images):
    """Validate ``update_firmware`` step input argument

    :param firmware_images: args to validate.
    :raises: InvalidParameterValue When argument is not valid
    """
    try:
        jsonschema.validate(firmware_images, _UPDATE_FIRMWARE_SCHEMA)
    except jsonschema.ValidationError as err:
        raise exception.InvalidParameterValue(
            _('Invalid firmware update %(firmware_images)s. Errors: %(err)s')
            % {'firmware_images': firmware_images, 'err': err})


def validate_firmware_interface_update_args(settings):
    """Validate ``update`` step input argument

    :param settings: args to validate.
    :raises: InvalidParameterValue When argument is not valid
    """
    try:
        jsonschema.validate(settings, _FIRMWARE_INTERFACE_UPDATE_SCHEMA)
    except jsonschema.ValidationError as err:
        raise exception.InvalidParameterValue(
            _('Invalid firmware update %(settings)s. Errors: %(err)s')
            % {'settings': settings, 'err': err})


def get_swift_temp_url(parsed_url):
    """Gets Swift temporary URL

    :param parsed_url: Parsed URL from URL in format
        swift://container/[sub-folder/]file
    :returns: Swift temporary URL
    """
    return swift.SwiftAPI().get_temp_url(
        parsed_url.netloc, parsed_url.path.lstrip('/'),
        CONF.redfish.swift_object_expiry_timeout)


def download_to_temp(node, url):
    """Downloads to temporary location from given URL

    :param node: Node for which to download to temporary location
    :param url: URL to download from
    :returns: File path of temporary location file is downloaded to
    """
    parsed_url = urlparse.urlparse(url)
    scheme = parsed_url.scheme.lower()
    if scheme not in ('http', 'swift', 'file'):
        raise exception.InvalidParameterValue(
            _('%(scheme)s is not supported for %(url)s.')
            % {'scheme': scheme, 'url': parsed_url.geturl()})

    tempdir = os.path.join(tempfile.gettempdir(), node.uuid)
    os.makedirs(tempdir, exist_ok=True)
    temp_file = os.path.join(
        tempdir,
        os.path.basename(parsed_url.path))
    LOG.debug('For node %(node)s firmware at %(url)s will be downloaded to '
              'temporary location at %(temp_file)s',
              {'node': node.uuid, 'url': url, 'temp_file': temp_file})
    if scheme == 'http':
        with open(temp_file, 'wb') as tf:
            image_service.HttpImageService().download(url, tf)
    elif scheme == 'swift':
        swift_url = get_swift_temp_url(parsed_url)
        with open(temp_file, 'wb') as tf:
            image_service.HttpImageService().download(swift_url, tf)
    elif scheme == 'file':
        with open(temp_file, 'wb') as tf:
            image_service.FileImageService().download(
                parsed_url.path, tf)

    return temp_file


def verify_checksum(node, checksum, file_path):
    """Verify checksum.

    :param node: Node for which file to verify checksum
    :param checksum: Expected checksum value
    :param file_path: File path for which to verify checksum
    :raises RedfishError: When checksum does not match
    """
    if len(checksum) <= 41:
        # SHA1: 40 bytes long
        calculated_checksum = fileutils.compute_file_checksum(
            file_path, algorithm='sha1')
    elif len(checksum) <= 64:
        calculated_checksum = fileutils.compute_file_checksum(
            file_path, algorithm='sha256')
    elif len(checksum) <= 128:
        calculated_checksum = fileutils.compute_file_checksum(
            file_path, algorithm='sha512')
    else:
        raise exception.RedfishError(
            _('Unable to identify checksum to perform firmware file checksum '
              'calculation. Please validate your input in and try again. '
              'Received: %(checksum)s')
            % {'checksum': checksum})
    if checksum != calculated_checksum:
        raise exception.RedfishError(
            _('For node %(node)s firmware file %(temp_file)s checksums do not '
              'match. Expected: %(checksum)s, calculated: '
              '%(calculated_checksum)s.')
            % {'node': node.uuid, 'temp_file': file_path, 'checksum': checksum,
               'calculated_checksum': calculated_checksum})


def stage(node, source, temp_file):
    """Stage temporary file to configured location

    :param node: Node for which to stage the file
    :param source: Where to stage the file. Corresponds to
        CONF.redfish.firmware_source.
    :param temp_file: File path of temporary file to stage
    :returns: Tuple of staged URL and source (http or swift) that needs
        cleanup of staged files afterwards.
    :raises RedfishError: If staging to HTTP server has failed.
    """
    staged_url = None
    filename = os.path.basename(temp_file)
    if source in ('http', 'local'):
        http_url = CONF.deploy.external_http_url or CONF.deploy.http_url
        staged_url = urlparse.urljoin(
            http_url, "/".join([_FIRMWARE_SUBDIR, node.uuid, filename]))
        staged_folder = os.path.join(
            CONF.deploy.http_root, _FIRMWARE_SUBDIR, node.uuid)
        staged_path = os.path.join(staged_folder, filename)
        LOG.debug('For node %(node)s temporary file %(temp_file)s will be '
                  'hard-linked or copied to %(staged_path)s and served over '
                  '%(staged_url)s',
                  {'node': node.uuid, 'temp_file': temp_file,
                   'staged_path': staged_path, 'staged_url': staged_url})
        os.makedirs(staged_folder, exist_ok=True)
        try:
            os.link(temp_file, staged_path)
            os.chmod(temp_file, CONF.redfish.file_permission)
        except OSError as oserror:
            LOG.debug("Could not hardlink file %(temp_file)s to location "
                      "%(staged_path)s. Will try to copy it. Error: %(error)s",
                      {'temp_file': temp_file, 'staged_path': staged_path,
                       'error': oserror})
            try:
                shutil.copyfile(temp_file, staged_path)
                os.chmod(staged_path, CONF.redfish.file_permission)
            except IOError as ioerror:
                raise exception.RedfishError(
                    _('For %(node)s failed to copy firmware file '
                      '%(temp_file)s to HTTP server root. Error %(error)s')
                    % {'node': node.uuid, 'temp_file': temp_file,
                       'error': ioerror})

    elif source == 'swift':
        container = CONF.redfish.swift_container
        timeout = CONF.redfish.swift_object_expiry_timeout
        swift_api = swift.SwiftAPI()
        object_name = "/".join([node.uuid, filename])
        swift_api.create_object(
            container,
            object_name,
            temp_file,
            object_headers={'X-Delete-After': str(timeout)})
        staged_url = swift_api.get_temp_url(
            container, object_name, timeout)
        LOG.debug('For node %(node)s temporary file at %(temp_file)s will be '
                  'served from Swift temporary URL %(staged_url)s',
                  {'node': node.uuid, 'temp_file': temp_file,
                   'staged_url': staged_url})

    need_cleanup = 'swift' if source == 'swift' else 'http'
    return staged_url, need_cleanup


def cleanup(node):
    """Clean up staged files

    :param node: Node for which to clean up. Should contain
        'firmware_cleanup' entry in `driver_internal_info` to indicate
        source(s) to be cleaned up.
    """
    # Cleaning up temporary just in case there is something when staging
    # to http or swift has failed.
    temp_dir = os.path.join(tempfile.gettempdir(), node.uuid)
    LOG.debug('For node %(node)s cleaning up temporary files, if any, from '
              '%(temp_dir)s.', {'node': node.uuid, 'temp_dir': temp_dir})
    shutil.rmtree(temp_dir, ignore_errors=True)

    cleanup = node.driver_internal_info.get('firmware_cleanup')
    if not cleanup:
        return

    if 'http' in cleanup:
        http_dir = os.path.join(
            CONF.deploy.http_root, _FIRMWARE_SUBDIR, node.uuid)
        LOG.debug('For node %(node)s cleaning up files from  %(http_dir)s.',
                  {'node': node.uuid, 'http_dir': http_dir})
        shutil.rmtree(http_dir, ignore_errors=True)

    if 'swift' in cleanup:
        swift_api = swift.SwiftAPI()
        container = CONF.redfish.swift_container
        LOG.debug('For node %(node)s cleaning up files from Swift container '
                  '%(container)s.',
                  {'node': node.uuid, 'container': container})
        objects = swift_api.connection.list_objects(container)
        for o in objects:
            if o.name and o.name.startswith(node.uuid):
                try:
                    swift_api.delete_object(container, o.name)
                except exception.SwiftOperationError as error:
                    LOG.warning('For node %(node)s failed to clean up '
                                '%(object)s. Error: %(error)s',
                                {'node': node.uuid, 'object': o.name,
                                 'error': error})
