<reponame>Food-X-Technologies/foodx_devops_tools
#  Copyright (c) 2021 Food-X Technologies
#
#  This file is part of foodx_devops_tools.
#
#  You should have received a copy of the MIT License along with
#  foodx_devops_tools. If not, see <https://opensource.org/licenses/MIT>.

"""General support for templating."""

import asyncio
import dataclasses
import logging
import pathlib

import pydantic

from foodx_devops_tools.puff import run_puff
from foodx_devops_tools.utilities.jinja2 import (
    FrameTemplates,
    TemplateParameters,
)

from ._exceptions import TemplateError

log = logging.getLogger(__name__)

JINJA_FILE_PREFIX = "jinja2."


class TemplatePaths(pydantic.BaseModel):
    """Collection of file paths for ARM templates."""

    source: pathlib.Path
    target: pathlib.Path


@dataclasses.dataclass
class TemplateFiles:
    """Collection of file paths for template processing."""

    arm_template: TemplatePaths
    arm_template_parameters: TemplatePaths


@dataclasses.dataclass
class ArmTemplateDeploymentFiles:
    """Paths to the two files needed for ARM template deployment."""

    arm_template: pathlib.Path
    parameters: pathlib.Path


def json_inlining(content: str) -> str:
    """
    Inline text content for inclusion in a JSON file (ARM template).

    Escape double quotes and retain newlines as escapes to embody the content
    in a single line.

    Args:
        content: Content to be "inlined".

    Returns:
        Modified content.
    """
    escaped_newlines = repr(content).strip("'").replace(r"\n", r"\n")
    escaped_quotes = escaped_newlines.replace('"', r"\"")

    return escaped_quotes


async def _apply_template(
    template_environment: FrameTemplates,
    source_file: pathlib.Path,
    target_file: pathlib.Path,
    parameters: TemplateParameters,
) -> None:
    """
    Apply frame-specific template and parameters ready for deployment.

    Args:
        template_environment:   Frame-specific template environment.
        source_file:            Source template file.
        target_directory:       Target directory in which to store fulfilled
                                template file.
        parameters:             Parameter to apply to the template file.

    Returns:
        Target file path of the fulfilled template.
    """
    log.debug(
        "Applying jinja2 templating, {0} (source), "
        "{1} (destination)".format(source_file, target_file)
    )
    await template_environment.apply_template(
        source_file.name, target_file, parameters
    )


def _verify_puff_target(file_path: pathlib.Path) -> None:
    if not file_path.is_file():
        message = (
            f"Expected puff generated ARM template parameter file is "
            f"missing, {file_path}"
        )
        log.error(message)
        raise TemplateError(message)


async def _prepare_working_directory(working_dir: pathlib.Path) -> None:
    """
    Ensure that the working directory exists.

    Args:
        working_dir: Expected path of working directory.

    Raises:
        TemplateError:  If the working directory path exists, but is not
                        actually a directory.
    """
    if not working_dir.exists():
        working_dir.mkdir(parents=True, exist_ok=True)
    elif not working_dir.is_dir():
        raise TemplateError(
            f"working directory name exists but is not a directory, "
            f"{working_dir}"
        )


def _log_arm_template_paths(arm_template: TemplatePaths) -> None:
    log.debug(f"source_arm_template_path, {arm_template.source}")
    log.debug(f"arm_target_file, {arm_template.target}")

    log.info(f"applying jinja2 to ARM template file, {arm_template.source}")


def _construct_arm_template_parameter_paths(
    arm_template_parameters: TemplatePaths,
) -> pathlib.Path:
    source_puff_file_path = arm_template_parameters.source

    # puffd_parameters_target_file: the expected arm template parameter file
    # generated by puff.
    puffd_parameters_target_file = arm_template_parameters.target
    log.debug(
        f"puffd_parameters_target_file json, {puffd_parameters_target_file}"
    )
    parameters_target_dir = puffd_parameters_target_file.parent
    log.debug(f"parameters_target_dir, {parameters_target_dir}")

    # jinjad_parameters_target_file: the expected arm template parameter file
    # generated by jinja2 template processing.
    jinjad_parameters_target_file = (
        parameters_target_dir / f"jinjad.{puffd_parameters_target_file.name}"
    )
    log.debug(
        f"jinjad_parameters_target_file json, {jinjad_parameters_target_file}"
    )

    log.info(
        f"applying jinja2 to ARM template parameter file,"
        f" {source_puff_file_path}"
    )

    return jinjad_parameters_target_file


async def prepare_deployment_files(
    template_files: TemplateFiles,
    parameters: TemplateParameters,
) -> ArmTemplateDeploymentFiles:
    """
    Prepare final ARM template and parameter files for deployment.

    Args:
        template_files: Paths to source files for processing.
        parameters: Parameters to be applied to templates.

    Returns:
        Paths to ARM template and ARM template parameter files.
    Raises:
        TemplateError:  If an error occurs during puff or template processing.
    """
    arm_source = template_files.arm_template.source
    arm_target = template_files.arm_template.target
    _log_arm_template_paths(template_files.arm_template)

    # the puff YAML file.
    parameters_source = template_files.arm_template_parameters.source
    # the arm template parameters file generated from the puff run.
    puffd_parameters_target = template_files.arm_template_parameters.target
    # the arm template parameter file generated by jinja2 processing.
    parameters_target = _construct_arm_template_parameter_paths(
        template_files.arm_template_parameters
    )

    # folders containing _jinja template_ source files
    template_paths = (
        [arm_source.parent, parameters_target.parent]
        if arm_source.parent != parameters_target.parent
        else [arm_source.parent]
    )
    log.debug(f"frame template paths, {template_paths}")
    template_environment = FrameTemplates(template_paths)
    template_environment.environment.filters["json_inlining"] = json_inlining

    await _prepare_working_directory(parameters_target.parent)
    if parameters_target.parent != arm_target.parent:
        # also prepare the distinct arm target directory
        await _prepare_working_directory(arm_target.parent)

    # transform the puff file to arm template parameter json files.
    await run_puff(
        parameters_source,
        False,
        False,
        disable_ascii_art=True,
        output_dir=puffd_parameters_target.parent,
    )
    _verify_puff_target(puffd_parameters_target)

    # now process jinja2 templates against JSON files.
    await asyncio.gather(
        _apply_template(
            template_environment,
            puffd_parameters_target,
            parameters_target,
            parameters,
        ),
        _apply_template(
            template_environment,
            arm_source,
            arm_target,
            parameters,
        ),
    )
    result = ArmTemplateDeploymentFiles(
        arm_template=arm_target,
        parameters=parameters_target,
    )
    return result

#  Copyright (c) 2021 Food-X Technologies
#
#  This file is part of foodx_devops_tools.
#
#  You should have received a copy of the MIT License along with
#  foodx_devops_tools. If not, see <https://opensource.org/licenses/MIT>.

"""General support for templating."""

import asyncio
import dataclasses
import logging
import pathlib

import pydantic

from foodx_devops_tools.puff import run_puff
from foodx_devops_tools.utilities.jinja2 import (
    FrameTemplates,
    TemplateParameters,
)

from ._exceptions import TemplateError

log = logging.getLogger(__name__)

JINJA_FILE_PREFIX = "jinja2."


class TemplatePaths(pydantic.BaseModel):
    """Collection of file paths for ARM templates."""

    source: pathlib.Path
    target: pathlib.Path


@dataclasses.dataclass
class TemplateFiles:
    """Collection of file paths for template processing."""

    arm_template: TemplatePaths
    arm_template_parameters: TemplatePaths


@dataclasses.dataclass
class ArmTemplateDeploymentFiles:
    """Paths to the two files needed for ARM template deployment."""

    arm_template: pathlib.Path
    parameters: pathlib.Path


def json_inlining(content: str) -> str:
    """
    Inline text content for inclusion in a JSON file (ARM template).

    Escape double quotes and retain newlines as escapes to embody the content
    in a single line.

    Args:
        content: Content to be "inlined".

    Returns:
        Modified content.
    """
    escaped_newlines = repr(content).strip("'").replace(r"\n", r"\n")
    escaped_quotes = escaped_newlines.replace('"', r"\"")

    return escaped_quotes


async def _apply_template(
    template_environment: FrameTemplates,
    source_file: pathlib.Path,
    target_file: pathlib.Path,
    parameters: TemplateParameters,
) -> None:
    """
    Apply frame-specific template and parameters ready for deployment.

    Args:
        template_environment:   Frame-specific template environment.
        source_file:            Source template file.
        target_directory:       Target directory in which to store fulfilled
                                template file.
        parameters:             Parameter to apply to the template file.

    Returns:
        Target file path of the fulfilled template.
    """
    log.debug(
        "Applying jinja2 templating, {0} (source), "
        "{1} (destination)".format(source_file, target_file)
    )
    await template_environment.apply_template(
        source_file.name, target_file, parameters
    )


def _verify_puff_target(file_path: pathlib.Path) -> None:
    if not file_path.is_file():
        message = (
            f"Expected puff generated ARM template parameter file is "
            f"missing, {file_path}"
        )
        log.error(message)
        raise TemplateError(message)


async def _prepare_working_directory(working_dir: pathlib.Path) -> None:
    """
    Ensure that the working directory exists.

    Args:
        working_dir: Expected path of working directory.

    Raises:
        TemplateError:  If the working directory path exists, but is not
                        actually a directory.
    """
    if not working_dir.exists():
        working_dir.mkdir(parents=True, exist_ok=True)
    elif not working_dir.is_dir():
        raise TemplateError(
            f"working directory name exists but is not a directory, "
            f"{working_dir}"
        )


def _log_arm_template_paths(arm_template: TemplatePaths) -> None:
    log.debug(f"source_arm_template_path, {arm_template.source}")
    log.debug(f"arm_target_file, {arm_template.target}")

    log.info(f"applying jinja2 to ARM template file, {arm_template.source}")


def _construct_arm_template_parameter_paths(
    arm_template_parameters: TemplatePaths,
) -> pathlib.Path:
    source_puff_file_path = arm_template_parameters.source

    # puffd_parameters_target_file: the expected arm template parameter file
    # generated by puff.
    puffd_parameters_target_file = arm_template_parameters.target
    log.debug(
        f"puffd_parameters_target_file json, {puffd_parameters_target_file}"
    )
    parameters_target_dir = puffd_parameters_target_file.parent
    log.debug(f"parameters_target_dir, {parameters_target_dir}")

    # jinjad_parameters_target_file: the expected arm template parameter file
    # generated by jinja2 template processing.
    jinjad_parameters_target_file = (
        parameters_target_dir / f"jinjad.{puffd_parameters_target_file.name}"
    )
    log.debug(
        f"jinjad_parameters_target_file json, {jinjad_parameters_target_file}"
    )

    log.info(
        f"applying jinja2 to ARM template parameter file,"
        f" {source_puff_file_path}"
    )

    return jinjad_parameters_target_file


async def prepare_deployment_files(
    template_files: TemplateFiles,
    parameters: TemplateParameters,
) -> ArmTemplateDeploymentFiles:
    """
    Prepare final ARM template and parameter files for deployment.

    Args:
        template_files: Paths to source files for processing.
        parameters: Parameters to be applied to templates.

    Returns:
        Paths to ARM template and ARM template parameter files.
    Raises:
        TemplateError:  If an error occurs during puff or template processing.
    """
    arm_source = template_files.arm_template.source
    arm_target = template_files.arm_template.target
    _log_arm_template_paths(template_files.arm_template)

    # the puff YAML file.
    parameters_source = template_files.arm_template_parameters.source
    # the arm template parameters file generated from the puff run.
    puffd_parameters_target = template_files.arm_template_parameters.target
    # the arm template parameter file generated by jinja2 processing.
    parameters_target = _construct_arm_template_parameter_paths(
        template_files.arm_template_parameters
    )

    # folders containing _jinja template_ source files
    template_paths = (
        [arm_source.parent, parameters_target.parent]
        if arm_source.parent != parameters_target.parent
        else [arm_source.parent]
    )
    log.debug(f"frame template paths, {template_paths}")
    template_environment = FrameTemplates(template_paths)
    template_environment.environment.filters["json_inlining"] = json_inlining

    await _prepare_working_directory(parameters_target.parent)
    if parameters_target.parent != arm_target.parent:
        # also prepare the distinct arm target directory
        await _prepare_working_directory(arm_target.parent)

    # transform the puff file to arm template parameter json files.
    await run_puff(
        parameters_source,
        False,
        False,
        disable_ascii_art=True,
        output_dir=puffd_parameters_target.parent,
    )
    _verify_puff_target(puffd_parameters_target)

    # now process jinja2 templates against JSON files.
    await asyncio.gather(
        _apply_template(
            template_environment,
            puffd_parameters_target,
            parameters_target,
            parameters,
        ),
        _apply_template(
            template_environment,
            arm_source,
            arm_target,
            parameters,
        ),
    )
    result = ArmTemplateDeploymentFiles(
        arm_template=arm_target,
        parameters=parameters_target,
    )
    return result
#  Copyright (c) 2021 Food-X Technologies # #  This file is part of foodx_devops_tools. # #  You should have received a copy of the MIT License along with #  foodx_devops_tools. If not, see <https://opensource.org/licenses/MIT>. General support for templating. Collection of file paths for ARM templates. Collection of file paths for template processing. Paths to the two files needed for ARM template deployment. Inline text content for inclusion in a JSON file (ARM template).      Escape double quotes and retain newlines as escapes to embody the content     in a single line.      Args:         content: Content to be "inlined".      Returns:         Modified content. Apply frame-specific template and parameters ready for deployment.      Args:         template_environment:   Frame-specific template environment.         source_file:            Source template file.         target_directory:       Target directory in which to store fulfilled                                 template file.         parameters:             Parameter to apply to the template file.      Returns:         Target file path of the fulfilled template. Ensure that the working directory exists.      Args:         working_dir: Expected path of working directory.      Raises:         TemplateError:  If the working directory path exists, but is not                         actually a directory. # puffd_parameters_target_file: the expected arm template parameter file # generated by puff. # jinjad_parameters_target_file: the expected arm template parameter file # generated by jinja2 template processing. Prepare final ARM template and parameter files for deployment.      Args:         template_files: Paths to source files for processing.         parameters: Parameters to be applied to templates.      Returns:         Paths to ARM template and ARM template parameter files.     Raises:         TemplateError:  If an error occurs during puff or template processing. # the puff YAML file. # the arm template parameters file generated from the puff run. # the arm template parameter file generated by jinja2 processing. # folders containing _jinja template_ source files # also prepare the distinct arm target directory # transform the puff file to arm template parameter json files. # now process jinja2 templates against JSON files.