__filename__="blog.py"
__author__="Bob Mottram"
__license__="AGPL3+"
__version__="1.1.0"
__maintainer__="Bob Mottram"
__email__="bob@freedombone.net"
__status__="Production"

import json
import time
import os
from collections import OrderedDict
from datetime import datetime
from datetime import date
from dateutil.parser import parse
from shutil import copyfile
from shutil import copyfileobj
from pprint import pprint

from content import replaceEmojiFromTags
from webinterface import contentWarningScriptOpen
from webinterface import getIconsDir
from webinterface import getPostAttachmentsAsHtml
from webinterface import htmlHeader
from webinterface import htmlFooter
from webinterface import addEmbeddedElements
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import locatePost
from utils import loadJson
from posts import createBlogsTimeline


def noOfBlogReplies(baseDir: str,httpPrefix: str,translate: {}, \
                    nickname: str,domain: str,domainFull: str, \
                    postId: str,depth=0) -> int:
    """Returns the number of replies on the post
    This is recursive, so can handle replies to replies
    """
    if depth>4:
        return 0
    if not postId:
        return 0

    tryPostBox=('tlblogs','inbox','outbox')
    boxFound=False
    for postBox in tryPostBox:
        postFilename= \
            baseDir+'/accounts/'+nickname+'@'+domain+'/'+postBox+'/'+ \
            postId.replace('/','#')+'.replies'
        if os.path.isfile(postFilename):
            boxFound=True
            break
    if not boxFound:
        # post may exist but has no replies
        for postBox in tryPostBox:
            postFilename= \
                baseDir+'/accounts/'+nickname+'@'+domain+'/'+postBox+'/'+ \
                postId.replace('/','#')
            if os.path.isfile(postFilename):
                return 1
        return 0

    replies=0
    with open(postFilename, "r") as f:
        lines=f.readlines()
        for replyPostId in lines:
            replyPostId= \
                replyPostId.replace('\n','').replace('.json','').replace('.replies','')
            replies+= \
                1+ \
                noOfBlogReplies(baseDir,httpPrefix,translate, \
                                nickname,domain,domainFull, \
                                replyPostId,depth+1)
    return replies


def getBlogReplies(baseDir: str,httpPrefix: str,translate: {}, \
                   nickname: str,domain: str,domainFull: str, \
                   postId: str,depth=0) -> str:
    """Returns the number of replies on the post
    """
    if depth>4:
        return ''
    if not postId:
        return ''

    tryPostBox=('tlblogs','inbox','outbox')
    boxFound=False
    for postBox in tryPostBox:
        postFilename= \
            baseDir+'/accounts/'+nickname+'@'+domain+'/'+postBox+'/'+ \
            postId.replace('/','#')+'.replies'
        if os.path.isfile(postFilename):
            boxFound=True
            break
    if not boxFound:
        # post may exist but has no replies
        for postBox in tryPostBox:
            postFilename= \
                baseDir+'/accounts/'+nickname+'@'+domain+'/'+postBox+'/'+ \
                postId.replace('/','#')
            if os.path.isfile(postFilename):
                postFilename= \
                    baseDir+'/accounts/'+nickname+'@'+domain+ \
                    '/postcache/'+ \
                    postId.replace('/','#')+'.html'
                if os.path.isfile(postFilename):
                    with open(postFilename, "r") as postFile:
                        return postFile.read()+'\n'
        return ''

    with open(postFilename, "r") as f:
        lines=f.readlines()
        repliesStr=''
        for replyPostId in lines:
            replyPostId= \
                replyPostId.replace('\n','').replace('.json','').replace('.replies','')
            postFilename= \
                baseDir+'/accounts/'+nickname+'@'+domain+ \
                '/postcache/'+ \
                replyPostId.replace('\n','').replace('/','#')+'.html'
            if not os.path.isfile(postFilename):
                continue
            with open(postFilename, "r") as postFile:
                repliesStr+=postFile.read()+'\n'
            repliesStr+= \
                getBlogReplies(baseDir,httpPrefix,translate, \
                               nickname,domain,domainFull, \
                               replyPostId,depth+1)

        # indicate the reply indentation level
        indentStr='>'
        for indentLevel in range(depth):
            indentStr+=' >'

        return repliesStr.replace(translate['SHOW MORE'],indentStr).replace('?tl=outbox','?tl=tlblogs')
    return ''


def htmlBlogPostContent(authorized: bool, \
                        baseDir: str,httpPrefix: str,translate: {}, \
                        nickname: str,domain: str,domainFull: str, \
                        postJsonObject: {}, \
                        handle: str,restrictToDomain: bool) -> str:
    """Returns the content for a single blog post
    """
    linkedAuthor=False
    actor=''
    blogStr=''
    messageLink=''
    if postJsonObject['object'].get('id'):
        messageLink=postJsonObject['object']['id'].replace('/statuses/','/')
    titleStr=''
    if postJsonObject['object'].get('summary'):
        titleStr=postJsonObject['object']['summary']
        blogStr+='<h1><a href="'+messageLink+'">'+titleStr+'</a></h1>\n'

    # get the handle of the author
    if postJsonObject['object'].get('attributedTo'):
        actor=postJsonObject['object']['attributedTo']
        authorNickname=getNicknameFromActor(actor)
        if authorNickname:
            authorDomain,authorPort=getDomainFromActor(actor)
            if authorDomain:
                # author must be from the given domain
                if restrictToDomain and authorDomain != domain:
                    return ''
                handle=authorNickname+'@'+authorDomain
    else:
        # posts from the domain are expected to have an attributedTo field
        if restrictToDomain:
            return ''

    if postJsonObject['object'].get('published'):
        if 'T' in postJsonObject['object']['published']:
            blogStr+='<h3>'+postJsonObject['object']['published'].split('T')[0]
            if handle:
                if handle.startswith(nickname+'@'+domain):
                    blogStr+= \
                        ' <a href="'+httpPrefix+'://'+domainFull+ \
                        '/users/'+nickname+'">'+handle+'</a>'
                    linkedAuthor=True
                else:
                    if author:
                        blogStr+= \
                            ' <a href="'+author+'">'+handle+'</a>'
                        linkedAuthor=True
                    else:
                        blogStr+=' '+handle
            blogStr+='</h3>\n'

    avatarLink=''
    replyStr=''
    announceStr=''
    likeStr=''
    bookmarkStr=''
    deleteStr=''
    muteStr=''
    isMuted=False
    attachmentStr,galleryStr= \
        getPostAttachmentsAsHtml(postJsonObject,'tlblogs',translate, \
                                 isMuted,avatarLink, \
                                 replyStr,announceStr,likeStr, \
                                 bookmarkStr,deleteStr,muteStr)
    if attachmentStr:
        blogStr+='<br><center>'+attachmentStr+'</center>'

    if postJsonObject['object'].get('content'):
        contentStr= \
            addEmbeddedElements(translate, \
                                postJsonObject['object']['content'])
        if postJsonObject['object'].get('tag'):
            contentStr= \
                replaceEmojiFromTags(contentStr, \
                                     postJsonObject['object']['tag'],'content')
        blogStr+='<br>'+contentStr+'\n'

    blogStr+='<br><hr>\n'

    if not linkedAuthor:
        blogStr+= \
            '<p class="about"><a class="about" href="'+ \
            httpPrefix+'://'+domainFull+ \
            '/users/'+nickname+'">'+translate['About the author']+'</a></p>\n'

    replies= \
        noOfBlogReplies(baseDir,httpPrefix,translate, \
                        nickname,domain,domainFull, \
                        postJsonObject['object']['id'])
    if replies>0:
        if not authorized:
            blogStr+= \
                '<p class="blogreplies">'+ \
                translate['Replies'].lower()+': '+str(replies)+'</p>\n'
        else:
            blogStr+='<h1>'+translate['Replies']+'</h1>\n'
            blogStr+='<script>'+contentWarningScriptOpen()+'</script>\n'
            if not titleStr:
                blogStr+= \
                    getBlogReplies(baseDir,httpPrefix,translate, \
                                   nickname,domain,domainFull, \
                                   postJsonObject['object']['id'])
            else:
                blogStr+= \
                    getBlogReplies(baseDir,httpPrefix,translate, \
                                   nickname,domain,domainFull, \
                                   postJsonObject['object']['id']).replace('>'+titleStr+'<','')
            blogStr+='<br><hr>\n'
    return blogStr


def htmlBlogPostRSS(authorized: bool, \
                    baseDir: str,httpPrefix: str,translate: {}, \
                    nickname: str,domain: str,domainFull: str, \
                    postJsonObject: {}, \
                    handle: str,restrictToDomain: bool) -> str:
    """Returns the RSS feed for a single blog post
    """
    messageLink=''
    if postJsonObject['object'].get('id'):
        messageLink=postJsonObject['object']['id'].replace('/statuses/','/')
        if not restrictToDomain or \
           (restrictToDomain and '/'+domain in messageLink):
            if postJsonObject['object'].get('summary'):
                published=postJsonObject['object']['published']
                pubDate=datetime.strptime(published,"%Y-%m-%dT%H:%M:%SZ")
                titleStr=postJsonObject['object']['summary']
                rssStr= '     <item>'
                rssStr+='         <title>'+titleStr+'</title>'
                rssStr+='         <link>'+messageLink+'</link>'
                rssStr+='         <pubDate>'+pubDate.strftime("%a, %d %b %Y %H:%M:%S UT")+'</pubDate>'
                rssStr+='     </item>'
    return rssStr


def htmlBlogPost(authorized: bool, \
                 baseDir: str,httpPrefix: str,translate: {}, \
                 nickname: str,domain: str,domainFull: str, \
                 postJsonObject: {}) -> str:
    """Returns a html blog post
    """
    blogStr=''

    cssFilename=baseDir+'/epicyon-profile.css'
    if os.path.isfile(baseDir+'/epicyon.css'):
        cssFilename=baseDir+'/epicyon.css'
    with open(cssFilename, 'r') as cssFile:
        blogCSS=cssFile.read()
        blogStr=htmlHeader(cssFilename,blogCSS)
        blogStr=blogStr.replace('.cwText','.cwTextInactive')

        blogStr+= \
            htmlBlogPostContent(authorized,baseDir,httpPrefix,translate, \
                                nickname,domain,domainFull,postJsonObject, \
                                None,False)

        # show rss link
        iconsDir=getIconsDir(baseDir)
        blogStr+='<p class="rssfeed">'
        blogStr+='<a href="'+httpPrefix+'://'+domainFull+'/blog/'+nickname+'/rss.xml">'
        blogStr+='<img loading="lazy" alt="RSS" title="RSS" src="/'+iconsDir+'/rss.png" />'
        blogStr+='</a></p>'

        return blogStr+htmlFooter()
    return None

def htmlBlogPage(authorized: bool, session, \
                 baseDir: str,httpPrefix: str,translate: {}, \
                 nickname: str,domain: str,port: int, \
                 noOfItems: int,pageNumber: int) -> str:
    """Returns a html blog page containing posts
    """
    if ' ' in nickname or '@' in nickname or '\n' in nickname:
        return None
    blogStr=''

    cssFilename=baseDir+'/epicyon-profile.css'
    if os.path.isfile(baseDir+'/epicyon.css'):
        cssFilename=baseDir+'/epicyon.css'
    with open(cssFilename, 'r') as cssFile:
        blogCSS=cssFile.read()
        blogStr=htmlHeader(cssFilename,blogCSS)
        blogStr=blogStr.replace('.cwText','.cwTextInactive')

        blogsIndex= \
            baseDir+'/accounts/'+nickname+'@'+domain+'/tlblogs.index'
        if not os.path.isfile(blogsIndex):
            return blogStr+htmlFooter()

        timelineJson= \
            createBlogsTimeline(session,baseDir, \
                                nickname,domain,port,httpPrefix, \
                                noOfItems,False,False,pageNumber)

        if not timelineJson:
            return blogStr+htmlFooter()

        domainFull=domain
        if port:
            if port!=80 and port!=443:
                domainFull=domain+':'+str(port)

        # show previous and next buttons
        if pageNumber!=None:
            iconsDir=getIconsDir(baseDir)
            navigateStr='<p>'
            if pageNumber>1:
                # show previous button
                navigateStr+= \
                    '<a href="'+httpPrefix+'://'+domainFull+'/blog/'+ \
                    nickname+'?page='+str(pageNumber-1)+'">'+ \
                    '<img loading="lazy" alt="<" title="<" '+ \
                    'src="/'+iconsDir+ \
                    '/prev.png" class="buttonprev"/></a>\n'
            if len(timelineJson['orderedItems'])>=noOfItems:
                # show next button
                navigateStr+= \
                    '<a href="'+httpPrefix+'://'+domainFull+'/blog/'+nickname+ \
                    '?page='+str(pageNumber+1)+'">'+ \
                    '<img loading="lazy" alt=">" title=">" '+ \
                    'src="/'+iconsDir+ \
                    '/prev.png" class="buttonnext"/></a>\n'
            navigateStr+='</p>'
            blogStr+=navigateStr

        for item in timelineJson['orderedItems']:
            if item['type']!='Create':
                continue

            blogStr+= \
                htmlBlogPostContent(authorized,baseDir,httpPrefix,translate, \
                                    nickname,domain,domainFull,item, \
                                    None,True)

        if len(timelineJson['orderedItems'])>=noOfItems:
            blogStr+=navigateStr

        # show rss link
        blogStr+='<p class="rssfeed">'
        blogStr+='<a href="'+httpPrefix+'://'+domainFull+'/blog/'+nickname+'/rss.xml">'
        blogStr+='<img loading="lazy" alt="RSS" title="RSS" src="/'+iconsDir+'/rss.png" />'
        blogStr+='</a></p>'

        return blogStr+htmlFooter()
    return None

def rssHeader(httpPrefix: str,nickname: str,domainFull: str,translate: {}) -> str:
    rssStr="<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"
    rssStr+="<rss version=\"2.0\">"
    rssStr+='<channel>'
    rssStr+='    <title>'+translate['Blog']+'</title>'
    rssStr+='    <link>'+httpPrefix+'://'+domainFull+'/users/'+nickname+'/rss.xml'+'</link>'
    return rssStr

def rssFooter() -> str:
    rssStr='</channel>'
    rssStr+='</rss>'
    return rssStr

def htmlBlogPageRSS(authorized: bool, session, \
                    baseDir: str,httpPrefix: str,translate: {}, \
                    nickname: str,domain: str,port: int, \
                    noOfItems: int,pageNumber: int) -> str:
    """Returns an rss feed containing posts
    """
    if ' ' in nickname or '@' in nickname or '\n' in nickname:
        return None

    domainFull=domain
    if port:
        if port!=80 and port!=443:
            domainFull=domain+':'+str(port)

    blogRSS=rssHeader(httpPrefix,nickname,domainFull,translate)

    blogsIndex= \
        baseDir+'/accounts/'+nickname+'@'+domain+'/tlblogs.index'
    if not os.path.isfile(blogsIndex):
        return blogRSS+rssFooter()

    timelineJson= \
        createBlogsTimeline(session,baseDir, \
                            nickname,domain,port,httpPrefix, \
                            noOfItems,False,False,pageNumber)

    if not timelineJson:
        return blogRSS+rssFooter()

    if pageNumber!=None:
        for item in timelineJson['orderedItems']:
            if item['type']!='Create':
                continue

            blogRSS+= \
                htmlBlogPostRSS(authorized,baseDir,httpPrefix,translate, \
                                nickname,domain,domainFull,item, \
                                None,True)

    return blogRSS+rssFooter()


def getBlogIndexesForAccounts(baseDir: str) -> {}:
    """ Get the index files for blogs for each account
    and add them to a dict
    """
    blogIndexes={}
    for subdir, dirs, files in os.walk(baseDir+'/accounts'):
        for acct in dirs:
            if '@' not in acct:
                continue
            if 'inbox@' in acct:
                continue
            accountDir=os.path.join(baseDir+'/accounts', acct)
            blogsIndex=accountDir+'/tlblogs.index'
            if os.path.isfile(blogsIndex):
                blogIndexes[acct]=blogsIndex
    return blogIndexes

def noOfBlogAccounts(baseDir: str) -> int:
    """Returns the number of blog accounts
    """
    ctr=0
    for subdir, dirs, files in os.walk(baseDir+'/accounts'):
        for acct in dirs:
            if '@' not in acct:
                continue
            if 'inbox@' in acct:
                continue
            accountDir=os.path.join(baseDir+'/accounts', acct)
            blogsIndex=accountDir+'/tlblogs.index'
            if os.path.isfile(blogsIndex):
                ctr+=1
    return ctr

def singleBlogAccountNickname(baseDir: str) -> str:
    """Returns the nickname of a single blog account
    """
    for subdir, dirs, files in os.walk(baseDir+'/accounts'):
        for acct in dirs:
            if '@' not in acct:
                continue
            if 'inbox@' in acct:
                continue
            accountDir=os.path.join(baseDir+'/accounts', acct)
            blogsIndex=accountDir+'/tlblogs.index'
            if os.path.isfile(blogsIndex):
                return acct.split('@')[0]
    return None

def htmlBlogView(authorized: bool, \
                 session,baseDir: str,httpPrefix: str, \
                 translate: {},domain: str,port: int, \
                 noOfItems: int) -> str:
    """Show the blog main page
    """
    blogStr=''

    cssFilename=baseDir+'/epicyon-profile.css'
    if os.path.isfile(baseDir+'/epicyon.css'):
        cssFilename=baseDir+'/epicyon.css'
    with open(cssFilename, 'r') as cssFile:
        blogCSS=cssFile.read()
        blogStr=htmlHeader(cssFilename,blogCSS)

        if noOfBlogAccounts(baseDir) <= 1:
            nickname=singleBlogAccountNickname(baseDir)
            if nickname:
                return htmlBlogPage(authorized,session, \
                                    baseDir,httpPrefix,translate, \
                                    nickname,domain,port, \
                                    noOfItems,1)

        domainFull=domain
        if port:
            if port!=80 and port!=443:
                domainFull=domain+':'+str(port)

        for subdir, dirs, files in os.walk(baseDir+'/accounts'):
            for acct in dirs:
                if '@' not in acct:
                    continue
                if 'inbox@' in acct:
                    continue
                accountDir=os.path.join(baseDir+'/accounts', acct)
                blogsIndex=accountDir+'/tlblogs.index'
                if os.path.isfile(blogsIndex):
                    blogStr+='<p class="blogaccount">'
                    blogStr+= \
                        '<a href="'+ \
                        httpPrefix+'://'+domainFull+'/blog/'+ \
                        acct.split('@')[0]+'">'+acct+'</a>'
                    blogStr+='</p>'

        return blogStr+htmlFooter()
    return None

def htmlEditBlog(mediaInstance: bool,translate: {}, \
                 baseDir: str,httpPrefix: str, \
                 path: str, \
                 pageNumber: int, \
                 nickname: str,domain: str, \
                 postUrl: str) -> str:
    """Edit a blog post after it was created
    """
    postFilename=locatePost(baseDir,nickname,domain,postUrl)
    if not postFilename:
        print('Edit blog: Filename not found for '+postUrl)
        return None

    postJsonObject=loadJson(postFilename)
    if not postJsonObject:
        print('Edit blog: json not loaded for '+postFilename)
        return None

    iconsDir=getIconsDir(baseDir)

    editBlogText= \
        '<p class="new-post-text">'+ \
        translate['Write your post text below.']+'</p>'

    if os.path.isfile(baseDir+'/accounts/newpost.txt'):
        with open(baseDir+'/accounts/newpost.txt', 'r') as file:
            editBlogText='<p class="new-post-text">'+file.read()+'</p>'

    cssFilename=baseDir+'/epicyon-profile.css'
    if os.path.isfile(baseDir+'/epicyon.css'):
        cssFilename=baseDir+'/epicyon.css'
    with open(cssFilename, 'r') as cssFile:
        editBlogCSS=cssFile.read()
        if httpPrefix!='https':
            editBlogCSS=editBlogCSS.replace('https://',httpPrefix+'://')

    if '?' in path:
        path=path.split('?')[0]
    pathBase=path

    editBlogImageSection ='    <div class="container">'
    editBlogImageSection+='      <label class="labels">'+translate['Image description']+'</label>'
    editBlogImageSection+='      <input type="text" name="imageDescription">'
    editBlogImageSection+='      <input type="file" id="attachpic" name="attachpic"'
    editBlogImageSection+='            accept=".png, .jpg, .jpeg, .gif, .webp, .mp4, .webm, .ogv, .mp3, .ogg">'
    editBlogImageSection+='    </div>'

    placeholderMessage=translate['Write something']+'...'
    endpoint='editblogpost'
    placeholderSubject=translate['Title']
    scopeIcon='scope_blog.png'
    scopeDescription=translate['Blog']

    dateAndLocation=''
    dateAndLocation='<div class="container">'

    dateAndLocation+= \
        '<p><input type="checkbox" class="profilecheckbox" name="schedulePost"><label class="labels">'+ \
        translate['This is a scheduled post.']+'</label></p>'

    dateAndLocation+= \
        '<p><img loading="lazy" alt="" title="" class="emojicalendar" src="/'+ \
        iconsDir+'/calendar.png"/>'
    dateAndLocation+='<label class="labels">'+translate['Date']+': </label>'
    dateAndLocation+='<input type="date" name="eventDate">'
    dateAndLocation+='<label class="labelsright">'+translate['Time']+':'
    dateAndLocation+='<input type="time" name="eventTime"></label></p>'
    dateAndLocation+='</div>'
    dateAndLocation+='<div class="container">'
    dateAndLocation+='<br><label class="labels">'+translate['Location']+': </label>'
    dateAndLocation+='<input type="text" name="location">'
    dateAndLocation+='</div>'

    editBlogForm=htmlHeader(cssFilename,editBlogCSS)

    mentionsStr=''

    editBlogForm+= \
        '<form enctype="multipart/form-data" method="POST" accept-charset="UTF-8" action="'+ \
        pathBase+'?'+endpoint+'?page='+str(pageNumber)+'">'
    editBlogForm+='  <input type="hidden" name="postUrl" value="'+postUrl+'">'
    editBlogForm+='  <input type="hidden" name="pageNumber" value="'+str(pageNumber)+'">'
    editBlogForm+='  <div class="vertical-center">'
    editBlogForm+='    <label for="nickname"><b>'+editBlogText+'</b></label>'
    editBlogForm+='    <div class="container">'

    editBlogForm+='      <div class="dropbtn">'
    editBlogForm+= \
        '        <img loading="lazy" alt="" title="" src="/'+iconsDir+ \
        '/'+scopeIcon+'"/><b class="scope-desc">'+scopeDescription+'</b>'
    editBlogForm+='      </div>'

    editBlogForm+= \
        '      <a href="'+pathBase+ \
        '/searchemoji"><img loading="lazy" '+ \
        'class="emojisearch" src="/emoji/1F601.png" title="'+ \
        translate['Search for emoji']+'" alt="'+ \
        translate['Search for emoji']+'"/></a>'
    editBlogForm+='    </div>'
    editBlogForm+='    <div class="container"><center>'
    editBlogForm+= \
        '      <a href="'+pathBase+ \
        '/inbox"><button class="cancelbtn">'+ \
        translate['Cancel']+'</button></a>'
    editBlogForm+= \
        '      <input type="submit" name="submitPost" value="'+ \
        translate['Submit']+'">'
    editBlogForm+='    </center></div>'
    if mediaInstance:
        editBlogForm+=editBlogImageSection
    editBlogForm+= \
        '    <label class="labels">'+placeholderSubject+'</label><br>'
    titleStr=''
    if postJsonObject['object'].get('summary'):
        titleStr=postJsonObject['object']['summary']
    editBlogForm+= \
        '    <input type="text" name="subject" value="'+titleStr+'">'
    editBlogForm+=''
    editBlogForm+= \
        '    <br><label class="labels">'+ \
        placeholderMessage+'</label>'
    messageBoxHeight=800

    contentStr= \
        postJsonObject['object']['content'].replace('<p>','').replace('</p>','\n')

    editBlogForm+= \
        '    <textarea id="message" name="message" style="height:'+ \
        str(messageBoxHeight)+'px">'+contentStr+'</textarea>'
    editBlogForm+=dateAndLocation
    if not mediaInstance:
        editBlogForm+=editBlogImageSection
    editBlogForm+='  </div>'
    editBlogForm+='</form>'

    editBlogForm= \
        editBlogForm.replace('<body>','<body onload="focusOnMessage()">')

    editBlogForm+=htmlFooter()
    return editBlogForm