#!/usr/bin/env python

try:
    import locale
    locale.setlocale(locale.LC_ALL, '')
except Exception:
    pass

import os
import json
import pprint
import traceback
import xml

import click
import requests
import bibtexparser
import html5lib
from docutils.core import publish_string
from docutils.parsers.rst import roles

HERE = os.path.abspath(os.path.dirname(__file__))
BASE_DIR = os.path.normpath(os.path.join(HERE, '..'))

ACCESS_TOKEN = os.environ['ZENODO_OAUTH_TOKEN']

BASE_URL = ''
VERBOSE = False

CONF = os.path.join(BASE_DIR, 'packaging/zenodo-upload.json')
bibtexfile = os.path.join(BASE_DIR, 'docs/source/references-libertem_live.bib')
readmefile = os.path.join(BASE_DIR, 'README.rst')
creatorsfile = os.path.join(BASE_DIR, 'packaging/creators.json')
contributorsfile = os.path.join(BASE_DIR, 'packaging/contributors.json')

idkeys = ['doi', 'eprint', 'url']


def fake_cite(name, rawtext, text, lineno, inliner,
              options={}, content=[]):
    """
    to prevent errors with sphinxcontrib bibtex roles, we register a fake here
    """
    return [], []


roles.register_local_role("cite", fake_cite)

config = json.load(open(CONF))

default_params = {'access_token': ACCESS_TOKEN}
default_headers = {"Content-Type": "application/json"}
deposition_path = 'deposit/depositions'


class ZenodoException(Exception):
    pass


def prettystring(r):
    result = 'Status code: %s \n' % r.status_code
    if r.text:
        result += (pprint.pformat(r.json()))
    return result


def prettylog(r):
    log(prettystring(r))


def log(s):
    if VERBOSE:
        print(s)


def load_bibtex():
    parser = bibtexparser.bparser.BibTexParser(common_strings=True)
    with open(bibtexfile, encoding='utf-8') as f:
        db = bibtexparser.load(f, parser)
    return db


def zenodo_deposition_get(snippet='', extra_params={}):
    params = dict(default_params)
    params.update(extra_params)
    r = requests.get(BASE_URL + deposition_path + snippet, params=params)
    return r


def zenodo_deposition_post(snippet='', json=None, data=None, extra_params={}, extra_headers={}):
    headers = dict(default_headers)
    headers.update(extra_headers)

    params = dict(default_params)
    params.update(extra_params)

    r = requests.post(BASE_URL + deposition_path + snippet,
        params=params,
        json=json,
        data=data,
        headers=headers)
    return r


def zenodo_deposition_put(snippet='', json={}, extra_params={}, extra_headers={}):
    headers = dict(default_headers)
    headers.update(extra_headers)

    params = dict(default_params)
    params.update(extra_params)

    r = requests.put(BASE_URL + deposition_path + snippet,
        params=params,
        json=json,
        headers=headers)
    return r


def zenodo_deposition_delete(snippet='', extra_params={}):
    params = dict(default_params)
    params.update(extra_params)
    r = requests.delete(BASE_URL + deposition_path + snippet, params=params)
    return r


def list_depositions():
    r = zenodo_deposition_get()
    if r.status_code != 200:
        raise ZenodoException(prettystring(r))
    return r


def create_deposition():
    r = zenodo_deposition_post()
    if r.status_code != 201:
        raise ZenodoException(prettystring(r))
    return r


def get_deposition(deposition_id):
    r = zenodo_deposition_get(snippet='/%s' % deposition_id)
    if r.status_code != 200:
        raise ZenodoException(prettystring(r))
    return r


def update_deposition(deposition_id, data):
    r = zenodo_deposition_put(snippet='/%s' % deposition_id, json=data)
    if r.status_code != 200:
        raise ZenodoException(prettystring(r))
    return r


def new_version(parent_id):
    snippet = '/%s/actions/newversion' % parent_id
    r = zenodo_deposition_post(snippet=snippet)
    if r.status_code != 201:
        raise ZenodoException(prettystring(r))
    return r


def get_latest_draft(r):
    data = r.json()
    url = data['links']['latest_draft']
    parts = url.split('/')
    # The last bit of the URL is the ID
    return parts[-1]


def get_file_ids(r):
    data = r.json()
    return [f['id'] for f in data['files']]


def delete_file(deposition_id, file_id):
    snippet = f'/{deposition_id}/files/{file_id}'
    r = zenodo_deposition_delete(snippet=snippet)
    if r.status_code != 204:
        raise ZenodoException(prettystring(r))
    return r


def upload_file(deposition_id, filename, remote_filename=None):
    if remote_filename is None:
        remote_filename = os.path.basename(filename)

    data = {'filename': remote_filename}
    snippet = '/%s/files' % deposition_id

    with open(filename, 'rb') as fh:

        files = {'file': fh}
        # We can't use zenodo_deposition_post() because the data is encoded as multipart/form-data
        # for upload, different from the JSON of other actions.
        r = requests.post(BASE_URL + deposition_path + snippet,
            data=data, files=files, params=default_params)
    if r.status_code != 201:
        raise ZenodoException(prettystring(r))
    return r


def make_creators():
    with open(creatorsfile, encoding='utf-8') as f:
        raw_creators = json.load(f)

    result = []
    for c in raw_creators:
        creator = {
            'name': c['authorname']
        }
        for key in ['affiliation', 'orcid']:
            try:
                creator[key] = c[key]
            except KeyError:
                pass
        result.append(creator)
    return result


def make_contributors():
    with open(contributorsfile, encoding='utf-8') as f:
        raw_contributors = json.load(f)

    result = []
    for c in raw_contributors:
        contributor = {
            'name': c['authorname'],
            'type': 'Other'
        }

        for key in ['affiliation', 'orcid']:
            try:
                contributor[key] = c[key]
            except KeyError:
                pass
        result.append(contributor)
    return result


def extract(html_string):
    document = html5lib.parse(html_string, namespaceHTMLElements=False)
    elem = document.find(".//div[@class='document']")

    def walk(elem, drop_first_p):
        # Convert <h1></h1> to <p><strong></strong></p>
        if elem.tag == 'h1':
            text = elem.text
            elem.clear()
            elem.tag = 'p'
            strong = xml.etree.ElementTree.Element('strong')
            strong.text = text
            elem.append(strong)
        # recurse
        for child in elem:
            # The first <p> contains the badges which don't work well on zenodo
            # FIXME removing the first <p> in the document is fragile, but for now the easiest path.
            # Do a proper solution if this stops working.
            if drop_first_p and child.tag == 'p':
                elem.remove(child)
                drop_first_p = False
            walk(child, drop_first_p)

    walk(elem, True)

    quicklinks = [
        ("Homepage", "https://libertem.github.io/LiberTEM-live"),
        ("GitHub repository", "https://github.com/LiberTEM/LiberTEM-live"),
        ("PyPI", "https://pypi.org/project/libertem-live/"),
    ]
    for i, (label, url) in enumerate(quicklinks):
        e = xml.etree.ElementTree.Element('strong')
        e.text = "%s: " % label
        link = xml.etree.ElementTree.SubElement(e, "a", attrib={'href': url})
        link.text = url
        xml.etree.ElementTree.SubElement(e, "br")

        elem.insert(i, e)

    res = html5lib.serialize(elem, omit_optional_tags=False)
    return res


def make_description():
    with open(readmefile) as srcf:
        readme_html = publish_string(srcf.read(), writer_name="html")
        extracted = extract(readme_html)
        return extracted


def make_related(db):
    related = []
    for e in db.entries:
        identifier = None
        for k in idkeys:
            if k in e:
                identifier = e[k]
                break
        if identifier is None:
            continue
        related.append(dict(identifier=identifier, relation='cites'))
    return related


def make_references(db):
    def format_reference(e):
        key = e['ID']
        authors = e.get('author', None)
        year = e.get('year', None)
        title = e.get('title', None)
        publisher = e.get('publisher', None)
        identifier = None
        for k in idkeys:
            if k in e:
                identifier = e[k]
                break
        result = "[%s]" % key
        if authors is not None:
            result += " %s" % authors
        if year is not None:
            result += " (%s):" % year
        else:
            result += ':'

        if title is not None:
            result += " %s." % title
        if publisher is not None:
            result += " %s." % publisher
        if identifier is not None:
            result += " %s" % identifier
        return result

    references = []

    for e in db.entries:
        # We may encounter unexpected things, although
        # the format function should be safe.
        try:
            references.append(format_reference(e))
        except Exception as ex:
            log(ex)
            log(e)

    return references


def read_version(version_file):
    res = {}
    with open(version_file) as f:
        exec(f.read(), res)
    return res['__version__']


def get_version_fn():
    return os.path.join(BASE_DIR, 'src', 'libertem_live', '__version__.py')


def make_data():
    db = load_bibtex()
    data = config['template']
    metadata = data['metadata']
    version = read_version(get_version_fn())
    metadata['creators'] = make_creators()
    metadata['contributors'] = make_contributors()
    metadata['description'] = make_description()
    metadata['related_identifiers'] = make_related(db)
    metadata['references'] = make_references(db)
    metadata['version'] = version
    metadata['title'] += version
    return data


@click.command()
@click.option('--concept', default="260887")
@click.option('--url', default='https://sandbox.zenodo.org/api/')
@click.option('--verbose', '-v', is_flag=True)
@click.option('--mask-zenodo-exception', is_flag=True)
@click.argument('files', nargs=-1, required=True, type=click.File())
def main(files, concept, url, verbose, mask_zenodo_exception):
    try:
        # raise ZenodoException("Test")
        global BASE_URL
        BASE_URL = url

        global VERBOSE
        VERBOSE = verbose

        depositions = list_depositions().json()
        log(
            "Found the following depositions: "
            f"{[(d['id'], d['title'], d['conceptrecid']) for d in depositions]}"
        )
        deposition_id = None
        submitted = None
        n_found = 0

        for deposition in depositions:
            if deposition['conceptrecid'] == concept:
                deposition_id = deposition['id']
                submitted = deposition['submitted']
                log(f"found {deposition_id} in state {deposition['state']}")
                n_found += 1

        if deposition_id is None or submitted is None:
            raise ZenodoException("Didn't find a record matching the concept ID", concept)

        if n_found != 1:
            raise ZenodoException(f"Found {n_found} matching records, expected exactly one")

        if submitted:
            log("Creating new version of deposition %s..." % deposition_id)
            r = new_version(parent_id=deposition_id)
            prettylog(r)
            deposition_id = r.json()['id']

        log("Updating draft deposition %s..." % deposition_id)
        r = update_deposition(deposition_id=deposition_id, data=make_data())
        prettylog(r)

        for file_id in get_file_ids(r):
            log("Deleting inherited file %s..." % file_id)
            r = delete_file(deposition_id, file_id)
            prettylog(r)

        for fh in files:
            log("Uploading file %s..." % fh.name)
            upload_result = upload_file(deposition_id=deposition_id, filename=fh.name)
            prettylog(upload_result)

        log("Finished!")
    except ZenodoException as e:
        if mask_zenodo_exception:
            print("Masking Zenodo exception:")
            traceback.print_exception(type(e), e, e.__traceback__)
        else:
            raise


if __name__ == '__main__':
    main()
