#!/usr/bin/env python3
# Copyright 2018 Ciena Corporation
#
# Licensed 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.
"""
Download intersphinx inventories

This script attempts to be compatible with the Travis CI caching feature to
reduce load on upstream servers and improve build reliability. It maintains
a local cache, tracking how long ago each inventory file was last downloaded.
This cache is shared among all Travis CI builds (see travis.yml). Pull request
build *always* use the cache, as Travis never persists changes made in pull
requests.
"""

import ast
from datetime import datetime, timedelta, timezone
import json
import os
from shutil import copyfileobj
import sys
from urllib.request import urlopen

#: How long to keep files before re-downloading?
CACHE_PERIOD = timedelta(days=7)

docs_dir = os.path.normpath(os.path.join(__file__, '../../docs'))
meta_path = os.path.join(docs_dir, '_cache/meta.json')


def read_intersphinx_mapping():
    """
    Search for a simple assignment to intersphinx_mapping in conf.py. We use the
    AST to avoid evaling the file which would require importing afkak etc.
    """
    conf_file = os.path.join(docs_dir, 'conf.py')
    with open(conf_file, 'r') as f:
        mod = ast.parse(f.read())
    for node in ast.iter_child_nodes(mod):
        if not isinstance(node, ast.Assign):
            continue
        [target] = node.targets
        if not isinstance(target, ast.Name):
            continue
        if target.id != 'intersphinx_mapping':
            continue
        return ast.literal_eval(node.value)
    else:
        print("Failed to find intersphinx_mapping assignment in", conf_file)
        sys.exit(1)


def read_cache_meta():
    """
    Load the cache metadata file.

    The format is a list of (url, timestamp) tuples, JSON-encoded.

    :resturns:
        `dict` mapping `str` URLs → `datetime.datetime` download timestamps.
    """
    try:
        with open(meta_path) as f:
            raw = json.load(f)
    except FileNotFoundError:
        raw = []
    return {url: datetime.fromtimestamp(last_dl, timezone.utc)
            for url, last_dl in raw}


def write_cache_meta(meta):
    obj = sorted([url, last_dl.timestamp()]
                 for url, last_dl in meta.items())
    with open(meta_path, 'w') as f:
        json.dump(obj, f, indent=2)


# Is this a Travis CI pull request?
# See https://docs.travis-ci.com/user/environment-variables/#Convenience-Variables
is_pr = os.environ.get('TRAVIS_PULL_REQUEST', 'false') != 'false'

stale_threshold = datetime.now(timezone.utc) - CACHE_PERIOD
meta = read_cache_meta()
for base_url, path in sorted(read_intersphinx_mapping().values()):
    url = base_url + 'objects.inv'
    inv_path = os.path.join(docs_dir, path)

    failures = 0
    while True:
        if failures >= 3:
            print("Failed to download", url)
            sys.exit(1)

        if os.path.isfile(inv_path):
            if is_pr:
                # There is no point in updating the cache on PRs as it is never
                # persisted, so we only download files which are missing.
                print("Using", path, "(pull request)")
                break

            if url in meta and meta[url] >= stale_threshold:
                print("Using {} (age {})".format(
                    path, datetime.now(timezone.utc) - meta[url],
                ))
                break

        print('Downloading', url, 'to', inv_path)
        tmp_path = inv_path + '.new'
        try:
            with open(tmp_path, 'wb') as fout:
                with urlopen(url, timeout=30) as fin:
                    if fin.status != 200:
                        print('Request failed: HTTP', fin.status, fin.reason, fin.geturl())
                        print('Will retry in 10 seconds')
                        time.sleep(10)
                        continue
                    copyfileobj(fin, fout)
        except Exception as e:
            print("Failed to download", url, e)
            failures += 1
        else:
            os.rename(tmp_path, inv_path)
            meta[url] = datetime.now(timezone.utc)
            write_cache_meta(meta)
        finally:
            if os.path.isfile(tmp_path):
                os.unlink(tmp_path)
