#!/usr/bin/env python3
#####
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#####
import os, sys, json, re
version = 2
if sys.hexversion < 0x03000000:
    import ConfigParser as configparser
else:
    import configparser
    version = 3

path = os.path.abspath(os.getcwd())

sys.path.append(path)
sys.path.append(os.path.basename(sys.argv[0]))
if 'SCRIPT_FILENAME' in os.environ:
    sys.path.insert(0, os.path.basename(os.environ['SCRIPT_FILENAME']))


# Fetch config (hack, hack, hack)
config = configparser.RawConfigParser()
config.read(path + '/../../steve.cfg')

# Some quick paths
homedir = config.get("general", "homedir")
pathinfo = os.environ['PATH_INFO'] if 'PATH_INFO' in os.environ else None


whoami = os.environ['REMOTE_USER'] if 'REMOTE_USER' in os.environ else None

from lib import response, voter, election, form, constants

if not whoami:
    response.respond(403, {'message': 'Could not verify your identity: No auth scheme found'})
elif not config.has_option('karma', whoami):
    response.respond(403, {'message': 'Could not verify your identity: No such user: %s' % whoami})
else:
    
    karma = int(config.get("karma", whoami))
    
    # Figure out what to do and where
    if pathinfo:
        l = pathinfo.split("/")
        if l[0] == "":
            l.pop(0)
        action = l[0]
        electionID = l[1] if len(l) > 1 else None
        if electionID:
            if re.search(r"([^A-Za-z0-9-.])", electionID):
                response.respond(400, {'message': "Invalid election ID supplied, must be [A-Za-z0-9-.]+"})
                sys.exit(0) # BAIL!
        issue = l[2] if len(l) > 2 else None
 
        # List all existing/previous elections?
        if action == "list":
            output = []
            errors = []
            path = os.path.join(homedir, "issues")
            elections = election.listElections()
            for electionID in elections:
                try:
                    basedata = election.getBasedata(electionID, hideHash = True)
                    if karma >= 5 or ('owner' in basedata and basedata['owner'] == whoami):
                        output.append(basedata)
                except Exception as err:
                    errors.append("Could not parse election '%s': %s" % (electionID, err))
            if len(errors) > 0:
                response.respond(206, { 'elections': output, 'errors': errors})
            else:
                response.respond(200, { 'elections': output})
        # Set up new election?
        elif action == "setup":
            if karma >= 4: # karma of 4 required to set up an election base
                if electionID:
                    if election.exists(electionID):
                        response.respond(403, {'message': "Election already exists!"})
                    else:
                        try:
                            required = ['title','owner','monitors']
                            xr = required
                            for i in required:
                                if not form.getvalue(i):
                                    raise Exception("Required fields missing: %s" % ", ".join(xr))
                                else:
                                    xr.pop(0)
                            election.createElection(
                                electionID,
                                form.getvalue('title'),
                                form.getvalue('owner'),
                                [x.strip() for x in form.getvalue('monitors').split(",")],
                                form.getvalue('starts'),
                                form.getvalue('ends'),
                                form.getvalue('open')
                            )
                            response.respond(201, {'message': 'Created!', 'id': electionID})
                        except Exception as err:
                            response.respond(500, {'message': "Could not create electionID: %s" % err})
                else:
                    response.respond(400, {'message': "No election name specified!"})
            else:
                response.respond(403, {'message': 'You do not have enough karma for this'})
                
        # Create an issue in an election
        elif action == "create":
            if karma >= 4: # karma of 4 required to set up an issue for the election
                if electionID:
                    if not issue:
                        response.respond(400, {'message': 'No issue ID specified'})
                    elif re.search(r"([^A-Za-z0-9-.])", issue):
                        response.respond(400, {'message': "Invalid issue ID supplied, must be [A-Za-z0-9-.]+"})
                    else:
                        issuepath = os.path.join(homedir, "issues", electionID, issue)
                        if os.path.isfile(issuepath + ".json"):
                            response.respond(400, {'message': 'An issue with this ID already exists'})
                        else:
                            try:
                                required = ['title','type']
                                xr = required
                                for i in required:
                                    if not form.getvalue(i):
                                        raise Exception("Required fields missing: %s" % ", ".join(xr))
                                    else:
                                        xr.pop(0)
                                if not election.validType(form.getvalue('type')):
                                    raise Exception('Invalid vote type: %s' % form.getvalue('type'))
                                else:
                                    candidates = []
                                    c = []
                                    s = []
                                    if form.getvalue('candidates'):
                                        try:
                                            c = json.loads(form.getvalue('candidates'))
                                            if form.getvalue('statements'):
                                                try:
                                                    s = json.loads(form.getvalue('statements'))
                                                except:
                                                    s = form.getvalue('statements').split("\n")
                                        except:
                                            c = form.getvalue('candidates').split("\n")
                                        z = 0
                                        for entry in [x for x in c if x and x.strip()]: # Skip blank entries
                                            candidates.append({'name': entry.strip(), 'statement': s[z] if len(s) > z else ""})
                                            z += 1
                                    # HACK: If candidate parsing is outsourced, let's do that instead (primarily for COP)
                                    voteType = election.getVoteType({'type': form.getvalue('type')})
                                    if 'parsers' in voteType and 'candidates' in voteType['parsers']:
                                        candidates = voteType['parsers']['candidates'](form.getvalue('candidates'))
                                        
                                    election.createIssue(electionID, issue, {
                                        'election': electionID,
                                        'id': issue,
                                        'title': form.getvalue('title'),
                                        'description': form.getvalue('description'),
                                        'type': form.getvalue('type'),
                                        'candidates': candidates,
                                        'seconds': [x.strip() for x in form.getvalue('seconds').split("\n")] if form.getvalue('seconds') else [],
                                        'nominatedby': form.getvalue('nominatedby')
                                    })
                                response.respond(201, {'message': 'Created!', 'id': issue})
                            except Exception as err:
                                response.respond(500, {'message': "Could not create issue: %s" % err})
                else:
                    response.respond(400, {'message': "No election specified!"})
            else:
                response.respond(403, {'message': 'You do not have enough karma for this'})
        
        # Delete an issue in an election
        elif action == "delete":
            if karma >= 4: # karma of 4 required to set up an issue for the election
                if electionID:
                    if not issue:
                        response.respond(400, {'message': 'No issue ID specified'})
                    else:
                        if election.exists(electionID, issue):
                            try:
                                election.deleteIssue(electionID, issue)
                                response.respond(200, {'message': "Issue deleted"})
                            except Exception as err:
                                response.respond(500, {'message': 'Could not delete issue: %s' % err})
                        else:
                            response.respond(404, {'message': "No such issue!"})
                else:
                    response.respond(400, {'message': "No electionID specified!"})
            else:
                response.respond(403, {'message': 'You do not have enough karma for this'})
        
        
        
        # Edit an issue or election
        elif action == "edit":
            if (issue and karma >= 4) or (karma >= 5 and electionID):
                if electionID:
                    if not issue:
                        if not election.exists(electionID,):
                            response.respond(404, {'message': 'No such election'})
                        else:
                            try:
                                basedata = election.getBasedata(electionID)
                                fields = ['title','owner','monitors','starts','ends']
                                for field in fields:
                                    val = form.getvalue(field)
                                    if val:
                                        if field == "monitors":
                                            val = [x.strip() for x in val.split(",")]
                                        basedata[field] = val
                                election.updateElection(electionID, basedata)
                                response.respond(200, {'message': "Changed saved"})
                            except Exception as err:
                                response.respond(500, {'message': "Could not edit election: %s" % err})
                    else:
                        if not election.exists(electionID, issue):
                            response.respond(404, {'message': 'No such issue'})
                        else:
                            try:
                                issuedata = election.getIssue(electionID, issue)
                                fields = ['title','description','type','statements','seconds_txt','candidates','seconds','nominatedby']
                                statements = []
                                seconds = []
                                for field in fields:
                                    val = form.getvalue(field)
                                    if val:
                                        if field == "candidates":
                                            try:
                                                xval = json.loads(val)
                                            except:
                                                xval = val.split("\n")
                                            val = []
                                            z = 0
                                            for entry in xval:
                                                val.append({
                                                    'name': entry.strip(),
                                                    'statement': statements[z] if len(statements) > z else "",
                                                    'seconds_txt': seconds[z] if len(seconds) > z else ""
                                                    })
                                                z += 1
                                        if field == "statements":
                                            try:
                                                xval = json.loads(val)
                                            except:
                                                xval = val.split("\n")
                                            val = []
                                            for entry in xval:
                                                statements.append(entry)
                                        if field == "seconds_txt":
                                            try:
                                                xval = json.loads(val)
                                            except:
                                                xval = val.split("\n")
                                            val = []
                                            for entry in xval:
                                                seconds.append(entry)
                                        if field == "seconds":
                                            val = [x.strip() for x in val.split("\n")]
                                        
                                            
                                        # HACK: If field  parsing is outsourced, let's do that instead (primarily for COP)
                                        voteType = election.getVoteType(issuedata)
                                        if 'parsers' in voteType and field in voteType['parsers']:
                                            val = voteType['parsers'][field](form.getvalue(field))
                                            
                                        issuedata[field] = val
                                election.updateIssue(electionID, issue, issuedata)
                                response.respond(200, {'message': "Changed saved"})
                            except Exception as err:
                                response.respond(500, {'message': "Could not edit issue: %s" % err})
                else:
                    response.respond(400, {'message': "No election specified!"})
            else:
                response.respond(403, {'message': 'You do not have enough karma for this'})
        
        elif action == "view" and karma >= 2:
            # View a list of issues for an election
            if electionID:
                js = []
                if election.exists(electionID):
                    basedata = {}
                    try:
                        basedata = election.getBasedata(electionID, hideHash = True)
                        for issue in election.listIssues(electionID):
                            try:
                                entry = election.getIssue(electionID, issue)
                                js.append(entry)
                            except Exception as err:
                                response.respond(500, {'message': 'Could not load issues: %s' % err})
                    except Exception as err:
                        response.respond(500, {'message': 'Could not load base data: %s' % err})
                    if 'hash' in basedata:
                        del basedata['hash']
                    response.respond(200, {'base_data': basedata, 'issues': js, 'baseurl': "%s/election.html?%s" % (config.get("general", "rooturl"), electionID)})
                else:
                    response.respond(404, {'message': 'No such election: %s' % electionID})
            else:
                    response.respond(404, {'message': 'Invalid election ID'})
        # Delete an issue
        elif action == "delete" and electionID and issue:
            if electionID and issue:
                basedata = election.getBasedata(electionID)
                if karma >= 4 or ('owner' in basedata and basedata['owner'] == whoami):
                    issuedata = election.getIssue(electionID, issue)
                    if issuedata:
                        election.deleteIssue(electionID, issue)
                        response.respond(200, {'message': 'Issue deleted'})
                    else:
                        response.respond(404, {'message': "Issue not found"})
                else:
                    response.respond(403, {'message': "You do not have karma to delete this issue"})
            else:
                    response.respond(404, {'message': 'No such election or issue'})
        
        # Send issue hash to monitors
        elif action == "debug" and electionID:
            if election.exists(electionID):
                basedata = election.getBasedata(electionID)
                if karma >= 4 or ('owner' in basedata and basedata['owner'] == whoami):
                    ehash, debug = election.getHash(electionID)
                    for email in basedata['monitors']:
                        voter.email(email, "Monitoring update for election #%s: %s" % (electionID, basedata['title']), debug)
                    response.respond(200, {'message': "Debug sent to monitors", 'hash': ehash, 'debug': debug})
                else:
                    response.respond(403, {'message': "You do not have karma to do this"})
            else:
                    response.respond(404, {'message': 'No such election'})
        
        # Get a temp voter ID for peeking
        elif action == "temp" and electionID:
            if electionID and election.exists(electionID):
                basedata = election.getBasedata(electionID)
                if karma >= 4 or ('owner' in basedata and basedata['owner'] == whoami):
                        voterid, xhash = voter.add(electionID, basedata, whoami + "@stv")
                        response.respond(200, {'id': voterid})
                else:
                    response.respond(403, {'message': "You do not have karma to peek at this election"})
            else:
                    response.respond(404, {'message': 'No such election'})
            
        # Invite folks to the election
        elif action == "invite" and karma >= 3:
            # invite one or more people to an election
            if electionID:
                email = form.getvalue('email')
                proxy = None
                m = re.match(r"^(\S+)\s+(\S+)$", email)
                if m:
                    email = m.group(1)
                    proxy = m.group(2)
                msgtype = form.getvalue('msgtype')
                msgtemplate = form.getvalue('msgtemplate')
                if not email or len(email) > 300 or not re.match(r"([^@]+@[^@]+)", email):
                    response.respond(400, {'message': 'Could not request voter ID: Invalid email address specified'})
                elif not msgtemplate or len(msgtemplate) < 10:
                    response.respond(400, {'message': 'No message template specified'})
                else:
                    js = []
                    if election.exists(electionID):
                        basedata = {}
                        try:
                            basedata = election.getBasedata(electionID)
                            if (not 'open' in basedata or basedata['open'] != "true") and msgtype == "open":
                                raise Exception("An open vote invite was requested, but this election is not public")
                            if msgtype != "open":
                                # If we have a proxy, we have to append the proxy name
                                # so as to not override the voters own ID
                                mailID = email
                                if proxy:
                                    mailID = "%s-%s" % (email, proxy)
                                # Generate voter ID
                                voterid, xhash = voter.add(electionID, basedata, mailID)
                                message = msgtemplate.replace("$votelink", "%s/election.html?%s/%s" % (config.get("general", "rooturl"), electionID, voterid))
                                message = message.replace("$title", basedata['title'])
                                subject = "Election open for votes: %s (%s)" % (electionID, basedata['title'])
                                if proxy:
                                    subject = "%s [PROXY FOR: %s]" % (subject, proxy)
                                voter.email(email, subject, message)
                            else:
                                message = msgtemplate.replace("$votelink", "%s/request_link.html?%s" % (config.get("general", "rooturl"), electionID))
                                message = message.replace("$title", basedata['title'])
                                subject = "Public election open for votes: %s (%s)" % (electionID, basedata['title'])
                                voter.email(email, subject, message)
                        except Exception as err:
                            response.respond(500, {'message': 'Could not load base data: %s' % err})                        
                        response.respond(200, {'message': "Vote link sent to %s" % email})
                    else:
                        response.respond(404, {'message': 'No such election'})
            else:
                    response.respond(404, {'message': 'No such election'})
        # Tally an issue
        elif action == "tally" and electionID:
            if electionID and issue:
                basedata = election.getBasedata(electionID)
                # Allow access at all times to owners/admins, monitors after closed
                if karma >= 4 or ('owner' in basedata and basedata['owner'] == whoami) or \
                (karma >= 2 and 'open' in basedata and basedata['open'] == False):
                    issuedata = election.getIssue(electionID, issue)
                    votes = election.getVotes(electionID, issue)
                    if issuedata and votes:
                        if election.validType(issuedata['type']):
                            result , pp = election.tally(votes, issuedata)
                            response.respond(200, result)
                        else:
                            response.respond(500, {'message': "Unknown vote type"})
                    elif not votes:
                        response.respond(404, {'message': "No votes found"})
                    else:
                        response.respond(404, {'message': "Issue not found"})
                else:
                    response.respond(403, {'message': "You do not have karma to tally the votes here"})
            else:
                    response.respond(404, {'message': 'No such election or issue'})
        # Close an election
        elif action == "close" and electionID:
            ro = form.getvalue('reopen')
            if ro and ro == "true":
                ro = True
            else:
                ro = False
            if election.exists(electionID):
                basedata = election.getBasedata(electionID)
                if karma >= 4 or ('owner' in basedata and basedata['owner'] == whoami):
                    try:
                        election.close(electionID, reopen=ro)
                        ehash, debug = election.getHash(electionID)
                        if ro:
                            for email in basedata['monitors']:
                                voter.email(email, "Monitoring update for election #%s: Election reopened!" % electionID, debug)
                            response.respond(200, {'message': "Election reopened"})
                        else:
                            murl =  "%s/admin/tally.html?%s" % (config.get("general", "rooturl"), electionID)
                            for email in basedata['monitors']:
                                voter.email(email, "Monitoring update for election #%s: Election closed!" % electionID, "%s\n\nFinal tally available at: %s" % (debug, murl))
                            response.respond(200, {'message': "Election closed"})
                    except Exception as err:
                        response.respond(500, {'message': "Could not close election: %s" % err})
                else:
                    response.respond(403, {'message': "You do not have karma to tally the votes here"})
            else:
                    response.respond(404, {'message': 'No such election or issue'})
        # Get registered vote stpye
        elif action == "types":
            types = {}
            for vtype in constants.VOTE_TYPES:
                types[vtype['key']] = vtype['description']
            response.respond(200, {'types': types})
        
        # Get vote data
        elif action == "monitor" and electionID:
            if electionID and issue:
                basedata = election.getBasedata(electionID, hideHash=True)
                if karma >= 2 or ('owner' in basedata and basedata['owner'] == whoami):
                    issuedata = election.getIssue(electionID, issue)
                    votes = election.getVotesRaw(electionID, issue)
                    jvotes = {}
                    for vote in votes:
                        jvotes[constants.hexdigest(vote['key'])] = {
                            'vote': vote['data']['vote'],
                            'timestamp': vote['data']['timestamp']
                        } # yeah, let's not show the actual UID here..
                    if issuedata and votes:
                        if election.validType(issuedata['type']):
                            ehash, blergh = election.getHash(electionID)
                            response.respond(200, {
                                'issue': issuedata,
                                'base': basedata,
                                'votes': jvotes,
                                'hash': ehash
                            })
                        else:
                            response.respond(500, {'message': "Unknown vote type"})
                    elif issuedata and not votes:
                        response.respond(404, {'message': "No votes found"})
                    else:
                        response.respond(404, {'message': "Issue not found"})
                else:
                    response.respond(403, {'message': "You do not have karma to tally the votes here"})
            else:
                    response.respond(404, {'message': 'No such election or issue'})
        # Vote backlog, including all recasts
        elif action == "backlog" and electionID:
            if electionID and issue:
                basedata = election.getBasedata(electionID, hideHash=True)
                if karma >= 2 or ('owner' in basedata and basedata['owner'] == whoami):
                    issuedata = election.getIssue(electionID, issue)
                    votes = election.getVoteHistory(electionID, issue)
                    jvotes = []
                    for vote in votes:
                        jvotes.append({
                            'vote': vote['data']['vote'],
                            'timestamp': vote['data']['timestamp'],
                            'uid': constants.hexdigest(vote['key'])
                        })
                    if issuedata and votes:
                        if election.validType(issuedata['type']):
                            ehash, blergh = election.getHash(electionID)
                            response.respond(200, {
                                'issue': issuedata,
                                'base': basedata,
                                'history': jvotes,
                                'hash': ehash
                            })
                        else:
                            response.respond(500, {'message': "Unknown vote type"})
                    elif issuedata and not votes:
                        response.respond(404, {'message': "No votes found"})
                    else:
                        response.respond(404, {'message': "Issue not found"})
                else:
                    response.respond(403, {'message': "You do not have karma to tally the votes here"})
            else:
                    response.respond(404, {'message': 'No such election or issue'})
                  
        else:
            response.respond(400, {'message': "No (or invalid) action supplied"})
    else:
        response.respond(500, {'message': "No path_info supplied"})
