#!/usr/bin/env python3

from requests import Session
from bs4 import BeautifulSoup as bs
from configparser import RawConfigParser
from datetime import datetime
from datetime import timedelta
from sys import platform
import os, sys
import errno
import argparse
import json
import getpass

# Get command line options
parser = argparse.ArgumentParser(description="A command line client for interacting with WeLearn.", formatter_class=argparse.RawTextHelpFormatter)

parser.add_argument("action", nargs=1, help="choose from\n\
    files       - downloads files/resources\n\
    assignments - lists assignments, downloads attachments\n\
    urls        - lists urls\n\
    courses     - lists enrolled courses\n\
    whoami      - shows the user's name and exits\n\
Abbreviations such as any one of 'f', 'a', 'u', 'c', 'w' are supported.")
parser.add_argument("courses", nargs="*", help="IDs of the courses to download files from. The word ALL selects everything from the [courses] section in .welearnrc or welearn.ini")
parser.add_argument("-d", "--dueassignments", action="store_true", help="show only due assignments with the 'assignments' action")
parser.add_argument("-i", "--ignoretypes", nargs="*", help="ignores the specified extensions when downloading, overrides .welearnrc")
parser.add_argument("-f", "--forcedownload", action="store_true", help="force download files even if already downloaded/ignored")
parser.add_argument("-p", "--pathprefix", nargs=1,  help="save the downloads to a custom path, overrides .welearnrc",)

args = parser.parse_args()

# Get the mode
action = ""
if "files".startswith(args.action[0]):
    action = "files"
elif "assignments".startswith(args.action[0]):
    action = "assignments"
elif "urls".startswith(args.action[0]):
    action = "urls"
elif "courses".startswith(args.action[0]):
    action = "courses"
elif "whoami".startswith(args.action[0]):
    action = "whoami"
else:
    print("Invalid action! Use the -h flag for usage.")
    sys.exit(errno.EPERM)

if args.dueassignments and action != "assignments":
    print("Can only use --dueassignments with 'assignments' action! Use the -h flag for usage.")
    sys.exit(errno.EPERM)

# Read the .welearnrc file from the home directory, and extract username and password
if platform == "linux" or platform == "linux2":
    configfile = os.path.expanduser("~/.welearnrc")
elif platform == "darwin":
    configfile = os.path.expanduser("~/.welearnrc")
elif platform == "win32":
    configfile = os.path.expanduser("~/welearn.ini")
config = RawConfigParser(allow_no_value=True)
config.read(configfile)

username = ""
password = ""
try:
    username = config["auth"]["username"]
except KeyError:
    username = input("Username : ")

try:
    password = config["auth"]["password"]
except KeyError:
    password = getpass.getpass("Password : ", stream=None)

# Also extract the list of 'ALL' courses
try:
    all_courses = list(config["courses"].keys())
except KeyError:
    all_courses = []
all_courses = map(str.strip, all_courses)
all_courses = map(str.upper, all_courses)
all_courses = list(all_courses)

# Select all courses from config if 'ALL' keyword is used
if 'ALL' in map(str.upper, args.courses):
    args.courses = all_courses

# Read ignore types from config
ignore_types = []
try:
    ignores = config["files"]["ignore"]
    ignore_types = ignores.split(",")
except KeyError:
    ignore_types = []

# Override config with options
if args.ignoretypes:
    ignore_types = args.ignoretypes

# Override ignore with force
if args.forcedownload:
    ignore_types = []

ignore_types = map(str.strip, ignore_types)
ignore_types = map(str.upper, ignore_types)
ignore_types = list(ignore_types)

# Read pathprefix from config
try:
    prefix_path = os.path.expanduser(config["files"]["pathprefix"])
    prefix_path = os.path.abspath(prefix_path)
    if not os.path.isdir(prefix_path):
        print(prefix_path, "does not exist! Please create an empty directory ", prefix_path)
        sys.exit(errno.ENOTDIR)
except KeyError:
    prefix_path = ""

# Override pathprefix config if -p flag is used
if args.pathprefix:
    prefix_path = os.path.expanduser(args.pathprefix[0])
    prefix_path = os.path.abspath(prefix_path)
    if not os.path.isdir(prefix_path):
        print(prefix_path, "does not exist!")
        sys.exit(errno.ENOTDIR)


link_cache_filepath = os.path.join(prefix_path, ".link_cache")

# Read from a cache of links
def read_link_cache():
    link_cache = dict()
    if os.path.exists(link_cache_filepath):
        with open(link_cache_filepath) as link_cache_file:
            link_cache = json.load(link_cache_file)
    return link_cache

# Update cached links
def write_link_cache(link_cache):
    with open(link_cache_filepath, "w") as link_cache_file:
        json.dump(link_cache, link_cache_file)

# Helper function to retrieve a file/resource from the server
def get_resource(res, prefix, course, cache, indent=0):
    filename = res['filename']
    course_dir = os.path.join(prefix, course)
    filepath = os.path.join(course_dir, filename)
    fileurl = res['fileurl']
    _, extension = os.path.splitext(filename)
    extension = str.upper(extension[1:])
    timemodified = int(res['timemodified'])

    # Only download if forced, or not already downloaded
    if not args.forcedownload and fileurl in link_cache:
        cache_time = int(link_cache[fileurl])
        # Check where the latest version of the file is in cache
        if timemodified == cache_time:
            return

    # Ignore files with specified extensions
    if extension in ignore_types:
        print(" " * indent + "Ignoring " + course, ":", filename)
        return

    # Create the course folder if not already existing
    if not os.path.exists(course_dir):
        os.makedirs(course_dir)

    # Download the file and write to the folder
    print(" " * indent + "Downloading " + course, ":", filename, end='')
    response = s.post(fileurl, data = {'token' : token})
    with open(filepath, "wb") as download:
        download.write(response.content)
    print(" ... DONE")

    # Add the file url to the cache
    cache[fileurl] = timemodified

def get_courses_by_id():
    # Get a list of all courses
    courses_response = s.post(server_url, \
        data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'core_course_get_courses_by_field'})
    # Parse as json
    courses = json.loads(courses_response.content)
        
    # Create a dictionary of course ids versus course names
    course_ids = dict()
    for course in courses['courses']:
        course_name = course['shortname']
        if course_name in args.courses:
            course_ids[course['id']] = course_name
    return course_ids

with Session() as s:
    # Login to WeLearn with supplied credentials
    login_url = "https://welearn.iiserkol.ac.in/login/token.php"
    login_response = s.post(login_url, data = {'username' : username, 'password' : password, 'service' : 'moodle_mobile_app'})
    token = json.loads(login_response.content)['token']

    server_url = "https://welearn.iiserkol.ac.in/webservice/rest/server.php"

    if action == "whoami":
        # Get core user information
        info_response = s.post(server_url, \
            data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'core_webservice_get_site_info'})
        # Parse as json
        info = json.loads(info_response.content)
        print(info['fullname'])
        sys.exit(0)

    elif action == "courses":
        # Get core user information
        info_response = s.post(server_url, \
            data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'core_webservice_get_site_info'})
        # Parse as json
        info = json.loads(info_response.content)
        userid = info["userid"]
        
        # Get enrolled courses information
        courses_response = s.post(server_url, \
            data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'core_enrol_get_users_courses', 'userid' : userid})
        # Parse as json
        courses = json.loads(courses_response.content)
        for course in courses:
            course_name = course["fullname"]
            star = " "
            if course["isfavourite"]:
                star = "*"
            print(f" {star} {course_name}")
        sys.exit(0)

    elif action == "assignments":
        link_cache = read_link_cache()
        # Get assignment data from server
        assignments_response = s.post(server_url, \
            data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'mod_assign_get_assignments'})
        # Parse as json
        assignments = json.loads(assignments_response.content)

        # Assignments are grouped by course
        for course in assignments['courses']:
            course_name = course['shortname']
            # Ignore unspecified courses
            if course_name not in args.courses:
                continue
            no_assignments = True
            for assignment in course['assignments']:
                # Get the assignment name, details, due date, and relative due date
                name = assignment['name']
                duedate = datetime.fromtimestamp(int(assignment['duedate']))
                due_str = duedate.strftime('%a %d %b, %Y, %H:%M:%S')
                duedelta = duedate - datetime.now()
                # Calculate whether the due date is in the future
                due = duedelta.total_seconds() > 0
                if args.dueassignments and not due:
                    continue
                no_assignments = False
                if not no_assignments:
                    print(course_name)
                # Show assignment details
                duedelta_str = f"{abs(duedelta.days)} days, {duedelta.seconds // 3600} hours"
                detail = bs(assignment['intro'], "html.parser").text
                print(f"    {name} - {detail}")
                for attachment in assignment['introattachments']:
                    print(f"        Attachment     : {attachment['filename']}")
                    get_resource(attachment, prefix_path, course_name, link_cache, indent=8)
                if due:
                    print(f"        Due on         : {due_str}")
                    print(f"        Time remaining : {duedelta_str}")
                else:
                    print(f"        Due on         : {due_str} ({duedelta_str} ago)")

                # Get submission details
                submission_response = s.post(server_url, \
                    data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'mod_assign_get_submission_status', \
                            'assignid' : assignment['id']})
                submission = json.loads(submission_response.content)
                submission_made = False
                for plugin in submission['lastattempt']['submission']['plugins']:
                    if plugin['name'] == "File submissions":
                        for filearea in plugin['fileareas']:
                            if filearea['area'] == 'submission_files':
                                for submitted_file in filearea['files']:
                                    submission_made = True
                                    filename = submitted_file['filename']
                                    submission_date = datetime.fromtimestamp(int(submitted_file['timemodified']))
                                    submission_date_str = submission_date.strftime('%a %d %b, %Y, %H:%M:%S')
                                    print(f"        Submission     : {filename} ({submission_date_str})")
                if not submission_made:
                    print(f"        Submission     : NONE")
                print()
        write_link_cache(link_cache)
        sys.exit(0)

    elif action == "urls":
        course_ids = get_courses_by_id()

        # Get a list of available urls
        urls_response = s.post(server_url, \
            data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'mod_url_get_urls_by_courses'})
        # Parse as json
        urls = json.loads(urls_response.content)
        
        # Iterate through all urls, and build a dictionary
        url_list = dict()
        for url in urls['urls']:
            if url['course'] in course_ids:
                course_name = course_ids[url['course']]
                if not course_name in url_list:
                    url_list[course_name] = []
                url_list[course_name].append(url)

        # Display all urls
        for course_name in args.courses:
            if not course_name in url_list:
                continue
            no_url = True
            for url in url_list[course_name]:
                if no_url:
                    print(course_name)
                no_url = False
                url_name = url["name"]
                url_detail = bs(url["intro"], "html.parser").text
                url_link = url["externalurl"]
                print(f"    {url_name} - {url_detail}")
                print(f"        Link : {url_link}")
                print()
            print()
        sys.exit(0)

    elif action == "files":
        link_cache = read_link_cache()
        course_ids = get_courses_by_id()

        # Get a list of available resources
        resources_response = s.post(server_url, \
            data = {'wstoken' : token, 'moodlewsrestformat' : 'json', 'wsfunction' : 'mod_resource_get_resources_by_courses'})
        # Parse as json
        resources = json.loads(resources_response.content)

        # Iterate through all resources, and only fetch ones from the specified course
        for resource in resources['resources']:
            if resource['course'] in course_ids:
                course_name = course_ids[resource['course']]
                for subresource in resource['contentfiles']:
                    get_resource(subresource, prefix_path, course_name, link_cache)
        
        write_link_cache(link_cache)

