#!/usr/bin/env python

# This is a frustration-free runner for the `excavator` utility.
# See `sandbox/scripts/tools/excavator` for its source code.

from __future__ import print_function, unicode_literals

import os
import sys
import json
import stat
import time
import errno
import string
import random
import hashlib

try:
    from urllib2 import urlopen
except ImportError:
    from urllib.request import urlopen

RETRIES = 5
HASH_PREFIX = 10
UPDATE_INTERVAL = 24 * 3600  # seconds

DIR_PATH = os.path.join(os.path.expanduser("~"), ".sandbox/excavator")
BINARY_PATH = os.path.join(DIR_PATH, "excavator")
META_PATH = os.path.join(DIR_PATH, ".meta")

RESOURCE_LIST_URL = (
    "https://sandbox.yandex-team.ru/api/v2/resource?"
    "limit=1&type=EXCAVATOR_BINARY&attrs={%22released%22:%20%22stable%22}&owner=SANDBOX"
)


def print_and_exit(message, exit_code=1):
    print(message, file=sys.stderr)
    sys.exit(exit_code)


def create_dirs(path):
    try:
        os.makedirs(path)
    except OSError as e:

        if e.errno != errno.EEXIST:
            raise
    return path


def _fetch(url, into):
    md5 = hashlib.md5()
    sys.stderr.write("Downloading {} ".format(url))
    conn = urlopen(url, timeout=10)
    sys.stderr.write("[")
    try:
        with open(into, "wb") as f:
            while True:
                block = conn.read(1024 * 1024)
                sys.stderr.write(".")
                if block:
                    md5.update(block)
                    f.write(block)
                else:
                    break
        return md5.hexdigest()

    finally:
        sys.stderr.write("] ")


def _atomic_fetch(url, into, md5):
    rnd = "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
    tmp_dest = into + "." + rnd
    try:
        real_md5 = _fetch(url, tmp_dest)
        if real_md5 != md5:
            raise Exception("MD5 mismatched: {} differs from {}".format(real_md5, md5))
        os.rename(tmp_dest, into)
        sys.stderr.write("OK\n")
    except Exception as e:
        print_and_exit("ERROR: {}".format(e))
    finally:
        try:
            os.remove(tmp_dest)
        except OSError:
            pass


def _get(urls, into, md5):
    dest_path = os.path.join(os.path.dirname(into), md5[:HASH_PREFIX])

    if not os.path.exists(dest_path):
        for i in range(RETRIES):
            # noinspection PyBroadException
            try:
                _atomic_fetch(urls[i % len(urls)], dest_path, md5)
                break
            except Exception:
                if i + 1 == RETRIES:
                    raise
                else:
                    time.sleep(i)
    os.rename(dest_path, into)
    return into


def run():
    env = os.environ.copy()
    os.execve(BINARY_PATH, [BINARY_PATH] + sys.argv[1:], env)


def main():
    binary_exists = os.path.exists(BINARY_PATH)
    meta_exists = os.path.exists(META_PATH)
    up_to_date = meta_exists and (time.time() - os.stat(META_PATH).st_ctime) < UPDATE_INTERVAL

    if binary_exists and up_to_date:
        run()

    meta = {}
    if meta_exists and binary_exists:
        with open(META_PATH, "r") as f:
            meta = json.load(f)

    try:
        if not binary_exists:
            # Log binary update only if there is no binary available to run (in case the update fails)
            print("Fetching the latest resource from Sandbox...", file=sys.stderr)

        conn = urlopen(RESOURCE_LIST_URL, timeout=10)
        if conn.getcode() != 200:
            print_and_exit(conn.read())

        resp = json.load(conn)
        if not resp["items"]:
            print_and_exit("No resources found")

        resource = resp["items"].pop()

        new_meta = {"id": resource["id"]}

        if meta != new_meta:
            _get([resource["http"]["proxy"]], BINARY_PATH, resource["md5"])
            st = os.stat(BINARY_PATH)
            os.chmod(BINARY_PATH, st.st_mode | stat.S_IEXEC)

        with open(META_PATH, "w") as f:
            json.dump(new_meta, f)

    except TypeError:
        print("not found MD5", file=sys.stderr)
    except Exception as e:
        if not os.path.exists(BINARY_PATH):
            # Log binary update only if there is no binary available to run (in case the update fails)
            print("Could not update binary: " + str(e), file=sys.stderr)

    if os.path.exists(BINARY_PATH):
        run()


if __name__ == "__main__":
    create_dirs(DIR_PATH)
    main()
