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

import db from "../config/db";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
import { NotificationsService } from "../services/notifications/notifications.service";
import { humanFileSize, log_error, megabytesToBytes } from "../shared/utils";
import { HTML_TAG_REGEXP, S3_URL } from "../shared/constants";
import { getBaseUrl } from "../cron_jobs/helpers";
import { ICommentEmailNotification } from "../interfaces/comment-email-notification";
import { sendTaskComment } from "../shared/email-notifications";
import { getRootDir, uploadBase64, getKey, getTaskAttachmentKey, createPresignedUrlWithClient } from "../shared/s3";
import { getFreePlanSettings, getUsedStorage } from "../shared/paddle-utils";

interface ITaskAssignee {
  team_member_id: string;
  project_member_id: string;
  name: string;
  email_notifications_enabled: string;
  avatar_url: string;
  user_id: string;
  email: string;
  socket_id: string;
  team_id: string;
  user_name: string;
}

interface IMailConfig {
  message: string;
  receiverEmail: string;
  receiverName: string;
  content: string;
  commentId: string;
  projectId: string;
  taskId: string;
  teamName: string;
  projectName: string;
  taskName: string;
}

interface IMention {
  team_member_id: string;
  name: string;
}

async function getAssignees(taskId: string): Promise<Array<ITaskAssignee>> {
  const result1 = await db.query("SELECT get_task_assignees($1) AS assignees;", [taskId]);
  const [d] = result1.rows;
  return d.assignees || [];
}

export default class TaskCommentsController extends WorklenzControllerBase {

  private static replaceContent(messageContent: string, mentions: IMention[]) {
    const mentionNames = mentions.map(mention => mention.name);

    const replacedContent = mentionNames.reduce(
      (content, mentionName, index) => {
        const regex = new RegExp(`@${mentionName}`, "g");
        return content.replace(regex, `{${index}}`);
      },
      messageContent
    );

    return replacedContent;
  }

  private static async getUserDataByTeamMemberId(senderUserId: string, teamMemberId: string, projectId: string) {
    const q = `
      SELECT id,
             socket_id,
             users.name AS user_name,
             (SELECT email_notifications_enabled
              FROM notification_settings
              WHERE notification_settings.team_id = (SELECT team_id FROM team_members WHERE id = $2)
                AND notification_settings.user_id = users.id),
             (SELECT name FROM teams WHERE id = (SELECT team_id FROM team_members WHERE id = $2)) AS team,
             (SELECT name FROM projects WHERE id = $3) AS project,
             (SELECT color_code FROM projects WHERE id = $3) AS project_color
      FROM users
      WHERE id != $1
        AND id IN (SELECT user_id FROM team_members WHERE id = $2);
    `;
    const result = await db.query(q, [senderUserId, teamMemberId, projectId]);
    const [data] = result.rows;
    return data;
  }

  private static async updateComment(commentId: string, messageId: string) {
    if (!commentId || messageId) return;
    try {
      await db.query("UPDATE task_comments SET ses_message_id = $2 WHERE id = $1;", [commentId, messageId]);
    } catch (e) {
      log_error(e);
    }
  }

  @HandleExceptions()
  public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    req.body.user_id = req.user?.id;
    req.body.team_id = req.user?.team_id;
    const { mentions, attachments, task_id } = req.body;
    const url = `${S3_URL}/${getRootDir()}`;

    let commentContent = req.body.content;
    if (mentions.length > 0) {
      commentContent = this.replaceContent(commentContent, mentions);
    }

    req.body.content = commentContent;

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

    const response = data.comment;

    const commentId = response.id;

    if (attachments.length !== 0) {
      for (const attachment of attachments) {
        const q = `
      INSERT INTO task_comment_attachments (name, type, size, task_id, comment_id, team_id, project_id)
      VALUES ($1, $2, $3, $4, $5, $6, $7)
      RETURNING id, name, type, task_id, comment_id, created_at, 
      CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type) AS url;
    `;

        const result = await db.query(q, [
          attachment.file_name,
          attachment.file_name.split(".").pop(),
          attachment.size,
          task_id,
          commentId,
          req.user?.team_id,
          attachment.project_id,
          url
        ]);

        const [data] = result.rows;

        const s3Url = await uploadBase64(attachment.file, getTaskAttachmentKey(req.user?.team_id as string, attachment.project_id, task_id, commentId, data.id, data.type));

        if (!data?.id || !s3Url)
          return res.status(200).send(new ServerResponse(false, null, "Attachment upload failed"));
      }
    }

    const mentionMessage = `<b>${req.user?.name}</b> has mentioned you in a comment on <b>${response.task_name}</b> (${response.team_name})`;
    // const mentions = [...new Set(req.body.mentions || [])] as string[]; // remove duplicates

    const assignees = await getAssignees(req.body.task_id);

    const commentMessage = `<b>${req.user?.name}</b> added a comment on <b>${response.task_name}</b> (${response.team_name})`;
    for (const member of assignees || []) {
      if (member.user_id && member.user_id === req.user?.id) continue;

      void NotificationsService.createNotification({
        userId: member.user_id,
        teamId: req.user?.team_id as string,
        socketId: member.socket_id,
        message: commentMessage,
        taskId: req.body.task_id,
        projectId: response.project_id
      });

      if (member.email_notifications_enabled)
        await this.sendMail({
          message: commentMessage,
          receiverEmail: member.email,
          receiverName: member.name,
          content: req.body.content,
          commentId: response.id,
          projectId: response.project_id,
          taskId: req.body.task_id,
          teamName: response.team_name,
          projectName: response.project_name,
          taskName: response.task_name
        });
    }

    const senderUserId = req.user?.id as string;

    for (const mention of mentions) {
      if (mention) {
        const member = await this.getUserDataByTeamMemberId(senderUserId, mention.team_member_id, response.project_id);
        if (member) {

          NotificationsService.sendNotification({
            team: member.team,
            receiver_socket_id: member.socket_id,
            message: mentionMessage,
            task_id: req.body.task_id,
            project_id: response.project_id,
            project: member.project,
            project_color: member.project_color,
            team_id: req.user?.team_id as string
          });

          if (member.email_notifications_enabled)
            await this.sendMail({
              message: mentionMessage,
              receiverEmail: member.email,
              receiverName: member.user_name,
              content: req.body.content,
              commentId: response.id,
              projectId: response.project_id,
              taskId: req.body.task_id,
              teamName: response.team_name,
              projectName: response.project_name,
              taskName: response.task_name
            });
        }

      }
    }

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

  @HandleExceptions()
  public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    req.body.user_id = req.user?.id;
    req.body.team_id = req.user?.team_id;
    const { mentions, comment_id } = req.body;

    let commentContent = req.body.content;
    if (mentions.length > 0) {
      commentContent = await this.replaceContent(commentContent, mentions);
    }

    req.body.content = commentContent;

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

    const response = data.comment;

    const mentionMessage = `<b>${req.user?.name}</b> has mentioned you in a comment on <b>${response.task_name}</b> (${response.team_name})`;
    // const mentions = [...new Set(req.body.mentions || [])] as string[]; // remove duplicates

    const assignees = await getAssignees(req.body.task_id);

    const commentMessage = `<b>${req.user?.name}</b> added a comment on <b>${response.task_name}</b> (${response.team_name})`;
    for (const member of assignees || []) {
      if (member.user_id && member.user_id === req.user?.id) continue;

      void NotificationsService.createNotification({
        userId: member.user_id,
        teamId: req.user?.team_id as string,
        socketId: member.socket_id,
        message: commentMessage,
        taskId: req.body.task_id,
        projectId: response.project_id
      });

      if (member.email_notifications_enabled)
        await this.sendMail({
          message: commentMessage,
          receiverEmail: member.email,
          receiverName: member.name,
          content: req.body.content,
          commentId: response.id,
          projectId: response.project_id,
          taskId: req.body.task_id,
          teamName: response.team_name,
          projectName: response.project_name,
          taskName: response.task_name
        });
    }

    const senderUserId = req.user?.id as string;

    for (const mention of mentions) {
      if (mention) {
        const member = await this.getUserDataByTeamMemberId(senderUserId, mention.team_member_id, response.project_id);
        if (member) {

          NotificationsService.sendNotification({
            team: member.team,
            receiver_socket_id: member.socket_id,
            message: mentionMessage,
            task_id: req.body.task_id,
            project_id: response.project_id,
            project: member.project,
            project_color: member.project_color,
            team_id: req.user?.team_id as string
          });

          if (member.email_notifications_enabled)
            await this.sendMail({
              message: mentionMessage,
              receiverEmail: member.email,
              receiverName: member.user_name,
              content: req.body.content,
              commentId: response.id,
              projectId: response.project_id,
              taskId: req.body.task_id,
              teamName: response.team_name,
              projectName: response.project_name,
              taskName: response.task_name
            });
        }

      }
    }

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

  private static async sendMail(config: IMailConfig) {
    const subject = config.message.replace(HTML_TAG_REGEXP, "");
    const taskUrl = `${getBaseUrl()}/worklenz/projects/${config.projectId}?tab=tasks-list&task=${config.taskId}&focus=comments`;
    const settingsUrl = `${getBaseUrl()}/worklenz/settings/notifications`;

    const data: ICommentEmailNotification = {
      greeting: `Hi ${config.receiverName}`,
      summary: subject,
      team: config.teamName,
      project_name: config.projectName,
      comment: config.content,
      task: config.taskName,
      settings_url: settingsUrl,
      task_url: taskUrl,
    };

    const messageId = await sendTaskComment(config.receiverEmail, data);
    if (messageId) {
      void TaskCommentsController.updateComment(config.commentId, messageId);
    }
  }

  @HandleExceptions()
  public static async getByTaskId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const result = await TaskCommentsController.getTaskComments(req.params.id); // task id
    return res.status(200).send(new ServerResponse(true, result.rows));
  }

  private static async getTaskComments(taskId: string) {
    const url = `${S3_URL}/${getRootDir()}`;

    const q = `SELECT task_comments.id,
                    tc.text_content AS content,
                    task_comments.user_id,
                    task_comments.team_member_id,
                    (SELECT name FROM team_member_info_view WHERE team_member_info_view.team_member_id = tm.id) AS member_name,
                    u.avatar_url,
                    task_comments.created_at,
                    (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
                      FROM (SELECT tmiv.name AS user_name,
                                  tmiv.email AS user_email
                            FROM task_comment_mentions tcm
                                    LEFT JOIN team_member_info_view tmiv ON tcm.informed_by = tmiv.team_member_id
                            WHERE tcm.comment_id = task_comments.id) rec) AS mentions,
                    (SELECT JSON_BUILD_OBJECT(
                            'likes',
                            JSON_BUILD_OBJECT(
                                'count', (SELECT COUNT(*)
                                          FROM task_comment_reactions tcr
                                          WHERE tcr.comment_id = task_comments.id
                                            AND reaction_type = 'like'),
                                'liked_members', COALESCE(
                                    (SELECT JSON_AGG(tmiv.name)
                                      FROM task_comment_reactions tcr
                                      JOIN team_member_info_view tmiv ON tcr.team_member_id = tmiv.team_member_id
                                      WHERE tcr.comment_id = task_comments.id
                                        AND tcr.reaction_type = 'like'),
                                    '[]'::JSON
                                ),
                               'liked_member_ids', COALESCE(
                                       (SELECT JSON_AGG(tmiv.team_member_id)
                                        FROM task_comment_reactions tcr
                                                 JOIN team_member_info_view tmiv ON tcr.team_member_id = tmiv.team_member_id
                                        WHERE tcr.comment_id = task_comments.id
                                          AND tcr.reaction_type = 'like'),
                                       '[]'::JSON
                                                )
                            )
                        )) AS reactions,
                    (SELECT COALESCE(JSON_AGG(rec), '[]'::JSON)
                      FROM (SELECT id, created_at, name, size, type, (CONCAT('/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type)) AS url
                            FROM task_comment_attachments tca
                            WHERE tca.comment_id = task_comments.id) rec)                                        AS attachments
              FROM task_comments
                      LEFT JOIN task_comment_contents tc ON task_comments.id = tc.comment_id
                      INNER JOIN team_members tm ON task_comments.team_member_id = tm.id
                      LEFT JOIN users u ON tm.user_id = u.id
              WHERE task_comments.task_id = $1
              ORDER BY task_comments.created_at;`;
    const result = await db.query(q, [taskId]); // task id

    for (const comment of result.rows) {
      if (!comment.content) comment.content = "";
      comment.rawContent = await comment.content;
      comment.content = await comment.content.replace(/\n/g, "</br>");
      comment.edit = false;
      const { mentions } = comment;
      if (mentions.length > 0) {
        const placeHolders = comment.content.match(/{\d+}/g);
        if (placeHolders) {
          placeHolders.forEach((placeHolder: { match: (arg0: RegExp) => string[]; }) => {
            const index = parseInt(placeHolder.match(/\d+/)[0]);
            if (index >= 0 && index < comment.mentions.length) {
              comment.rawContent = comment.rawContent.replace(placeHolder, `@${comment.mentions[index].user_name}`);
              comment.content = comment.content.replace(placeHolder, `<span class="mentions"> @${comment.mentions[index].user_name} </span>`);
            }
          });
        }
      }

      for (const attachment of comment.attachments) {
        attachment.size = humanFileSize(attachment.size);
        attachment.url = url + attachment.url;
      }

    }

    return result;
  }

  @HandleExceptions()
  public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `DELETE
               FROM task_comments
               WHERE id = $1
                 AND task_id = $2
                 AND user_id = $3;`;
    const result = await db.query(q, [req.params.id, req.params.taskId, req.user?.id || null]);
    return res.status(200).send(new ServerResponse(true, result.rows));
  }

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

  private static async checkIfAlreadyExists(commentId: string, teamMemberId: string | undefined, reaction_type: string) {
    if (!teamMemberId) return;
    try {
      const q = `SELECT EXISTS(SELECT 1 FROM task_comment_reactions WHERE comment_id = $1 AND team_member_id = $2 AND reaction_type = $3)`;
      const result = await db.query(q, [commentId, teamMemberId, reaction_type]);
      const [data] = result.rows;
      return data.exists;
    } catch (error) {
      log_error(error);
    }
  }

  private static async getTaskCommentData(commentId: string) {
    if (!commentId) return;
    try {
      const q = `SELECT tc.user_id,
                      t.project_id,
                      t.name AS task_name,
                      (SELECT team_id FROM projects p WHERE p.id = t.project_id) AS team_id,
                      (SELECT name FROM teams te WHERE id = (SELECT team_id FROM projects p WHERE p.id = t.project_id)) AS team_name,
                      (SELECT u.socket_id FROM users u WHERE u.id = tc.user_id) AS socket_id,
                      (SELECT name FROM team_member_info_view tmiv WHERE tmiv.team_member_id = tcr.team_member_id) AS reactor_name
                FROM task_comments tc
                        LEFT JOIN tasks t ON t.id = tc.task_id
                        LEFT JOIN task_comment_reactions tcr ON tc.id = tcr.comment_id
                WHERE tc.id = $1;`;
      const result = await db.query(q, [commentId]);
      const [data] = result.rows;
      return data;
    } catch (error) {
      log_error(error);
    }
  }

  @HandleExceptions()
  public static async updateReaction(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const { id } = req.params;
    const { reaction_type, task_id } = req.query;

    const exists = await this.checkIfAlreadyExists(id, req.user?.team_member_id, reaction_type as string);

    if (exists) {
      const deleteQ = `DELETE FROM task_comment_reactions WHERE comment_id = $1 AND team_member_id = $2;`;
      await db.query(deleteQ, [id, req.user?.team_member_id]);
    } else {
      const q = `INSERT INTO task_comment_reactions (comment_id, user_id, team_member_id) VALUES ($1, $2, $3);`;
      await db.query(q, [id, req.user?.id, req.user?.team_member_id]);

      const getTaskCommentData = await TaskCommentsController.getTaskCommentData(id);
      const commentMessage = `<b>${getTaskCommentData.reactor_name}</b> liked your comment on <b>${getTaskCommentData.task_name}</b> (${getTaskCommentData.team_name})`;

      if (getTaskCommentData && getTaskCommentData.user_id !== req.user?.id) {
        void NotificationsService.createNotification({
          userId: getTaskCommentData.user_id,
          teamId: req.user?.team_id as string,
          socketId: getTaskCommentData.socket_id,
          message: commentMessage,
          taskId: req.body.task_id,
          projectId: getTaskCommentData.project_id
        });
      }
    }

    const result = await TaskCommentsController.getTaskComments(task_id as string);
    return res.status(200).send(new ServerResponse(true, result.rows));
  }

  @HandleExceptions()
  public static async createAttachment(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    req.body.user_id = req.user?.id;
    req.body.team_id = req.user?.team_id;
    const { attachments, task_id } = req.body;

    const q = `INSERT INTO task_comments (user_id, team_member_id, task_id)
                  VALUES ($1, (SELECT id
                              FROM team_members
                              WHERE user_id = $1
                                AND team_id = $2::UUID), $3)
                  RETURNING id;`;
    const result = await db.query(q, [req.user?.id, req.user?.team_id, task_id]);
    const [data] = result.rows;

    const commentId = data.id;

    const url = `${S3_URL}/${getRootDir()}`;

    for (const attachment of attachments) {
      if (req.user?.subscription_status === "free" && req.user?.owner_id) {
        const limits = await getFreePlanSettings();
    
        const usedStorage = await getUsedStorage(req.user?.owner_id);
        if ((parseInt(usedStorage) + attachment.size) > megabytesToBytes(parseInt(limits.free_tier_storage))) {
          return res.status(200).send(new ServerResponse(false, [], `Sorry, the free plan cannot exceed ${limits.free_tier_storage}MB of storage.`));
        }
      }
      
      const q = `
        INSERT INTO task_comment_attachments (name, type, size, task_id, comment_id, team_id, project_id)
        VALUES ($1, $2, $3, $4, $5, $6, $7)
        RETURNING id, name, type, task_id, comment_id, created_at, 
        CONCAT($8::TEXT, '/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type) AS url;
      `;

      const result = await db.query(q, [
        attachment.file_name,
        attachment.size,
        attachment.file_name.split(".").pop(),
        task_id,
        commentId,
        req.user?.team_id,
        attachment.project_id,
        url
      ]);

      const [data] = result.rows;

      const s3Url = await uploadBase64(attachment.file, getTaskAttachmentKey(req.user?.team_id as string, attachment.project_id, task_id, commentId, data.id, data.type));

      if (!data?.id || !s3Url)
        return res.status(200).send(new ServerResponse(false, null, "Attachment upload failed"));
    }

    const assignees = await getAssignees(task_id);

    const commentMessage = `<b>${req.user?.name}</b> added a new attachment as a comment on <b>${commentId.task_name}</b> (${commentId.team_name})`;

    for (const member of assignees || []) {
      if (member.user_id && member.user_id === req.user?.id) continue;

      void NotificationsService.createNotification({
        userId: member.user_id,
        teamId: req.user?.team_id as string,
        socketId: member.socket_id,
        message: commentMessage,
        taskId: task_id,
        projectId: commentId.project_id
      });

      if (member.email_notifications_enabled)
        await this.sendMail({
          message: commentMessage,
          receiverEmail: member.email,
          receiverName: member.name,
          content: req.body.content,
          commentId: commentId.id,
          projectId: commentId.project_id,
          taskId: task_id,
          teamName: commentId.team_name,
          projectName: commentId.project_name,
          taskName: commentId.task_name
        });
    }

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


  @HandleExceptions()
  public static async download(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
    const q = `SELECT CONCAT($2::TEXT, '/', team_id, '/', project_id, '/', task_id, '/', comment_id, '/', id, '.', type) AS key
               FROM task_comment_attachments
               WHERE id = $1;`;
    const result = await db.query(q, [req.query.id, getRootDir()]);
    const [data] = result.rows;

    if (data?.key) {
      const url = await createPresignedUrlWithClient(data.key, req.query.file as string);
      return res.status(200).send(new ServerResponse(true, url));
    }

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