import moment from "moment";

import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";

import db from "../config/db";

import { ServerResponse } from "../models/server-response";
import { S3_URL, TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
import { getDates, getMinMaxOfTaskDates, getMonthRange, getWeekRange } from "../shared/tasks-controller-utils";
import { getColor, getRandomColorCode, humanFileSize, log_error, toMinutes } from "../shared/utils";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { NotificationsService } from "../services/notifications/notifications.service";
import { getTaskCompleteInfo } from "../socket.io/commands/on-quick-task";
import { getAssignees, getTeamMembers } from "../socket.io/commands/on-quick-assign-or-remove";
import TasksControllerV2 from "./tasks-controller-v2";
import { IO } from "../shared/io";
import { SocketEvents } from "../socket.io/events";
import TasksControllerBase from "./tasks-controller-base";
import { insertToActivityLogs } from "../services/activity-logs/activity-logs.service";
import { IActivityLog } from "../services/activity-logs/interfaces";
import { getKey, getRootDir, uploadBase64 } from "../shared/s3";

export default class TasksController extends TasksControllerBase {
  private static notifyProjectUpdates(socketId: string, projectId: string) {
    IO.getSocketById(socketId)
      ?.to(projectId)
      .emit(SocketEvents.PROJECT_UPDATES_AVAILABLE.toString());
  }

  public static async uploadAttachment(attachments: any, teamId: string, userId: string) {
    try {
      const promises = attachments.map(async (attachment: any) => {
        const { file, file_name, project_id, size } = attachment;
        const type = file_name.split(".").pop();

        const q = `
        INSERT INTO task_attachments (name, task_id, team_id, project_id, uploaded_by, size, type)
        VALUES ($1, $2, $3, $4, $5, $6, $7)
        RETURNING id, name, size, type, created_at, CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', id, '.', type) AS url;
      `;

        const result = await db.query(q, [
          file_name,
          null,
          teamId,
          project_id,
          userId,
          size,
          type,
          `${S3_URL}/${getRootDir()}`
        ]);

        const [data] = result.rows;
        await uploadBase64(file, getKey(teamId, project_id, data.id, data.type));
        return data.id;
      });

      const attachmentIds = await Promise.all(promises);
      return attachmentIds;
    } catch (error) {
      log_error(error);
    }
  }

  @HandleExceptions()
  public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const userId = req.user?.id as string;
    const teamId = req.user?.team_id as string;

    if (req.body.attachments_raw) {
      req.body.attachments = await this.uploadAttachment(req.body.attachments_raw, teamId, userId);
    }

    const q = `SELECT create_task($1) AS task;`;
    const result = await db.query(q, [JSON.stringify(req.body)]);
    const [data] = result.rows;

    for (const member of data?.task.assignees || []) {
      NotificationsService.createTaskUpdate(
        "ASSIGN",
        userId,
        data.task.id,
        member.user_id,
        member.team_id
      );
    }

    return res.status(200).send(new ServerResponse(true, data.task));
  }

  @HandleExceptions()
  public static async getGanttTasks(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT get_gantt_tasks($1) AS gantt_tasks;`;
    const result = await db.query(q, [req.user?.id ?? null]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data.gantt_tasks));
  }

  private static sendAssignmentNotifications(task: any, userId: string) {
    const newMembers = task.new_assignees.filter((member1: any) => {
      return !task.old_assignees.some((member2: any) => {
        return member1.team_member_id === member2.team_member_id;
      });
    });
    const removedMembers = task.old_assignees.filter((member1: any) => {
      return !task.new_assignees.some((member2: any) => {
        return member1.team_member_id === member2.team_member_id;
      });
    });

    for (const member of newMembers) {
      NotificationsService.createTaskUpdate(
        "ASSIGN",
        userId,
        task.id,
        member.user_id,
        member.team_id
      );
    }

    for (const member of removedMembers) {
      NotificationsService.createTaskUpdate(
        "UNASSIGN",
        userId,
        task.id,
        member.user_id,
        member.team_id
      );
    }
  }

  public static async notifyStatusChange(userId: string, taskId: string, statusId: string) {
    try {
      const q2 = "SELECT handle_on_task_status_change($1, $2, $3) AS res;";
      const results1 = await db.query(q2, [userId, taskId, statusId]);
      const [d] = results1.rows;
      const changeResponse = d.res;

      // notify to all task members of the change
      for (const member of changeResponse.members || []) {
        if (member.user_id === userId) continue;
        NotificationsService.createNotification({
          userId: member.user_id,
          teamId: member.team_id,
          socketId: member.socket_id,
          message: changeResponse.message,
          taskId,
          projectId: changeResponse.project_id
        });
      }
    } catch (error) {
      log_error(error);
    }
  }

  @HandleExceptions()
  public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const userId = req.user?.id as string;

    await this.notifyStatusChange(userId, req.body.id, req.body.status_id);

    const q = `SELECT update_task($1) AS task;`;
    const result = await db.query(q, [JSON.stringify(req.body)]);
    const [data] = result.rows;
    const task = data.task || null;

    if (task) {
      this.sendAssignmentNotifications(task, userId);
    }

    return res.status(200).send(new ServerResponse(true, result.rows));
  }

  @HandleExceptions()
  public static async updateDuration(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const { id } = req.params;
    const { start, end } = req.body;

    const q = `
      UPDATE tasks
      SET start_date = ($1)::TIMESTAMP,
          end_date   = ($2)::TIMESTAMP
      WHERE id = ($3)::UUID
      RETURNING id;
    `;
    const result = await db.query(q, [start, end, id]);
    const [data] = result.rows;
    if (data?.id)
      return res.status(200).send(new ServerResponse(true, {}));
    return res.status(200).send(new ServerResponse(false, {}, "Task update failed!"));
  }

  @HandleExceptions()
  public static async updateStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const { status_id, task_id } = req.params;
    const { project_id, from_index, to_index } = req.body;

    const q = `SELECT update_task_status($1, $2, $3, $4, $5) AS status;`;
    const result = await db.query(q, [task_id, project_id, status_id, from_index, to_index]);
    const [data] = result.rows;
    if (data?.status) return res.status(200).send(new ServerResponse(true, {}));

    return res.status(200).send(new ServerResponse(false, {}, "Task update failed!"));
  }

  @HandleExceptions()
  public static async getTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const { id } = req.params;
    const q = `SELECT get_project_gantt_tasks($1) AS gantt_tasks;`;
    const result = await db.query(q, [id]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data?.gantt_tasks));
  }

  @HandleExceptions()
  public static async getTasksBetweenRange(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const { project_id, start_date, end_date } = req.query;
    const q = `
      SELECT pm.id,
             (SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(rec))), '[]' ::JSON)
              FROM (SELECT t.id,
                           t.name,
                           t.start_date,
                           t.project_id,
                           t.priority_id,
                           t.done,
                           t.end_date,
                           (SELECT color_code
                            FROM projects
                            WHERE projects.id = t.project_id) AS color_code,
                           (SELECT name FROM task_statuses WHERE id = t.status_id) AS status
                    FROM tasks_assignees ta,
                         tasks t
                    WHERE t.archived IS FALSE
                      AND ta.project_member_id = pm.id
                      AND t.id = ta.task_id
                      AND start_date IS NOT NULL
                      AND end_date IS NOT NULL
                    ORDER BY start_date) rec) AS tasks
      FROM project_members pm
      WHERE project_id = $1;
    `;
    const result = await db.query(q, [project_id]);
    const obj: any = {};

    const minMaxDates: { min_date: string, max_date: string } = await getMinMaxOfTaskDates(project_id as string);

    const dates = await getDates(minMaxDates.min_date || start_date as string, minMaxDates.max_date || end_date as string);
    const months = await getWeekRange(dates);

    for (const element of result.rows) {
      obj[element.id] = element.tasks;
      for (const task of element.tasks) {
        const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days"));
        const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days"));
        task.min = min + 1;
        task.max = max > 0 ? max + 2 : max;
      }
    }

    return res.status(200).send(new ServerResponse(true, { tasks: [obj], dates, months }));
  }

  @HandleExceptions()
  public static async getGanttTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `
      SELECT id,
             name,
             start_date,
             project_id,
             priority_id,
             done,
             end_date,
             (SELECT color_code
              FROM projects
              WHERE projects.id = project_id) AS color_code,
             (SELECT name FROM task_statuses WHERE id = tasks.status_id) AS status,
             parent_task_id,
             parent_task_id IS NOT NULL AS is_sub_task,
             (SELECT name FROM tasks WHERE id = tasks.parent_task_id) AS parent_task_name,
             (SELECT COUNT('*')::INT FROM tasks WHERE parent_task_id = tasks.id) AS sub_tasks_count
      FROM tasks
      WHERE archived IS FALSE
        AND project_id = $1
        AND parent_task_id IS NULL
      ORDER BY start_date;
    `;
    const result = await db.query(q, [req.query.project_id]);

    const minMaxDates: {
      min_date: string,
      max_date: string
    } = await getMinMaxOfTaskDates(req.query.project_id as string);

    if (!minMaxDates.max_date && !minMaxDates.min_date) {
      minMaxDates.min_date = moment().format();
      minMaxDates.max_date = moment().add(45, "days").format();
    }

    const dates = await getDates(minMaxDates.min_date, minMaxDates.max_date);
    const weeks = await getWeekRange(dates);
    const months = await getMonthRange(dates);

    for (const task of result.rows) {
      const min: number = dates.findIndex((date) => moment(task.start_date).isSame(date.date, "days"));
      const max: number = dates.findIndex((date) => moment(task.end_date).isSame(date.date, "days"));
      task.show_sub_tasks = false;
      task.sub_tasks = [];
      task.min = min + 1;
      task.max = max > 0 ? max + 2 : max;
    }

    return res.status(200).send(new ServerResponse(true, { tasks: result.rows, dates, weeks, months }));
  }

  @HandleExceptions()
  public static async getProjectTasksByTeam(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT get_resource_gantt_tasks($1) AS gantt_tasks;`;
    const result = await db.query(q, [req.user?.id ?? null]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data?.gantt_tasks));
  }

  @HandleExceptions()
  public static async getSelectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT get_selected_tasks($1) AS tasks`;
    const result = await db.query(q, [req.params.id]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data?.tasks));
  }

  @HandleExceptions()
  public static async getUnselectedTasksByProject(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT get_unselected_tasks($1) AS tasks`;
    const result = await db.query(q, [req.params.id]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data?.tasks));
  }

  /** Should migrate getProjectTasksByStatus to this */
  @HandleExceptions()
  public static async getProjectTasksByStatusV2(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {

    // Get all statuses
    const q1 = `
      SELECT task_statuses.id, task_statuses.name, stsc.color_code
      FROM task_statuses
             INNER JOIN sys_task_status_categories stsc ON task_statuses.category_id = stsc.id
      WHERE project_id = $1
        AND team_id = $2
      ORDER BY task_statuses.sort_order;
    `;
    const result1 = await db.query(q1, [req.query.project, req.user?.team_id]);
    const statuses = result1.rows;

    const dataset = [];

    // Query tasks of statuses
    for (const status of statuses) {
      const q2 = `SELECT get_tasks_by_status($1, $2) AS tasks`;
      const result2 = await db.query(q2, [req.params.id, status]);
      const [data] = result2.rows;

      for (const task of data.tasks) {
        task.name_color = getColor(task.name);
        task.names = this.createTagList(task.assignees);
        task.names.map((a: any) => a.color_code = getColor(a.name));
      }
      dataset.push(data);
    }

    return res.status(200).send(new ServerResponse(true, dataset));
  }

  @HandleExceptions()
  public static async getProjectTasksByStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT get_tasks_by_status($1,$2) AS tasks`;
    const result = await db.query(q, [req.params.id, req.query.status]);
    const [data] = result.rows;

    for (const task of data.tasks) {
      task.name_color = getColor(task.name);
      task.names = this.createTagList(task.assignees);
      task.all_labels = task.labels;
      task.labels = this.createTagList(task.labels, 3);
      task.names.map((a: any) => a.color_code = getColor(a.name));
    }

    return res.status(200).send(new ServerResponse(true, data?.tasks));
  }

  @HandleExceptions()
  public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `DELETE
               FROM tasks
               WHERE id = $1;`;
    const result = await db.query(q, [req.params.id]);
    return res.status(200).send(new ServerResponse(true, result.rows));
  }

  @HandleExceptions()
  public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT get_task_form_view_model($1, $2, $3, $4) AS view_model;`;
    const result = await db.query(q, [req.user?.id ?? null, req.user?.team_id ?? null, req.query.task_id ?? null, (req.query.project_id as string) || null]);
    const [data] = result.rows;

    const default_model = {
      task: {},
      priorities: [],
      projects: [],
      statuses: [],
      team_members: [],
    };

    const task = data.view_model.task || null;

    if (!task)
      return res.status(200).send(new ServerResponse(true, default_model));

    if (data.view_model && task) {
      task.assignees.map((a: any) => {
        a.color_code = getColor(a.name);
        return a;
      });

      task.names = WorklenzControllerBase.createTagList(task.assignees);

      const totalMinutes = task.total_minutes;
      const hours = Math.floor(totalMinutes / 60);
      const minutes = totalMinutes % 60;

      task.total_hours = hours;
      task.total_minutes = minutes;
      task.assignees = (task.assignees || []).map((i: any) => i.team_member_id);

      task.timer_start_time = moment(task.timer_start_time).valueOf();

      task.status_color = task.status_color + TASK_STATUS_COLOR_ALPHA;
    }

    for (const member of (data.view_model?.team_members || [])) {
      member.color_code = getColor(member.name);
    }

    const t = await getTaskCompleteInfo(task);
    const info = await TasksControllerV2.getTaskCompleteRatio(t.parent_task_id || t.id);

    if (info) {
      t.complete_ratio = info.ratio;
      t.completed_count = info.total_completed;
      t.total_tasks_count = info.total_tasks;
    }

    data.view_model.task = t;

    return res.status(200).send(new ServerResponse(true, data.view_model || default_model));
  }

  @HandleExceptions()
  public static async createQuickTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT create_quick_task($1) AS task_id;`;
    req.body.reporter_id = req.user?.id ?? null;
    req.body.team_id = req.user?.team_id ?? null;
    req.body.total_minutes = toMinutes(req.body.total_hours, req.body.total_minutes);
    const result = await db.query(q, [JSON.stringify(req.body)]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data));
  }

  @HandleExceptions()
  public static async createHomeTask(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT create_home_task($1);`;
    let endDate = req.body.end_date;
    switch (endDate) {
      case "Today":
        endDate = moment().format();
        break;
      case "Tomorrow":
        endDate = moment().add(1, "days").format();
        break;
      case "Next Week":
        endDate = moment().add(1, "weeks").endOf("isoWeek").format();
        break;
      case "Next Month":
        endDate = moment().add(1, "months").endOf("month").format();
        break;
      case "No Due Date":
        endDate = null;
        break;
      default:
        endDate = null;
    }
    req.body.end_date = endDate;
    req.body.reporter_id = req.user?.id ?? null;
    req.body.team_id = req.user?.team_id ?? null;
    const result = await db.query(q, [JSON.stringify(req.body)]);
    const [data] = result.rows;
    return res.status(200).send(new ServerResponse(true, data.create_home_task.task));
  }

  @HandleExceptions()
  public static async bulkChangeStatus(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT bulk_change_tasks_status($1, $2) AS task;`;
    const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]);
    const [data] = result.rows;

    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);

    return res.status(200).send(new ServerResponse(true, { failed_tasks: data.task }));
  }

  @HandleExceptions()
  public static async bulkChangePriority(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT bulk_change_tasks_priority($1, $2) AS task;`;
    const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]);
    const [data] = result.rows;

    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);

    return res.status(200).send(new ServerResponse(true, data));
  }

  @HandleExceptions()
  public static async bulkChangePhase(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT bulk_change_tasks_phase($1, $2) AS task;`;
    const result = await db.query(q, [JSON.stringify(req.body), req.user?.id]);
    const [data] = result.rows;

    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);

    return res.status(200).send(new ServerResponse(true, data));
  }

  @HandleExceptions()
  public static async bulkDelete(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const deletedTasks = req.body.tasks.map((t: any) => t.id);

    const result: any = { deleted_tasks: deletedTasks };

    const q = `SELECT bulk_delete_tasks($1) AS task;`;
    await db.query(q, [JSON.stringify(req.body)]);
    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
    return res.status(200).send(new ServerResponse(true, result));
  }

  @HandleExceptions()
  public static async bulkArchive(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT bulk_archive_tasks($1) AS task;`;
    req.body.type = req.query.type;
    await db.query(q, [JSON.stringify(req.body)]);
    const tasks = req.body.tasks.map((t: any) => t.id);
    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
    return res.status(200).send(new ServerResponse(true, tasks));
  }

  @HandleExceptions()
  public static async bulkAssignMe(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {

    req.body.team_id = req.user?.team_id;
    req.body.user_id = req.user?.id;

    const [task] = req.body.tasks || [];

    const q = `SELECT bulk_assign_to_me($1) AS task;`;
    await db.query(q, [JSON.stringify(req.body)]);

    const assignees = await getAssignees(task.id);
    const members = await getTeamMembers(req.body.team_id);
    // for inline display
    const names = WorklenzControllerBase.createTagList(assignees);

    const data = { id: task.id, members, assignees, names };

    const activityLog: IActivityLog = {
      task_id: task.id,
      attribute_type: "assignee",
      user_id: req.user?.id,
      log_type: "assign",
      old_value: null,
      new_value: req.user?.id,
      next_string: req.user?.name
    };

    insertToActivityLogs(activityLog);

    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
    return res.status(200).send(new ServerResponse(true, data));
  }

  @HandleExceptions()
  public static async bulkAssignLabel(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {

    if (req.body.text) {
      const q0 = `SELECT bulk_assign_or_create_label($1) AS label;`;

      req.body.team_id = req.user?.team_id;
      req.body.color = getRandomColorCode();

      await db.query(q0, [JSON.stringify(req.body)]);
    } else {
      const q = `SELECT bulk_assign_label($1, $2) AS task;`;
      await db.query(q, [JSON.stringify(req.body), req.user?.id as string]);
    }

    TasksController.notifyProjectUpdates(req.user?.socket_id as string, req.query.project as string);
    return res.status(200).send(new ServerResponse(true, null));
  }

  @HandleExceptions()
  public static async bulkAssignMembers(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const { tasks, members, project_id } = req.body;
    try {
      for (const task of tasks) {
        for (const member of members) {
          await TasksController.createTaskBulkAssignees(member.id, project_id, task.id, req.user?.id as string);
        }
      }
      TasksController.notifyProjectUpdates(req.user?.socket_id as string, project_id as string);
      return res.status(200).send(new ServerResponse(true, null));
    } catch (error) {
      return res.status(500).send(new ServerResponse(false, "An error occurred"));
    }
  }

  public static async createTaskAssignee(memberId: string, projectId: string, taskId: string, userId: string) {
    const q = `SELECT create_task_assignee($1,$2,$3,$4)`;
    const result = await db.query(q, [memberId, projectId, taskId, userId]);
    return result.rows;
  }

  public static async createTaskBulkAssignees(memberId: string, projectId: string, taskId: string, userId: string) {
    const q = `SELECT create_bulk_task_assignees($1,$2,$3,$4)`;
    const result = await db.query(q, [memberId, projectId, taskId, userId]);
    return result.rows;
  }

  @HandleExceptions()
  public static async getProjectTaskAssignees(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `
      SELECT project_members.team_member_id AS id,
             tmiv.name,
             tmiv.email,
             tmiv.avatar_url
      FROM project_members
             LEFT JOIN team_member_info_view tmiv ON project_members.team_member_id = tmiv.team_member_id
      WHERE project_id = $1
        AND EXISTS(SELECT 1 FROM tasks_assignees WHERE project_member_id = project_members.id);
    `;
    const result = await db.query(q, [req.params.id]);

    for (const member of result.rows) {
      member.color_code = getColor(member.name);
    }

    return res.status(200).send(new ServerResponse(true, result.rows));
  }
}
