#!/usr/bin/python3
import asyncio, os, sys, tempfile
from datetime import datetime
import time
from tabulate import tabulate
from colorama import Fore, Style

import api, lib.util

async def add_task(task_args, server_name, port):
    task = {
        "title": None,
        "tags": [],
        "desc": None,
        "assign": [],
        "project": [],
        "due": None,
        "rank": None,
        "created_at": lib.util.now(),
        "state": "open"
    }
    # Everything that isn't an attribute is part of the title
    # Open text editor if desc isn't set to write desc text
    title_words = []
    for arg in task_args:
        if arg[0] == "+":
            tag = arg
            if tag in task["tags"]:
                print(f"error: duplicate tag {tag} in task", file=sys.stderr)
                sys.exit(-1)
            task["tags"].append(tag)
        elif arg[0] == "@":
            assign = arg
            if assign in task["assign"]:
                print(f"error: duplicate assign {assign} in task", file=sys.stderr)
                sys.exit(-1)
            task["assign"].append(assign)
        elif ":" in arg:
            attr, val = arg.split(":", 1)
            set_task_attr(task, attr, val)
        else:
            title_words.append(arg)

    title = " ".join(title_words)
    if len(title) == 0:
        print("Error: Title is required")
        exit(-1)
    task["title"] = title
    if task["desc"] is None:
        task["desc"] = prompt_description_text(task)
    
    if task["desc"].strip() == '':
        print("Abort adding the task due to empty description.")
        exit(-1)
    
    if task["rank"] is not None:
        task["rank"] = round(task["rank"], 4)
    
    try:
        if task["ref_id"].strip() == '':
            task.pop('ref_id')
        if task["workspace"].strip() == '':
            task.pop('workspace')
    except KeyError:
        pass

    if await api.add_task(task, server_name, port):
        print(f"Created task '{title}'.")

def prompt_text(comment_lines):
    temp = tempfile.NamedTemporaryFile()
    temp.write(b"\n")
    for line in comment_lines:
        temp.write(line.encode() + b"\n")
    temp.flush()
    editor = os.environ.get('EDITOR') if os.environ.get('EDITOR') else 'nano'    
    os.system(f"{editor} {temp.name}")
    desc = open(temp.name, "r").read()
    # Remove comments and empty lines from desc
    cleaned = []
    for line in desc.split("\n"):
        if line == "# ------------------------ >8 ------------------------":
            break
        if line.startswith("#"):
            continue
        cleaned.append(line)

    return "\n".join(cleaned)

def prompt_description_text(task):
    return prompt_text([
        "# Write task description above this line.",
        "# These lines will be removed.",
        "# An empty description aborts adding the task",
        "\n# ------------------------ >8 ------------------------",
        "# Do not modify or remove the line above.",
        "# Everything below it will be ignored.",
        f"\n{tabulate_task(task, True)}"
    ])

def prompt_comment_text():
    return prompt_text([
        "# Write comments above this line",
        "# These lines will be removed"
    ])

def set_task_attr(task, attr, val):
    # templ = lib.util.task_template
    assert attr in ["desc", "rank", "due", "project"]
    # assert templ[attr] != list

    if val.lower() == "none":
        task[attr] = None
    else:
        val = convert_attr_val(attr, val)
        task[attr] = val

    lib.util._enforce_task_format(task)

def convert_attr_val(attr, val):
    templ = lib.util.task_template

    if attr in ["desc", "title"]:
        assert templ[attr] == str
        return val
    elif attr == "rank":
        try:
            return float(val)
        except ValueError:
            print(f"error: rank value {val} isn't convertable to float",
                  file=sys.stderr)
            sys.exit(-1)
    elif attr == "due":
        # Other date formats not yet supported... ez to add
        assert len(val) == 4
        date = datetime.now().date()
        year = int(date.strftime("%Y"))%100
        try:
            dt = datetime.strptime(f"18:00 {val}{year}", "%H:%M %d%m%y")
        except ValueError:
            print(f"error: unknown date format {val}")
            sys.exit(-1)
        due = lib.util.datetime_to_unix(dt)
        return due
    elif attr == "project":
        try:
            return [val]
        except ValueError:
            print(f"error: project value {val} isn't convertable to list",
                  file=sys.stderr)
            sys.exit(-1)
    else:
        print(f"error: unhandled attr '{attr}' = {val}")
        sys.exit(-1)

async def show_active_tasks(workspace, server_name, port):
    refids = await api.get_ref_ids(server_name, port)
    tasks = []
    for refid in refids:
        tasks.append(await api.fetch_task(refid, server_name, port))
    list_tasks(tasks, workspace, [])

async def show_deactive_tasks(month_ts, workspace, server_name, port):
    tasks = await api.fetch_deactive_tasks(month_ts, server_name, port)
    list_tasks(tasks, workspace, [])

def list_tasks(tasks, workspace, filters):
    print(f"Workspace: {workspace}")
    headers = ["ID", "Title", "Status", "Project",
               "Tags", "assign", "Rank", "Due", "RefID"]
    table_rows = []
    for id, task in enumerate(tasks, 1):
        if task is None:
            continue
        if is_filtered(task, filters):
            continue
        ref_id = task["ref_id"][:6]
        title = task["title"]
        status = task["state"]
        # project = task["project"] if task["project"] is not None else ""
        tags = " ".join(f"+{tag}" for tag in task["tags"])
        assign = " ".join(f"@{assign}" for assign in task["assign"])
        project = " ".join(f"{project}" for project in task["project"])
        if task["due"] is None:
            due = ""
        else:
            dt = lib.util.unix_to_datetime(task["due"])
            due = dt.strftime("%H:%M %d/%m/%y")

        rank = round(task["rank"], 4) if task["rank"] is not None else ""

        if status == "start":
            id =        Fore.GREEN + str(id)         + Style.RESET_ALL
            title =     Fore.GREEN + str(title)      + Style.RESET_ALL
            status =    Fore.GREEN + str(status)     + Style.RESET_ALL
            project =   Fore.GREEN + str(project)    + Style.RESET_ALL
            tags =      Fore.GREEN + str(tags)       + Style.RESET_ALL
            assign =    Fore.GREEN + str(assign)     + Style.RESET_ALL
            rank =      Fore.GREEN + str(rank)       + Style.RESET_ALL
            due =       Fore.GREEN + str(due)        + Style.RESET_ALL
            ref_id =    Fore.GREEN + str(ref_id)     + Style.RESET_ALL
        elif status == "pause":
            id =        Fore.YELLOW + str(id)        + Style.RESET_ALL
            title =     Fore.YELLOW + str(title)     + Style.RESET_ALL
            status =    Fore.YELLOW + str(status)    + Style.RESET_ALL
            project =   Fore.YELLOW + str(project)   + Style.RESET_ALL
            tags =      Fore.YELLOW + str(tags)      + Style.RESET_ALL
            assign =    Fore.YELLOW + str(assign)    + Style.RESET_ALL
            rank =      Fore.YELLOW + str(rank)      + Style.RESET_ALL
            due =       Fore.YELLOW + str(due)       + Style.RESET_ALL
            ref_id =    Fore.YELLOW + str(ref_id)    + Style.RESET_ALL
        elif status == "stop":
            id =        Fore.RED + str(id)           + Style.RESET_ALL
            title =     Fore.RED + str(title)        + Style.RESET_ALL
            status =    Fore.RED + str(status)       + Style.RESET_ALL
            project =   Fore.RED + str(project)      + Style.RESET_ALL
            tags =      Fore.RED + str(tags)         + Style.RESET_ALL
            assign =    Fore.RED + str(assign)       + Style.RESET_ALL
            rank =      Fore.RED + str(rank)         + Style.RESET_ALL
            due =       Fore.RED + str(due)          + Style.RESET_ALL
            ref_id =    Fore.RED + str(ref_id)       + Style.RESET_ALL
        else:
            #id =       Style.DIM  + str(id)         + Style.RESET_ALL
            #title =    Style.DIM  + str(title)      + Style.RESET_ALL
            #status =   Style.DIM  + str(status)     + Style.RESET_ALL
            project =   Style.DIM  + str(project)    + Style.RESET_ALL
            tags =      Style.DIM  + str(tags)       + Style.RESET_ALL
            #assign =   Style.DIM  + str(assign)     + Style.RESET_ALL
            rank =      Style.DIM  + str(rank)       + Style.RESET_ALL
            due =       Style.DIM  + str(due)        + Style.RESET_ALL
            #ref_id =   Style.DIM  + str(ref_id)     + Style.RESET_ALL

        rank_value = task["rank"] if task["rank"] is not None else 0
        row = [
            id,
            title,
            status,
            project,
            tags,
            assign,
            rank,
            due,
            ref_id
        ]
        table_rows.append((rank_value, row))

    table = [row for (_, row) in
             sorted(table_rows, key=lambda item: item[0], reverse=True)]
    print(tabulate(table, headers=headers))

async def show_task(refid, server_name, port):
    task = await api.fetch_task(refid, server_name, port)
    task_table(task)
    return 0

async def show_archive_task(ref_id, month_ts, server_name, port):
    task = await api.fetch_archive_task(ref_id, month_ts, server_name, port)
    task_table(task)
    return 0

def tabulate_task(task, prompt):
    tags = " ".join(f"+{tag}" for tag in task["tags"])
    assign = " ".join(f"@{assign}" for assign in task["assign"])
    project = " ".join(f"{project}" for project in task["project"])
    rank = round(task["rank"], 4) if task["rank"] is not None else ""
    if task["due"] is None:
        due = ""
    else:
        dt = lib.util.unix_to_datetime(task["due"])
        due = dt.strftime("%H:%M %d/%m/%y")

    assert task["created_at"] is not None
    dt = lib.util.unix_to_datetime(task["created_at"])
    created_at = dt.strftime("%H:%M %d/%m/%y")

    if prompt:
        task["ref_id"] = ''
        task["workspace"] = ''

    table = [
        ["RefID:", task["ref_id"]],
        ["Title:", task["title"]],
        ["Workspace:", task["workspace"]],
        ["Description:", task["desc"]],
        ["Status:", task["state"]],
        ["Project:", project],
        ["Tags:", tags],
        ["Assign:", assign],
        ["Rank:", rank],
        ["Due:", due],
        ["Created:", created_at],
    ]
    return tabulate(table, headers=["Attribute", "Value"])

def task_table(task):
    print(tabulate_task(task, False))

    table = []
    for event in task["events"]:
        act, who, when, args = event["action"], event["author"], event["timestamp"], event["content"]
        when = lib.util.unix_to_datetime(when)
        when = when.strftime("%H:%M %d/%m/%y")
        
        if act == "due" and when is not None:
            table.append([
                Style.DIM + f"{who} changed {act} to {when}" + Style.RESET_ALL,
                "",
                Style.DIM + when + Style.RESET_ALL
            ])
        elif act == "tags" or act == "assign":
            val = f"{args}"
            event = f"{who} added {val} to {act}"
            if val[0] == "-":
                event = f"{who} removed {val} from {act}"
            table.append([
                Style.DIM + event + Style.RESET_ALL,
                "",
                Style.DIM + when + Style.RESET_ALL
            ])
        elif act == "state":
            status = args
            if status == "pause":
                status_verb = "paused"
            elif status in ["start", "open"]:
                status_verb = f"{status}ed"
            elif status == "stop":
                status_verb = f"stopped"
            else:
                print(f"internal error: unhandled task state {status}",
                      file=sys.stderr)
                sys.exit(-2)

            table.append([
                f"{who} {status_verb} task",
                "",
                Style.DIM + when + Style.RESET_ALL
            ])
        elif act == "comment":
            continue
        else:
            table.append([
                Style.DIM + f"{who} changed {act} to {args}" + Style.RESET_ALL,
                "",
                Style.DIM + when + Style.RESET_ALL
            ])
    print(tabulate(table))

    table = []
    for event in task['events']:
        act, who, when, args = event["action"], event["author"], event["timestamp"], event["content"]
        when = lib.util.unix_to_datetime(when)
        when = when.strftime("%H:%M %d/%m/%y")
        if act == "comment":
            comment = args
            table.append([
                f"{who}>",
                wrap_comment(comment, 58),
                Style.DIM + when + Style.RESET_ALL
            ])
    if len(table) > 0:
        print("Comments:")
    print(tabulate(table))

def wrap_comment(comment, width):
    lines = []
    line_start = 0
    for i, char in enumerate(comment):
        if char == ' ' and (i - line_start >= width):
            lines.append(comment[line_start:i + 1])
            line_start = i + 1

    if line_start < len(comment):
        lines.append(comment[line_start:])
    return '\n'.join(lines)

async def modify_task(refid, args, server_name, port):
    changes = {}    
    for arg in args:
        # This must go before the next elif block
        if arg.startswith("@") or arg.startswith("-@"):
            changes["assign"] = []
            changes["assign"].append(arg)
        elif arg.startswith("+") or arg.startswith("-"):
            changes["tags"] = []
            changes["tags"].append(arg)
        elif ":" in arg:
            attr, val = arg.split(":", 1)
            if val.lower() == "none":
                if attr not in ["project", "rank", "due"]:
                    print(f"error: invalid you cannot set {attr} to none",
                          file=sys.stderr)
                    return -1
                val = None
            else:
                val = convert_attr_val(attr, val)
            changes[str(attr)] = val
        else:
            print(f"warning: unknown arg '{arg}'. Skipping...", file=sys.stderr)
    await api.modify_task(refid, changes, server_name, port)
    return 0

async def change_task_status(refid, status, server_name, port):
    task = await api.fetch_task(refid, server_name, port)
    assert task is not None
    title = task["title"]

    if not await api.change_task_status(refid, status, server_name, port):
        return -1

    if status == "start":
        print(f"Started task '{title}'")
    elif status == "pause":
        print(f"Paused task '{title}'")
    elif status == "stop":
        print(f"Completed task '{title}'")
    elif status == "open":
        print(f"Opened task '{title}'")

    return 0

async def comment(refid, args, server_name, port):
    if not args:
        comment = prompt_comment_text()
    else:
        comment = " ".join(args)

    if not await api.add_task_comment(refid, comment, server_name, port):
        return -1

    # Two json rpcs back to back cause Unexpected EOF error
    time.sleep(0.1)
    task = await api.fetch_task(refid, server_name, port)
    assert task is not None
    title = task["title"]
    print(f"Commented on task'{title}'")
    return 0

def is_filtered(task, filters):
    for fltr in filters:
        if fltr.startswith("+"):
            tag = fltr[1:]
            if tag not in task["tags"]:
                return True
        elif fltr.startswith("@"):
            assign = fltr[1:]
            if assign not in task["assign"]:
                return True
        elif ":" in fltr:
            attr, val = fltr.split(":", 1)
            if val.lower() == "none":
                if attr not in ["project", "rank", "due"]:
                    print(f"error: invalid you cannot set {attr} to none",
                            file=sys.stderr)
                    sys.exit(-1)
                if task[attr] is not None:
                    return True
            elif attr == "state" :
                if val not in ["open", "start", "pause"]:
                    print(f"error: invalid, filter by {attr} can only be [\"open\", \"start\", \"pause\"]",
                            file=sys.stderr)
                    sys.exit(-1)
                if task["state"] != val:
                    return True
            else:
                val = convert_attr_val(attr, val)
                if task[attr] != val:
                    return True
        else:
            print(f"error: unknown arg '{fltr}'", file=sys.stderr)
            sys.exit(-1)

    return False

def find_free_id(task_ids):
    for i in range(1, 1000):
        if i not in task_ids:
            return i
    1

def map_ids(task_ids, ref_ids):
    return dict(zip(task_ids, ref_ids))

async def main():
    val = str('127.0.0.1:23330')
    allowed_states = ["start", "pause", "stop", "open"]

    for i in range(1, len(sys.argv)):
        if sys.argv[i] == "-e":
            val = sys.argv[i+1]
            del sys.argv[i]
            del sys.argv[i]
            break
    
    server_name, port = val.split(':')
    
    refids = await api.get_ref_ids(server_name, port)
    free_ids = []
    tasks = []
    for refid in refids:
        tasks.append(await api.fetch_task(refid, server_name, port))
        free_ids.append(find_free_id(free_ids))

    data = map_ids(free_ids, refids)

    workspace = await api.get_workspace(server_name, port)

    if len(sys.argv) == 1:
        await show_active_tasks(workspace, server_name, port)
        return 0

    if any(x in ["-h", "--help", "help"] for x in sys.argv):
        print('''USAGE:
    tau [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -h, --help                   Print help information

SUBCOMMANDS:
    add        Add a new task.
    archive    Show completed tasks.
    comment    Write comment for task by id.
    modify     Modify an existing task by id.
    pause      Pause task(s).
    start      Start task(s).
    stop       Stop task(s).
    switch     Switch between configured workspaces.
    show       List filtered tasks.
    help       Show this help text.

Examples:
    tau add task one due:0312 rank:1.022 project:zk +lol @sk desc:desc +abc +def
    tau add task two rank:1.044 project:cr +mol @up desc:desc2
    tau add task three due:0512 project:zy +trol @kk desc:desc3 +who
    tau 1 modify @upgr due:1112 rank:none
    tau 1 modify -@up
    tau 1 modify -mol -xx
    tau 1,2 modify +dev @erto
    tau 1-3 start
    tau 1 comment "this is an awesome comment"
    tau 2 pause
    tau show @erto state:start  # list started tasks that are assigned to 'erto'
    tau show +dev project:zk    # list tasks with 'dev' tag project 'zk'
    tau switch darkfi           # switch to configured 'darkfi' workspace
    tau archive                 # current month's completed tasks
    tau archive 1122            # completed tasks in Nov. 2022
    tau archive 1 1122          # show info of task completed in Nov. 2022
''')
        return 0
    elif sys.argv[1] == "add":
        task_args = sys.argv[2:]
        await add_task(task_args, server_name, port)
        return 0
    elif sys.argv[1] == "archive":
        if len(sys.argv) == 4:
            if len(sys.argv[3]) == 4:
                month = sys.argv[3]
                month_ts = lib.util.month_to_unix(month)
            else:
                print("error: month must be of format MMYY")
                return -1
                
            archive_refids = await api.get_archive_ref_ids(month_ts, server_name, port)
            afree_ids = []
            atasks = []
            for arefid in archive_refids:
                atasks.append(await api.fetch_archive_task(arefid, month_ts, server_name, port))
                afree_ids.append(find_free_id(afree_ids))

            adata = map_ids(afree_ids, archive_refids)

            if len(sys.argv[2]) < 4:
                try:
                    tid = int(sys.argv[2])
                    arefid = adata[tid]
                except (ValueError, KeyError):
                    print("error: invalid ID", file=sys.stderr)
                    return -1
            else:
                print("error: invalid ID", file=sys.stderr)
                return -1
            
            
            if (errc := await show_archive_task(arefid, month_ts, server_name, port)) < 0:
                return errc
        elif len(sys.argv) == 3:
            if len(sys.argv[2]) == 4:
                month = sys.argv[2]
                month_ts = lib.util.month_to_unix(month)
                await show_deactive_tasks(month_ts, workspace, server_name, port)
            elif len(sys.argv[2]) < 4:
                month_ts = lib.util.month_to_unix()
                archive_refids = await api.get_archive_ref_ids(month_ts, server_name, port)
                afree_ids = []
                atasks = []
                for arefid in archive_refids:
                    atasks.append(await api.fetch_archive_task(arefid, month_ts, server_name, port))
                    afree_ids.append(find_free_id(afree_ids))

                adata = map_ids(afree_ids, archive_refids)

                try:
                    tid = int(sys.argv[2])
                    arefid = adata[tid]
                except (ValueError, KeyError):
                    print("error: invalid ID", file=sys.stderr)
                    return -1
                
                if (errc := await show_archive_task(arefid, month_ts, server_name, port)) < 0:
                    return errc
            else:
                print("error: usage format is: tau archive [ID] [MONTH]")
                return -1
        else:
            month_ts = lib.util.month_to_unix()
            await show_deactive_tasks(month_ts, workspace, server_name, port)
        
        return 0
    elif sys.argv[1] == "show":
        if len(sys.argv) > 2:
            filters = sys.argv[2:]
            list_tasks(tasks, workspace, filters)
        else:
            await show_active_tasks(workspace, server_name, port)
        return 0
    elif sys.argv[1] == "switch":
        if not len(sys.argv) == 3:
            print("Error: you must provide workspace name")
            return 0
        if not await api.switch_workspace(sys.argv[2], server_name, port):
            print(f"Error: Workspace \"{sys.argv[2]}\" is not configured.")
        else:
            print(f"You are now on \"{sys.argv[2]}\" workspace.")
        return 0
    elif sys.argv[1] == "export":
        if len(sys.argv) == 2:
            path = "~/.local/darkfi"
        else:
            path = sys.argv[2]
        if await api.export_to(path, server_name, port):
            print(f"Exported tasks successfuly to {path}")
        return 0
    elif sys.argv[1] == "import":
        if len(sys.argv) == 2:
            path = "~/.local/darkfi"
        else:
            path = sys.argv[2]
        if await api.import_from(path, server_name, port):
            print(f"Imported tasks successfuly from {path}")
        return 0

    try:
        id = sys.argv[1]
        subcommands = ["modify", "comment"]
        if any(id in ls for ls in [allowed_states, subcommands]):
            user_input = input("This command has no filter, and will modify all tasks. Are you sure? [y/N] ")
            if user_input.lower() in ['y', 'yes']:
                refid = list(refids)
                args = sys.argv[1:]
            else:
                print("Command prevented from running.")
                exit(-1)
        else:
            lines = id.split(',')
            numbers = []
            for line in lines:
                if line == '':
                    continue
                elif '-' in line:
                    t = line.split('-')
                    numbers += range(int(t[0]), int(t[1]) + 1)
                else:
                    numbers.append(int(line))
            refid = []
            for i in numbers:
                refid.append(data[i])
            args = sys.argv[2:]
    except (ValueError, KeyError):
        print("error: invalid ID", file=sys.stderr)
        return -1
    except EOFError:
        print('\nOperation is cancelled')
        return -1

    if not args:
        for rid in refid:
            await show_task(rid, server_name, port)
        return 0
    
    subcmd, args = args[0], args[1:]

    if subcmd == "modify":
        if not args:
            print("Error: modify subcommand must have at least one argument.")
            exit(-1)
        for rid in refid:
            if (errc := await modify_task(rid, args, server_name, port)) < 0:
                return errc
            time.sleep(0.1)
            await show_task(rid, server_name, port)
    elif subcmd in allowed_states:
        status = subcmd
        for rid in refid:
            if (errc := await change_task_status(rid, status, server_name, port)) < 0:
                return errc
            time.sleep(0.1)
    elif subcmd == "comment":
        for rid in refid:
            if (errc := await comment(rid, args, server_name, port)) < 0:
                return errc
    else:
        print(f"error: unknown subcommand '{subcmd}'")
        return -1

    return 0

asyncio.run(main())

