#!/usr/bin/env python3

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.

"""
This script cooridnates other scripts to put together a release.

Generated images will have a label in the form apache/heron:<tag> and will be placed in the //distro/ directory.

## Examples

List available target distrobutions on separate stdout lines:
  ./docker-images

Build and tag a single distorbution image then print where the archive's path:
  ./docker-images build 0.1.0-debian11 debian11

Build and tag all distrobution images then print each archive's path:
  ./docker-images build "$(git describe --tags)" --all

"""
from pathlib import Path

import logging
import re
import shutil
import subprocess
import sys
import tempfile
import typing

ROOT = Path(__file__).resolve().parent.parent.parent
BUILD_ARTIFACTS = ROOT / "docker/scripts/build-artifacts.sh"
BUILD_IMAGE = ROOT / "docker/scripts/build-docker.sh"


class BuildFailure(Exception):
    """Raised to indicate a failure buliding."""


class BadDistrobutionName(BuildFailure):
    """Raised when a bad distrobution name is provided."""


def configure_logging(debug: bool):
    """Use standard logging config and write to stdout and a logfile."""
    logging.basicConfig(
        format="[%(asctime)s] %(levelname)s: %(message)s",
        level=(logging.DEBUG if debug else logging.INFO),
    )


def log_run(args: typing.List[str], log: typing.IO[str]) -> subprocess.CompletedProcess:
    """Run an executable and direct its output to the given log file."""
    return subprocess.run(
        args, stdout=log, stderr=log, universal_newlines=True, check=True
    )


def build_dockerfile(
    scratch: Path, dist: str, tag: str, out_dir: Path, log: typing.IO[str]
) -> Path:
    """
    Raises CalledProcessError if either of the external scripts fail.
    """
    logging.info("building package for %s", dist)
    log_run([str(BUILD_ARTIFACTS), dist, tag, scratch], log)
    logging.info("building docker image for %s", dist)
    log_run([str(BUILD_IMAGE), dist, tag, scratch], log)
    tar = Path(scratch) / f"heron-docker-{tag}-{dist}.tar.gz"
    tar_out = out_dir / tar.name
    tar.replace(tar_out)
    logging.info("docker image complete: %s", tar_out)
    return tar_out


def available_distrobutions() -> typing.List[str]:
    """Return a list of available target distrobutions."""
    compile_files = (ROOT / "docker/compile").glob("Dockerfile.*")
    dist_files = (ROOT / "docker/dist").glob("Dockerfile.dist.*")
    compile_distros = {re.sub(r"^Dockerfile\.", "", f.name) for f in compile_files}
    dist_distros = {re.sub(r"^Dockerfile\.dist\.", "", f.name) for f in dist_files}
    distros = compile_distros & dist_distros
    mismatch = (compile_distros | dist_distros) ^ distros
    if mismatch:
        logging.warning(
            "docker distros found without both compile+dist files: %s", mismatch
        )

    return sorted(distros)


def build_target(tag: str, target: str) -> typing.List[Path]:
    """Build docker images for the given target distrobutions."""
    debug = True

    distros = available_distrobutions()
    logging.debug("available distro targets: %s", distros)
    if target == "--all":
        targets = distros
    elif target not in distros:
        raise BadDistrobutionName(f"distrobution {target!r} does not exist")
    else:
        targets = [target]

    out_dir = ROOT / "dist"
    out_dir.mkdir(exist_ok=True)

    for target in targets:
        scratch = Path(tempfile.mkdtemp(prefix=f"build-{target}-"))
        log_path = scratch / "log.txt"
        log = log_path.open("w")
        logging.debug("building %s", target)

        try:
            tar = build_dockerfile(scratch, target, tag, out_dir, log)
        except Exception as e:
            logging.error(
                "an error occurred building %s. See log in %s", target, log_path
            )
            if isinstance(e, subprocess.CalledProcessError):
                raise BuildFailure("failure in underlying build scripts") from e
            raise

        if not debug:
            shutil.rmtree(scratch)
        yield tar


def cli(args=sys.argv):
    operation = sys.argv[1]
    if operation == "list":
        print("\n".join(available_distrobutions()))
    elif operation == "build":
        tag, target = sys.argv[2:]
        try:
            for archive in build_target(tag=tag, target=target):
                print(archive)
        except BuildFailure as e:
            logging.error(e)
            pass
    else:
        logging.error("unknown operation %r", operation)


if __name__ == "__main__":
    configure_logging(debug=True)
    cli()
