#!/usr/bin/env python
"""This script is meant to run for `heptapod-*` tag pipelines only.

Starting with the target changeset of the release tag, it copies over the
frontend assets tarball to the expected location for the tag. It climbs up
the release branch along first parents to retry in case of error
(typically because the tarball has not been previously generated for the
given revision, which would happen, e.g., if we only change unrelated
dependencies or even just `HEPTAPOD_CHANGELOG.md`.
"""

import argparse
import os
import logging
from pathlib import Path
from subprocess import (
    check_output,
    run,
)
import sys

ASSETS_CHANGING_FILES = (
    'app/assets',
    'package.json'
)
HEPTAPOD_TARBALL_UPLOAD_KEY = os.environ['HEPTAPOD_TARBALL_UPLOAD_KEY']
MR_LABELS = os.environ.get('CI_MERGE_REQUEST_LABELS', '').lower()
FORCE_COMPILATION_LABEL = 'pipeline:update-cache'
TEST_ENV = os.environ.get('NODE_ENV', 'test').strip() == 'test'

THIS_FILE = Path(__file__)
BASEDIR = THIS_FILE.parent

logger = logging.getLogger(THIS_FILE.name)


def intermediate_artifact(sha):
    prefix = 'frontend-assets-test' if TEST_ENV else 'frontend-assets'
    return f'public/intermediate/src/{prefix}-{sha}.tar.xz'


def target_artifact_from_env():
    tag = os.environ.get('CI_COMMIT_TAG')
    if tag is not None and not TEST_ENV:
        tarball = tag.replace('heptapod', 'frontend-assets') + '.tar.xz'
        return f'public/src/{tarball}'

    return intermediate_artifact(os.environ['CI_COMMIT_SHA'])


def promote(source_sha, target_artifact):
    src = intermediate_artifact(source_sha)
    if src == target_artifact:  # happens at startup when not on a tag
        return False

    logger.info("Attempting server-side copy of %r to %r", src,
                target_artifact)
    result = run((THIS_FILE.parent / 'heptapod-sftp-copy-signed-artifact',
                  'upload.heptapod.net',
                  os.environ['HEPTAPOD_TARBALL_UPLOAD_KEY'],
                  src, target_artifact,
                  ))
    if result.returncode == 0:
        logger.info("Promoted frontend assets of %r as %r",
                    source_sha, target_artifact)
        return True
    else:
        logger.warning("Failed to promote frontend assets of %r. Most common "
                       "cause is them not existing on the upload server",
                       source_sha)


def reset(source_sha):
    run(('hg', 'up', source_sha), check=True)
    logger.info("Got back to original %r to avoid side effects such as"
                "modifying build scripts before failing", source_sha)


class FallbackNotPossible(RuntimeError):
    """Raised by :func:`fallback` to indicate it has reached an end."""


def fallback(start_branch):
    status = check_output(
        ('hg', 'status', '--rev', 'p1(.)') + ASSETS_CHANGING_FILES
    ).strip()

    if status:
        logger.error("Cannot go back one revision, as `hg status` reports "
                     "changes in files affecting assets %r",
                     ASSETS_CHANGING_FILES)
        raise FallbackNotPossible

    prev_branch, node = check_output(
        ('hg', 'log', '-T', '{branch}\n{node}\n', '-r', 'p1(.)')
    ).decode('ascii').splitlines()
    if prev_branch != start_branch:
        logger.warning("cannot go back one revision, as it is on branch %r "
                       "instead of the expected %r", prev_branch, start_branch)
        raise FallbackNotPossible

    logger.info("Going back one revision, to first parent %r", node)
    run(('hg', 'up', node), check=True)
    return node


def compile_upload(sha):
    reset(sha)
    logger.info("Compiling frontend assets for node %r", sha)
    run([BASEDIR / 'compile_assets'], check=True)
    logger.info("Uploading frontend assets for node %r", sha)
    run([BASEDIR / 'heptapod-upload-frontend-assets'], check=True)


def main():
    parser = argparse.ArgumentParser(
        description="Promote sha-labelled frontend assets of ancestor "
        "changesets to tag or current changeset, "
        "with fallback to compilation",
    )
    parser.add_argument(
        "--no-compile", action="store_true",
        help="If promotion fails, exit with error code instead of compiling."
    )
    cl_args = parser.parse_args()
    logger.info("Merge Request labels: %r", MR_LABELS)
    force_compile = FORCE_COMPILATION_LABEL in MR_LABELS
    if force_compile and cl_args.no_compile:
        parser.error("Cannot use `--no-compile` because compilation is "
                     "forced by environment variable.")

    source_sha = initial_sha = os.environ['CI_COMMIT_SHA']
    if force_compile:
        logger.warning("Compilation forced by environment variable. No "
                       "attempt at promoting assets of any ancestor "
                       "changeset")
        return compile_upload(initial_sha)

    start_branch = check_output(
        ('hg', 'log', '-T', '{branch}', '-r', '.')
    ).decode('ascii').strip()

    target_artifact = target_artifact_from_env()
    try:
        while not promote(source_sha, target_artifact):
            source_sha = fallback(start_branch)
    except FallbackNotPossible:
        if cl_args.no_compile:
            logger.error("Cannot promote, and explicitely told not to "
                         "compile assets, thus ending in error")
            sys.exit(1)

        compile_upload(initial_sha)


# stdout to avoid the need to flush stderr each time we call a subprocess
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
main()
