import hashlib
import random
import re
import string
import dokk.sparql as sparql

from datetime import datetime, timezone
from dokk import settings
from rdflib import Graph, URIRef, Literal
from string import Template

def new_random_token(length = 32):
    """
    Return a random string.
    
    TODO This function uses random.SystemRandom() for compatibility with older
         Python3 releases. Python 3.6 has introduced the "secrets" module which
         is a cryptographically secure replacement for the "random" module.
    
    :param length: The length of the string to generate
    """
    
    # https://docs.python.org/3/library/random.html#random.SystemRandom
    sys_random = random.SystemRandom()
    
    return ''.join(sys_random.choice(string.ascii_letters + string.digits)
                                     for i in range(length))

def normalize_name(page_name):
    """
    Normalize a page name for URLs (replace white spaces with underscore).
    
    :param page_name: Page name.
    """
    
    # Strip white spaces from string beginning and ending
    page_name = page_name.strip()
    
    # Replace all white spaces with one underscore
    page_name = re.sub(' +', '_', page_name)
    
    return page_name

def slugify(string):
    """
    Make a string a valid file name.
    
    :param string: The string to slugify.
    """
    
    # Re all slashes spaces with one underscore
    string = re.sub('/', '_', string)
    
    return string

def topic_exists(id):
    """
    Check if a topic already exists.
    
    :param id: ID of the topic.
    """
    
    query = Template("""
        PREFIX : <dokk:/>
        PREFIX dokk: <https://ontology.dokk.org/>

        ASK {
                [] a dokk:Topic ;
                dokk:id "$id" .
            }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response['boolean']

def get_topic(id):
    """
    Retrieve a topic.
    
    :param id: Topic ID.
    """
    
    query = Template("""
        PREFIX : <dokk:/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:Topic ;
                    dokk:id "$id" .
                }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response

def get_topics():
    """
    Retrieve all topics.
    """
    
    query = """
        PREFIX dokk: <https://ontology.dokk.org/>

        SELECT  (STRAFTER(STR(?from), "dokk:/") AS ?node1)
                ?image
                ?description
                ?link
                (STRAFTER(STR(?to), "dokk:/") AS ?node2)
        WHERE
        {
            ?from a ?node_type .
            
            FILTER (?node_type IN (dokk:Topic, dokk:File)) .
            
            OPTIONAL { ?from dokk:image ?image }
            OPTIONAL { ?from dokk:description ?description }
            
            OPTIONAL {
                ?from ?link ?to .
                ?to a dokk:Topic .
            }
        }
    """
    
    response = sparql.query_graph(query)
    
    return response

def add_topic(topic_id):
    """
    Add a new topic node.
    """
    
    # Retrieve the pages with the node data
    topic = get_last_revision('topic/' + topic_id)
    topic = topic['content'] if 'content' in topic else ''
    
    # Create the graph and load data
    node = Graph()
    try:
        node.parse(data=topic, format="turtle")
    except Exception as e:
        print("Cannot parse Turtle: ", e)
        return
    
    # If "topic" file contained any wrong subject, we remove it
    for subject in node.subjects():
        if str(subject) != 'dokk:/' + topic_id:
            node.remove((subject, None, None))
    
    query = Template("""
        PREFIX : <dokk:/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
        
        DELETE  { ?s ?p ?o }
        WHERE   {
                    ?s a dokk:Topic ;
                    dokk:id "$id" ;
                    ?p ?o .
                };
        
        INSERT  {
                    <dokk:/$uri> a dokk:Topic ;
                    dokk:id "$id" .
                }
        WHERE   {};
        
        INSERT DATA { $ntriples };
    """).substitute(
        uri         = topic_id,
        id          = sparql.escape_literal(topic_id),
        ntriples    = node.serialize(format='nt').decode('UTF-8'))
    
    sparql.update_graph(query)

def article_exists(id):
    """
    Check if a Article already exists.
    
    :param id: ID of the article.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/article/>
        PREFIX dokk: <https://ontology.dokk.org/>

        ASK {
                [] a dokk:Article ;
                dokk:id "$id" .
            }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response['boolean']

def get_article(id):
    """
    Retrieve a Article.
    
    :param id: Article ID.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/article/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:Article ;
                    dokk:id "$id" .
                }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response

def list_articles(search_term):
    """
    List articles that match a search term.
    
    :param search_term: Search term to match, even partially.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/article/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

        SELECT ?id
        WHERE   {
                    [] a dokk:Article ;
                    dokk:id ?id .
                    
                    FILTER REGEX(?id, "^$search_term", "i")
                }
        ORDER BY ?id
        LIMIT 100
    """).substitute(search_term = sparql.escape_literal(search_term))
    
    response = sparql.query_graph(query)
    
    return response

def search_articles(search_term, limit=10, offset=0):
    """
    Search articles.
    
    :param search_term: Search term to match, even partially.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/article/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>

        SELECT ?uri ?id
        WHERE   {
                    ?uri a dokk:Article ;
                    dokk:id ?id .
                    
                    FILTER CONTAINS(LCASE(?id), LCASE("$search_term"))
                }
        ORDER BY ?id
        LIMIT $limit
        OFFSET $offset
    """).substitute(
        search_term = sparql.escape_literal(search_term),
        limit = limit, offset = offset)
    
    response = sparql.query_graph(query)
    
    return response

def add_article(article_id):
    """
    Add a new article node.
    """
    
    # Retrieve the pages with the node data
    article = get_last_revision('article/' + article_id)
    article = article['content'] if 'content' in article else ''
    
    query = Template("""
        PREFIX : <dokk:/dokk/article/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
        
        DELETE  { ?s ?p ?o }
        WHERE   {
                    ?s a dokk:Article ;
                    dokk:id "$id" ;
                    ?p ?o .
                };
        
        INSERT  {
                    <dokk:/dokk/article/$uri> a dokk:Article ;
                    dokk:id "$id" ;
                    dokk:content "$content" .
                }
        WHERE   {};
    """).substitute(
        uri     = article_id,
        id      = sparql.escape_literal(article_id),
        content = sparql.escape_literal(article))
    
    sparql.update_graph(query)

def get_query(id):
    """
    Retrieve a Query.
    
    :param id: Query ID.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/query/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:Query ;
                    dokk:id "$id" .
                }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response

def add_query(id):
    """
    Add a new Query node.
    """
    
    # Retrieve the pages with the node data
    query = get_last_revision('query/' + id)
    query = query['content'] if 'content' in query else ''
    
    sparql_query = Template("""
        PREFIX : <dokk:/dokk/query/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
        
        DELETE  { ?s ?p ?o }
        WHERE   {
                    ?s a dokk:Query ;
                    dokk:id "$id" ;
                    ?p ?o .
                };
        
        INSERT  {
                    <dokk:/dokk/query/$uri> a dokk:Query ;
                    dokk:id "$id" ;
                    dokk:content "$content" .
                }
        WHERE   {};
    """).substitute(
        uri     = id,
        id      = sparql.escape_literal(id),
        content = sparql.escape_literal(query))
    
    sparql.update_graph(sparql_query)

def list_queries():
    """
    Return a list of all queries available in this instance.
    """
    
    query = Template("""
        PREFIX :     <dokk:/dokk/query/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#>

        SELECT ?id
        WHERE   {
                    [] a dokk:Query ;
                    dokk:id ?id .
                }
        ORDER BY ?id
    """).substitute()
    
    response = sparql.query_graph(query)
    
    return response

def get_template(id):
    """
    Retrieve a Template.
    
    :param id: Template ID.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/template/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:Template ;
                    dokk:id "$id" .
                }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response

def add_template(id):
    """
    Add a new Template node.
    """
    
    # Retrieve the pages with the node data
    template = get_last_revision('template/' + id)
    template = template['content'] if 'content' in template else ''
    
    query = Template("""
        PREFIX : <dokk:/dokk/template/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
        
        DELETE  { ?s ?p ?o }
        WHERE   {
                    ?s a dokk:Template ;
                    dokk:id "$id" ;
                    ?p ?o .
                };
        
        INSERT  {
                    <dokk:/dokk/template/$uri> a dokk:Template ;
                    dokk:id "$id" ;
                    dokk:content "$content" .
                }
        WHERE   {};
    """).substitute(
        uri     = id,
        id      = sparql.escape_literal(id),
        content = sparql.escape_literal(template))
    
    sparql.update_graph(query)

def list_templates():
    """
    Return a list of all templates available in this instance.
    """
    
    query = Template("""
        PREFIX :     <dokk:/dokk/template/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd:  <http://www.w3.org/2001/XMLSchema#>

        SELECT ?id
        WHERE   {
                    [] a dokk:Template ;
                    dokk:id ?id .
                }
        ORDER BY ?id
    """).substitute()
    
    response = sparql.query_graph(query)
    
    return response

def get_file(id):
    """
    Retrieve a node of type dokk:File.
    
    :param id: File ID.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/file/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:File ;
                    dokk:id "file/$id" .
                }
    """).substitute(id = sparql.escape_literal(id))
    
    response = sparql.query_graph(query)
    
    return response

def add_file(id):
    """
    Add a new File node when uploading a file.
    """
    
    # Retrieve the revision with the node data
    file = get_last_revision('file/' + id)
    file = file['content'] if 'content' in file else ''
    
    # Create the graph and load data
    node = Graph()
    try: node.parse(data=file, format="turtle")
    except: return
    
    # If "file" file contained any wrong subject, we remove it
    for subject in node.subjects():
        if str(subject) != 'dokk:/dokk/file/' + id:
            node.remove((subject, None, None))
    
    query = Template("""
        PREFIX : <dokk:/dokk/file/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
        
        DELETE  { ?s ?p ?o }
        WHERE   {
                    ?s a dokk:File ;
                    dokk:id "$id" ;
                    ?p ?o .
                };
        
        INSERT  {
                    <dokk:/dokk/file/$uri> a dokk:File ;
                    dokk:id "$id" .
                }
        WHERE   {};
        
        INSERT DATA { $ntriples };
    """).substitute(
        uri         = id,
        id          = 'dokk/file/' + sparql.escape_literal(id),
        ntriples    = node.serialize(format='nt').decode('UTF-8'))
    
    sparql.update_graph(query)

def exists(page_name):
    """
    Check if a revision already exists.
    
    :param page_name: Page name.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/revision/>
        PREFIX dokk: <https://ontology.dokk.org/>

        ASK {
                [] a dokk:Revision ;
                dokk:page "$page_name" .
            }
    """).substitute(page_name = sparql.escape_literal(page_name))
    
    response = sparql.query_graph(query)
    
    return response['boolean']

def uuid_exists(page_uuid):
    """
    Check if a revision UUID exists.
    
    :param page_uuid: Revision UUID.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/revision/>
        PREFIX dokk: <https://ontology.dokk.org/>

        ASK {
                [] a dokk:Revision ;
                dokk:uuid "$uuid" .
            }
    """).substitute(uuid = sparql.escape_literal(page_uuid))
    
    response = sparql.query_graph(query)
    
    return response['boolean']

def add_revision(page_name, rev_number, author, content, edit_summary):
    """
    Create a new revision of a page.
    
    :param page_name: Page name. Example: "article/Rome"
    :param author: User who made the changes
    :param rev_number: The number of the last revision since the user started editing
                       the page. This is used as a proof that the user is indeed
                       editing the last revision. If rev_number is not the current
                       revision, it means that somebody else has edited the same
                       page in the meanwhile, and thus we do NOT want to merge
                       these changes without further review
    :param content: The page content for the new revision
    :param edit_summary: Brief summary of the changes to be applied
    
    :return: True if the new revision was added successfully. Otherwise False.
    """
    
    # Check rev_number is a valid integer, otherwise assume it's the first revision
    # of this page.
    try:
        rev_number = int(rev_number)
        new_rev_number = rev_number + 1
    except:
        rev_number = None
        new_rev_number = 0
    
    # Assign a datetime for this revision
    rev_datetime = datetime.now(timezone.utc).isoformat()
    
    # Assign a UUID to this revision
    rev_uuid = new_random_token()
    
    query = Template("""
        PREFIX : <dokk:/dokk/revision/>
        PREFIX dokk: <https://ontology.dokk.org/>
        PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
        
        INSERT  {
                    <dokk:/dokk/revision/$uri>
                    a dokk:Revision ;
                    dokk:uuid "$uuid" ;
                    dokk:revision_number $revision_number ;
                    dokk:page "$page" ;
                    dokk:author "$author" ;
                    dokk:datetime "$datetime"^^xsd:dateTime ;
                    dokk:content "$content" ;
                    dokk:edit_summary "$edit_summary" .
                }
        WHERE   {
                    FILTER NOT EXISTS   {
                                            []  a dokk:Revision;
                                                dokk:revision_number $revision_number ;
                                                dokk:page "$page" .
                                        }
                }
    """).substitute(
        uri             = '{}/{}'.format(new_rev_number, page_name),
        revision_number = new_rev_number,
        uuid            = sparql.escape_literal(rev_uuid),
        page            = sparql.escape_literal(page_name),
        author          = sparql.escape_literal(author),
        datetime        = sparql.escape_literal(rev_datetime),
        content         = sparql.escape_literal(content),
        edit_summary    = sparql.escape_literal(edit_summary))
    
    # An error occurred with Fuseki
    if not sparql.update_graph(query):
        return False
    
    # Check if the new revision exists.
    # If the UUID does not exist, it means the node was not inserted into the graph.
    return uuid_exists(rev_uuid)

def get_last_revision(page_name):
    """
    Retrieve last revision of a page.
    
    :param page_name: Page name.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/revision/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:Revision ;
                    dokk:page "$page_name" ;
                    dokk:revision_number ?rev_number .
                }
        ORDER BY DESC(?rev_number)
        LIMIT 1
    """).substitute(page_name = sparql.escape_literal(page_name))
    
    response = sparql.query_graph(query)
    
    return response

def get_revision(page_name, revision_number):
    """
    Retrieve one revision by page name and number.
    
    :param page_name: Page name.
    :param number: Revision number.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/revision/>
        PREFIX dokk: <https://ontology.dokk.org/>

        DESCRIBE *
        WHERE   {
                    ?s a dokk:Revision ;
                    dokk:page "$page_name" ;
                    dokk:revision_number $revision_number .
                }
    """).substitute(
        page_name = sparql.escape_literal(page_name),
        revision_number = int(revision_number))
    
    response = sparql.query_graph(query)
    
    return response

def list_revisions(page_name):
    """
    Retrieve a list of revisions for one page.
    
    :param page_name: Page name.
    """
    
    query = Template("""
        PREFIX : <dokk:/dokk/revision/>
        PREFIX dokk: <https://ontology.dokk.org/>

        SELECT *
        WHERE   {
                    [] a dokk:Revision ;
                    dokk:uuid ?uuid ;
                    dokk:revision_number ?revision_number ;
                    dokk:page "$page_name" ;
                    dokk:author ?author ;
                    dokk:datetime ?datetime ;
                    dokk:edit_summary ?edit_summary .
                }
        ORDER BY DESC(?revision_number)
    """).substitute(page_name = sparql.escape_literal(page_name))
    
    response = sparql.query_graph(query)
    
    return response

