#!/usr/bin/python3
# A mini-app for accepting Zoom webhooks

import configparser
import json
import psycopg2
import psycopg2.extras
import traceback
import dateutil.parser

### Configuration ###

config = configparser.ConfigParser()
config.read('zoom.ini')

### Database connection ###

db_connection = None
db = None

def db_connect():
    global db_connection, db
    db_connection = psycopg2.connect(
                host='localhost',
                user=config['db']['user'],
                password=config['db']['passwd'],
                dbname=config['db']['name'],
            )
    db = db_connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)

def db_query(query, args=()):
    if db is None:
        db_connect()
    try:
        db.execute(query, args)
    except psycopg2.DatabaseError:
        # Reconnect if the connection died (timeout, server restart etc.)
        db_connect()
        db.execute(query, args)

### Utilities ###

# FIXME: Move to shared code?

def parse_time(iso_time):
    return dateutil.parser.isoparse(iso_time)

### Application ###

class HookApp:

    def __init__(self, env, start_response):
        self.env = env
        self.wsgi_start = start_response

    def log(self, msg):
        print(msg, file=self.env['wsgi.errors'], flush=True)

    def http_error(self, code, msg, extra_headers=[]):
        self.wsgi_start("{} {}".format(code, msg), extra_headers + [
                ('Content-Type', 'text/plain')
            ])
        return ["{} {}".format(code, msg)]

    def create_regular_meeting(self, uid, meeting):
        meeting_id = meeting["id"]
        self.log(f"Meeting {meeting_id}: Planning")

        db_query("""
                INSERT INTO zoom_meetings
                (meeting_id, uuid, host_id, topic, type, start_time, duration)
                VALUES (%s, %s, %s, %s, %s, %s, %s)
            """, (
                meeting_id,
                meeting['uuid'],
                uid,
                meeting['topic'],
                meeting['type'],
                parse_time(meeting['start_time']),
                meeting['duration'],
            ))


    def create_recurring_meeting(self, uid, meeting):
        meeting_id = meeting["id"]

        for occ in meeting["occurrences"]:
            occ_id = occ["occurrence_id"]
            self.log(f"Meeting {meeting_id}: Planning occurrence {occ_id}")

            db_query("""
                    INSERT INTO zoom_meetings
                    (meeting_id, uuid, occurrence_id, host_id, topic, type, start_time, duration)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
                """, (
                    meeting_id,
                    meeting['uuid'],
                    occ_id,
                    uid,
                    meeting['topic'],
                    meeting['type'],
                    parse_time(occ['start_time']),
                    occ['duration'],
                ))


    def create_meeting(self, js):
        payload = js["payload"]
        meeting = payload["object"]
        meeting_id = meeting["id"]

        host_user_id = meeting["host_id"]
        db_query("SELECT * FROM zoom_users WHERE user_id=%s", (host_user_id,))
        user = db.fetchone()
        if user is None:
            self.log(f"Meeting {meeting_id}: Host {host_user_id} not found in zoom_users")
            return

        type = meeting["type"]
        if type == 2:
            self.create_regular_meeting(user.id, meeting)
        elif type == 8:
            self.create_recurring_meeting(user.id, meeting)
        else:
            self.log(f"Meeting {meeting_id}: Unknown type {type}")
            return


    def delete_regular_meeting(self, meeting):
        meeting_id = meeting["id"]

        self.log(f"Meeting {meeting_id}: Deleting")
        db_query("""
                DELETE FROM zoom_meetings
                WHERE meeting_id=%s
            """, (
                meeting_id,
            ))


    def delete_recurring_meeting(self, meeting):
        meeting_id = meeting["id"]

        if "occurrences" not in meeting:
            return self.delete_regular_meeting(meeting)

        for occ in meeting["occurrences"]:
            occ_id = occ["occurrence_id"]
            self.log(f"Meeting {meeting_id}: Deleting occurrence {occ_id}")
            db_query("""
                    DELETE FROM zoom_meetings
                    WHERE meeting_id=%s AND occurrence_id=%s
                """, (
                    meeting_id,
                    occ_id,
                ))


    def delete_meeting(self, js):
        payload = js["payload"]
        meeting = payload["object"]
        meeting_id = meeting["id"]

        type = meeting["type"]
        if type == 2:
            self.delete_regular_meeting(meeting)
        elif type == 8:
            self.delete_recurring_meeting(meeting)
        else:
            self.log(f"Meeting {meeting_id}: Unknown type {type}")
            return


    def update_common_attrs(self, meeting_id, occurrence_id, new):
        if "topic" in new:
            self.log(f"Meeting {meeting_id}.{occurrence_id}: Updating topic")
            db_query("UPDATE zoom_meetings SET topic=%s WHERE meeting_id=%s AND (occurrence_id=%s OR %s = '0')",
                     (new["topic"], meeting_id, occurrence_id, occurrence_id))

        for a in ['uuid', 'host_id']:
            if a in new:
                self.log(f"Meeting {meeting_id}.{occurrence_id}: Change of {a} not supported")


    def update_schedule(self, meeting_id, occurrence_id, new):
        if "start_time" in new:
            self.log(f"Meeting {meeting_id}.{occurrence_id}: Updating start time")
            db_query("UPDATE zoom_meetings SET start_time=%s WHERE meeting_id=%s AND (occurrence_id=%s OR %s = '0')",
                     (parse_time(new['start_time']), meeting_id, occurrence_id, occurrence_id))

        if "duration" in new:
            self.log(f"Meeting {meeting_id}.{occurrence_id}: Updating duration")
            db_query("UPDATE zoom_meetings SET duration=%s WHERE meeting_id=%s AND (occurrence_id=%s OR %s = '0')",
                     (new['duration'], meeting_id, occurrence_id, occurrence_id))


    def update_regular_meeting(self, meeting_id, old, new):
        self.log(f"Meeting {meeting_id}: Updating regular meeting")
        self.update_common_attrs(meeting_id, 0, new)
        self.update_schedule(meeting_id, 0, new)


    def update_recurring_meeting_single(self, meeting_id, old, new):
        self.log(f"Meeting {meeting_id}: Updating single occurrence")
        for occ in new["occurrences"]:
            self.update_common_attrs(meeting_id, occ["occurrence_id"], new)
            self.update_schedule(meeting_id, occ["occurrence_id"], new)     # e.g., duration can be set here
            self.update_schedule(meeting_id, occ["occurrence_id"], occ)


    def update_recurring_meeting_all(self, meeting_id, old, new):
        meeting_id = new["id"]
        self.log(f"Meeting {meeting_id}: Updating all occurrences")

        if "occurrences" not in new:
            self.update_common_attrs(meeting_id, 0, new)
            return

        db_query("SELECT * FROM zoom_meetings WHERE meeting_id=%s", (meeting_id,))
        orig = db.fetchone()
        if orig is None:
            self.log(f"Meeting {meeting_id}: Update did not find previous version")
            return

        db_query("DELETE FROM zoom_meetings WHERE meeting_id=%s", (meeting_id,))

        for occ in new["occurrences"]:
            occurrence_id = occ["occurrence_id"]
            self.log(f"Meeting {meeting_id}.{occurrence_id}: Re-creating")

            db_query("""
                    INSERT INTO zoom_meetings
                    (meeting_id, uuid, occurrence_id, host_id, topic, type, start_time, duration)
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
                """, (
                    meeting_id,
                    new.get('uuid', orig.uuid),
                    occurrence_id,
                    orig.host_id,
                    new.get('topic', orig.topic),
                    new.get('type', orig.type),
                    parse_time(occ['start_time']),
                    occ['duration'],
                ))


    def update_regular_to_recurring(self, meeting_id, old, new):
        self.log(f"Meeting {meeting_id}: Type change to recurring")
        self.update_recurring_meeting_all(meeting_id, old, new)


    def update_recurring_to_regular(self, meeting_id, old, new):
        self.log(f"Meeting {meeting_id}: Type change to regular")

        db_query("SELECT * FROM zoom_meetings WHERE meeting_id=%s", (meeting_id,))
        orig = db.fetchone()
        if orig is None:
            self.log(f"Meeting {meeting_id}: Update did not find previous version")
            return

        db_query("DELETE FROM zoom_meetings WHERE meeting_id=%s", (meeting_id,))

        db_query("""
                INSERT INTO zoom_meetings
                (meeting_id, uuid, host_id, topic, type, start_time, duration)
                VALUES (%s, %s, %s, %s, %s, %s, %s)
            """, (
                meeting_id,
                new.get('uuid', orig.uuid),
                orig.host_id,
                new.get('topic', orig.topic),
                new['type'],
                parse_time(new['start_time']),
                new['duration'],
            ))


    def update_meeting(self, js):
        payload = js["payload"]
        new = payload["object"]
        old = payload["old_object"]
        meeting_id = new["id"]

        new_type = new.get("type", -1)
        old_type = old.get("type", -1)
        if old_type != new_type:
            if old_type == 2 and new_type == 8:
                self.update_regular_to_recurring(meeting_id, old, new)
            elif old_type == 8 and new_type == 2:
                self.update_recurring_to_regular(meeting_id, old, new)
            else:
                self.log(f"Meeting {meeting_id}: Unsupported type change from {old_type} to {new_type}")
            return

        scope = payload.get("scope", "")
        if scope == "":
            self.update_regular_meeting(meeting_id, old, new)
        elif scope == "all":
            self.update_recurring_meeting_all(meeting_id, old, new)
        elif scope == "single":
            self.update_recurring_meeting_single(meeting_id, old, new)
        else:
            self.log(f"Meeting {meeting_id}: Unsupported update scope {scope}")


    def run(self):
        method = self.env['REQUEST_METHOD']
        if method != 'POST':
            return self.http_error(405, 'Method not allowed', [('Allow', 'POST')])

        if self.env.get('HTTP_AUTHORIZATION', '') != config['hooks']['verification_token']:
            self.log('Verification token does not match!')
            return self.http_error(401, 'Authorization failed')

        body = self.env['wsgi.input'].read()
        js = json.loads(body)
        self.log(js)

        event = js["event"]
        if event == "meeting.created":
            self.create_meeting(js)
        elif event == "meeting.deleted":
            self.delete_meeting(js)
        elif event == "meeting.updated":
            self.update_meeting(js)
        else:
            self.log(f"Unknown event: {event}")

        db_connection.commit()

        self.wsgi_start("204 No Content", [])
        return b""


def application(env, start_response):
    app = HookApp(env, start_response)
    try:
        return app.run()
    except Exception as exc:
        app.log(traceback.print_exception(etype=None, value=exc, tb=exc.__traceback__))
        return app.http_error(500, "Internal server error")