# flake8: noqa
# type:ignore

import csv
import json
import os
from datetime import datetime, timedelta

from mm_stats.auth import PostgresDb
from mm_stats.definitions import DUMP_PATH


def make_json(names: list, values: list) -> dict:
    """
    Convert list do json dict.

    Parameters:
    names: list
        list containing the names of the keys
    values: list
        list containing the values matching the order of names

    Returns:
    dict: dict
        json dict
    """
    dict = {}
    for value in range(0, len(values)):
        dict[names[value]] = values[value]
    return dict


def get_max_value(column: str, table: str):
    """
    Get max value of a column in a table.

    Parameters:
    column: str
        name of column where max value is wanted
    table: list
        table containing said column

    Returns:
    max_value:
        maximum value, can be int/double/etc
    """
    sql = """select max({}) from {}""".format(column, table)
    db = PostgresDb()
    max_value = db.retr_query(sql)[0][0]
    return max_value


def reader(skip: int, in_path: str) -> tuple:
    """
    Get each project individually.

    Parameter:
    skip : int
        lines to be skipped till next project starts.
        first iteration it is 0

    Returns:
    skip : int
        updated number for reaching next project
    all_activities : list
        all found activities of one project
    """
    with open(in_path, "r") as infile:
        for x in range(skip):
            infile.readline()

        all_activities = []

        line = infile.readline()

        if line == "":
            return -1, []
        activity = json.loads(line[:-2])
        all_activities.append(activity)
        project_id = activity["project_id"]
        for line in infile:
            activity = json.loads(line[:-2])
            if activity["project_id"] == project_id:
                skip += 1
                all_activities.append(activity)
            else:
                skip += 1
                return skip, all_activities

        skip += 1
        return skip, all_activities


"""these keywords are used to search for which type an actions is"""
locked = ["LOCKED_FOR_VALIDATION", "LOCKED_FOR_MAPPING"]
unlocked = ["AUTO_UNLOCKED_FOR_MAPPING", "AUTO_UNLOCKED_FOR_VALIDATION"]
finish = [
    "MAPPED",
    "VALIDATED",
    "BADIMAGERY",
    "READY",
    "INVALIDATED",
    "SPLIT",
    "COMMENT",
]


class UserEntry:
    """Contain data about a specific user action."""

    def __init__(self, project_id: int = None, edit: dict = None):
        if edit is None:
            self.project_id = None
            self.task_id = None
            self.action = None
            self.user = None
            self.startTime = None
            self.endTime = None
        else:
            self.project_id = project_id
            self.task_id = edit["task_id"]
            self.user = edit["actionBy"]
            self.action = None
            self.startTime = None
            self.endTime = None

    def set_startTime(self, time) -> None:
        """Time has to be python object for calculations."""
        try:
            self.startTime = datetime.fromisoformat(time)
        except TypeError:
            self.startTime = time

    def set_endTime(self, time) -> None:
        """Time has to be python object for calculations."""
        try:
            self.endTime = datetime.fromisoformat(time)
        except TypeError:
            self.endTime = time

    def generateUserEdit(self) -> dict:
        """
        Return Edit as List and checks if all attributes are set.

        Currently unused.
        Was used for creation of json. Convert Times back to isoformated string.
        """
        Edit = {}
        assert self.project_id is not None, "no project_id"
        Edit["project_id"] = self.project_id
        assert self.task_id is not None, "no task_id"
        Edit["task_id"] = self.task_id
        assert self.action is not None, "no action"
        Edit["action"] = self.action
        assert self.user is not None, "no user"
        Edit["user"] = self.user
        assert self.startTime is not None, "no startTime"
        Edit["startTime"] = self.startTime.isoformat()
        assert self.endTime is not None, "no endTime"
        Edit["endTime"] = self.endTime.isoformat()
        return Edit

    def generateUserEdit2(self) -> list:
        """
        Return Edit as List and checks if all attributes are set.

        Currently used for creation of csv. Convert Times back to isoformated string.
        """
        assert self.project_id is not None, "no project_id"
        assert self.task_id is not None, "no task_id"
        assert self.action is not None, "no action"
        assert self.user is not None, "no user"
        assert self.startTime is not None, "no startTime"
        assert self.endTime is not None, "no endTime"
        Edit = [
            self.project_id,
            self.task_id,
            self.action,
            self.user,
            self.startTime.isoformat(),
            self.endTime.isoformat(),
        ]
        return Edit


def in_string(string: str, keywords: list) -> bool:
    """
    Search keywords in string.

    Parameters:
    string : str
        string which is searched for keywords
    keywords: list
        list which contains keywords, which are strings

    Returns:
    anonymous boolean
          True if at least one keyword is found, False if not
    """
    try:
        for keyword in keywords:
            if keyword in string:
                return True
        return False
    except TypeError:
        return False


def searchEnd(project: int, data: list, edit: int) -> tuple:
    """
    If the start of an action is located, this function is called.

    It iterates towards the beginning of the list to search for end of action.
    Parameters:
    project : int
        project_id
    data : list
        all activity data of a project
    edit : int
        start of action, search from this index forward
    """
    global finish
    global locked
    global unlocked
    try:
        start_task = data[edit]["task_id"]
        for pEnd in range(edit - 1, -2, -1):
            if pEnd == -1:
                raise IndexError
            if data[pEnd]["task_id"] == start_task:  # search potential End
                # same user finished task -> best case
                if data[edit]["actionBy"] == data[pEnd]["actionBy"] and in_string(
                    data[pEnd]["actionText"], finish
                ):
                    data[pEnd]["marked"] = True
                    e1 = UserEntry(project, data[edit])
                    e1.set_startTime(data[edit]["actionDate"])
                    e1.set_endTime(data[pEnd]["actionDate"])
                    e1.action = data[pEnd]["actionText"]
                    return e1, None

                # other user finished task -> should not happen but does
                # both edits are saved, first as unfinished and timestamp +2:00
                # second as actionText and timestamp - 2:00
                elif data[edit]["actionBy"] != data[pEnd]["actionBy"] and in_string(
                    data[pEnd]["actionText"], finish
                ):

                    e1 = UserEntry(project, data[edit])
                    e1.action = "unfinished"
                    e1.set_startTime(data[edit]["actionDate"])
                    e1.set_endTime(e1.startTime + timedelta(hours=2))
                    try:
                        if data[pEnd]["marked"] is True:
                            return e1, None
                    except KeyError:
                        data[pEnd]["marked"] = True
                        e2 = UserEntry(project, data[pEnd])
                        e2.action = data[pEnd]["actionText"]
                        e2.set_endTime(data[pEnd]["actionDate"])
                        e2.set_startTime(e2.endTime - timedelta(hours=2))
                        return e1, e2

                # user did not finish task
                elif in_string(data[pEnd]["actionText"], locked + unlocked + ["SPLIT"]):

                    e1 = UserEntry(project, data[edit])
                    e1.set_startTime(data[edit]["actionDate"])
                    e1.set_endTime(e1.startTime + timedelta(hours=2))
                    e1.action = "unfinished"
                    return e1, None

    except IndexError:  # task doesnt appear again, maby very recent entry

        e1 = UserEntry(project, data[edit])
        e1.set_startTime(data[edit]["actionDate"])
        e1.set_endTime(e1.startTime + timedelta(hours=2))
        e1.action = "unfinished"
        return e1, None


def checkEdit(project: int, data: list, edit: int) -> tuple:
    """
    Check if edit is the start of a user action.

    parameters:
    project : int
        project_id
    data : list
        all activity data of a project
    edit : int
        index of activity to be inspected

    returns:
        a tuple containing one or 2 Edits or two times None if Edit
        is not a start of an action
    """
    global locked
    global unlocked
    global finish
    e1 = None
    e2 = None
    # user started an action
    if in_string(data[edit]["action"], locked) is True:
        e1, e2 = searchEnd(project, data, edit)
    # happens if user is idle for 2 hours
    elif in_string(data[edit]["action"], unlocked) is True:

        e1 = UserEntry(project, data[edit])
        e1.set_startTime(data[edit]["actionDate"])
        e1.set_endTime(e1.startTime + timedelta(hours=2))
        e1.action = "unfinished"

    # this case should not happen but it happens. Only occurs if task is marked
    # as finished but was never locked before. -> we assume it was edited in previous
    # 2 hours otherwise it would have been unlocked
    elif in_string(data[edit]["actionText"], finish[:2]):
        try:  # task was previously detected after it was locked -> normal case
            if data[edit]["marked"] is True:
                e1 = "already detected"  # needed to dodge Error in getActivity
        except KeyError:  # task has not yet been detected

            e1 = UserEntry(project, data[edit])
            e1.set_endTime(data[edit]["actionDate"])
            e1.set_startTime(e1.endTime - timedelta(hours=2))
            e1.action = data[edit]["actionText"]

    if e1 is not None:
        return e1, e2
    else:
        return None, None


def append_to_csv(data: list, path: str) -> None:
    """
    Append list to csv-path.

    Parameters:
    object : list
        list to be written to csv
    path : string
        location of the csv
    """
    with open(path, "a", newline="") as csv_file:
        writer = csv.writer(csv_file, delimiter=",", quotechar='"')
        writer.writerow(data)


def activities_to_sessions(
    in_path: str, from_dump: bool = False, data: list = None
) -> str:
    """
    Coordinate creation of user-activites.csv.

    Parameters:
    in_path: str
        path where user_activity.csv is located. Only needed in public-API mode.
    from_dump: bool
        True -> from_dump mode, False -> from public_API mode
    data: dict
        Only needed in from_dump mode. Contains all activities of one project.
    """
    out_path = os.path.join(DUMP_PATH, "{}.csv".format("sessions"))

    if from_dump is False:
        if os.path.exists(out_path):
            os.remove(out_path)
        header = ["project_id", "task_id", "action", "user", "startTime", "endTime"]
        append_to_csv(header, out_path)

    end = False
    skip = 0
    while end is False:
        if from_dump is False:
            skip, data = reader(skip, in_path)
            if skip == -1:
                end = True
                continue
        else:
            data = data
            end = True

        project_id = data[0]["project_id"]
        # data is chronological, thats why we search from behind
        for edit in range(len(data) - 1, -1, -1):
            e1, e2 = checkEdit(project_id, data, edit)
            if e1 == "already detected" or e1 is None:
                continue
            act = e1.generateUserEdit2()  # should always be an edit
            append_to_csv(act, out_path)
            if e2 is not None:
                act = e2.generateUserEdit2()
                append_to_csv(act, out_path)

    return out_path


def get_next_chunk(min_id: int) -> list:
    """
    Get max 50 projects from db.

    Parameters:
    min_id: int
        starting project_id
    Returns:
    result: list
        list containing max 50 projects
    """
    db = PostgresDb()
    sql = """select *
             from data_preparation.task_history as t
             where project_id >= {} and project_id < {}
             order by t.project_id, action_date desc""".format(
        min_id, min_id + 50
    )
    result = db.retr_query(sql)
    return result


def create_sessions_file() -> str:
    """Create sessions file from HOT TM dump.

    Uses postgresDB to get Activity data, then executes
    activites_to_sessions in from_dump mode.
    """
    outpath = os.path.join(DUMP_PATH, "{}.csv".format("sessions"))
    if os.path.exists(outpath):
        os.remove(outpath)
    header = ["project_id", "task_id", "action", "user", "startTime", "endTime"]
    append_to_csv(header, outpath)

    max_project_id = get_max_value("project_id", "data_preparation.task_history")
    project_range = range(0, max_project_id + 1, 50)
    for min_value in project_range:
        chunk = get_next_chunk(min_value)
        if len(chunk) == 0:
            continue

        names = [
            "project_id",
            "task_id",
            "action",
            "actionText",
            "actionDate",
            "actionBy",
        ]
        current_project_id = chunk[0][1]
        one_project = []
        for entry in chunk:
            entry_json = make_json(names, entry[1:])
            if entry_json["project_id"] == current_project_id:
                one_project.append(entry_json)
            else:
                activities_to_sessions(in_path="None", from_dump=True, data=one_project)

                one_project.clear()
                one_project.append(entry_json)
                current_project_id = entry_json["project_id"]

        activities_to_sessions(in_path="None", from_dump=True, data=one_project)

    return outpath
