# Copyright 2021 Verizon Media
#
#    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 base64
import gzip
import io
import os
import tempfile

from ironic_lib import utils as ironic_utils
from oslo_log import log as logging
import pycdlib
import requests

from ironic.common import exception
from ironic.conf import CONF

LOG = logging.getLogger(__name__)


def _get_config_drive_dict_from_iso(
        iso_reader, drive_dict,
        target_path='/var/lib/cloud/seed/config_drive'):
    """Traverse the config drive iso and extract content and filenames

    :param iso_reader: pycdlib.PyCdlib object representing ISO files.
    :param drive_dict: Mutable dictionary to store path and contents.
    :param target_path: Path on the local disk in which the files in config
                        drive files has to be written.
    """
    for path, dirlist, filelist in iso_reader.walk(iso_path='/'):
        for f in filelist:
            # In iso9660 file extensions are mangled. Example '/FOO/BAR;1'.
            iso_file_path = os.path.join(path, f)
            file_record = iso_reader.get_record(iso_path=iso_file_path)
            # This converts /FOO/BAR;1 -> /foo/bar
            posix_file_path = iso_reader.full_path_from_dirrecord(
                file_record, rockridge=True
            )
            # Path to which the file in config drive to be written on the
            # server.
            posix_file_path = posix_file_path.lstrip('/')
            target_file_path = os.path.join(target_path, posix_file_path)
            b_buf = io.BytesIO()
            iso_reader.get_file_from_iso_fp(
                iso_path=iso_file_path, outfp=b_buf
            )
            b_buf.seek(0)
            content = b"".join(b_buf.readlines()).decode('utf-8')
            drive_dict[target_file_path] = content


def read_iso9600_config_drive(config_drive):
    """Read config drive and store it's contents in a dict

    :param config_drive: Config drive in iso9600 format
    :returns: A dict containing path as key and contents of the configdrive
              file as value.
    """
    config_drive_dict = dict()
    with tempfile.NamedTemporaryFile(suffix='.iso') as iso:
        iso.write(config_drive)
        iso.flush()
        try:
            iso_reader = pycdlib.PyCdlib()
            iso_reader.open(iso.name)
            _get_config_drive_dict_from_iso(iso_reader, config_drive_dict)
            iso_reader.close()
        except Exception as e:
            msg = "Error reading the config drive iso: %s" % e
            LOG.error(msg)
    return config_drive_dict


def decode_and_extract_config_drive_iso(config_drive_iso_gz):
    try:
        iso_gz_obj = io.BytesIO(base64.b64decode(config_drive_iso_gz))
        iso_gz_obj.seek(0)
    except Exception as exc:
        if isinstance(config_drive_iso_gz, bytes):
            LOG.debug('Config drive is not base64 encoded (%(error)s), '
                      'assuming binary', {'error': exc})
            iso_gz_obj = config_drive_iso_gz
        else:
            error_msg = ('Config drive is not base64 encoded or the content '
                         'is malformed. %(cls)s: %(err)s.'
                         % {'err': exc, 'cls': type(exc).__name__})
            raise exception.InstanceDeployFailure(error_msg)

    try:
        with gzip.GzipFile(fileobj=iso_gz_obj, mode='rb') as f:
            config_drive_iso = f.read()
    except Exception as exc:
        error_msg = "Decoding/Extraction of config drive failed: %s" % exc
        raise exception.InstanceDeployFailure(error_msg)
    return config_drive_iso


def _fetch_config_drive_from_url(url):
    try:
        config_drive = requests.get(
            url, timeout=CONF.webserver_connection_timeout).content
    except requests.exceptions.RequestException as e:
        raise exception.InstanceDeployFailure(
            "Can't download the configdrive content from '%(url)s'. "
            "Reason: %(reason)s" %
            {'url': url, 'reason': e})
    return config_drive


def _write_config_drive_content(content, file_path):
    """Generate post ks script to write each userdata content."""

    content = base64.b64encode(str.encode(content))
    kickstart_data = []
    kickstart_data.append("\n")
    kickstart_data.append("%post\n")
    kickstart_data.append(("DIRPATH=`/usr/bin/dirname "
                           "{file_path}`\n").format(
        file_path=file_path))
    kickstart_data.append("/bin/mkdir -p $DIRPATH\n")
    kickstart_data.append("CONTENT='{content}'\n".format(
        content=content))
    kickstart_data.append("echo $CONTENT | "
                          "/usr/bin/base64 --decode > "
                          "{file_path}".format(file_path=file_path))
    kickstart_data.append("\n")
    kickstart_data.append(
        "/bin/chmod 600 {file_path}\n".format(file_path=file_path)
    )
    kickstart_data.append("%end\n\n")

    return "".join(kickstart_data)


def prepare_config_drive(task,
                         config_drive_path='/var/lib/cloud/seed/config_drive'):
    """Prepare config_drive for writing to kickstart file"""
    LOG.debug("Preparing config_drive to write to kickstart file")
    node = task.node
    config_drive = node.instance_info.get('configdrive')
    ks_config_drive = ''
    if not config_drive:
        return ks_config_drive

    if ironic_utils.is_http_url(config_drive):
        config_drive = _fetch_config_drive_from_url(config_drive)

    if not isinstance(config_drive, dict):
        # The config drive is in iso6600 format, gzipped and base-64-encoded.
        # Convert it to a dict.
        config_drive_iso = decode_and_extract_config_drive_iso(config_drive)
        config_drive = read_iso9600_config_drive(config_drive_iso)

    for key in sorted(config_drive.keys()):
        target_path = os.path.join(config_drive_path, key)
        ks_config_drive += _write_config_drive_content(
            config_drive[key], target_path
        )

    return ks_config_drive
