#!/usr/bin/env python3

import os.path as osp
import argparse
import logging
from http import HTTPStatus
import synapseclient
from lrgasp import LrgaspException, handle_prog_errors
from lrgasp import loggingOps
from lrgasp import entry_metadata

from lrgasp.synapse_access import add_login_args, syn_connect, prog_error_syn_excepts, get_project_by_name, FileTree
from lrgasp.experiment_metadata import get_models_gtf, get_read_model_map_tsv, get_rna_fasta, get_expression_tsv

upload_logger = loggingOps.setupStderrLogger("upload", level=logging.INFO)
upload_logger.propagate = False


def parse_args():
    desc = """Upload an LGASP entry to Synapse. This uploads LRGASP metedata defined files from an
    entry directory to a  Synapse project.  This does not submit for evaluation.  A entry maybe resubmitted
    if needed, updating changed files

    Use --logDebug when reporting a problem.
    """
    parser = argparse.ArgumentParser(description=desc)
    add_login_args(parser)
    loggingOps.addCmdOptions(parser)
    parser.add_argument("project_spec",
                        help="name or synapse id of Synaapes project where data is to be uploaded. "
                        "this may include sub-directories that are create as needed")
    parser.add_argument("entry_dirs", nargs='+',
                        help="entry directories")
    return parser.parse_args()

def entry_tree_add_experiment(entry_tree, entry_node, experiment_md):
    experiment_node = entry_tree.add(experiment_md.experiment_id, entry_node, isdir=True)
    entry_tree.add(experiment_md.experiment_json, experiment_node)

    # these are None is not defined for experiment type
    data_files = (get_models_gtf(experiment_md),
                  get_read_model_map_tsv(experiment_md),
                  get_rna_fasta(experiment_md),
                  get_expression_tsv(experiment_md))
    for data_file in data_files:
        if data_file is not None:
            entry_tree.add(data_file, experiment_node)

def build_entry_tree(entry_md):
    "construct file tree from entry metadata"
    entry_tree = FileTree()
    entry_node = entry_tree.add(entry_md.entry_id, None, isdir=True)
    entry_tree.add(entry_md.entry_json, entry_node)
    for experiment_md in entry_md.experiments:
        entry_tree_add_experiment(entry_tree, entry_node, experiment_md)
    return entry_tree

def load_entry(entry_dir):
    entry_md = entry_metadata.load_dir(entry_dir)
    entry_metadata.load_experiments_metadata(entry_md)
    entry_tree = build_entry_tree(entry_md)
    return entry_md, entry_tree

def ensure_dir(syn, parent_synid, dirname):
    dir_synid = syn.findEntityId(dirname, parent_synid)
    if dir_synid is None:
        dir_synid = syn.store(synapseclient.Folder(dirname, parent_synid))
    return dir_synid

def recursive_make_dir(syn, parent_synid, dirpath):
    "returns synid of lowest directory in path"
    parts = dirpath.split('/', 1)
    dir_synid = ensure_dir(syn, parent_synid, parts[0])
    if len(parts) > 1:
        dir_synid = recursive_make_dir(syn, dir_synid, parts[1])
    return dir_synid

def get_syn_upload_project(syn, project_synid):
    "Make sure folder exist and is writable. Return the entity"
    try:
        project_entity = syn.get(project_synid)
    except synapseclient.core.exceptions.SynapseHTTPError as ex:
        status_code = ex.response.status_code
        if status_code == HTTPStatus.NOT_FOUND:
            raise LrgaspException(f"upload destination folder {project_synid} does not exist") from ex
        elif status_code == HTTPStatus.FORBIDDEN:
            raise LrgaspException(f"synapse user '{syn.username}' does not have access to upload destination folder {project_synid}") from ex
        else:
            raise LrgaspException(f"unexpected HTTP error {status_code} attempting to check upload destination folder {project_synid}") from ex

    # check user has access
    perms = set(syn.getPermissions(project_entity, syn.username))
    needed_perms = set(['READ', 'CREATE', 'UPDATE', 'DELETE', 'DOWNLOAD'])
    if (perms & needed_perms) != needed_perms:
        raise LrgaspException(f"user '{syn.username}' does not have required permissions for {project_synid}, missing {needed_perms - perms}")
    return project_entity

def upload_directory(syn, entry_md, parent_synid, fnode):
    synid = syn.store(synapseclient.Folder(fnode.filename, parent_synid))
    for child_fnode in fnode.children:
        recursive_upload(syn, entry_md, synid, child_fnode)

def upload_file(syn, entry_md, parent_synid, fnode):
    fpath = fnode.get_path(osp.dirname(entry_md.entry_dir))
    upload_logger.info(f"syncing {fpath}")
    syn.store(synapseclient.File(fpath, name=fnode.filename, parent=parent_synid))

def recursive_upload(syn, entry_md, parent_synid, fnode):
    if fnode.isdir:
        upload_directory(syn, entry_md, parent_synid, fnode)
    else:
        upload_file(syn, entry_md, parent_synid, fnode)

def upload_entry(syn, entry_dir, parent_synid):
    entry_md, entry_tree = load_entry(entry_dir)
    recursive_upload(syn, entry_md, parent_synid, entry_tree.root)

def parse_project_spec(syn, project_spec):
    parts = project_spec.split('/', 1)
    if parts[0].startswith('syn'):
        project_synid = parts[0]
    else:
        project_synid = get_project_by_name(syn, parts[0])
    return (project_synid,
            parts[1] if len(parts) > 1 else None)

def get_upload_parent(syn, project_spec):
    "get directory under project where upload should happen"

    project_synid, project_dirpath = parse_project_spec(syn, project_spec)

    # make sure project exists and is writable, create sub-directories of project as needed
    get_syn_upload_project(syn, project_synid)
    parent_synid = project_synid
    if project_dirpath is not None:
        parent_synid = recursive_make_dir(syn, parent_synid, project_dirpath)
    return parent_synid


def main(args):
    loggingOps.setupFromCmd(args)
    try:
        syn = syn_connect(args)
        parent_synid = get_upload_parent(syn, args.project_spec)
        for entry_dir in args.entry_dirs:
            upload_entry(syn, entry_dir, parent_synid)
    except prog_error_syn_excepts as ex:
        handle_prog_errors(ex)

main(parse_args())
