import os
import psycopg2
from psycopg2.extras import RealDictCursor
from datetime import datetime
from urllib.parse import urlparse
from flask import g


from utils import get_all_exercises_from_topsets, get_stats_from_topsets, get_workouts


class DataBase():
    def __init__(self, app):
        db_url = urlparse(os.environ['DATABASE_URL'])
        # if db_url is null then throw error
        if not db_url:
            raise Exception("No DATABASE_URL environment variable set")

    def getDB(self):
        db = getattr(g, 'database', None)
        if db is None:
            db_url = urlparse(os.environ['DATABASE_URL'])
            g.database = psycopg2.connect(
                database=db_url.path[1:],
                user=db_url.username,
                password=db_url.password,
                host=db_url.hostname,
                port=db_url.port
            )
            db = g.database
        return db

    def close_connection(exception):
        db = getattr(g, 'database', None)
        if db is not None:
            db.close()

    def execute(self, query, args=(), one=False, commit=False):
        conn = self.getDB()
        cur = conn.cursor(cursor_factory=RealDictCursor)
        cur.execute(query, args)
        rv = None
        if cur.description is not None:
            rv = cur.fetchall()
        if commit:
            try:
                conn.commit()
            except:
                conn.rollback()
        cur.close()

        return (rv[0] if rv else None) if one else rv

    def get_exercises(self):
        exercises = self.execute(
            'SELECT exercise_id AS "ExerciseId", name AS "Name" FROM exercise')
        return [{"ExerciseId": e['ExerciseId'], "Name": e['Name']} for e in exercises]

    def get_exercise(self, exercise_id):
        exercise = self.execute(
            'SELECT exercise_id AS "ExerciseId", name AS "Name" FROM exercise WHERE exercise_id=%s LIMIT 1', [exercise_id], one=True)
        return exercise

    def create_exercise(self, name):
        new_exercise = self.execute('INSERT INTO exercise (name) VALUES (%s) RETURNING exercise_id AS "ExerciseId"',
                                    [name], commit=True, one=True)
        return new_exercise['ExerciseId']

    def delete_exercise(self, exercise_id):
        self.execute('DELETE FROM exercise WHERE exercise_id=%s', [
            exercise_id], commit=True)

    def update_exercise(self, exercise_id, name):
        self.execute('UPDATE Exercise SET Name=%s WHERE exercise_id=%s', [
            name, exercise_id], commit=True)

    def get_people(self):
        people = self.execute(
            'SELECT person_id AS "PersonId", name AS "Name" FROM person')
        return people

    def is_valid_person(self, person_id):
        person = self.execute(
            'SELECT person_id AS "PersonId" FROM person WHERE person_id=%s LIMIT 1', [person_id], one=True)
        return person

    def create_person(self, name):
        new_person = self.execute('INSERT INTO person (name) VALUES (%s) RETURNING person_id AS "PersonId"', [
                                  name], commit=True, one=True)
        return new_person['PersonId']

    def delete_person(self, person_id):
        self.execute('DELETE FROM topset WHERE workout_id IN (SELECT workout_id FROM workout WHERE person_id=%s)', [
            person_id], commit=True)
        self.execute('DELETE FROM workout WHERE person_id=%s',
                     [person_id], commit=True)
        self.execute('DELETE FROM person WHERE person_id=%s',
                     [person_id], commit=True)

    def update_person_name(self, person_id, name):
        self.execute('UPDATE person SET name=%s WHERE person_id=%s', [
            name, person_id], commit=True)

    def is_valid_workout(self, person_id, workout_id):
        workout = self.execute('SELECT W.workout_id AS "WorkoutId" FROM Person P, Workout W WHERE P.person_id=W.person_id AND P.person_id=%s AND W.workout_id=%s LIMIT 1', [
            person_id, workout_id], one=True)
        return workout

    def is_valid_topset(self, person_id, workout_id, topset_id):
        topset = self.execute("""
            SELECT T.topset_id AS "TopSetId"
            FROM Person P, Workout W, TopSet T
            WHERE W.person_id=W.person_id AND W.workout_id=T.workout_id AND P.person_id=%s AND W.workout_id = %s AND T.topset_id = %s
            LIMIT 1""", [person_id, workout_id, topset_id], one=True)
        return topset

    def delete_workout(self, workout_id):
        self.execute('DELETE FROM topset WHERE workout_id=%s',
                     [workout_id], commit=True)
        self.execute('DELETE FROM workout WHERE workout_id=%s',
                     [workout_id], commit=True)

    def update_topset(self, exercise_id, repetitions, weight, topset_id):
        self.execute('UPDATE topset SET exercise_id=%s, repetitions=%s, weight=%s WHERE topSet_id=%s', [
            exercise_id, repetitions, weight, topset_id], commit=True)

    def create_topset(self, workout_id, exercise_id, repetitions, weight):
        new_top_set = self.execute('INSERT INTO topset (workout_id, exercise_id, repetitions, weight) VALUES (%s, %s, %s, %s) RETURNING topset_id AS "TopSetId"', [
            workout_id, exercise_id, repetitions, weight], commit=True, one=True)
        return new_top_set['TopSetId']

    def delete_topset(self, topset_id):
        self.execute('DELETE FROM topset WHERE topset_id=%s', [
            topset_id], commit=True)

    def create_workout(self, person_id):
        now = datetime.now()
        date_string = now.strftime('%Y-%m-%d')
        # check if a workout has already been created for this person today that doesnt contain any topsets and if so return the WorkoutId
        workout = self.execute('SELECT workout_id AS "WorkoutId" FROM workout WHERE person_id=%s AND start_date=%s AND workout_id NOT IN (SELECT workout_id FROM topset)', [
            person_id, date_string], one=True)
        if workout:
            print(
                f'Workout already created for PersonId {person_id} starting at {date_string} so returning WorkoutId {workout["WorkoutId"]} rather then creating new workout')
            return workout['WorkoutId']

        print(
            f'Creating workout for PersonId {person_id} starting at {date_string}')
        new_workout = self.execute('INSERT INTO workout (person_id, start_date) VALUES (%s, %s) RETURNING workout_id AS "WorkoutId"', [
            person_id, date_string], commit=True, one=True)
        return new_workout['WorkoutId']

    def get_people_and_workout_count(self, person_id):
        return self.execute("""
            SELECT
                P.person_id AS "PersonId",
                P.name AS "Name",
                COUNT(W.workout_id) AS "NumberOfWorkouts",
                CASE P.person_id
                    WHEN %s
                        THEN 1
                        ELSE 0
                    END "IsActive"
            FROM
                Person P LEFT JOIN Workout W ON P.person_id = W.person_id
            GROUP BY
                P.person_id
            ORDER BY
                P.person_id""", [person_id])

    def update_workout_start_date(self, workout_id, start_date):
        self.execute('UPDATE workout SET start_date=%s WHERE workout_id=%s', [
            start_date, workout_id], commit=True)

    def get_person(self, person_id):
        topsets = self.execute("""
            SELECT
                P.person_id AS "PersonId",
                P.name AS "PersonName",
                W.workout_id AS "WorkoutId",
                W.start_date AS "StartDate",
                T.topset_id AS "TopSetId",
                E.exercise_id AS "ExerciseId",
                E.name AS "ExerciseName",
                T.repetitions AS "Repetitions",
                T.weight AS "Weight",
                round((100 * T.Weight::numeric::integer)/(101.3-2.67123 * T.Repetitions),0)::numeric::integer AS "Estimated1RM"
            FROM Person P
                LEFT JOIN Workout W ON P.person_id=W.person_id
                LEFT JOIN TopSet T ON W.workout_id=T.workout_id
                LEFT JOIN Exercise E ON T.exercise_id=E.exercise_id
            WHERE P.person_id=%s""", [person_id])

        return {
            'PersonId': next((t['PersonId'] for t in topsets), -1),
            'PersonName': next((t['PersonName'] for t in topsets), 'Unknown'),
            'Stats': get_stats_from_topsets(topsets),
            'Exercises': get_all_exercises_from_topsets(topsets),
            'Workouts': get_workouts(topsets)
        }

    def get_workout(self, person_id, workout_id):
        topsets = self.execute("""
            SELECT
                P.person_id AS "PersonId",
                P.name AS "PersonName",
                W.workout_id AS "WorkoutId",
                W.start_date AS "StartDate",
                T.topset_id AS "TopSetId",
                E.exercise_id AS "ExerciseId",
                E.name AS "ExerciseName",
                T.repetitions AS "Repetitions",
                T.weight AS "Weight",
                W.note AS "Note"
            FROM Person P
                LEFT JOIN Workout W ON P.person_id=W.person_id
                LEFT JOIN TopSet T ON W.workout_id=T.workout_id
                LEFT JOIN Exercise E ON T.exercise_id=E.exercise_id
            WHERE P.person_id=%s
                AND W.workout_id = %s
            ORDER BY T.topset_id""", [person_id, workout_id])

        note = next((t['Note'] for t in topsets), '')

        return {
            'PersonId': person_id,
            'PersonName': next((t['PersonName'] for t in topsets), 'Unknown'),
            'WorkoutId': workout_id,
            'StartDate': topsets[0]['StartDate'],
            'Exercises': self.get_exercises(),
            'TopSets': [{"TopSetId": t['TopSetId'], "ExerciseId": t['ExerciseId'], "ExerciseName": t['ExerciseName'], "Weight": t['Weight'], "Repetitions": t['Repetitions']} for t in topsets if t['TopSetId'] is not None],
            'Note': note
        }

    def get_topset(self, person_id, workout_id, topset_id):
        topset = self.execute("""
            SELECT
                P.person_id AS "PersonId",
                P.name AS "PersonName",
                W.workout_id AS "WorkoutId",
                W.start_date AS "StartDate",
                T.topset_id AS "TopSetId",
                E.exercise_id AS "ExerciseId",
                E.name AS "ExerciseName",
                T.repetitions AS "Repetitions",
                T.weight AS "Weight"
            FROM Person P
                INNER JOIN Workout W ON P.person_id=W.person_id
                INNER JOIN TopSet T ON W.workout_id=T.workout_id
                INNER JOIN Exercise E ON T.exercise_id=E.exercise_id
            WHERE P.person_id=%s
                AND W.workout_id = %s
                AND T.topset_id = %s""", [person_id, workout_id, topset_id], one=True)

        return {
            'PersonId': topset['PersonId'],
            'PersonName': topset['PersonName'],
            'WorkoutId': workout_id,
            'StartDate': topset['StartDate'],
            'Exercises': self.get_exercises(),
            "TopSetId": topset['TopSetId'],
            "ExerciseId": topset['ExerciseId'],
            "ExerciseName": topset['ExerciseName'],
            "Weight": topset['Weight'],
            "Repetitions": topset['Repetitions']
        }

    def get_all_topsets(self):
        all_topsets = self.execute("""
            SELECT
            P.person_id AS "PersonId",
            P.name AS "PersonName",
            W.workout_id AS "WorkoutId",
            W.start_date AS "StartDate",
            T.topset_id AS "TopSetId",
            E.exercise_id AS "ExerciseId",
            E.name AS "ExerciseName",
            T.repetitions AS "Repetitions",
            T.weight AS "Weight",
            round((100 * T.Weight::numeric::integer)/(101.3-2.67123 * T.Repetitions),0)::numeric::integer AS "Estimated1RM"
        FROM Person P
            LEFT JOIN Workout W ON P.person_id=W.person_id
            LEFT JOIN TopSet T ON W.workout_id=T.workout_id
            LEFT JOIN Exercise E ON T.exercise_id=E.exercise_id""")

        return all_topsets

    def get_tags_for_person(self, person_id):
        return self.execute("""
            SELECT
                T.tag_id AS "TagId",
                T.person_id AS "PersonId",
                T.name AS "TagName",
                T.filter AS "TagFilter"
            FROM
                Tag T
            WHERE
                T.person_id = %s
            ORDER BY
                T.name""", [person_id])

    def add_or_update_tag_for_person(self, person_id, tag_name, tag_filter):
        # check if a tag exists for dashboard with the same tag_name
        tag = self.execute('SELECT tag_id AS "TagId" FROM Tag WHERE person_id=%s AND name=%s LIMIT 1', [
            person_id, tag_name], one=True)
        if tag:
            # update the tag
            self.execute('UPDATE Tag SET filter=%s WHERE tag_id=%s', [
                         tag_filter, tag['TagId']], commit=True)
        else:
            self.execute('INSERT INTO Tag (person_id, name, filter) VALUES (%s, %s, %s)', [
                person_id, tag_name, tag_filter], commit=True)

    def delete_tag_for_person(self, person_id, tag_id):
        self.execute('DELETE FROM Tag WHERE person_id=%s AND tag_id=%s', [
            person_id, tag_id], commit=True)

    def get_tags_for_dashboard(self):
        return self.execute("""
            SELECT
                T.tag_id AS "TagId",
                T.person_id AS "PersonId",
                T.name AS "TagName",
                T.filter AS "TagFilter"
            FROM
                Tag T
            WHERE
                T.person_id IS NULL
            ORDER BY
                T.name""", [])

    def add_or_update_tag_for_dashboard(self, tag_name, tag_filter):
        # check if a tag exists for dashboard with the same tag_name
        tag = self.execute('SELECT tag_id AS "TagId" FROM Tag WHERE person_id IS NULL AND name=%s LIMIT 1', [
            tag_name], one=True)
        if tag:
            # update the tag
            self.execute('UPDATE Tag SET filter=%s WHERE tag_id=%s', [
                         tag_filter, tag['TagId']], commit=True)
        else:
            self.execute('INSERT INTO Tag (name, filter) VALUES (%s, %s)', [
                tag_name, tag_filter], commit=True)

    def delete_tag_for_dashboard(self, tag_id):
        self.execute('DELETE FROM Tag WHERE tag_id=%s', [tag_id], commit=True)

    def update_workout_note_for_person(self, person_id, workout_id, note):
        self.execute('UPDATE workout SET note=%s WHERE person_id=%s AND workout_id=%s', [
            note, person_id, workout_id], commit=True)

    def add_tag_for_workout(self, workout_id, tags_id):
        # First, delete tags that are not in the new selection
        self.execute(
            """
            DELETE FROM workout_tag 
            WHERE workout_id = %s AND tag_id NOT IN %s
            """,
            [workout_id, tuple(tags_id)], commit=True
        )

        # Then, attempt to insert the new tags
        for tag_id in tags_id:
            self.execute(
                """
                INSERT INTO workout_tag (workout_id, tag_id) 
                VALUES (%s, %s)
                ON CONFLICT (workout_id, tag_id) DO NOTHING
                """,
                [workout_id, tag_id], commit=True
            )

        # Now fetch updated list of workout tags
        workout_tags = self.execute("""
            SELECT
                T.tag_id AS "tag_id",
                T.person_id AS "person_id",
                T.name AS "tag_name",
                T.filter AS "tag_filter"
            FROM Workout_Tag WT
                LEFT JOIN Tag T ON WT.tag_id=T.tag_id
            WHERE WT.workout_id=%s""", [workout_id])

        return workout_tags

    def create_tag_for_workout(self, person_id, workout_id, tag_name):
        workout_exercises = self.execute("""
            SELECT
                E.exercise_id AS "exercise_id",
                E.name AS "exercise_name"
            FROM Workout W
                LEFT JOIN TopSet T ON W.workout_id=T.workout_id
                LEFT JOIN Exercise E ON T.exercise_id=E.exercise_id
            WHERE W.workout_id=%s""", [workout_id])

        tag_filter = "?" + \
            "&".join(
                f"exercise_id={e['exercise_id']}" for e in workout_exercises)

        # create tag for person
        row = self.execute('INSERT INTO Tag (person_id, name, filter) VALUES (%s, %s, %s) RETURNING tag_id AS "tag_id"', [
            person_id, tag_name, tag_filter], commit=True, one=True)

        # add tag to workout
        self.execute('INSERT INTO Workout_Tag (workout_id, tag_id) VALUES (%s, %s)', [
            workout_id, row['tag_id']], commit=True)

        # Now fetch updated list of workout tags
        workout_tags = self.execute("""
            SELECT
                T.tag_id AS "tag_id",
                T.person_id AS "person_id",
                T.name AS "tag_name",
                T.filter AS "tag_filter"
            FROM Workout_Tag WT
                LEFT JOIN Tag T ON WT.tag_id=T.tag_id
            WHERE WT.workout_id=%s""", [workout_id])

        return workout_tags

    def get_workout_tags(self, person_id, workout_id):
        person_tags = self.execute("""
            SELECT
                T.tag_id AS "tag_id",
                T.name AS "tag_name",
                T.filter AS "tag_filter"
            FROM Tag T
            WHERE T.person_id=%s""", [person_id])

        workout_tags = self.execute("""
            SELECT
                T.tag_id AS "tag_id",
                T.person_id AS "person_id",
                T.name AS "tag_name",
                T.filter AS "tag_filter"
            FROM Workout_Tag WT
                LEFT JOIN Tag T ON WT.tag_id=T.tag_id
            WHERE WT.workout_id=%s""", [workout_id])

        selected_workout_tag_ids = [wt['tag_id'] for wt in workout_tags]

        return (person_tags, workout_tags, selected_workout_tag_ids)
