# coding=utf-8
import datetime
import collections
from functools import wraps
import hedy
import json
import jsonbin
import logging
import os
from os import path
import re
import requests
import uuid
import yaml
from flask_commonmark import Commonmark
from werkzeug.urls import url_encode
from config import config
from auth import auth_templates, current_user, requires_login, is_admin
from utils import db_get, db_get_many, db_set, timems, type_check, object_check, db_del

# app.py
from flask import Flask, request, jsonify, render_template, session, abort, g, redirect
from flask_compress import Compress

# Hedy-specific modules
import courses
import hedyweb

# Define and load all available language data
ALL_LANGUAGES = {
    'en': 'English',
    'nl': 'Nederlands',
    'es': 'Español',
    'fr': 'Français',
    'pt_br': 'Português',
    'de': 'Deutsch',
}

LEVEL_DEFAULTS = collections.defaultdict(courses.NoSuchDefaults)
for lang in ALL_LANGUAGES.keys():
    LEVEL_DEFAULTS[lang] = courses.LevelDefaults(lang)

HEDY_COURSE = collections.defaultdict(courses.NoSuchCourse)
for lang in ALL_LANGUAGES.keys():
    HEDY_COURSE[lang] = courses.Course('hedy', lang, LEVEL_DEFAULTS[lang])

SPACE_EU_COURSE = {'nl': courses.Course('space_eu', 'nl', LEVEL_DEFAULTS['nl']),
                   'en': courses.Course('space_eu', 'en', LEVEL_DEFAULTS['en']),
                   'es': courses.Course('space_eu', 'es', LEVEL_DEFAULTS['es'])
                   }

ONLINE_MASTERS_COURSE = courses.Course('online_masters', 'nl', LEVEL_DEFAULTS['nl'])

TRANSLATIONS = hedyweb.Translations()

# Load main menu (do it once, can be cached)
with open(f'main/menu.json', 'r') as f:
    main_menu_json = json.load(f)


logging.basicConfig(
    level=logging.DEBUG,
    format='[%(asctime)s] %(levelname)-8s: %(message)s')

app = Flask(__name__, static_url_path='')

# HTTP -> HTTPS redirect
# https://stackoverflow.com/questions/32237379/python-flask-redirect-to-https-from-http/32238093
if os.getenv ('REDIRECT_HTTP_TO_HTTPS'):
    @app.before_request
    def before_request():
        if request.url.startswith('http://'):
            url = request.url.replace('http://', 'https://', 1)
            # We use a 302 in case we need to revert the redirect.
            return redirect(url, code=302)

# Unique random key for sessions
app.config['SECRET_KEY'] = uuid.uuid4().hex

Compress(app)
Commonmark(app)
logger = jsonbin.JsonBinLogger.from_env_vars()

if not os.getenv('HEROKU_RELEASE_CREATED_AT'):
    logging.warning('Cannot determine release; enable Dyno metadata by running "heroku labs:enable runtime-dyno-metadata -a <APP_NAME>"')

@app.route('/parse', methods=['POST'])
def parse():
    body = request.json
    if not body:
        return "body must be an object", 400
    if 'code' not in body:
        return "body.code must be a string", 400
    if 'level' not in body:
        return "body.level must be a string", 400

    code = body ['code']
    level = int(body ['level'])
    # Language should come principally from the request body,
    # but we'll fall back to browser default if it's missing for whatever
    # reason.
    lang = body.get('lang', requested_lang())

    # For debugging
    print(f"got code {code}")

    response = {}
    username = current_user(request) ['username'] or None

    # Check if user sent code
    if not code:
        response["Error"] = "no code found, please send code."
    # is so, parse
    else:
        try:
            hedy_errors = TRANSLATIONS.get_translations(lang, 'HedyErrorMessages')
            result = hedy.transpile(code, level)
            response["Code"] = "# coding=utf8\n" + result
        except hedy.HedyException as E:
            # some 'errors' can be fixed, for these we throw an exception, but also
            # return fixed code, so it can be ran
            if E.args[0] == "Invalid Space":
                error_template = hedy_errors[E.error_code]
                response["Code"] = "# coding=utf8\n" + E.arguments['fixed_code']
                response["Warning"] = error_template.format(**E.arguments)
            elif E.args[0] == "Parse":
                error_template = hedy_errors[E.error_code]
                # Localize the names of characters
                # Localize the names of characters
                if 'character_found' in E.arguments:
                    E.arguments['character_found'] = hedy_errors[E.arguments['character_found']]
                response["Error"] = error_template.format(**E.arguments)
            else:
                error_template = hedy_errors[E.error_code]
                response["Error"] = error_template.format(**E.arguments)
        except Exception as E:
            print(f"error transpiling {code}")
            response["Error"] = str(E)

    logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': level,
        'lang': lang,
        'code': code,
        'server_error': response.get('Error'),
        'version': version(),
        'username': username
    })

    return jsonify(response)

@app.route('/report_error', methods=['POST'])
def report_error():
    post_body = request.json

    logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': post_body.get('level'),
        'code': post_body.get('code'),
        'client_error': post_body.get('client_error'),
        'version': version(),
        'username': current_user(request) ['username'] or None
    })

    return 'logged'

def programs_page (request):
    username = current_user(request) ['username']
    if not username:
        return "unauthorized", 403

    lang = requested_lang()
    query_lang = request.args.get('lang') or ''
    if query_lang:
        query_lang = '?lang=' + query_lang

    from_user = request.args.get('user') or None
    if from_user and not is_admin (request):
        return "unauthorized", 403

    texts=TRANSLATIONS.data [lang] ['Programs']

    result = db_get_many ('programs', {'username': from_user or username}, True)
    programs = []
    now = timems ()
    for item in result:
        measure = texts ['minutes']
        date = round ((now - item ['date']) / 60000)
        if date > 90:
            measure = texts ['hours']
            date = round (date / 60)
        if date > 36:
            measure = texts ['days']

            date = round (date / 24)

        programs.append ({'id': item ['id'], 'code': item ['code'], 'date': texts ['ago-1'] + ' ' + str (date) + ' ' + measure + ' ' + texts ['ago-2'], 'level': item ['level'], 'name': item ['name']})

    return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, auth=TRANSLATIONS.data [lang] ['Auth'], programs=programs, username=username, current_page='programs', query_lang=query_lang, from_user=from_user)

# @app.route('/post/', methods=['POST'])
# for now we do not need a post but I am leaving it in for a potential future

# routing to index.html
@app.route('/hedy', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/hedy/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/hedy/<level>/<step>', methods=['GET'])
def index(level, step):
    session_id()  # Run this for the side effect of generating a session ID
    g.level = level = int(level)
    g.lang = requested_lang()
    g.prefix = '/hedy'

    # If step is a string that has more than two characters, it must be an id of a program
    if step and type_check (step, 'str') and len (step) > 2:
        result = db_get ('programs', {'id': step})
        if not result:
            return 'No such program', 404
        # Allow both the owner of the program and the admin user to access the program
        user = current_user (request)
        if user ['username'] != result ['username'] and not is_admin (request):
            return 'No such program!', 404
        loaded_program = result ['code']
        # We default to step 1 to provide a meaningful default assignment
        step = 1
    else:
        loaded_program = None

    return hedyweb.render_assignment_editor(
        request=request,
        course=HEDY_COURSE[g.lang],
        level_number=level,
        assignment_number=step,
        menu=render_main_menu('hedy'),
        translations=TRANSLATIONS,
        version=version(),
        loaded_program=loaded_program)

@app.route('/onlinemasters', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/onlinemasters/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/onlinemasters/<level>/<step>', methods=['GET'])
def onlinemasters(level, step):
    session_id()  # Run this for the side effect of generating a session ID
    g.level = level = int(level)
    g.lang = lang = requested_lang()
    g.prefix = '/onlinemasters'

    return hedyweb.render_assignment_editor(
        request=request,
        course=ONLINE_MASTERS_COURSE,
        level_number=level,
        assignment_number=step,
        translations=TRANSLATIONS,
        version=version(),
        menu=None,
        loaded_program=None)

@app.route('/space_eu', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/space_eu/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/space_eu/<level>/<step>', methods=['GET'])
def space_eu(level, step):
    session_id()  # Run this for the side effect of generating a session ID
    g.level = level = int(level)
    g.lang = requested_lang()
    g.prefix = '/space_eu'

    return hedyweb.render_assignment_editor(
        request=request,
        course=SPACE_EU_COURSE[g.lang],
        level_number=level,
        assignment_number=step,
        translations=TRANSLATIONS,
        version=version(),
        menu=None,
        loaded_program=None)



@app.route('/error_messages.js', methods=['GET'])
def error():
    error_messages = TRANSLATIONS.get_translations(requested_lang(), "ClientErrorMessages")
    return render_template("error_messages.js", error_messages=json.dumps(error_messages))


@app.errorhandler(500)
def internal_error(exception):
    import traceback
    print(traceback.format_exc())
    return "<h1>500 Internal Server Error</h1>"

@app.route('/index.html')
@app.route('/')
def default_landing_page():
    return main_page('start')

@app.route('/<page>')
def main_page(page):
    if page == 'favicon.ico':
        abort(404)

    lang = requested_lang()
    effective_lang = lang

    if page in ['signup', 'login', 'my-profile', 'recover', 'reset', 'admin']:
        return auth_templates(page, lang, render_main_menu(page), request)

    if page == 'programs':
        return programs_page(request)

    # Default to English if requested language is not available
    if not path.isfile(f'main/{page}-{effective_lang}.md'):
        effective_lang = 'en'

    try:
        with open(f'main/{page}-{effective_lang}.md', 'r') as f:
            contents = f.read()
    except IOError:
        abort(404)

    front_matter, markdown = split_markdown_front_matter(contents)

    menu = render_main_menu(page)
    return render_template('main-page.html', mkd=markdown, lang=lang, menu=menu, username=current_user(request) ['username'], auth=TRANSLATIONS.data [lang] ['Auth'], **front_matter)


def session_id():
    """Returns or sets the current session ID."""
    if 'session_id' not in session:
        session['session_id'] = uuid.uuid4().hex
    return session['session_id']


def requested_lang():
    """Return the user's requested language code.

    If not in the request parameters, use the browser's accept-languages
    header to do language negotiation.
    """
    lang = request.args.get("lang")
    if lang: return lang

    return request.accept_languages.best_match(ALL_LANGUAGES.keys(), 'en')

@app.template_global()
def current_language():
    return make_lang_obj(requested_lang())

@app.template_global()
def hedy_link(level_nr, assignment_nr, subpage=None, lang=None):
    """Make a link to a Hedy page."""
    parts = [g.prefix]
    parts.append('/' + str(level_nr))
    if str(assignment_nr) != '1' or subpage:
        parts.append('/' + str(assignment_nr if assignment_nr else '1'))
    if subpage and subpage != 'code':
        parts.append('/' + subpage)
    parts.append('?')
    parts.append('lang=' + (lang if lang else requested_lang()))
    return ''.join(parts)

@app.template_global()
def other_languages():
    cl = requested_lang()
    return [make_lang_obj(l) for l in ALL_LANGUAGES.keys() if l != cl]


def make_lang_obj(lang):
    """Make a language object for a given language."""
    return {
        'sym': ALL_LANGUAGES[lang],
        'lang': lang
    }


@app.template_global()
def modify_query(**new_values):
    args = request.args.copy()

    for key, value in new_values.items():
        args[key] = value

    return '{}?{}'.format(request.path, url_encode(args))


def no_none_sense(d):
    """Remove all None values from a dict."""
    return {k: v for k, v in d.items() if v is not None}


def version():
    """Get the version from the Heroku environment variables."""
    if not os.getenv('DYNO'):
        # Not on Heroku
        return 'DEV'

    vrz = os.getenv('HEROKU_RELEASE_CREATED_AT')
    the_date = datetime.date.fromisoformat(vrz[:10]) if vrz else datetime.date.today()

    commit = os.getenv('HEROKU_SLUG_COMMIT', '????')[0:6]
    return the_date.strftime('%b %d') + f' ({commit})'


def split_markdown_front_matter(md):
    parts = re.split('^---', md, 1, re.M)
    if len(parts) == 1:
        return {}, md
    # safe_load returns 'None' if the string is empty
    front_matter = yaml.safe_load(parts[0]) or {}
    return front_matter, parts[1]


def render_main_menu(current_page):
    """Render a list of (caption, href, selected, color) from the main menu."""
    return [dict(
        caption=item.get(requested_lang(), item.get('en', '???')),
        href='/' + item['_'],
        selected=(current_page == item['_']),
        accent_color=item.get('accent_color', 'white')
    ) for item in main_menu_json['nav']]

# *** PROGRAMS ***

# Not very restful to use a GET to delete something, but indeed convenient; we can do it with a single link and avoiding AJAX.
@app.route('/programs/delete/<program_id>', methods=['GET'])
@requires_login
def delete_program (user, program_id):
    result = db_get ('programs', {'id': program_id})
    if not result or result ['username'] != user ['username']:
        return "", 404
    db_del ('programs', {'id': program_id})
    return redirect ('/programs')

@app.route('/programs', methods=['POST'])
@requires_login
def save_program (user):

    body = request.json
    if not type_check (body, 'dict'):
        return 'body must be an object', 400
    if not object_check (body, 'code', 'str'):
        return 'code must be a string', 400
    if not object_check (body, 'name', 'str'):
        return 'name must be a string', 400
    if not object_check (body, 'level', 'int'):
        return 'level must be an integer', 400

    # We execute the saved program to see if it would generate an error or not
    error = None
    try:
        hedy_errors = TRANSLATIONS.get_translations(requested_lang(), 'HedyErrorMessages')
        result = hedy.transpile(body ['code'], body ['level'])
    except hedy.HedyException as E:
        error_template = hedy_errors[E.error_code]
        error = error_template.format(**E.arguments)
    except Exception as E:
        error = str(E)

    name = body ['name']

    # We check if a program with a name `xyz` exists in the database for the username. If it does, we exist whether `xyz (1)` exists, until we find a program `xyz (NN)` that doesn't exist yet.
    # It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
    # For now, we bring all existing programs for the user and then search within them for repeated names.
    existing = db_get_many ('programs', {'username': user ['username']}, True)
    name_counter = 0
    for program in existing:
        if re.match ('^' + re.escape (name) + '( \(\d+\))*', program ['name']):
            name_counter = name_counter + 1
    if name_counter:
        name = name + ' (' + str (name_counter) + ')'

    db_set('programs', {
        'id': uuid.uuid4().hex,
        'session': session_id(),
        'date': timems (),
        'lang': requested_lang(),
        'version': version(),
        'level': body ['level'],
        'code': body ['code'],
        'name': name,
        'server_error': error,
        'username': user ['username']
    })

    return jsonify({})

# *** AUTH ***

import auth
auth.routes(app, requested_lang)

# *** START SERVER ***

if __name__ == '__main__':
    # Threaded option to enable multiple instances for multiple user access support
    app.run(threaded=True, port=config ['port'])

app.py
# coding=utf-8
import datetime
import collections
from functools import wraps
import hedy
import json
import jsonbin
import logging
import os
from os import path
import re
import requests
import uuid
import yaml
from flask_commonmark import Commonmark
from werkzeug.urls import url_encode
from config import config
from auth import auth_templates, current_user, requires_login, is_admin
from utils import db_get, db_get_many, db_set, timems, type_check, object_check, db_del

# app.py
from flask import Flask, request, jsonify, render_template, session, abort, g, redirect
from flask_compress import Compress

# Hedy-specific modules
import courses
import hedyweb

# Define and load all available language data
ALL_LANGUAGES = {
    'en': 'English',
    'nl': 'Nederlands',
    'es': 'Español',
    'fr': 'Français',
    'pt_br': 'Português',
    'de': 'Deutsch',
    'it': 'Italiano'
}

LEVEL_DEFAULTS = collections.defaultdict(courses.NoSuchDefaults)
for lang in ALL_LANGUAGES.keys():
    LEVEL_DEFAULTS[lang] = courses.LevelDefaults(lang)

HEDY_COURSE = collections.defaultdict(courses.NoSuchCourse)
for lang in ALL_LANGUAGES.keys():
    HEDY_COURSE[lang] = courses.Course('hedy', lang, LEVEL_DEFAULTS[lang])

SPACE_EU_COURSE = {'nl': courses.Course('space_eu', 'nl', LEVEL_DEFAULTS['nl']),
                   'en': courses.Course('space_eu', 'en', LEVEL_DEFAULTS['en']),
                   'es': courses.Course('space_eu', 'es', LEVEL_DEFAULTS['es'])
                   }

ONLINE_MASTERS_COURSE = courses.Course('online_masters', 'nl', LEVEL_DEFAULTS['nl'])

TRANSLATIONS = hedyweb.Translations()

# Load main menu (do it once, can be cached)
with open(f'main/menu.json', 'r') as f:
    main_menu_json = json.load(f)


logging.basicConfig(
    level=logging.DEBUG,
    format='[%(asctime)s] %(levelname)-8s: %(message)s')

app = Flask(__name__, static_url_path='')

# HTTP -> HTTPS redirect
# https://stackoverflow.com/questions/32237379/python-flask-redirect-to-https-from-http/32238093
if os.getenv ('REDIRECT_HTTP_TO_HTTPS'):
    @app.before_request
    def before_request():
        if request.url.startswith('http://'):
            url = request.url.replace('http://', 'https://', 1)
            # We use a 302 in case we need to revert the redirect.
            return redirect(url, code=302)

# Unique random key for sessions
app.config['SECRET_KEY'] = uuid.uuid4().hex

Compress(app)
Commonmark(app)
logger = jsonbin.JsonBinLogger.from_env_vars()

if not os.getenv('HEROKU_RELEASE_CREATED_AT'):
    logging.warning('Cannot determine release; enable Dyno metadata by running "heroku labs:enable runtime-dyno-metadata -a <APP_NAME>"')

@app.route('/parse', methods=['POST'])
def parse():
    body = request.json
    if not body:
        return "body must be an object", 400
    if 'code' not in body:
        return "body.code must be a string", 400
    if 'level' not in body:
        return "body.level must be a string", 400

    code = body ['code']
    level = int(body ['level'])
    # Language should come principally from the request body,
    # but we'll fall back to browser default if it's missing for whatever
    # reason.
    lang = body.get('lang', requested_lang())

    # For debugging
    print(f"got code {code}")

    response = {}
    username = current_user(request) ['username'] or None

    # Check if user sent code
    if not code:
        response["Error"] = "no code found, please send code."
    # is so, parse
    else:
        try:
            hedy_errors = TRANSLATIONS.get_translations(lang, 'HedyErrorMessages')
            result = hedy.transpile(code, level)
            response["Code"] = "# coding=utf8\n" + result
        except hedy.HedyException as E:
            # some 'errors' can be fixed, for these we throw an exception, but also
            # return fixed code, so it can be ran
            if E.args[0] == "Invalid Space":
                error_template = hedy_errors[E.error_code]
                response["Code"] = "# coding=utf8\n" + E.arguments['fixed_code']
                response["Warning"] = error_template.format(**E.arguments)
            elif E.args[0] == "Parse":
                error_template = hedy_errors[E.error_code]
                # Localize the names of characters
                # Localize the names of characters
                if 'character_found' in E.arguments:
                    E.arguments['character_found'] = hedy_errors[E.arguments['character_found']]
                response["Error"] = error_template.format(**E.arguments)
            else:
                error_template = hedy_errors[E.error_code]
                response["Error"] = error_template.format(**E.arguments)
        except Exception as E:
            print(f"error transpiling {code}")
            response["Error"] = str(E)

    logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': level,
        'lang': lang,
        'code': code,
        'server_error': response.get('Error'),
        'version': version(),
        'username': username
    })

    return jsonify(response)

@app.route('/report_error', methods=['POST'])
def report_error():
    post_body = request.json

    logger.log({
        'session': session_id(),
        'date': str(datetime.datetime.now()),
        'level': post_body.get('level'),
        'code': post_body.get('code'),
        'client_error': post_body.get('client_error'),
        'version': version(),
        'username': current_user(request) ['username'] or None
    })

    return 'logged'

def programs_page (request):
    username = current_user(request) ['username']
    if not username:
        return "unauthorized", 403

    lang = requested_lang()
    query_lang = request.args.get('lang') or ''
    if query_lang:
        query_lang = '?lang=' + query_lang

    from_user = request.args.get('user') or None
    if from_user and not is_admin (request):
        return "unauthorized", 403

    texts=TRANSLATIONS.data [lang] ['Programs']

    result = db_get_many ('programs', {'username': from_user or username}, True)
    programs = []
    now = timems ()
    for item in result:
        measure = texts ['minutes']
        date = round ((now - item ['date']) / 60000)
        if date > 90:
            measure = texts ['hours']
            date = round (date / 60)
        if date > 36:
            measure = texts ['days']

            date = round (date / 24)

        programs.append ({'id': item ['id'], 'code': item ['code'], 'date': texts ['ago-1'] + ' ' + str (date) + ' ' + measure + ' ' + texts ['ago-2'], 'level': item ['level'], 'name': item ['name']})

    return render_template('programs.html', lang=requested_lang(), menu=render_main_menu('programs'), texts=texts, auth=TRANSLATIONS.data [lang] ['Auth'], programs=programs, username=username, current_page='programs', query_lang=query_lang, from_user=from_user)

# @app.route('/post/', methods=['POST'])
# for now we do not need a post but I am leaving it in for a potential future

# routing to index.html
@app.route('/hedy', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/hedy/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/hedy/<level>/<step>', methods=['GET'])
def index(level, step):
    session_id()  # Run this for the side effect of generating a session ID
    g.level = level = int(level)
    g.lang = requested_lang()
    g.prefix = '/hedy'

    # If step is a string that has more than two characters, it must be an id of a program
    if step and type_check (step, 'str') and len (step) > 2:
        result = db_get ('programs', {'id': step})
        if not result:
            return 'No such program', 404
        # Allow both the owner of the program and the admin user to access the program
        user = current_user (request)
        if user ['username'] != result ['username'] and not is_admin (request):
            return 'No such program!', 404
        loaded_program = result ['code']
        # We default to step 1 to provide a meaningful default assignment
        step = 1
    else:
        loaded_program = None

    return hedyweb.render_assignment_editor(
        request=request,
        course=HEDY_COURSE[g.lang],
        level_number=level,
        assignment_number=step,
        menu=render_main_menu('hedy'),
        translations=TRANSLATIONS,
        version=version(),
        loaded_program=loaded_program)

@app.route('/onlinemasters', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/onlinemasters/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/onlinemasters/<level>/<step>', methods=['GET'])
def onlinemasters(level, step):
    session_id()  # Run this for the side effect of generating a session ID
    g.level = level = int(level)
    g.lang = lang = requested_lang()
    g.prefix = '/onlinemasters'

    return hedyweb.render_assignment_editor(
        request=request,
        course=ONLINE_MASTERS_COURSE,
        level_number=level,
        assignment_number=step,
        translations=TRANSLATIONS,
        version=version(),
        menu=None,
        loaded_program=None)

@app.route('/space_eu', methods=['GET'], defaults={'level': 1, 'step': 1})
@app.route('/space_eu/<level>', methods=['GET'], defaults={'step': 1})
@app.route('/space_eu/<level>/<step>', methods=['GET'])
def space_eu(level, step):
    session_id()  # Run this for the side effect of generating a session ID
    g.level = level = int(level)
    g.lang = requested_lang()
    g.prefix = '/space_eu'

    return hedyweb.render_assignment_editor(
        request=request,
        course=SPACE_EU_COURSE[g.lang],
        level_number=level,
        assignment_number=step,
        translations=TRANSLATIONS,
        version=version(),
        menu=None,
        loaded_program=None)



@app.route('/error_messages.js', methods=['GET'])
def error():
    error_messages = TRANSLATIONS.get_translations(requested_lang(), "ClientErrorMessages")
    return render_template("error_messages.js", error_messages=json.dumps(error_messages))


@app.errorhandler(500)
def internal_error(exception):
    import traceback
    print(traceback.format_exc())
    return "<h1>500 Internal Server Error</h1>"

@app.route('/index.html')
@app.route('/')
def default_landing_page():
    return main_page('start')

@app.route('/<page>')
def main_page(page):
    if page == 'favicon.ico':
        abort(404)

    lang = requested_lang()
    effective_lang = lang

    if page in ['signup', 'login', 'my-profile', 'recover', 'reset', 'admin']:
        return auth_templates(page, lang, render_main_menu(page), request)

    if page == 'programs':
        return programs_page(request)

    # Default to English if requested language is not available
    if not path.isfile(f'main/{page}-{effective_lang}.md'):
        effective_lang = 'en'

    try:
        with open(f'main/{page}-{effective_lang}.md', 'r') as f:
            contents = f.read()
    except IOError:
        abort(404)

    front_matter, markdown = split_markdown_front_matter(contents)

    menu = render_main_menu(page)
    return render_template('main-page.html', mkd=markdown, lang=lang, menu=menu, username=current_user(request) ['username'], auth=TRANSLATIONS.data [lang] ['Auth'], **front_matter)


def session_id():
    """Returns or sets the current session ID."""
    if 'session_id' not in session:
        session['session_id'] = uuid.uuid4().hex
    return session['session_id']


def requested_lang():
    """Return the user's requested language code.

    If not in the request parameters, use the browser's accept-languages
    header to do language negotiation.
    """
    lang = request.args.get("lang")
    if lang: return lang

    return request.accept_languages.best_match(ALL_LANGUAGES.keys(), 'en')

@app.template_global()
def current_language():
    return make_lang_obj(requested_lang())

@app.template_global()
def hedy_link(level_nr, assignment_nr, subpage=None, lang=None):
    """Make a link to a Hedy page."""
    parts = [g.prefix]
    parts.append('/' + str(level_nr))
    if str(assignment_nr) != '1' or subpage:
        parts.append('/' + str(assignment_nr if assignment_nr else '1'))
    if subpage and subpage != 'code':
        parts.append('/' + subpage)
    parts.append('?')
    parts.append('lang=' + (lang if lang else requested_lang()))
    return ''.join(parts)

@app.template_global()
def other_languages():
    cl = requested_lang()
    return [make_lang_obj(l) for l in ALL_LANGUAGES.keys() if l != cl]


def make_lang_obj(lang):
    """Make a language object for a given language."""
    return {
        'sym': ALL_LANGUAGES[lang],
        'lang': lang
    }


@app.template_global()
def modify_query(**new_values):
    args = request.args.copy()

    for key, value in new_values.items():
        args[key] = value

    return '{}?{}'.format(request.path, url_encode(args))


def no_none_sense(d):
    """Remove all None values from a dict."""
    return {k: v for k, v in d.items() if v is not None}


def version():
    """Get the version from the Heroku environment variables."""
    if not os.getenv('DYNO'):
        # Not on Heroku
        return 'DEV'

    vrz = os.getenv('HEROKU_RELEASE_CREATED_AT')
    the_date = datetime.date.fromisoformat(vrz[:10]) if vrz else datetime.date.today()

    commit = os.getenv('HEROKU_SLUG_COMMIT', '????')[0:6]
    return the_date.strftime('%b %d') + f' ({commit})'


def split_markdown_front_matter(md):
    parts = re.split('^---', md, 1, re.M)
    if len(parts) == 1:
        return {}, md
    # safe_load returns 'None' if the string is empty
    front_matter = yaml.safe_load(parts[0]) or {}
    return front_matter, parts[1]


def render_main_menu(current_page):
    """Render a list of (caption, href, selected, color) from the main menu."""
    return [dict(
        caption=item.get(requested_lang(), item.get('en', '???')),
        href='/' + item['_'],
        selected=(current_page == item['_']),
        accent_color=item.get('accent_color', 'white')
    ) for item in main_menu_json['nav']]

# *** PROGRAMS ***

# Not very restful to use a GET to delete something, but indeed convenient; we can do it with a single link and avoiding AJAX.
@app.route('/programs/delete/<program_id>', methods=['GET'])
@requires_login
def delete_program (user, program_id):
    result = db_get ('programs', {'id': program_id})
    if not result or result ['username'] != user ['username']:
        return "", 404
    db_del ('programs', {'id': program_id})
    return redirect ('/programs')

@app.route('/programs', methods=['POST'])
@requires_login
def save_program (user):

    body = request.json
    if not type_check (body, 'dict'):
        return 'body must be an object', 400
    if not object_check (body, 'code', 'str'):
        return 'code must be a string', 400
    if not object_check (body, 'name', 'str'):
        return 'name must be a string', 400
    if not object_check (body, 'level', 'int'):
        return 'level must be an integer', 400

    # We execute the saved program to see if it would generate an error or not
    error = None
    try:
        hedy_errors = TRANSLATIONS.get_translations(requested_lang(), 'HedyErrorMessages')
        result = hedy.transpile(body ['code'], body ['level'])
    except hedy.HedyException as E:
        error_template = hedy_errors[E.error_code]
        error = error_template.format(**E.arguments)
    except Exception as E:
        error = str(E)

    name = body ['name']

    # We check if a program with a name `xyz` exists in the database for the username. If it does, we exist whether `xyz (1)` exists, until we find a program `xyz (NN)` that doesn't exist yet.
    # It'd be ideal to search by username & program name, but since DynamoDB doesn't allow searching for two indexes at the same time, this would require to create a special index to that effect, which is cumbersome.
    # For now, we bring all existing programs for the user and then search within them for repeated names.
    existing = db_get_many ('programs', {'username': user ['username']}, True)
    name_counter = 0
    for program in existing:
        if re.match ('^' + re.escape (name) + '( \(\d+\))*', program ['name']):
            name_counter = name_counter + 1
    if name_counter:
        name = name + ' (' + str (name_counter) + ')'

    db_set('programs', {
        'id': uuid.uuid4().hex,
        'session': session_id(),
        'date': timems (),
        'lang': requested_lang(),
        'version': version(),
        'level': body ['level'],
        'code': body ['code'],
        'name': name,
        'server_error': error,
        'username': user ['username']
    })

    return jsonify({})

# *** AUTH ***

import auth
auth.routes(app, requested_lang)

# *** START SERVER ***

if __name__ == '__main__':
    # Threaded option to enable multiple instances for multiple user access support
    app.run(threaded=True, port=config ['port'])

app.py
diff --git a/app.py b/app.py
index 6290f006f74..128b93e48d3 100644
--- a/app.py
+++ b/app.py
@@ -34,6 +34,7 @@
     'fr': 'Français',
     'pt_br': 'Português',
     'de': 'Deutsch',
+    'it': 'Italiano'
 }
 
 LEVEL_DEFAULTS = collections.defaultdict(courses.NoSuchDefaults)
diff --git a/coursedata/course/hedy/it.yaml b/coursedata/course/hedy/it.yaml
new file mode 100644
index 00000000000..69e4098749a
--- /dev/null
+++ b/coursedata/course/hedy/it.yaml
@@ -0,0 +1,11 @@
+---
+# This file seems rather empty but the assignments all use the defaults from 'level-defaults'
+course:
+  - level: "1"
+  - level: "2"
+  - level: "3"
+  - level: "4"
+  - level: "5"
+  - level: "6"
+  - level: "7"
+
diff --git a/coursedata/level-defaults/it.yaml b/coursedata/level-defaults/it.yaml
new file mode 100644
index 00000000000..cd958cf9d06
--- /dev/null
+++ b/coursedata/level-defaults/it.yaml
@@ -0,0 +1,139 @@
+---
+  1:
+    intro_text: "Nel primo livello puoi usare questi comandi:"
+    start_code: "print ciao mondo!"
+    commands:
+      -
+        name: "print"
+        explanation: "Scrivi qualcosa con 'print'."
+        example: "Esempio: print Ciao benvenuta/o su Hedy!"
+        demo_code: "print Ciao benvenuta/o su Hedy!"
+      -
+        name: "ask"
+        explanation: "Chiedi qualcosa con 'ask'."
+        example: "Esempio: ask Qual è il tuo colore preferito?"
+        demo_code: "ask Qual è il tuo colore preferito?"
+      -
+        name: "echo"
+        explanation: "Ripeti qualcosa usando 'echo'."
+        example: "Esempio: echo Quindi il tuo colore preferito è..."
+        demo_code: "ask Qual è il tuo colore preferito?\necho Quindi il tuo colore preferito è..."
+  2:
+    intro_text: "'Print' funziona ancora come nel primo livello, invece 'ask' è stato cambiato ed ha bisogno di dare un nome alla risposta che potrai poi scrivere con 'print'. 'Echo' non è più necessario. Puoi anche usare questi comandi:"
+    start_code: "print ciao mondo!"
+    commands:
+      -
+        name: "is"
+        explanation: "Dà un nomignolo da usare nel programma ad una parola"
+        example: "Esempio: nome is Hedy."
+        demo_code: "nome is Hedy\nprint benvenuta nome"
+      -
+        name: "ask"
+        explanation: "Chiedi qualcosa con 'ask'. Attenzione! 'ask' ora ha bisogno di un nome."
+        example: "Esempio: colore is ask Qual è il tuo colore preferito?"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nprint colore è il tuo preferito!"
+      -
+        name: "scegli_acaso"
+        explanation: "Scegli una parola a caso da un gruppo con 'at' e 'random'"
+        example: "Esempio: animali is cane, gatto, canguro."
+        demo_code: "animali is cane, gatto, canguro\nprint animali at random"
+  3:
+    start_code: "print 'Ciao mondo'"
+    intro_text: "Nel terzo livello devi usare le virgolette per le parole che vuoi scrivere con 'print'. 'echo' non è più necessario, ora puoi scrivere parole con 'print'!"
+    commands:
+      -
+        name: "print"
+        explanation: "Scrivi esattamente quello che metti tra virgolette"
+        example: "Esempio: print 'Ciao benvenuta/o su Hedy.'"
+        demo_code: "print 'Ciao benvenuta/o su Hedy.'"
+      -
+        name: "is"
+        explanation: "Dai un nomignolo ad un testo e scrivilo con 'print' senza usare le virgolette"
+        example: "Esempio: nome is Hedy."
+        demo_code: "nome is Hedy\nprint 'Il mio nome è ' nome"
+      -
+        name: "ask"
+        explanation: "Chiedi qualcosa con 'ask'. Attenzione! Per scrivere con 'print' devi usare le virgolette!"
+        example: "Esempio: colore is ask Qual è il tuo colore preferito?"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nprint colore ' è il tuo preferito!'"
+  4:
+    start_code: "nome is ask Come ti chiami?\nif nome is Hedy print 'che bello!' else print 'mah'"
+    intro_text: "'ask' e 'print' funzionano esattamente come funzionavano nel terzo livello. Nel quarto livello si aggiunge la congiunzione 'if'!"
+    commands:
+      -
+        name: "print"
+        explanation: "Scrivi esattamente quello che metti tra virgolette"
+        example: "Example: print 'Ciao benvenuta/o su Hedy.'"
+        demo_code: "print 'Ciao benvenuta/o su Hedy.'"
+      -
+        name: "ask"
+        explanation: "Chiedi qualcosa con 'ask'. Attenzione! Per scrivere con 'print' devi usare le virgolette!"
+        example: "Esempio: colore is ask Qual è il tuo colore preferito?"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nprint colore ' è il tuo preferito!'"
+      -
+        name: "if"
+        explanation: "Fai una scelta"
+        example: "Esempio: if colore is verde print 'carino!' else print 'mah'"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nif colore is verde print 'carino!' else print 'mah'"
+  5:
+    start_code: "repeat 3 times print 'Hedy è divertente!'"
+    intro_text: "'ask', 'print' e 'if' funzionano esattamente come funzionavano nel quarto livello. Il quinto livello introduce il comando 'repeat x times. 'repeat x times' può essere usato per ripetere una parte di codice 'x' volte."
+    commands:
+      -
+        name: "print"
+        explanation: "Scrivi esattamente quello che metti tra virgolette"
+        example: "Example: print 'Ciao benvenuta/o su Hedy.'"
+        demo_code: "print 'Ciao benvenuta/o su Hedy.'"
+      -
+        name: "ask"
+        explanation: "Chiedi qualcosa con 'ask'. Attenzione! Per scrivere con 'print' devi usare le virgolette!"
+        example: "Esempio: colore is ask Qual è il tuo colore preferito?"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nprint colore ' è il tuo preferito!'"
+      -
+        name: "if"
+        explanation: "Fai una scelta"
+        example: "Esempio: if colore is verde print 'carino!' else print 'mah'"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nif colore is verde print 'carino!' else print 'mah'"
+      -
+        name: "repeat x times"
+        explanation: "'repeat x times' e 'if' combinati insieme"
+        example: "Esempio: if colore is verde repeat 3 times print 'carino!' else repeat 5 times print 'mah'"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nif colore is verde repeat 3 times print 'carino' else repeat 5 times print 'mah'"
+  6:
+    start_code: "print '5 per 5 è ' 5 * 5"
+    intro_text: "'ask', 'print', 'if' e 'repeat x times' sono esattamente come nei livelli 4 e 5. Livello 6 aggiunge qualcosa di nuovo... Ora puoi fare i calcoli. "
+    commands:
+      -
+        name: "print"
+        explanation: "Scrivi esattamente quello che metti tra virgolette"
+        example: "Example: print '5 per 5 è ' 5 * 5"
+        demo_code: "print '5 per 5 è ' 5 * 5"
+      -
+        name: "ask e if con calcoli"
+        explanation: "Chiedi di fare un calcolo e giudicalo con 'if'. Attenzione: 'Print' richiede sempre le virgolette."
+        example: "Esempio: risposta is ask Quanto fa 10 più 10?"
+        demo_code: "risposta is ask Quanto fa 10 più 10?\nif risposta is 20 print 'Yes!' else print 'Ops'"
+      -
+        name: "repeat"
+        explanation: "repeat e if combinati"
+        example: "Example: if colore is verde repeat 3 times print 'carino!' else repeat 5 times print 'mah'"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nif colore is verde repeat 3 times print 'carino!' else repeat 5 times print 'mah'"
+  7:
+    start_code: "repeat 5 times\n    print 'Ciao a tutti'\n    print 'Questo sarà scritto 5 volte'"
+    intro_text: "ask e print funzionano come prima ma repeat e if sono cambiati! Ora puoi usare interi blocchi di codice ma dovrai ricordarti di usare l'indentazione. Questo significa mettere 4 spazi all'inizio della riga. Questo funziona anche quando vuoi scrivere un blocco di una sola riga. Se usi repeat e if dovrai ricordarti di mettere l'indentazione per ognuno dei due blocchi. Leggi l'esempio per più informazioni!"
+    commands:
+      -
+        name: "print"
+        explanation: "Scrivi qualcosa. Ricorda di usare le virgolette per scrivere parole e frasi."
+        example: "Esempio: print '5 volte 5 è ' 5 * 5"
+        demo_code: "print '5 volte 5 è ' 5 * 5"
+      -
+        name: "if con più righe"
+        explanation: "Chiedi la risposta per una somma e controlla se è corretta. Ora possiamo scrivere due righe."
+        example: "Esempio: risposta is ask Quanto fa 5 più 5?"
+        demo_code: "risposta is ask Quanto fa 5 più 5?\nif risposta è 10\n    print 'Ben fatto!'\n    print 'Esatto, la risposta corretta era ' risposta\ninvece\n    scrivi 'Ops!'\n    scrivi 'La risposta è ' risposta"
+      -
+        name: "if e repeat insieme"
+        explanation: "if e repeat insieme"
+        example: "Esempio: if colore is verde repeat 3 times print 'carino!' else repeat 5 times print 'mah'"
+        demo_code: "colore is ask Qual è il tuo colore preferito?\nif colore is verde\n    repeat 3 times\n    print 'carino!'\nelse\n    repeat 5 times\n    print 'mah'"
diff --git a/coursedata/texts/it.yaml b/coursedata/texts/it.yaml
new file mode 100644
index 00000000000..e7a835f16e1
--- /dev/null
+++ b/coursedata/texts/it.yaml
@@ -0,0 +1,127 @@
+---
+ui:
+  level_title: "Livello"
+  step_title: "Compito"
+  code_title: "Codice"
+  docs_title: "Spiegazione"
+  contact: "Contatti"
+  video_title: "Video"
+  run_code_button: "Eseguire codice"
+  save_code_button: "Salva codice"
+  untitled: "Senza nome"
+  regress_button: "Torna a livello"
+  advance_button: "Vai a livello"
+  advance_step_button: "Vai a compito"
+  try_button: "Prova questo"
+  enter_text: "Inserisci la risposta qui..."
+  enter: "Invio"
+  assignment_header: "Compito"
+  show_explanation: "Mostra spiegazione"
+  hide_explanation: "Nascondi spiegazione"
+ClientErrorMessages:
+  Transpile_warning: "Attenzione!"
+  Transpile_error: "Non possiamo eseguire il tuo programma."
+  Connection_error: "Non siamo riuscito a contattare il server."
+  Other_error: "Ops! Forse abbiamo fatto un errore."
+  Execute_error: "Qualcosa è andato storto nell'esecuzione del tuo codice."
+HedyErrorMessages:
+  Wrong Level: "Questo è il codice Hedy corretto ma non al livello giusto. Hai scritto un codice per il livello {original_level} al livello {working_level}."
+  Incomplete: "Ops! Hai dimenticato un pezzo di codice! Alla linea {line_number} devi mettere del codice dopo {incomplete_command}."
+  Invalid: "{invalid_command} non è un commando Hedy per il livello {level}. Intendevi {guessed_command}?"
+  Invalid Space: "Ops! Hai iniziato una linea con uno spazio alla linea {line_number}. Gli spazi confondono i computer, puoi rimuoverla?"
+  Parse: "Il codice che hai scritto non è un codice Hedy valido. C'è un errore alla linea {location[0]}, alla posizione {location[1]}. Hai scritto {character_found} ma questo non è permesso."
+  VarUndefined: "Hai provato a scrivere {name} ma non l'hai inizializzato."
+  space: "uno spazio"
+  comma: "una virgola"
+  question mark: "un punto di domanda"
+  newline: "una nuova riga"
+  period: "un punto"
+  exclamation mark: "un punto esclamativo"
+  dash: "una lineetta"
+  star: "una stella"
+  single quotes: "un apostrofo"
+  double quotes: "virgolette"
+  slash: "una barra obliqua"
+Auth:
+  create_account: "Crea account"
+  create_account_explanation: "Avere un account ti permette di salvare i tuoi programmi."
+  username: "Nome utente"
+  email: "Email"
+  password: "Password"
+  password_repeat: "Ripeti password"
+  birth_year: "Anno di nascita (opzionale)"
+  gender: "Genere (opzionale)"
+  select: "Seleziona"
+  female: "Femmina"
+  male: "Maschio"
+  other: "Altro"
+  country: "Paese"
+  subscribe_newsletter: "Iscriviti alla newsletter"
+  already_account: "Hai già account?"
+  login_long: "Accedi al tuo account"
+  login: "Accedi"
+  no_account: "Non hai ancora un account?"
+  profile: "Mio profilo"
+  update_profile: "Aggiorna profilo"
+  logout: "Esci"
+  destroy: "Cancella account permanentemente"
+  current_password: "Password attuale"
+  new_password: "Nuova password"
+  repeat_new_password: "Ripeti nuova password"
+  change_password: "Cambia password"
+  are_you_sure: "Sei sicuro? Questa azione è permanente."
+  please_username: "Per favore inserisci un nome utente."
+  username_three: "Il nome utente deve contenere almeno tre caratteri."
+  username_special: "Il nome utente non può contenere `:` o `@`."
+  please_password: "Per favore inserisci la password."
+  password_six: "La password deve contenere almeno sei caratteri"
+  repeat_match: "La password ripetuta non combacia."
+  valid_email: "Per favore inserisci un'email valida."
+  valid_year: "Per favore inserisci un anno tra il 1900 e "
+  ajax_error: "C'è stato un problema, per favore riprova."
+  please_username_email: "Per favore inserisci un nome utente o un'email."
+  profile_updated: "Profilo aggiornato."
+  password_updated: "Password aggiornata."
+  signup_success: "Tutto fatto! Per favore entra nel tuo account :)."
+  forgot_password: "Dimenticato la tua password?"
+  recover_password: "Richiedi di resettare la password"
+  send_password_recovery: "Inviami un link per recuperare la password"
+  sent_password_recovery: "Dovresti ricevere presto un'email con le istruzioni per resettare la tua password."
+  reset_password: "Resetta password"
+  password_resetted: "La tua password è stata resettata con successo. Per favore entra nel tuo account."
+  invalid_username_password: "Nome utente/password non validi."
+  invalid_username: "Nome utente non valido."
+  invalid_password: "Password non valida."
+  invalid_reset_link: "Link per resettare password invalido."
+  exists_email: "Questa email è già in uso."
+  exists_username: "Questo nome utente è già in uso."
+  email_hello: "Ciao!"
+  email_goodbye: "Grazie!\nIl team di Hedy"
+  email_welcome_verify_subject: "Benvenuta/o su Hedy"
+  email_welcome_verify_body: "Il tuo account Hedy è stato creato con successo. Benvenuta\\o!\nPer favore clicca su questo link per confermare il tuo indirizzo email:"
+  email_change_password_subject: "La tua password di Hedy è stata cambiata"
+  email_change_password_body: "La tua password di Hedy è stata cambiata. Se ne sei a conoscenza, bene.\nSe non sei statu tu a cambiare la password, per favore contattaci immediatamente rispondendo a questa email."
+  email_recover_password_subject: "Resetta la tua password di Hedy"
+  email_recover_password_body: "Cliccando su questo link puoi resettare la tua password di Hedy. Se non hai richiesto un reset della tua password ignora questa email."
+  email_reset_password_subject: "La tua password di Hedy è stata resettata"
+  email_reset_password_body: "La tua password di Hedy è stata resettata con una nuova. Se ne sei a conoscenza, bene.\nSe non sei statu tu a cambiare la password, per favore contattaci immediatamente rispondendo a questa email."
+  # These variables are added here to make the code simpler, but conceptually they belong to the UI.
+  program_header: "I miei programmi"
+  save_prompt: "Devi avere un account per salvare i tuoi programmi. Vuoi entrare nel tuo account ora?"
+  unsaved_changes: "Hai un programma non salvato. Vuoi uscire senza salvare?"
+  save_success: "Successo"
+  save_success_detail: "Programma salvato con successo"
+  answer_question: "Non puoi eseguire il programma prima di rispondere alla domanda"
+Programs:
+  recent: "I miei programmi recenti"
+  level: "Livello"
+  minutes: "minuti"
+  hours: "ore"
+  days:  "giorni"
+  ago-1: ""
+  ago-2: "fa"
+  open: "Apri"
+  delete: "Elimina"
+  delete_confirm: "Sei sicura\\o di voler eliminare questo programma?"
+  no_programs: "Non hai ancora nessun programma."
+  write_first: "Scrivi il tuo primo programma!"

Add Italian translations

