#!/usr/bin/env python3
"""
Deployment manager daemon for aegea deploy.

For more information, see ``aegea deploy --help``.
"""

from __future__ import absolute_import, division, print_function, unicode_literals
import os, sys, subprocess, json, logging, argparse, shutil, signal
import boto3, requests
from datetime import datetime

class ARN:
    fields = "arn partition service region account_id resource".split()

    def __init__(self, arn="arn:aws::::", **kwargs):
        self.__dict__.update(dict(zip(self.fields, arn.split(":", 5)), **kwargs))

    def __str__(self):
        return ":".join(getattr(self, field) for field in self.fields)

def get_metadata(path):
    return requests.get("http://169.254.169.254/latest/meta-data/{}".format(path)).content.decode()

def get_topic(name):
    sns_topic_arn = ARN(service="sns", region=sns.meta.client.meta.region_name, account_id=account_id, resource=name)
    return sns.Topic(str(sns_topic_arn))

def clean_old_builds(build_root, prefix, min_old_builds=2):
    build_dirs = [os.path.join(build_root, d) for d in os.listdir(build_root) if d.startswith(prefix)]
    build_dirs = [d for d in build_dirs if os.path.isdir(d) and not os.path.islink(d)]
    build_dirs = sorted(build_dirs, key=lambda d: os.stat(d).st_mtime)
    for d in build_dirs[:-min_old_builds]:
        logging.warn("Deleting old build %s", d)
        shutil.rmtree(d, ignore_errors=True)

def run_git_command(*cmd, **kwargs):
    kwargs["env"] = dict(kwargs.get("env", os.environ), GIT_SSH_COMMAND="aegea-git-ssh-helper")
    return subprocess.check_call(["git"] + list(cmd), **kwargs)

def clone_and_build(branch, destination):
    if not os.path.exists(os.path.dirname(destination)):
        os.makedirs(os.path.dirname(destination))
    deploy_dir = "{}-{}-{}".format(args.gh_repo_name, branch, datetime.now().strftime("%Y-%m-%d-%H-%M-%S"))
    deploy_dir = os.path.join(os.path.dirname(destination), deploy_dir)
    # TODO: sanity check: is event timestamp within X of now?
    logging.info("Deploying %s to %s", branch, deploy_dir)
    if os.path.lexists(destination):
        run_git_command("clone", "--recurse-submodules", "--branch", branch, destination, deploy_dir)
        run_git_command("remote", "set-url", "origin", args.github_url, cwd=deploy_dir)
        run_git_command("fetch", "origin", branch, cwd=deploy_dir)
        run_git_command("checkout", "-B", branch, "--track", "origin/" + branch, cwd=deploy_dir)
    else:
        run_git_command("clone", "--recurse-submodules", "--branch", branch, "--depth=1", args.github_url, deploy_dir)
    if "BUILD_COMMAND" in os.environ:
        subprocess.check_call(os.environ["BUILD_COMMAND"], shell=True, cwd=deploy_dir, executable="/bin/bash")
    else:
        subprocess.check_call(["make", "-C", deploy_dir])
    os.symlink(deploy_dir, destination + "_staged")
    try:
        os.unlink(destination)
    except Exception:
        pass
    os.rename(destination + "_staged", destination)
    if "RELOAD_COMMAND" in os.environ:
        subprocess.check_call(os.environ["RELOAD_COMMAND"], shell=True, cwd=deploy_dir, executable="/bin/bash")
    else:
        subprocess.check_call(["make", "-C", deploy_dir, "reload"])

def update_status(**updates):
    try:
        bucket = s3.Bucket("deploy-status-" + account_id)
        return bucket.put_object(Key=os.path.join(os.path.basename(queue.url), "status"),
                                 Body=json.dumps(updates).encode("utf-8"))
    except Exception as e:
        logging.error(e)

def get_deploy_desc():
    try:
        cmd = ["git", "describe", "--always", "--all"]
        return subprocess.check_output(cmd, cwd=args.deploy_location).decode().strip()
    except Exception:
        return "Unknown"

def get_deploy_rev():
    try:
        cmd = ["git", "rev-parse", "--short", "HEAD"]
        return subprocess.check_output(cmd, cwd=args.deploy_location).decode().strip()
    except Exception:
        return "Unknown"

def deploy(branch):
    try:
        update_status(Status="Deploying " + branch, Ref=get_deploy_desc(), Commit=get_deploy_rev())
        clone_and_build(branch, args.deploy_location)
        clean_old_builds(os.path.dirname(args.deploy_location), prefix=args.gh_repo_name + "-")
        update_status(Status="OK", Ref=get_deploy_desc(), Commit=get_deploy_rev())
    except Exception:
        update_status(Status="Failed", Ref=get_deploy_desc(), Commit=get_deploy_rev())
        raise

def init_queue():
    topic_name = "github-{}-{}-events".format(args.gh_owner_name, args.gh_repo_name)
    topic = get_topic(topic_name)
    # queue_name = "{}-{}-{}".format(topic_name, instance_id, os.getpid())
    queue_name = "github-{}-{}".format(topic_name, instance_id)
    queue = sqs.create_queue(QueueName=queue_name)

    policy = dict(Version="2012-10-17",
                  Statement=[dict(Effect="Allow", Principal="*", Action="sqs:SendMessage",
                                  Resource=queue.attributes["QueueArn"],
                                  Condition=dict(ArnEquals={"aws:SourceArn": topic.arn}))])
    # In multi-node deployments, use DelaySeconds to stagger builds
    queue.set_attributes(Attributes=dict(Policy=json.dumps(policy),
                                         MessageRetentionPeriod="300",
                                         VisibilityTimeout="10"))
    topic.subscribe(Protocol="sqs", Endpoint=queue.attributes["QueueArn"])
    return queue

def parse_args():
    logging.basicConfig(level=logging.INFO)
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--repo",
                        required="SYSTEMD_INSTANCE_NAME" not in os.environ and "GITHUB_REPO" not in os.environ)
    parser.add_argument("--branch",
                        required="SYSTEMD_INSTANCE_NAME" not in os.environ and "GITHUB_BRANCH" not in os.environ)
    parser.add_argument("--deploy-location", default="/opt")
    parser.add_argument("--deploy-key-name")
    args = parser.parse_args()

    if "GITHUB_OWNER" in os.environ and "GITHUB_REPO" in os.environ and "GITHUB_BRANCH" in os.environ:
        args.gh_owner_name = os.environ["GITHUB_OWNER"]
        args.gh_repo_name = os.environ["GITHUB_REPO"]
        args.branch = os.environ["GITHUB_BRANCH"]
    elif "SYSTEMD_INSTANCE_NAME" in os.environ:
        if "-repo-" in os.environ["SYSTEMD_INSTANCE_NAME"] and "-branch-" in os.environ["SYSTEMD_INSTANCE_NAME"]:
            args.gh_owner_name, repo_and_branch = os.environ["SYSTEMD_INSTANCE_NAME"].split("-repo-", 1)
            args.gh_repo_name, args.branch = repo_and_branch.split("-branch-", 1)
        else:
            args.gh_owner_name, args.gh_repo_name, args.branch = os.environ["SYSTEMD_INSTANCE_NAME"].split("-", 2)
    else:
        args.gh_owner_name, args.gh_repo_name = args.repo.split("/", 1)

    args.github_url = "git@github.com:{}/{}".format(args.gh_owner_name, args.gh_repo_name)
    args.deploy_location = os.path.abspath(os.path.join(args.deploy_location, args.gh_owner_name, args.gh_repo_name))
    if not args.deploy_key_name:
        args.deploy_key_name = "deploy.{}.{}".format(args.gh_owner_name, args.gh_repo_name)
    return args

args = parse_args()
az = get_metadata("placement/availability-zone")
instance_id = get_metadata("instance-id")
session = boto3.Session(region_name=az[:-1])
sqs, sns, s3 = session.resource("sqs"), session.resource("sns"), session.resource("s3")
account_id = ARN(json.loads(get_metadata("iam/info"))["InstanceProfileArn"]).account_id
queue = init_queue()

if not os.path.exists(args.deploy_location):
    try:
        deploy(args.branch)
    except Exception as e:
        logging.error("Failed in launch deployment: %s", e)
        update_status(Status="Failed", Ref=get_deploy_desc(), Commit=get_deploy_rev())

signal.signal(signal.SIGUSR1, lambda signum, frame: deploy(args.branch))

while True:
    for message in queue.receive_messages(WaitTimeSeconds=20):
        event = json.loads(json.loads(message.body)["Message"])
        if event["ref"] == "refs/heads/" + args.branch:
            deploy(args.branch)
            message.delete()
