#  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
